package leaf.ui;

import static info.clearthought.layout.TableLayoutConstants.FILL;
import static leaf.ui.LeafLogic.ACTION_CANCEL;
import static leaf.ui.LeafLogic.ACTION_STARTUP;
import info.clearthought.layout.TableLayout;
import info.clearthought.layout.TableLayoutConstraints;

import java.awt.Cursor;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;

import javax.swing.BorderFactory;
import javax.swing.DefaultListSelectionModel;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.table.TableModel;

import leaf.data.Pair;
import leaf.ui.LeafLogic.Action;
import leaf.ui.LeafLogic.LeafLogicActionBinding;
import leaf.ui.LeafLogic.LeafUIActionBinding;

import com.evolute.utils.tables.BaseTable;
import com.evolute.utils.tables.ColumnizedMappable;
import com.evolute.utils.tables.VectorTableModel;
import com.evolute.utils.tracker.TrackableWindow;

public class LeafWindow extends JFrame implements TrackableWindow, ListSelectionListener, TreeSelectionListener, ActionListener, PropertyChangeListener
{

	private static final long serialVersionUID = 1L;

	private static final int DEFAULT_HEIGHT = 480;

	private static final int DEFAULT_WIDTH = 640;

	/**
	 * Registers DataComponent in a list of actions
	 * 
	 * @author tsimao
	 * 
	 */
	@Retention(RetentionPolicy.RUNTIME)
	public @interface ActionActivation
	{
		/**
		 * Array of actions to execute when a select is listened in this
		 * JComponent
		 * 
		 * @return
		 */
		String[] onSelect();

		/**
		 * Array of actions to execute when a change is listened in this
		 * JComponent
		 * 
		 * @return
		 */
		String[] onChange();

	}

	/**
	 * Binds an Object to actions
	 * 
	 * @author tsimao
	 * 
	 */
	@Retention(RetentionPolicy.RUNTIME)
	public @interface LeafObject
	{

		/**
		 * Actions that use this field
		 */
		String[] useWith();
	}

	/**
	 * Declares a JPanel as a leaf
	 * 
	 * @author tsimao
	 * 
	 */
	@Retention(RetentionPolicy.RUNTIME)
	public @interface LeafPanel
	{
	}

	/**
	 * This window's logic controller
	 */
	private final LeafLogic logicController;

	private List<JPanel> subPanels = new ArrayList<JPanel>();

	/**
	 * Actions
	 */
	private Map<String, Action> mapActionByName = new HashMap<String, Action>();

	/**
	 * Fields
	 */
	private Map<String, List<Object>> mapWindowOnSelectFieldByActionName = new HashMap<String, List<Object>>();
	private Map<String, List<Object>> mapWindowOnChangeFieldByActionName = new HashMap<String, List<Object>>();
	private Map<String, Field> mapLeafObjectByActionName = new HashMap<String, Field>();
	private Map<Field, Object> mapInstanceByField = new HashMap<Field, Object>();

	/**
	 * Methods
	 */
	private Map<String, List<Method>> mapWindowMethodsByActionName = new HashMap<String, List<Method>>();
	private Map<String, Method> mapLogicMethodByActionName = new HashMap<String, Method>();
	private Map<Method, Object> mapInstanceByMethod = new HashMap<Method, Object>();

	/**
	 * Meta-info
	 */
	private Map<Object, Annotation> mapAnnotationByObject = new HashMap<Object, Annotation>();

	/**
	 * Run later actions
	 */
	private Queue<Pair<String, Object>> listRunLater = new LinkedList<Pair<String, Object>>();

	/**
	 * Creates a new LeafWindow binded with given 'logicController'
	 * 
	 * @param logicController
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 */
	public LeafWindow(LeafLogic logicController)
	{
		super();
		setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
		this.logicController = logicController;
		if( logicController != null )
		{
			logicController.addWindow( this );
		}
	}

	@Override
	public void open()
	{
		setVisible( true );
	}

	public void close()
	{
		SwingUtilities.invokeLater( new Runnable()
		{
			public void run()
			{
				setVisible( false );
				dispose();
			}
		} );
	}

	@Override
	public boolean closeIfPossible()
	{
		close();
		return true;
	}

	@Override
	public void refresh()
	{
	}

	/**
	 * Aborts current action. Aborts pending actions as well if 'all' is true
	 * 
	 * @param all
	 */
	public void abortAction( boolean all )
	{
		runGivenAction( ACTION_CANCEL, null );
		throw new LeafRuntimeException( all );
	}

	protected boolean runAction( String actionName )
	{
		return runAction( actionName, null );
	}

	/**
	 * Returns false if an error occurred
	 * 
	 * @param actionName
	 * @param argument
	 * @return
	 */
	protected boolean runAction( String actionName, Object argument )
	{
		boolean ok = true;
		if( argument == null )
		{
			Field field = mapLeafObjectByActionName.get( actionName );
			if(field != null)
			{
				Object instance = mapInstanceByField.get( field );
				if( instance != null)
				{
					try
					{
						argument = field.get( instance );
					} catch( IllegalArgumentException e )
					{
						e.printStackTrace(System.out);
					} catch( IllegalAccessException e )
					{
						e.printStackTrace( System.out);
					}
				}
			}
		}
		try
		{
			runGivenAction( actionName, argument );
		} catch( LeafRuntimeException leafRuntimeException )
		{
			ok = !leafRuntimeException.isAbort();
		}
		if( ok )
		{
			runPendingActions();
		}
		else
		{
			listRunLater.clear();
		}
		return ok;
	}

	public void runActionLater( String action )
	{
		runActionLater( action, null );
	}
	
	public void runActionLater( String action, Object argument )
	{
		if( action != null && mapActionByName.containsKey( action ) )
		{
			listRunLater.add( new Pair<String, Object>( action, argument ) );
		}
	}

	public void completeSetup()
	{
		try
		{
			loadLeafs();
			loadActions();
			loadFields();
			loadMethods();
			runAction( ACTION_STARTUP, null );
			setVisible( true );
		} catch( Exception e )
		{
			e.printStackTrace( System.out );
		}
	}
	
	public static void setupTopBottomSimpleActionsPanel(JPanel where, JPanel top, JPanel bottom)
	{
		TableLayout layout = new TableLayout(new double[]{TableLayout.FILL}, new double[]{TableLayout.MINIMUM, TableLayout.FILL,TableLayout.MINIMUM});
		where.setLayout( layout );
		where.add( top, new TableLayoutConstraints(0,0) );
		where.add( new JPanel(), new TableLayoutConstraints(0,1) );
		where.add( bottom, new TableLayoutConstraints(0,2) );
	}
	
	public static void setupSimpleDataPanel( JPanel where, String name, JComponent... field )
	{
		double[] cols = new double[] {
			FILL
		};
		double[] rows = new double[field.length];
		for( int i = 0; i < field.length; rows[i++] = TableLayout.PREFERRED )
			;
		rows[rows.length - 1] = FILL;
		TableLayout layout = new TableLayout( cols, rows );
		layout.setHGap( 5 );
		layout.setVGap( 5 );
		where.setLayout( layout );
		if( name != null )
		{
			where.setBorder( BorderFactory.createTitledBorder( BorderFactory.createEtchedBorder(), name ) );
		}

		for( int i = 0; i < field.length; ++i )
		{
			where.add( field[i], new TableLayoutConstraints( 0, i ) );
		}
	}

	private void loadLeafs() throws IllegalArgumentException, IllegalAccessException
	{
		Field fields[] = this.getClass().getDeclaredFields();
		if( fields != null )
		{
			for( Field field : fields )
			{
				if( field.getAnnotation( LeafPanel.class ) != null && field.get( this ) != null )
				{
					subPanels.add( (JPanel) field.get( this ) );
				}
			}
		}
	}

	private void loadActions() throws IllegalArgumentException, IllegalAccessException
	{
		Field[] allLogicFields = this.logicController.getClass().getFields();
		for( Field field : allLogicFields )
		{
			Action action = field.getAnnotation( Action.class );
			if( action != null )
			{
				String value = (String) field.get( this );
				if( value != null )
				{
					mapActionByName.put( value, action );
					mapWindowMethodsByActionName.put( value, new ArrayList<Method>() );
					mapWindowOnSelectFieldByActionName.put( value, new ArrayList<Object>() );
					mapWindowOnChangeFieldByActionName.put( value, new ArrayList<Object>() );
				}
			}
		}
	}

	private void loadFields( Field[] fields, Object instance )
	{
		try
		{
			for( Field field : fields )
			{
				ActionActivation componentBehaviour = field.getAnnotation( ActionActivation.class );
				if( componentBehaviour != null )
				{
					String[] allChanges = componentBehaviour.onChange();
					if( allChanges != null )
					{
						for( String onChange : allChanges )
						{
							if( mapActionByName.containsKey( onChange ) )
							{
								// valid action
								mapAnnotationByObject.put( field.get( instance ), componentBehaviour );
								mapWindowOnChangeFieldByActionName.get( onChange ).add( field.get( instance ) );
								if( !mapInstanceByField.containsKey( field ) )
								{
									addListenerForField( componentBehaviour, field, instance );
									mapInstanceByField.put( field, instance );
								}
							}
						}
					}

					String[] allSelect = componentBehaviour.onSelect();
					if( allSelect != null )
					{
						for( String onSelect : allSelect )
						{
							if( mapActionByName.containsKey( onSelect ) )
							{
								// valid action
								mapAnnotationByObject.put( field.get( instance ), componentBehaviour );
								mapWindowOnSelectFieldByActionName.get( onSelect ).add( field.get( instance ) );
								if( !mapInstanceByField.containsKey( field ) )
								{
									addListenerForField( componentBehaviour, field, instance );
									mapInstanceByField.put( field, instance );
								}
							}
						}
					}
				}

				LeafObject leafObject = field.getAnnotation( LeafObject.class );
				if( leafObject != null )
				{
					String[] useWith = leafObject.useWith();
					if( useWith != null )
					{
						for( String current : useWith )
						{
							if( mapActionByName.containsKey( current ) )
							{
								// valid action
								mapLeafObjectByActionName.put( current, field );
								mapInstanceByField.put( field, instance );
							}
						}
					}
				}
			}
		} catch( IllegalAccessException exception )
		{
			exception.printStackTrace( System.out );
		}
	}

	private void loadFields()
	{
		Field[] allFields = this.getClass().getDeclaredFields();
		if( allFields != null )
		{
			loadFields( allFields, this );

		}
		allFields = logicController.getClass().getDeclaredFields();
		if( allFields != null )
		{
			loadFields( allFields, logicController );

		}
		for( JPanel panel : subPanels )
		{
			allFields = panel.getClass().getDeclaredFields();
			if( allFields != null )
			{
				loadFields( allFields, panel );
			}
		}
	}

	private void loadWindowMethods( Method[] windowMethods, Object instance )
	{
		for( Method method : windowMethods )
		{
			LeafUIActionBinding actionBinding = method.getAnnotation( LeafUIActionBinding.class );
			if( actionBinding != null )
			{
				String[] actions = actionBinding.action();
				for( String actionName : actions )
				{
					if( mapActionByName.containsKey( actionName ) )
					{
						// valid action
						mapWindowMethodsByActionName.get( actionName ).add( method );

						mapAnnotationByObject.put( method, actionBinding );
						mapInstanceByMethod.put( method, instance );
					}
				}
			}
		}
	}

	private void loadLogicMethods()
	{
		Method[] allLogicMethods = this.logicController.getClass().getDeclaredMethods();
		if( allLogicMethods != null )
		{
			for( Method method : allLogicMethods )
			{
				LeafLogicActionBinding actionBinding = method.getAnnotation( LeafLogicActionBinding.class );
				if( actionBinding != null )
				{
					String[] actions = actionBinding.actions();
					if( actions != null )
					{
						for( String actionName : actions )
						{
							if( mapActionByName.containsKey( actionName ) )
							{
								// valid action
								mapAnnotationByObject.put( method, actionBinding );
								mapLogicMethodByActionName.put( actionName, method );
								mapInstanceByMethod.put( method, logicController );
							}
						}
					}
				}
			}
		}
	}

	private void loadMethods()
	{
		loadLogicMethods();

		Method[] allWindowMethods = this.getClass().getDeclaredMethods();
		if( allWindowMethods != null )
		{
			loadWindowMethods( allWindowMethods, this );
		}

		for( JPanel panel : subPanels )
		{
			allWindowMethods = panel.getClass().getDeclaredMethods();
			if( allWindowMethods != null )
			{
				loadWindowMethods( allWindowMethods, panel );
			}
		}

	}

	private Object getObjectForAction( String actionName )
	{
		Object result = null;
		Field field = mapLeafObjectByActionName.get( actionName );
		if( field != null )
		{
			Object instance = mapInstanceByField.get( field );
			if( instance != null )
			{
				try
				{
					result = field.get( instance );
				} catch( IllegalArgumentException e )
				{
					e.printStackTrace();
				} catch( IllegalAccessException e )
				{
					e.printStackTrace();
				}
			}
		}
		return result;
	}

	public void runPendingActions()
	{
		while( listRunLater.size() > 0 )
		{
			Pair<String, Object > p = listRunLater.poll();
			runAction( p.getCar(), p.getCdr() );
		}
	}

	/**
	 * Executes given action
	 */
	private void runGivenAction( String actionName, Object argument )
	{
		System.out.println( "Running: " + actionName );
		try
		{
			this.setCursor( Cursor.getPredefinedCursor( Cursor.WAIT_CURSOR ) );
			if( actionName != null && mapActionByName.containsKey( actionName ) )
			{
				Action action = mapActionByName.get( actionName );
				if( action.isSave() )
				{
					Object windowArgument = getObjectForAction( actionName );
					if( windowArgument == null )
					{
						windowArgument = argument;
					}
					Object logicArgument = windowArgument;
					for( Method currentWindowMethod : mapWindowMethodsByActionName.get( actionName ) )
					{
						Object currentLogicArgument = runWindowMethod( currentWindowMethod, windowArgument != null ? windowArgument : argument );
						logicArgument = logicArgument == null ? currentLogicArgument : logicArgument;
					}
					runLogicMethod( mapLogicMethodByActionName.get( actionName ), logicArgument );
				}
				else
				{
					Object windowArgument = runLogicMethod( mapLogicMethodByActionName.get( actionName ), argument );
					for( Method currentWindowMethod : mapWindowMethodsByActionName.get( actionName ) )
					{
						runWindowMethod( currentWindowMethod, windowArgument != null ? windowArgument : argument );
					}
				}
			}
		} finally
		{
			this.setCursor( Cursor.getDefaultCursor() );
		}
	}

	private Object runLogicMethod( Method logicMethod, Object argument ) throws LeafRuntimeException
	{
		Object result = null;
		try
		{
			if( logicMethod != null )
			{
				if( logicMethod.getParameterTypes().length > 0 )
				{
					result = logicMethod.invoke( logicController, argument );
				}
				else
				{
					result = logicMethod.invoke( logicController );
				}
			}
		} catch( IllegalArgumentException e )
		{
			System.out.println("Error in: " + logicMethod.getName() );
			System.out.println("Got: " + argument + " expected: " + (logicMethod.getParameterTypes().length > 0 ? logicMethod.getParameterTypes()[0].getCanonicalName() : "(nothing)"));
			e.printStackTrace( System.out );
		} catch( IllegalAccessException e )
		{
			e.printStackTrace( System.out );
		} catch( InvocationTargetException e )
		{
			if( e.getCause() instanceof LeafRuntimeException )
			{
				throw (LeafRuntimeException) e.getCause();
			}
			else
			{
				e.printStackTrace( System.out );
			}
		}
		return result;
	}

	private Object runWindowMethod( Method windowMethod, Object argument ) throws LeafRuntimeException
	{
		Object result = null;
		try
		{
			if( windowMethod != null )
			{
				if( windowMethod.getParameterTypes().length > 0 )
				{
					result = windowMethod.invoke( mapInstanceByMethod.get( windowMethod ), argument );
				}
				else
				{
					result = windowMethod.invoke( mapInstanceByMethod.get( windowMethod ) );
				}
			}
		} catch( IllegalArgumentException e )
		{
			System.out.println("Error in: " + windowMethod.getName() );
			System.out.println("Got: " + argument + " expected: " + (windowMethod.getParameterTypes().length > 0 ? windowMethod.getParameterTypes()[0].getCanonicalName() : "(nothing)"));
			e.printStackTrace( System.out );
		} catch( IllegalAccessException e )
		{
			e.printStackTrace( System.out );
		} catch( InvocationTargetException e )
		{
			if( e.getCause() instanceof LeafRuntimeException )
			{
				throw (LeafRuntimeException) e.getCause();
			}
			else
			{
				e.printStackTrace( System.out );
			}
		}
		return result;
	}

	private void addListenerForField( ActionActivation annotation, Field field, Object instance )
	{
		if( instance instanceof JFrame || instance instanceof JPanel )
		{
			try
			{
				Object value = field.get( instance );
				if( value instanceof BaseTable )
				{
					((BaseTable) value).getSelectionModel().addListSelectionListener( this );
				}
				else if( value instanceof JTree )
				{
					((JTree) value).addTreeSelectionListener( this );
				}
				else if( value instanceof JButton )
				{
					((JButton) value).addActionListener( this );
				}
				else if( value instanceof LeafInputField )
				{
					((LeafInputField) value).addPropertyChangeListener( this );
				}
			} catch( IllegalAccessException e )
			{
				e.printStackTrace( System.out );
			} catch( NullPointerException e )
			{
				e.printStackTrace( System.out );
			}
		}
	}

	private Object getArgumentListSelectionEvent( String actionName, ListSelectionEvent event )
	{
		Object source = event.getSource();
		List<Object> allComponents = mapWindowOnSelectFieldByActionName.get( actionName );
		for( Object component : allComponents )
		{
			if( component instanceof BaseTable && ((BaseTable) component).getSelectionModel().equals( source ) )
			{
				int [] indexes = ((BaseTable) component).getSelectedRows();
				if( indexes != null && indexes.length > 0 )
				{
					TableModel model = ((BaseTable) component).getModel();
					if( model instanceof VectorTableModel )
					{
						if(indexes.length == 1 && indexes[0] > -1)
						{
							return ((ColumnizedMappable) ((VectorTableModel) model).getRowAt( indexes[0] )).getID();
						}
						else
						{
							List<Object> allSelected = new ArrayList<Object>();
							for(int i = 0; i < indexes.length; ++i)
							{
								allSelected.add( ((ColumnizedMappable) ((VectorTableModel) model).getRowAt( indexes[0] )).getID() );
							}
							return allSelected;
						}
					}
					else if( model instanceof LeafTableModel )
					{
						if(indexes.length == 1 && indexes[0] > -1)
						{
							return ((LeafTableModel) model).getKey( indexes[0] );
						}
						else
						{
							List<Object> allSelected = new ArrayList<Object>();
							for(int i = 0; i < indexes.length; ++i)
							{
								allSelected.add( ((LeafTableModel) model).getKey( indexes[0] ));
							}
							return allSelected;
						}
					}
				}
			}
		}
		return null;
	}

	private List<String> getActionListSelectionEvent( ListSelectionEvent event )
	{
		List<String> result = new ArrayList<String>();
		if( event.getSource() instanceof DefaultListSelectionModel )
		{
			DefaultListSelectionModel model = (DefaultListSelectionModel) event.getSource();
			BaseTable table = null;
			for( List<Object> allComponents : mapWindowOnSelectFieldByActionName.values() )
			{
				// for each registered table
				for( Object component : allComponents )
				{
					if( component instanceof BaseTable && ((BaseTable) component).getSelectionModel().equals( model ) )
					{
						table = (BaseTable) component;
					}
					if( table != null )
					{
						break;
					}
				}
				if( table != null )
				{
					break;
				}
			}
			Annotation an = mapAnnotationByObject.get( table );
			if( an != null && an instanceof ActionActivation )
			{
				String[] actions = ((ActionActivation) an).onSelect();
				for( String actionName : actions )
				{
					result.add( actionName );
				}
			}
		}
		return result;
	}

	// returns selected node
	private Object getArgumentTreeSelectionEvent( String actionName, TreeSelectionEvent event )
	{
		List<Object> components = mapWindowOnSelectFieldByActionName.get( actionName );
		for( Object component : components )
		{
			if( component instanceof JTree && event.getPath() != null )
			{
				Object[] nodes = event.getPath().getPath();
				if( nodes != null && nodes.length > 0 )
				{
					return nodes[nodes.length - 1];
				}
			}
		}
		return null;
	}

	private List<String> getActionTreeSelectionEvent( TreeSelectionEvent event )
	{
		List<String> result = new ArrayList<String>();
		Annotation an = mapAnnotationByObject.get( event.getSource() );
		if( an != null && an instanceof ActionActivation )
		{
			String[] actions = ((ActionActivation) an).onSelect();
			for( String actionName : actions )
			{
				result.add( actionName );
			}
		}
		return result;
	}

	private List<String> getActionActionEvent( ActionEvent event )
	{
		List<String> result = new ArrayList<String>();
		Annotation an = mapAnnotationByObject.get( event.getSource() );
		if( an != null && an instanceof ActionActivation )
		{
			String[] actions = ((ActionActivation) an).onSelect();
			for( String actionName : actions )
			{
				result.add( actionName );
			}
		}
		return result;
	}

	private List<String> getActionsForPropertyChangeEvent( PropertyChangeEvent evt )
	{
		List<String> result = new ArrayList<String>();
		Annotation an = mapAnnotationByObject.get( evt.getSource() );
		if( an != null )
		{
			if( an instanceof ActionActivation )
			{
				if( evt.getSource() instanceof LeafInputField )
				{
					if( LeafInputField.PROPERTY_CHANGED_CONSTANT.equals( evt.getPropertyName() ) )
					{
						String[] actions = ((ActionActivation) an).onChange();
						for( String actionName : actions )
						{
							result.add( actionName );
						}
					}
				}
			}
		}
		return result;
	}

	@Override
	public void valueChanged( TreeSelectionEvent event )
	{
		List<String> actions = getActionTreeSelectionEvent( event );
		for( String action : actions )
		{
			Object argument = getArgumentTreeSelectionEvent( action, event );
			if( !runAction( action, argument ) )
			{
				break;
			}
		}
	}

	/**
	 * Listens to ListSelectionEvents
	 */
	@Override
	public void valueChanged( ListSelectionEvent event )
	{
		if( !event.getValueIsAdjusting() )
		{
			List<String> actionNames = getActionListSelectionEvent( event );
			for( String action : actionNames )
			{
				Object argument = getArgumentListSelectionEvent( action, event );
				if( !runAction( action, argument ) )
				{
					break;
				}
			}
		}
	}

	@Override
	public void actionPerformed( ActionEvent event )
	{
		List<String> actionNames = getActionActionEvent( event );
		if( actionNames.size() > 0 )
		{
			for( int i = 1; i < actionNames.size(); ++i )
			{
				runActionLater( actionNames.get( i ) );
			}
			runAction( actionNames.get( 0 ) );
		}
	}

	@Override
	public void propertyChange( PropertyChangeEvent evt )
	{
		List<String> actionNames = getActionsForPropertyChangeEvent( evt );
		if( actionNames.size() > 0 )
		{
			for( int i = 1; i < actionNames.size(); ++i )
			{
				runActionLater( actionNames.get( i ) );
			}
			runAction( actionNames.get( 0 ) );
		}
	}

}
