import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.event.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Stack;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.JToolBar;
import javax.swing.SpringLayout;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UIManager.LookAndFeelInfo;
import javax.swing.event.*;
import javax.swing.table.DefaultTableModel;
import javax.swing.text.AttributeSet;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyleContext;

@SuppressWarnings("serial")
public class LiveEdit extends JFrame implements TableModelListener, ActionListener, emDependent{
	JTabbedPane infoArea;
	JSplitPane splitter;
	JPanel rightPanel, fieldInfo, sourceInfo, inputPanel, settingsInfo, compileModelInfo, modelPanel;
	JScrollPane scrollPane, sourceScroll;
	JTable table;
	JTextField input;
	JButton inputBtn, toolBtnBack, toolBtnField, toolBtnMethod, toolBtnClasses;
	JToolBar toolbar;
	JPanel toolbarLeft;
	JLabel classTitle;
	JaDEN emModel;
	public static String modelLoader;
	public static String defaultModel = "JaDEN_Blank";
	public String currentModel;
	Object currentClass = null;
	Stack<Object> explored = new Stack<Object>();
	Timer timer;
	
	//Source code tab
	JTextPane sourcePane;
	
	//Live display
	private final int MODE_FIELDS = 0;
	private final int MODE_METHODS = 1;
	private final int MODE_CLASSES = 2;
	
	private int tableMode = MODE_FIELDS;
	private static String tableColumns[][] = {
		{"Name", "Type", "Value", "Dependency", ""},
		{"Name", "Type", "Value", "Arguments", ""},
		{"Name", "Is Model", "Is Placeable", "", ""},
	};
	private static LiveEdit instance;
	
	
	public LiveEdit(){
		instance = this;
	}
	public void init(){
		//Evaluator.dynamics = true;
		
		setSize(1000, 500);
		setLayout(new BorderLayout());
		setUIStyle();
		setBackground(new Color(204, 208, 216));
		
		rightPanel = makePanel();
		rightPanel.setLayout(new SpringLayout());
		rightPanel.setBorder(BorderFactory.createMatteBorder(0, 1, 0, 0, Color.BLACK));
		//add(rightPanel, BorderLayout.EAST);
		
		timer = new Timer(1/10, new ActionListener(){
			public void actionPerformed(ActionEvent arg0) {
				updateModel();
			}
		});
		//timer.start();

		//Tabbed pane
		infoArea = new JTabbedPane();
		infoArea.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, Color.BLACK));
		rightPanel.add(infoArea);
		
		fieldInfo = makePanel();
		infoArea.addTab("Fields/Methods", fieldInfo);
		
		//sourceInfo = makePanel();
		//infoArea.addTab("Source Code", sourceInfo);
		
		//settingsInfo = makePanel();
		//infoArea.addTab("Settings", settingsInfo);
		
		compileModelInfo = makePanel();
		infoArea.addTab("Compile Model", compileModelInfo);
		
		//Table for fields / methods information & live editing
		Object data[][] = new Object[0][5];
		table = new JTable(new DefaultTableModel(data, tableColumns[tableMode]){
			public boolean isCellEditable(int row, int column){
				if(column == 4 || column == 1) return false;
				return true;
			}
		}){
			public String getToolTipText(MouseEvent e){
				int row = rowAtPoint(e.getPoint());
				int column = columnAtPoint(e.getPoint());
				if(row == -1) return null;
				return getModel().getValueAt(row, column).toString();
			}
		};
		table.setPreferredScrollableViewportSize(new Dimension(500, 300));
		table.getModel().addTableModelListener(this);
		table.getColumnModel().getColumn(4).setPreferredWidth(10);
		table.addMouseListener(new MouseListener(){
			public void mouseClicked(MouseEvent e) {
				DefaultTableModel model = (DefaultTableModel) table.getModel();
				int column = table.getSelectedColumn();
				if(column != 4) return;
				
				if(tableMode == MODE_FIELDS){
					System.out.println("Explore variable " + model.getValueAt(table.getSelectedRow(), 0));
					Object cls = null;
					if(currentClass.getClass().isArray()) cls = Evaluator.getArrayValue(currentClass, (Integer) model.getValueAt(table.getSelectedRow(), 0));
					else cls = Evaluator.getFieldValue(currentClass, (String) model.getValueAt(table.getSelectedRow(), 0));
					if(cls != null){
						explored.push(currentClass);
						toolBtnBack.setEnabled(true);
						setCurrentClass(cls);
						refreshTable();
					}
				}else if(tableMode == MODE_METHODS){
					String name = (String) model.getValueAt(table.getSelectedRow(), 0);
					String typesStr = (String) model.getValueAt(table.getSelectedRow(), 2);
					String types[] = typesStr.split(", ");
					if(typesStr.equals("")) types = new String[0];
					Class params[] = new Class[types.length];
					
					try{
						for(int i = 0; i < types.length; i++){//Get all argument types
							params[i] = Class.forName(types[i]);
						}
						
						if(params.length > 0){
							System.out.println("define argument values");
							return;
						}
						Method m = currentClass.getClass().getDeclaredMethod(name, params);
						m.setAccessible(true);
						m.invoke((Modifier.isStatic(m.getModifiers()) ? null : currentClass), new Object[]{});
					}catch(Exception me){
						me.printStackTrace();
					}
				}else if(tableMode == MODE_CLASSES){
					Class cls = Evaluator.classes[table.getSelectedRow()];
					System.out.println(cls);
					
					tableMode = MODE_FIELDS;
					explored.push(currentClass);
					toolBtnBack.setEnabled(true);
					setCurrentClass(cls);
					refreshTable();
				}
			}
			public void mouseEntered(MouseEvent e) {}
			public void mouseExited(MouseEvent e) {}
			public void mousePressed(MouseEvent e) {}
			public void mouseReleased(MouseEvent e) {}
		});

		scrollPane = new JScrollPane(table);
		scrollPane.setBorder(BorderFactory.createEmptyBorder());
		
		inputPanel = makePanel();
		inputPanel.setLayout(null);
		inputPanel.setBounds(0, 0, 500, 30);
		rightPanel.add(inputPanel);
		
		input = new JTextField("");
		input.setBounds(0, 0, 425, 25);
		inputPanel.add(input);
		
		inputBtn = new JButton("Execute");
		inputBtn.setBounds(425, 0, 75, 25);
		inputBtn.addActionListener(this);
		inputBtn.setActionCommand("inputBtn");
		inputPanel.add(inputBtn);
		
		toolbar = new JToolBar();
		toolbar.setLayout(new BorderLayout());
		toolbar.setFloatable(false);
		fieldInfo.add(toolbar, BorderLayout.NORTH);
		fieldInfo.add(scrollPane, BorderLayout.CENTER);
		
		classTitle = new JLabel();
		classTitle.setPreferredSize(new Dimension(150, 20));
		
		toolbar.add(classTitle, BorderLayout.EAST);
		toolbarLeft = new JPanel();
		toolbarLeft.setBackground(new Color(0, 0, 0, 0));
		toolbar.add(toolbarLeft, BorderLayout.WEST);
		
		toolBtnBack = createBtn("< Back", "toolBtnBack", false);
		toolBtnField = createBtn("Fields", "toolBtnField", (tableMode != MODE_FIELDS));
		toolBtnMethod = createBtn("Methods", "toolBtnMethod", (tableMode != MODE_METHODS));
		toolBtnClasses = createBtn("Classes", "toolBtnClasses", (tableMode != MODE_CLASSES));
		
		//SOURCE CODE VIEWER
		sourcePane = new JTextPane();
		sourceScroll = new JScrollPane(sourcePane);
		sourceScroll.setPreferredSize( new Dimension( 500, 395 ) );
		compileModelInfo.add(sourceScroll);
		
		StyleContext sc = StyleContext.getDefaultStyleContext();
	    AttributeSet aset = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, Color.BLUE);

	    int len = sourcePane.getDocument().getLength();
	    sourcePane.setCharacterAttributes(aset, false);
		
		modelPanel = makePanel();
		splitter = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, modelPanel, rightPanel);
		splitter.setDividerLocation(500);
		splitter.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, new PropertyChangeListener(){
			public void propertyChange(PropertyChangeEvent pe){
				//emModel.WIDTH = splitter.getDividerLocation();
				System.out.println();
			}
		});
		add(splitter, BorderLayout.CENTER);
		
		SpringUtilities.makeCompactGrid(rightPanel, 2, 1, 0, 0, 0, 0);
		
		
		//Load the model, if model name entered as first argument use that
		//otherwise load the default blank model
		if(modelLoader != null){
			loadModel(modelLoader);
		}else{
			loadModel(LiveEdit.defaultModel);
		}
	}
	
	private void getSource(){
		File file = Evaluator.locateSource(emModel);
		if(file == null) return;
		
		try {
			FileReader read = new FileReader(file);
			BufferedReader pr = new BufferedReader(read);
			
			String code = "";
			String line;
			while((line = pr.readLine()) != null){
				code += line + "\n";
			}
			pr.close();
			read.close();
			
			//sourcePane.setText(code);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	private void setUIStyle(){
		try {
		    for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
		        if ("Nimbus".equals(info.getName())) {
		            UIManager.setLookAndFeel(info.getClassName());
		            break;
		        }
		    }
		} catch (Exception e) {}
	}
	private JPanel makePanel(){
		JPanel pane = new JPanel();
		pane.setLayout(new BorderLayout());
		return pane;
	}
	
	private JButton createBtn(String text, String cmd, boolean enabled){
		JButton btn = new JButton(text);
		btn.setActionCommand(cmd);
		btn.addActionListener(this);
		btn.setEnabled(enabled);
		toolbarLeft.add(btn);
		return btn;
	}
	
	public void setCurrentClass(Object cls){
		currentClass = cls;
		if(currentClass instanceof Class) classTitle.setText(((Class) currentClass).getName() + " (Class)");//classTitle.setText(((Component) currentClass).getName());
		else classTitle.setText(currentClass.getClass().getName() + " (Instance)");
		refreshTable();
	}
	
	private void updateModel(){
		repaint();
	}

	public void paint(Graphics g){
		super.paint(g);
		
		//if(emModel != null) emModel.paint(g);
	}
	
	boolean hold = false;
	public void tableChanged(TableModelEvent e){
		if(e.getType() != TableModelEvent.UPDATE) return;
		if(hold){
			hold = false;
			return;
		}
		
		int row = e.getFirstRow();
		int column = e.getColumn();
		
		DefaultTableModel model = (DefaultTableModel) e.getSource();
		if(tableMode == MODE_FIELDS && row == model.getRowCount() - 1 && column == 0){ //Create new dynamic field
			addNewField((String) model.getValueAt(row, 0));
			return;
		}
		if(tableMode == MODE_FIELDS && column == 3){ //Update dependency
			Evaluator.updateDependency(currentClass, (String) model.getValueAt(row, 0), (String) model.getValueAt(row, 3));
			refreshTable();
			return;
		}
		if(column != 2) return;
		
		String expr = (String) model.getValueAt(row, column).toString();
		System.out.println(expr);
		Object output = Evaluator.evaluateExpression(expr, currentClass);
		System.out.println(output);
		
		if(currentClass.getClass().isArray()){
			Array.set(currentClass, (Integer) model.getValueAt(row, 0), output);
			hold = true;
			model.setValueAt(output, row, 2);
			refreshTable();
			return;
		}
		
		String fieldName = (String) model.getValueAt(row, 0);
		
		try {
			Evaluator.setVariable(currentClass, fieldName, output);
			
			hold = true;
			model.setValueAt(output, row, 2);
			refreshTable();
		} catch (Exception e1) {
			e1.printStackTrace();
		}
	}
	
	public static Class getType(Class type){
		if(type.getName().equals("java.lang.Byte")) return byte.class;
		if(type.getName().equals("java.lang.Short")) return short.class;
		if(type.getName().equals("java.lang.Integer")) return int.class;
		if(type.getName().equals("java.lang.Long")) return long.class;
		if(type.getName().equals("java.lang.Boolean")) return boolean.class;
		if(type.getName().equals("java.lang.Char")) return char.class;
		if(type.getName().equals("java.lang.Double")) return double.class;
		if(type.getName().equals("java.lang.Long")) return long.class;
		return type;
	}
	
	public static String formatField(String fieldName){
		return fieldName.toUpperCase().substring(0, 1) + (fieldName.length() > 1 ? fieldName.substring(1, fieldName.length()) : "");
	}

	private void addNewField(String name){
		DefaultTableModel model = (DefaultTableModel) table.getModel();
		model.addRow(new Object[]{"", "", "", "", ""});
		
		Variable var = Evaluator.createVariable(currentClass, name);
		hold = true;
		model.setValueAt(var.type, model.getRowCount() - 2, 1);
		hold = true;
		model.setValueAt(var.get(), model.getRowCount() - 2, 2);
	}
	
	private void refreshTable(){ // super lazy update => refreshes everything
		DefaultTableModel model = (DefaultTableModel) table.getModel();
		for(int i = 0; i < model.getRowCount(); i++) {
			model.removeRow(i);
			i--;
		}
		if(currentClass == null) return;
 		
		
		try {
			Object fields[][] = new Object[0][5];
			if(tableMode == MODE_FIELDS) fields = Evaluator.getFields(currentClass);
			else if(tableMode == MODE_METHODS) fields = Evaluator.getMethods(currentClass);
			else if(tableMode == MODE_CLASSES) fields = Evaluator.getClasses(currentClass);
			
			for(int i = 0; i < fields.length; i++){
				if(fields[i][0] == null || fields[i][0].equals("")) continue;
				model.addRow(fields[i]);
			}
			
			if(tableMode == MODE_FIELDS) model.addRow(new Object[]{"", "", "", "", ""});
		}catch(Exception e){
			e.printStackTrace();
		}
		scrollPane.repaint();
	}

	@Override
	public void actionPerformed(ActionEvent ae) {
		boolean modeChanged = false;
		if(ae.getActionCommand().equals("inputBtn")){
			Evaluator.evaluateInput(currentClass, input.getText());
		}else if(ae.getActionCommand().equals("toolBtnBack")){
			if(explored.size() == 0) return;
			setCurrentClass(explored.pop());
			((JButton) ae.getSource()).setEnabled(explored.size() > 0);
		}else if(ae.getActionCommand().equals("toolBtnField")){
			tableMode = MODE_FIELDS;
			modeChanged = true;
		}else if(ae.getActionCommand().equals("toolBtnMethod")){
			tableMode = MODE_METHODS;
			modeChanged = true;
		}else if(ae.getActionCommand().equals("toolBtnClasses")){
			tableMode = MODE_CLASSES;
			modeChanged = true;
		}
		
		if(modeChanged){
			for(int c = 0; c < tableColumns[tableMode].length; c++){
				table.getColumnModel().getColumn(c).setHeaderValue(tableColumns[tableMode][c]);
			}
			
			toolBtnField.setEnabled(tableMode != MODE_FIELDS);
			toolBtnMethod.setEnabled(tableMode != MODE_METHODS);
			toolBtnClasses.setEnabled(tableMode != MODE_CLASSES);
		}
		refreshTable();
	}

	@Override
	public void emUpdate(String property) {
		refreshTable();
		scrollPane.repaint();
		
	}
	@Override
	public void emUpdate(Object cls, String property) {
		refreshTable();
		scrollPane.repaint();
		Evaluator.updatedVariable(cls, property);
		repaint();
	}
	
	//LiveEdit methods
	public static void loadModel(String name){
		System.out.println("Loading the model: " + name);
		if(!name.equals("JaDEN_Blank")) loadModel("JaDEN_Blank");
		
		instance.currentModel = name;
		Evaluator.resetVariables();
		
		try{
			Class cls = Class.forName(name);
			Method main = cls.getDeclaredMethod("main", new Class[]{String[].class});
			main.invoke(null, new Object[]{new String[]{}});
			instance.setCurrentClass(cls);
			instance.emModel = null;
		}catch(Exception me){// No Main Method found, 
			try{
				Class cls = Class.forName(name);
				Object inst = cls.newInstance();
				instance.setCurrentClass(inst);
				instance.modelPanel.removeAll();
				
				System.out.println(instance.currentClass);
				instance.emModel = (JaDEN) instance.currentClass;
				instance.emModel.init();
				instance.emModel.emAddDependent(instance);
				instance.modelPanel.add(instance.emModel);
				//instance.modelPanel.repaint();
			}catch(Exception ae){
				ae.printStackTrace();
			}
		}
		
		instance.refreshTable();
		instance.getSource();
	}
	public static void newModel(){
		loadModel("JaDEN_Blank");
	}
	public static void reloadModel(){
		loadModel(instance.currentModel);
	}
	
	public static void compileModel(){
		String code = instance.sourcePane.getText();
		String name = code.substring(code.indexOf("public class ") + 13);
		name = name.substring(0, name.indexOf(" "));
		System.out.println("Compiling: " + name);
		
		boolean success = Evaluator.compileModel(name, code);
		
		if(success){
			System.out.println("Model compiled successfully!");
			System.out.println("Attempting loading");
			instance.setVisible(false);
			createNewEnvironment(name);
		}else{
			JOptionPane.showMessageDialog(null, "Failed to compile the model.");
			System.out.println("Failed to compile...");
		}
	}
	
	public static void createNewEnvironment(final String model){
		String list = System.getProperty("java.class.path");
		
		final File root;
		if(list.contains(";")) root = new File(list.substring(0, list.indexOf(";")));
		else if(list.contains(":")) root = new File(list.substring(0, list.indexOf(":")));
		else root = new File(list);
		
		try {
			new ProcessBuilder("java", "-classpath", System.getProperty("java.class.path"), "LiveEdit_Launch", model).start();
			System.exit(0);
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		JOptionPane.showMessageDialog(null, "Failed create new environment to run model.");
	}
}