package bk;

import java.util.ArrayList;

import javax.swing.JTree;

import animation.AlgorithmStage;
import animation.BackwardAction;
import animation.PseudoCodeNode;
import bk.ExtremalLayoutCalc.LayoutType;
import graph.LayeredGraphNode;

/**
 * The stage of compacting the layout.
 * @author kolja
 *
 */
public class Compaction implements AlgorithmStage{

	private enum CompactionState
	{
		PLACE_BLOCKS,
		APPLY_SHIFT
	}
	
	private class StackFrame
	{
		public LayeredGraphNode v;
		public LayeredGraphNode u;
		public LayeredGraphNode w;
	}
	
	private CompactionState state;
	private LayeredGraphNode graph;
	private int vIndex;
	
	private ArrayList< StackFrame > stack; // TODO: evtl richtigen "Stack" benutzen
	private ArrayList< BackwardAction > actions; // TODO: evtl richtigen "Stack" benutzen
	private LayoutType layout;
	private PseudoCodeNode placeNode;
    private PseudoCodeNode placeLoopNode;
    private PseudoCodeNode applyNode;
    private PseudoCodeNode applyLoopNode;
    private boolean inside;
	
	
	public Compaction( LayeredGraphNode graph, LayoutType layout )
	{
	    this.layout = layout;
		this.graph = graph;
		state = CompactionState.PLACE_BLOCKS;
		stack = new ArrayList<>(); // der call-stack des rekursiven algorithmus
		vIndex = 0;
		actions = new ArrayList<>();
		inside = false;
	}
	
	/**
	 * calculates the minimum spacing needed between two left borders of nodes.
	 * @return the spacing
	 */
	public double calcSpacing()
	{
		double max = 0;
		for( LayeredGraphNode n : graph.getContainedNodes() )
			max = Math.max( max, n.getWidth( layout ) );
		//return max + 25;
		return max + 5;
	}
	
	private LayeredGraphNode getNodeFromIndex( int index )
	{
	    for( ArrayList< LayeredGraphNode > l : graph.getContainedLayers() )
	    {
	        if( index >= l.size() )
	            index -= l.size();
	        else
	            return l.get( index );
	    }
	    return null;
	}
	
	@Override
	public StageStatus forwardStep() {
		int acSize = actions.size();
		if( state == CompactionState.PLACE_BLOCKS ) // blöcke platzieren
		{
		    inside = true;
		    placeNode.setSelected( true );
		    placeLoopNode.setSelected( true );
			if( stack.size() == 0 ) // äußere schleife, placeblocks bisher nicht aufgerufen
			{
				ArrayList< LayeredGraphNode > nodes = graph.getContainedNodes();
				boolean found = false; // knoten mit v = root[v] gefunden?
				int oldVIndex = vIndex; // nötig für "undo"
				
				// suche knoten mit v = root[v] und undefiniertem x-Wert
				for( ; vIndex < nodes.size(); vIndex++ )
				{
					if( getNodeFromIndex( vIndex ).isXUndefined( layout ) && getNodeFromIndex( vIndex ) == getNodeFromIndex( vIndex ).getRoot( layout ) )
					{
	                    found = true;
						break;
					}
				}
				
				// kein knoten gefunden
				if( !found )
				{
				    // wechsele in die phase des Blöckeshiftens
		            placeNode.setSelected( false );
		            placeLoopNode.setSelected( false );
		            applyNode.setSelected( true );
		            applyLoopNode.setSelected( true );
					state = CompactionState.APPLY_SHIFT;
					inside = false;
					vIndex = 0;
					actions.add( 0, ()-> {
	                    applyNode.setSelected( false );
	                    applyLoopNode.setSelected( false );
                        placeNode.setSelected( true );
                        placeLoopNode.setSelected( true );
						vIndex = oldVIndex;
						inside = false;
						state = CompactionState.PLACE_BLOCKS;
					} );
				}
				else // Knoten gefunden
				{
					StackFrame f = new StackFrame(); // enthält lokale variablen
					f.v = getNodeFromIndex( vIndex );
					double oldX = f.v.getX( layout ); // nötig für "undo"
					f.v.setX( 0, true, layout );
					f.w = f.v;
                    f.w.setSelected( layout ); // zeige knoten als aktiven knoten an
                    System.out.println( "call place_block( " + f.v + " )" );
					stack.add( 0, f );
					
					// die "undo"-action
					actions.add( 0, ()-> {
					    inside = true;
						stack.get( 0 ).v.setX( oldX, false, layout );
						stack.get( 0 ).v.setSelected( layout );
						stack.remove( 0 );
						vIndex = oldVIndex;
						state = CompactionState.PLACE_BLOCKS;
					});
				}
			}
			else // zurzeit innerhalb einer placeblock methode
			{
				StackFrame sf = stack.get( 0 );
				if( sf.u == null ) // zu beginn der placeblock methode
				{
                    int posW = graph.getContainedLayers().get( sf.w.getLayer() ).indexOf( sf.w );
					if( (posW >= 1 && (layout == LayoutType.BOTTOM_TOP_LEFT || layout == LayoutType.TOP_BOTTOM_LEFT)) || (posW < graph.getContainedLayers().get( sf.w.getLayer() ).size() - 1 && (layout == LayoutType.BOTTOM_TOP_RIGHT || layout == LayoutType.TOP_BOTTOM_RIGHT)) ) // if pos[w] > 1"
					{
					    int offset = -1;
					    if( layout == LayoutType.BOTTOM_TOP_RIGHT || layout == LayoutType.TOP_BOTTOM_RIGHT )
					        offset = 1;
                        sf.u = graph.getContainedLayers().get( sf.w.getLayer() ).get( posW + offset ).getRoot( layout );
						
						if( sf.u.isXUndefined( layout ) ) // nötig placeblock aufzurufen?
						{// ja
							StackFrame nsf = new StackFrame(); // enthält lokale variablen
							nsf.v = sf.u;
							double oldX = nsf.v.getX( layout ); // nötig für "undo"
							nsf.v.setX( 0, true, layout );
							nsf.w = nsf.v;
                            nsf.w.setSelected( layout ); // zeige knoten als aktiven knoten an
		                    System.out.println( "call place_block( " + nsf.v + " )" );
							stack.add( 0, nsf );
		                    
		                    // die "undo"-action
							actions.add( 0, ()-> {
							    inside = true;
								stack.get( 0 ).v.setX( oldX, false, layout );
								stack.get( 0 ).v.setSelected( layout );
								stack.remove( 0 );
								stack.get( 0 ).u = null;
							});
						}
						else // nein
						{
						    // tue nix
							sf.w.setSelected( layout );
							actions.add( 0, ()-> {
                                inside = true;
								stack.get( 0 ).u = null;
							});
						}
					}
					else 
					{ // w = align[w]
						LayeredGraphNode oldW = sf.w;
						sf.w = sf.w.getAlignedTo( layout );
						sf.w.setSelected( layout );
						if( sf.w == sf.v ) // schleifenabbruchbedingung
						{ //abbrechen, placeblock beendet
                            System.out.println( "return place_block( " + sf.v + " )" );
							stack.remove( 0 );
							actions.add( 0, ()-> {
                                inside = true;
								stack.add( 0, sf );
								sf.w = oldW;
                                sf.w.setSelected( layout );
							});
						}
						else
						{ //nur "undo aktion" hinzufügen
							actions.add( 0, ()-> {
                                inside = true;
								sf.w = oldW;
								sf.w.setSelected( layout );
							});
						}
					}	
				}
				else // ein "placeBlock(u)" aufruf hat gerade returned
				{
				    // alte Werte merken für undo
					LayeredGraphNode oldSink = sf.v.getSink( layout );	
					LayeredGraphNode sinkOfU = sf.u.getSink( layout );
					double oldShift = sinkOfU.getShift( layout );
					double oldX = sf.v.getX( layout );
					boolean oldDef = !sf.v.isXUndefined( layout );
					
					// v für visualisierung markieren
					sf.w.setSelected( layout );
                    
                    if( sf.v.getSink( layout ) == sf.v ) // sink[v] = v?
                        sf.v.setSink( sf.u.getSink( layout ), layout ); // sink[v] := sink[u]
                    int multiplyer = 1;
                    if( layout == LayoutType.BOTTOM_TOP_RIGHT || layout == LayoutType.TOP_BOTTOM_RIGHT )
                        multiplyer = -1;
                    
					if( sf.v.getSink( layout ) != sf.u.getSink( layout ) ) // sink[v] != sink [u]?
						sf.u.getSink( layout ).setShift( // shift[sink[u]] =
						  Math.min( sf.u.getSink( layout ).getShift( layout ),  // min(shift[sink[u]]
						          multiplyer * (Math.abs(sf.v.getX( layout )) - Math.abs(sf.u.getX( layout )) - calcSpacing()) ), layout ); // y_v - y_u - s
					else
					    // y_v = max {y_v, y_u + s}
						sf.v.setX( multiplyer * Math.max( Math.abs( sf.v.getX( layout ) ), Math.abs( sf.u.getX( layout ) ) + calcSpacing() ), true, layout );
					

                    // alte Werte merken für undo
					LayeredGraphNode oldW = sf.w;
                    LayeredGraphNode oldU = sf.u;
                    
					sf.w = sf.w.getAlignedTo( layout ); // w = align[w]
					sf.u = null; // u wird nächsten schleifendurchlauf neu gesetzt
					
					if( sf.w == sf.v ) // schleifenabbruchbedingung
					{ //abbrechen, placeblock beendet  
					    System.out.println( "return place_block( " + sf.v + " )" );
						stack.remove( 0 );
						actions.add( 0, ()-> {
                            inside = true;
							stack.add( 0, sf );
							stack.get( 0 ).v.setSink(  oldSink, layout );
							sinkOfU.setShift( oldShift, layout );
							sf.u = oldU;
							sf.v.setX( oldX, oldDef, layout );
							sf.w = oldW;
                            sf.w.setSelected( layout );
						});
					}
					else
					{ //nur "undo aktion" hinzufügen
						actions.add( 0, ()-> {
                            inside = true;
							stack.get( 0 ).v.setSink(  oldSink, layout );
							sinkOfU.setShift( oldShift, layout );
							sf.u = oldU;
							sf.v.setX( oldX, oldDef, layout );
							sf.w = oldW;
                            sf.w.setSelected( layout );
						});
					}
				}
			}
		}
		else if( state == CompactionState.APPLY_SHIFT )// "Compute absolute coordinates"
		{
            inside = true;
		    if( vIndex >= graph.getContainedNodes().size() )
		    {
                inside = false;
                applyNode.setSelected( false );
                applyLoopNode.setSelected( false );
                return StageStatus.FINISHED;
		    }
			LayeredGraphNode v = graph.getContainedNodes().get( vIndex );
			double oldX = v.getX( layout );
			boolean oldDef = !v.isXUndefined( layout );
			
			v.setSelected( layout );
			v.setX( v.getRoot( layout ).getX( layout ), true, layout ); // y_v = y_root[v]
			if( v == v.getRoot( layout ) && v.getSink( layout ).getShift( layout ) < Double.POSITIVE_INFINITY )
				v.setX( v.getX( layout ) + v.getSink( layout ).getShift( layout ), true, layout );
			actions.add( 0, ()-> {
                inside = true;
                applyNode.setSelected( true );
                applyLoopNode.setSelected( true );
				v.setX( oldX, oldDef, layout );
				v.setSelected( layout );
				vIndex--;
			} );
			vIndex++;
			if( vIndex >= graph.getContainedNodes().size() )
			{
                applyNode.setSelected( false );
                applyLoopNode.setSelected( false );
				return StageStatus.FINISHED;
			}
		}
		if( actions.size() != acSize + 1 )
			System.out.println( "ERROR" );
		return StageStatus.UNFINISHED;
	}

	@Override
	public StageStatus backwardStep() {
		if( actions.size() == 0 )
		{
            inside = false;
            placeNode.setSelected( false );
            placeLoopNode.setSelected( false );
			return StageStatus.FINISHED;
		}
		actions.get( 0 ).reverse();
		actions.remove( 0 );
		return StageStatus.UNFINISHED;
	}

    @Override
    public PseudoCodeNode createPseudocodeTree( JTree tree ) {
        PseudoCodeNode root = new PseudoCodeNode( "Horizontal compaction", tree );
        placeNode = new PseudoCodeNode( "Root coordinates relative to sink", tree );
        placeLoopNode = new PseudoCodeNode( "Loop through root nodes...", tree );
        placeNode.add( placeLoopNode );
        applyNode = new PseudoCodeNode( "Absolute coordinates", tree );
        applyLoopNode = new PseudoCodeNode( "Loop through all nodes...", tree );
        applyNode.add( applyLoopNode );
        root.add( placeNode );
        root.add( applyNode );
        return root;
    }

    @Override
    public StageStatus forwardStepOver() {
        if( !inside )
        {
            CompactionState oldState = state;
            StageStatus stage = StageStatus.UNFINISHED;
            while( state == oldState && stage != StageStatus.FINISHED )
                stage = forwardStep();
            return stage;
        }
        else
            return forwardStep();
    }

    @Override
    public StageStatus forwardStepOut() {
        if( !inside )
        {
            StageStatus status = StageStatus.UNFINISHED;
            while( status != StageStatus.FINISHED )
                status = forwardStep();
            return status;
        }
        else
        {
            CompactionState oldState = state;
            StageStatus stage = StageStatus.UNFINISHED;
            while( state == oldState && stage != StageStatus.FINISHED )
                stage = forwardStep();
            return stage;
        }
    }

    @Override
    public StageStatus backwardStepOver() {
        if( !inside )
        {
            CompactionState oldState = state;
            StageStatus stage = StageStatus.UNFINISHED;
            while( state == oldState && stage != StageStatus.FINISHED )
                stage = backwardStep();
            return stage;
        }
        else
            return backwardStep();
    }

    @Override
    public StageStatus backwardStepOut() {
        if( !inside )
        {
            StageStatus status = StageStatus.UNFINISHED;
            while( status != StageStatus.FINISHED )
                status = backwardStep();
            return status;
        }
        else
        {
            CompactionState oldState = state;
            StageStatus stage = StageStatus.UNFINISHED;
            while( state == oldState && stage != StageStatus.FINISHED )
                stage = backwardStep();
            return stage;
        }
    }
}
