package Algorithms.Animated.BK;

import java.util.ArrayList;

import Algorithms.Animated.AlgorithmStage;
import Algorithms.Animated.BackwardAction;
import Algorithms.Animated.BK.ExtremalLayoutCalc.LayoutType;
import Model.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;
	
	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<>();
	}
	
	public double calcSpacing()
	{
		double max = 0;
		for( LayeredGraphNode n : graph.getContainedNodes() )
			max = Math.max( max, n.getWidth( layout ) );
		//return max + 25;
		return max;
	}
	
	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
		{
			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
					state = CompactionState.APPLY_SHIFT;
					vIndex = 0;
					actions.add( 0, ()-> {
						vIndex = oldVIndex;
						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.v.setSelected( layout ); // zeige knoten als aktiven knoten an
					f.w = f.v;
                    System.out.println( "call place_block( " + f.v + " )" );
					stack.add( 0, f );
					
					// die "undo"-action
					actions.add( 0, ()-> {
						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 ) // if pos[w] > 1"
					{

                        sf.u = graph.getContainedLayers().get( sf.w.getLayer() ).get( posW - 1 ).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.v.setSelected( layout ); // zeige knoten als aktiven knoten an
							nsf.w = nsf.v;
		                    System.out.println( "call place_block( " + nsf.v + " )" );
							stack.add( 0, nsf );
		                    
		                    // die "undo"-action
							actions.add( 0, ()-> {
								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.u.setSelected( layout ); 
							actions.add( 0, ()-> {
								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, ()-> {
								stack.add( 0, sf );
								sf.v.setSelected( layout );
								sf.w = oldW;
							});
						}
						else
						{ //nur "undo aktion" hinzufügen
							actions.add( 0, ()-> {
								sf.w = oldW;
								sf.v.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.v.setSelected( layout );
                    
                    if( sf.v.getSink( layout ) == sf.v ) // sink[v] = v?
                        sf.v.setSink( sf.u.getSink( layout ), layout ); // sink[v] := sink[u]

					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]]
						          sf.v.getX( layout ) - sf.u.getX( layout ) - calcSpacing() ), layout ); // y_v - y_u - s
					else
					    // y_v = max {y_v, y_u + s}
						sf.v.setX( Math.max( sf.v.getX( layout ), 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, ()-> {
							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.v.setSelected( layout );
							sf.w = oldW;
						});
					}
					else
					{ //nur "undo aktion" hinzufügen
						actions.add( 0, ()-> {
							stack.get( 0 ).v.setSink(  oldSink, layout );
							sinkOfU.setShift( oldShift, layout );
							sf.u = oldU;
							sf.v.setX( oldX, oldDef, layout );
							sf.v.setSelected( layout );
							sf.w = oldW;
						});
					}
				}
			}
		}
		else if( state == CompactionState.APPLY_SHIFT )// "Compute absolute coordinates"
		{
			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, ()-> {
				v.setX( oldX, oldDef, layout );
				v.setSelected( layout );
				vIndex--;
			} );
			vIndex++;
			if( vIndex >= graph.getContainedNodes().size() )
				return StageStatus.FINISHED;
		}
		if( actions.size() != acSize + 1 )
			System.out.println( "ERROR" );
		return StageStatus.UNFINISHED;
	}

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

}
