package animation;

import javax.swing.JTree;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreePath;

import animation.StackFrame.FrameType;

/**
 * represents a line of pseudocode
 * @author kolja
 *
 */
public class PseudoCodeNode extends DefaultMutableTreeNode {

    public static enum CodeAction
    {
        SKIP,
        STOP,
        CONTINUE
    }

    public static enum CodeStatus
    {
        UNFINISHED,
        BREAKPOINT,
        FINISHED
    }
    
    private static final long serialVersionUID = 1L;

    private static int nextNodeId = 0;
    private final int nodeId;
    private AnimationController controller;
    private AnimatedAlgorithm alg;
    private JTree tree;
    private CodeLine code;
    private boolean selected;
    private boolean breakPoint;
    private boolean function;

    private int currentCodeLine; // next forward code line
    
    public PseudoCodeNode( String description, JTree tree, CodeLine line, AnimatedAlgorithm alg )
    {
        super( description );
        this.alg = alg;
        synchronized( PseudoCodeNode.class )
        {
        	nodeId = nextNodeId++;
        }
        selected = false;
        this.tree = tree;
        breakPoint = false;
        currentCodeLine = -1;
        code = line;
        function = false;
    }
    
    public void setController( AnimationController c )
    {
        if( children != null )
        {
            for( Object ch : children )
            {
                ((PseudoCodeNode)ch).setController( c );
            }
        }
        controller = c;
    }
    
    public void writeToStack( Memory m )
    {
    	if( m.isDefined( "_pos" + nodeId, false ) )
    		throw new IllegalStateException( "variable _pos" + nodeId + " should not exist in current stack frame, but it exists" );
    	if( m.isDefined( "_func" + nodeId, false ) )
    		throw new IllegalStateException( "variable _func" + nodeId + " should not exist in current stack frame, but it exists" );
    	m.declare( "_pos" + nodeId, currentCodeLine, false );
    	m.declare( "_func" + nodeId, function, false );
    	currentCodeLine = -1;
    	function = false;
    	setSelected( false );
    	if( children == null )
    		return;
    	for( Object c : children )
    		((PseudoCodeNode)c).writeToStack( m );
    }
    
    public void loadFromStack( Memory m )
    {
    	if( !m.isDefined( "_pos" + nodeId, false ) )
    		throw new IllegalStateException( "variable _pos" + nodeId + "should exist in current stack frame, but it is undefined" );
    	if( !m.isDefined( "_func" + nodeId, false ) )
    		throw new IllegalStateException( "variable _func" + nodeId + "should exist in current stack frame, but it is undefined" );
    	currentCodeLine = m.read( "_pos" + nodeId, false );
    	function = m.read( "_func" + nodeId, false );
    	m.undeclare( "_pos" + nodeId, false );
    	m.undeclare( "_func" + nodeId, false );
    	if( children == null )
    		return;
    	for( Object c : children )
    		((PseudoCodeNode)c).loadFromStack( m );
    }
    
    @Override
    public void add( MutableTreeNode node )
    {
        ((PseudoCodeNode)node).setController( controller );
        super.add( node );
    }
    
    /**
     * 
     * @return the tree that this node belongs to
     */
    public JTree getTree()
    {
        return tree;
    }
    
    /**
     * checks if this node should be highlighted
     * @return true if it should, false otherwise
     */
    public boolean isSelected()
    {
        return selected;
    }
    
    /**
     * checks if one of the subnodes of this node is selected.
     * @return true if one is, false otherwise
     */
    public boolean hasSelectedSubnode()
    {
    	if( children != null )
    	{
	    	for( Object ch : children )
	        {
	            if( ((PseudoCodeNode)ch).isSelected() || ((PseudoCodeNode)ch).hasSelectedSubnode() )
	            	return true;
	        }
    	}
    	return false;
    }
    
    private void expandToRoot()
    {
    	if( parent != null )
    		((PseudoCodeNode)parent).expandToRoot();
        tree.expandPath( new TreePath( this.getPath() ) );
    }
    
    /**
     * highlight this line of pseudocode.
     * should be called when the line is entered, as it triggers breakpoints
     * @param selected whether to select or deselect this line
     * @return false iff a breakpoint was reached and the node was set to be selected.
     */
    public CodeAction setSelected( boolean selected )
    {
        if( selected && breakPoint )
            controller.setContinuous( false );
        this.selected = selected;
        if( selected )
        {
            if( controller == null || controller.getStepOption() != 1 || breakPoint )
            	expandToRoot();
        }
        else
        {
            if( controller == null || controller.getStepOption() != 1 )
                tree.collapsePath( new TreePath( this.getPath() ) );
        }
        if( breakPoint && selected )
            return CodeAction.STOP; // Breakpoint
        if( controller != null && controller.getStepOption() == 1 && !tree.isVisible( new TreePath( this.getPath() ) ) )
            return CodeAction.SKIP; // Step would be to detailed
        return CodeAction.CONTINUE; // Normal
    }
    
    /**
     * set a breakpoint at this line of code
     * @param breakPoint whether there should be a breakpoint or node
     */
    public void setBreakPoint( boolean breakPoint )
    {
        this.breakPoint = breakPoint;
    }
    
    /**
     * check if there is a breakpoint set at this line of code
     * @return true, iff there is a breakpoint
     */
    public boolean hasBreakPoint()
    {
        return breakPoint;
    }
    
    private PseudoCodeNode getForwardNode()
    {
        if( currentCodeLine == -1 )
            return this;
        if( children != null && children.size() > currentCodeLine )
            return ((PseudoCodeNode)children.get( currentCodeLine )).getForwardNode();
        return this;
    }
    
    private PseudoCodeNode getBackwardNode()
    {
        if( currentCodeLine - 1 <= -1 )
            return this;
        if( children != null && children.size() >= currentCodeLine - 1 )
            return ((PseudoCodeNode)children.get( currentCodeLine - 1 )).getBackwardNode();
        return this;
    }
    
    private CodeStatus stepInto( Memory m )
    {
        currentCodeLine = 0;
        if( children == null || children.size() == 0 )
        {
            setSelected( false );
            return CodeStatus.FINISHED;
        }
        else
        {
            setSelected( false );
            return selectChild( 0, m );
        }
    }
    
    /**
     * Perform one atomic step of the algorithm. Stops at the end of the program.
     * @return whether the whole stage is finished (afterwards).
     * For example if all steps are finished, then {@code FINISHED} is returned.
     */
    public CodeStatus forwardStep( Memory m )
    {
        if( currentCodeLine == -1 )
        {
			if( !m.isDefined( "node_" + nodeId + "_call", false ) )
			{
				StackFrame tmp = null;
				if( function )
				{
					tmp = m.removeFrame();
					m.addFrame( tmp ); // a little abuse of the stack to get direct access of the current frame before it could be destroyed by the user defined function
				}
				ControlFlow cf = code.runForward( m );
	            switch( cf.getStatus() )
	            {
	            case ControlFlow.STEP_INTO:
	            	writeToStack( m );
	            	function = true;
	                switch( stepInto( m ) )
	                {
					case BREAKPOINT:
						return CodeStatus.BREAKPOINT;
					case FINISHED:
						currentCodeLine = -1;
	                    switch( setSelected( true ) )
	                    {
						case SKIP:
							return forwardStepOverIntern( m );
						case STOP:
							return CodeStatus.BREAKPOINT;
						default:
							break;
	                    }
					case UNFINISHED:
						return CodeStatus.UNFINISHED;
	                }
	            case ControlFlow.STEP_OVER:
	            	if( function )
	            	{
	            		m.addFrame( tmp ); // add old stack frame
		            	loadFromStack( m ); // load stored variables
	                	m.removeFrame(); // remove the stack frame
	            	}
	                return CodeStatus.FINISHED;
	            case ControlFlow.CALL:
	    			alg.addActiveFunction( cf.getFunction() );
	    			m.declare( "node_" + nodeId + "_call", cf.getFunction(), false );
	    			setSelected( false );
	    			m.declare( "callback", this, true );
	    			switch( cf.getFunction().setSelected( true ) )
	    			{
					case CONTINUE:
						break;
					case SKIP:
						switch( cf.getFunction().forwardStepOverIntern( m ) )
	        			{
						case BREAKPOINT:
							return CodeStatus.BREAKPOINT;
						case FINISHED:
							setSelected( true );
						case UNFINISHED:
							return CodeStatus.UNFINISHED;
	        			}
						break;
					case STOP:
						return CodeStatus.BREAKPOINT;
	    			}
	    			return CodeStatus.UNFINISHED;
	            }
			}
			else
			{
				m.undeclare( "node_" + nodeId + "_call", false );
				return CodeStatus.FINISHED;
			}
        }
        else
        {
            if( children == null || children.size() <= currentCodeLine )
            {
            	throw new IllegalStateException( "Some wired stuff is going on" );
            }
            switch( ( (PseudoCodeNode)children.get( currentCodeLine ) ).forwardStep( m ) )
            {
            case BREAKPOINT:
                return CodeStatus.BREAKPOINT;
            case FINISHED:
                ( (PseudoCodeNode)children.get( currentCodeLine ) ).setSelected( false );
                currentCodeLine++;
                if( children.size() <= currentCodeLine )
                {
                    currentCodeLine = -1;
                    switch( setSelected( true ) )
                    {
					case SKIP:
						return forwardStepOverIntern( m );
					case STOP:
						return CodeStatus.BREAKPOINT;
					default:
						break;
                    }
                }
                else
                {
                    CodeStatus status = selectChild( currentCodeLine, m );
                    if( status == CodeStatus.FINISHED )
                    {
                        currentCodeLine = -1;
                        status = CodeStatus.UNFINISHED;
                        switch( setSelected( true ) )
                        {
    					case SKIP:
    						return forwardStepOverIntern( m );
    					case STOP:
    						return CodeStatus.BREAKPOINT;
    					default:
    						break;
                        }
                    }
                    return status;
                }
            case UNFINISHED:
                return CodeStatus.UNFINISHED;
            }
        }
        return CodeStatus.UNFINISHED;
    }
    
    private CodeStatus selectChild( int index, Memory m  )
    {
        switch( ( (PseudoCodeNode)children.get( index ) ).setSelected( true ) )
        {
        case CONTINUE:
            return CodeStatus.UNFINISHED;
        case SKIP:
            switch( ( (PseudoCodeNode)children.get( index ) ).forwardStepOverIntern( m ) )
            {
            case BREAKPOINT:
                return CodeStatus.BREAKPOINT;
            case FINISHED:
                ( (PseudoCodeNode)children.get( index ) ).setSelected( false );
                currentCodeLine++;
                if( children == null || currentCodeLine >= children.size() )
                {
                    return CodeStatus.FINISHED;
                }
                else
                {
                    return selectChild( currentCodeLine, m );
                }
            case UNFINISHED:
            	throw new IllegalStateException( "Skipping a node returned UNFINISHED" );
            }
        case STOP:
            return CodeStatus.BREAKPOINT;
        }
        return CodeStatus.UNFINISHED;
    }

    /**
     * Perform steps until the next line of code on the same level of indentation as this line
     * is reached. Stops at the end of the program.
     * @return whether the whole stage is finished (afterwards).
     * For example if all steps are finished, then {@code FINISHED} is returned.
     */
    public CodeStatus forwardStepOver( Memory m )
    {
        return getForwardNode().forwardStepOverIntern( m );
    }
    
    private CodeStatus forwardStepOverIntern( Memory m )
    {
        CodeStatus status = null;
        do {
            status = forwardStep( m );
        } while( status == CodeStatus.UNFINISHED );
        return status;
    }

    /**
     * Perform steps until the next line of code on the level of indentation above this lines 
     * level is reached. Stops at the end of the program.
     * @return whether the whole stage is finished (afterwards).
     * For example if all steps are finished, then {@code FINISHED} is returned.
     */
    public CodeStatus forwardStepOut( Memory m )
    {
        return getForwardNode().forwardStepOutIntern( m );
    }
    
    private CodeStatus forwardStepOutIntern( Memory m )
    {
        if( parent != null )
            return ((PseudoCodeNode)parent).forwardStepOverIntern( m );
        return forwardStepOverIntern( m );
    }
    
    /**
     * Undo one atomic step of the algorithm. Stops at the beginning of the program.
     * @return whether the whole stage is finished in backwards direction (afterwards).
     * For example if all steps have been reverted, then {@code FINISHED} is returned.
     */
    public CodeStatus backwardStep( Memory m )
    {
        // TODO
        return null;
    }

    /**
     * Perform backward steps until the previous line of code on the same level of indentation 
     * as this line is reached. Stops at the end of the program.
     * @return whether the whole stage is finished in backwards direction (afterwards).
     * For example if all steps have been reverted, then {@code FINISHED} is returned.
     */
    public CodeStatus backwardStepOver( Memory m )
    {
        return getBackwardNode().backwardStepOverIntern( m );
    }
    
    private CodeStatus backwardStepOverIntern( Memory m )
    {
        CodeStatus status = null;
        do {
            status = backwardStep( m );
        } while( status == CodeStatus.UNFINISHED );
        return status;
    }


    /**
     * Perform backward steps until the previous line of code on the level of indentation above 
     * this lines level is reached. Stops at the end of the program.
     * @return whether the whole stage is finished in backwards direction (afterwards).
     * For example if all steps have been reverted, then {@code FINISHED} is returned.
     */
    public CodeStatus backwardStepOut( Memory m )
    {
        return getBackwardNode().backwardStepOutIntern( m );
    }
    
    private CodeStatus backwardStepOutIntern( Memory m )
    {
        if( parent != null )
            return ((PseudoCodeNode)parent).backwardStepOver( m );
        return backwardStepOutIntern( m );
    }
}
