/*
 *  @(#)BreadthPathGenerator.java
 *
 * Copyright (C) 2002-2003 Matt Albrecht
 * groboclown@users.sourceforge.net
 * http://groboutils.sourceforge.net
 *
 *  Part of the GroboUtils package at:
 *  http://groboutils.sourceforge.net
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a
 *  copy of this software and associated documentation files (the "Software"),
 *  to deal in the Software without restriction, including without limitation
 *  the rights to use, copy, modify, merge, publish, distribute, sublicense,
 *  and/or sell copies of the Software, and to permit persons to whom the 
 *  Software is furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in 
 *  all copies or substantial portions of the Software. 
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL 
 *  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
 *  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
 *  DEALINGS IN THE SOFTWARE.
 */
package net.sourceforge.groboutils.mbtf.v1.engine;


import java.util.Vector;
import java.util.Stack;
import java.util.Hashtable;

import net.sourceforge.groboutils.mbtf.v1.IPath;
import net.sourceforge.groboutils.mbtf.v1.IState;
import net.sourceforge.groboutils.mbtf.v1.ITransition;
import net.sourceforge.groboutils.mbtf.v1.IPathGenerator;

import org.apache.log4j.Logger;


/**
 * Implements breadth-first path generation.
 * <P>
 * @todo       Complete the discover end-state transition paths for states.
 * @todo       Add in depth-related-terminal-node ability to add in end-state
 *             paths.  Q: how should it handle multiple end-state paths, in
 *             the case of multiple end-states?
 *
 * @author     Matt Albrecht <a href="mailto:groboclown@users.sourceforge.net">groboclown@users.sourceforge.net</a>
 * @version    $Date: 2003/02/10 22:52:26 $
 * @since      June 12, 2002
 */
public class BreadthPathGenerator implements IPathGenerator
{
    private static final Logger LOG = Logger.getLogger(
        BreadthPathGenerator.class );

    /**
     * Parsed form of an IState instance.  This is a self-populating list.
     * It will also populate the constructor hashtable with all enterable
     * states from this node.
     */
    static class InnerState
    {
        private IState state;
        private InnerTransition[] dest;
        private Vector sources = new Vector();
        private Vector endStatePaths = new Vector();
        
        public InnerState( IState s, Hashtable stateHash )
        {
            if (s == null || stateHash == null)
            {
                throw new IllegalArgumentException("no null args");
            }
            LOG.debug("Adding state '"+s.getName()+"' to inner set");
            
            this.state = s;
            
            // add ourself to the hash BEFORE parsing the transitions.
            stateHash.put( s, this );
            
            // generate the transition table using pre-generated states
            ITransition t[] = s.getTransitions();
            if (t == null)
            {
                throw new IllegalStateException("state "+s+
                    " getTransitions() returned null.");
            }
            int len = t.length;
            this.dest = new InnerTransition[ len ];
            for (int i = len; --i >= 0;)
            {
                if (t[i] == null)
                {
                    throw new IllegalStateException("Transition "+i+
                        " for state "+s.getName()+" is null.");
                }
                IState nextState = t[i].getDestinationState();
                if (nextState == null)
                {
                    throw new IllegalStateException("Transition "+i+
                        " for state "+s.getName()+
                        " has null destination state.");
                }
                
                InnerState ns = (InnerState)stateHash.get( nextState );
                if (ns == null)
                {
                    ns = new InnerState( nextState, stateHash );
                }
                LOG.debug("Adding transition "+t[i]+" from "+this+" to "+ns);
                this.dest[i] = new InnerTransition( t[i], ns, this );
                
                // we link to that state, so add ourself as a source transition
                ns.addSourceTransition( this.dest[i] );
            }
        }
        
        
        public InnerStatePath createStatePath()
        {
            return new InnerStatePath( this );
        }
        
        
        public InnerTransition[] getDestinations()
        {
            return this.dest;
        }
        
        
        public InnerTransition[] getSources()
        {
            InnerTransition[] t = new InnerTransition[ this.sources.size() ];
            this.sources.copyInto( t );
            return t;
        }
        
        
        public IState getState()
        {
            return this.state;
        }
        
        
        private void addSourceTransition( InnerTransition it )
        {
            if (it != null && !this.sources.contains( it ))
            {
                this.sources.addElement( it );
            }
        }
        
        
        public String toString()
        {
            if (getState() != null)
            {
                return getState().toString();
            }
            return "[initial state]";
        }
    }
    
    
    private static class InnerTransition
    {
        private ITransition trans;
        private InnerState nextState;
        private InnerState prevState;
        
        
        public InnerTransition( ITransition trans, InnerState next,
                InnerState prev )
        {
            this.trans = trans;
            this.nextState = next;
            this.prevState = prev;
        }
        
        
        public ITransition getTransition()
        {
            return this.trans;
        }
        
        
        public InnerState getNextState()
        {
            return this.nextState;
        }
        
        
        public InnerState getSourceState()
        {
            return this.prevState;
        }
        
        
        public String toString()
        {
            return "["+this.trans+" from "+getSourceState()+" to "+
                getNextState()+"]";
        }
    }
    
    
    /**
     * A path-generation state for a state which:
     *      1. knows how to generate all sub-paths
     *      2. keeps the current next-transition position
     */
    private static class InnerStatePath
    {
        private InnerState is;
        private InnerTransition[] trans;
        private InnerStatePath[] nextPaths;
        private int currentIndex;
        
        protected InnerStatePath( InnerState is )
        {
            this.is = is;
            
            // ensure we keep the same ordering of the transitions
            this.trans = is.getDestinations();
            if (this.trans == null)
            {
                this.trans = new InnerTransition[0];
            }
            
            reset();
        }
        
        
        // only used for the very first pseudo-node
        protected InnerStatePath( InnerState[] states )
        {
            this.is = null;
            int len = states.length;
            this.trans = new InnerTransition[ len ];
            for (int i = len; --i >= 0;)
            {
                this.trans[i] = new InnerTransition( null, states[i], null );
            }
            
            reset();
        }
        
        
        public InnerStatePath getCurrentPath()
        {
            if (this.nextPaths.length <= 0)
            {
                return null;
            }
            int index = this.currentIndex;
            // integrity assertion - should never be true
            if (index >= this.nextPaths.length)
            {
                throw new IllegalStateException(
                    "Inner index is outside expected range." );
            }
            
            if (this.nextPaths[ index ] == null)
            {
                this.nextPaths[ index ] = getCurrentTransition().
                    getNextState().createStatePath();
            }
            LOG.debug("current path index = "+(index+1)+" / "+
                this.nextPaths.length);
            return this.nextPaths[ index ];
        }
        
        
        public InnerTransition getCurrentTransition()
        {
            if (this.trans.length <= 0)
            {
                return null;
            }
            // integrity assertion - should never be true
            if (this.currentIndex >= this.trans.length)
            {
                throw new IllegalStateException(
                    "Inner index is outside expected range." );
            }
            
            return this.trans[ this.currentIndex ];
        }
        
        
        /**
         * Logic for advancing to the next transition item.  If the advance
         * causes a reset in the loop, then this returns true, otherwise
         * false.
         */
        public boolean advanceTransition()
        {
            boolean ret = false;
            
            ++this.currentIndex;
            
            if (this.currentIndex >= this.trans.length)
            {
                // reset our list to correctly start from 0 again.
                reset();
                
                // we flipped over back to the beginning again.
                ret = true;
            }
            if (this.currentIndex < this.trans.length)
            {
                LOG.debug("advanced path to "+getCurrentTransition());
            }
            
            return ret;
        }
        
        
        /**
         * This is the core logic for breadth-first searching.  The other
         * piece required is current depth handling.
         *
         * @return true if no paths were added or if the current state node
         *      encountered the end of its list (that is, added the last trans
         *      in its list, then reset its list pointer).
         */
        public boolean addPath( int remainingDepth, Vector path )
        {
            // keep our current index until the child's addPath returns true.
            
            // test if we're a terminating node
            if (this.trans.length <= 0)
            {
                return true;
            }
            
            LOG.debug("Entering addPath with state "+this.is);
            
            boolean ret = false;
            InnerTransition it = getCurrentTransition();
            ITransition trans = it.getTransition();
            int nextDepth = remainingDepth;
            
            // this condition only really covers the very first pseudo-node
            if (trans != null)
            {
                path.addElement( trans );
                --nextDepth;
            }
            
            // if the depth allows it, add another transition to the path
            if (remainingDepth > 0)
            {
                if (getCurrentPath().addPath( nextDepth, path ))
                {
                    // child said it reached its end, so increment our
                    // index
                    LOG.debug("addPath state "+this.is+
                        " advancing Transition");
                    ret = advanceTransition();
                }
            }
            else
            {
                // for this depth, this node acts as a terminating node.
                LOG.debug( "no remaining depth to enter" );
                ret = true;
                
                // XXXXXXXXXXXXXXXXXXXXXXXXX
                // TODO: add in path-to-end node.
            }
            
            LOG.debug("Leaving addPath with state "+this.is+
                " and return code "+ret);
            
            return ret;
        }
        
        
        protected void reset()
        {
            int len = this.trans.length;
            
            // keep the state path list empty - these are
            // only created on an as-needed basis
            this.nextPaths = new InnerStatePath[ len ];
            this.currentIndex = 0;
        }
    }
    
    
    // translated all the IState start states into our InnerState format
    private InnerState startStates[];
    
    // this will contain all start states as children, and dummy transitions
    // to them.
    private InnerStatePath startNode;
    
    // the current max-depth to search
    private int currentDepth;
    
    
    /**
     * Capable of generating all paths from the set of all startStates to the
     * set of all endStates.  If there are no endStates, then the generated
     * paths are not required to end on at least one.  If there is at least
     * one endState, then each path will be guaranteed to terminate with
     * an endState.  If there is no possible path between any startState and
     * at least one endState, then an error is generated.
     *
     * @param startStates list of all possible starting states.  This cannot
     *      be empty.
     * @param endStates list of all possible end states.  This may be empty
     *      or <tt>null</tt>.
     */
    public BreadthPathGenerator( IState startStates[], IState endStates[] )
    {
        // check easy-to-check parts first
        if (startStates == null)
        {
            throw new IllegalArgumentException("no null start states allowed");
        }
        if (endStates == null)
        {
            endStates = new IState[0];
        }
        
        // Create all reachable InnerState objects in the hashtable.
        Hashtable knownStates = new Hashtable();
        int startLen = startStates.length;
        this.startStates = new InnerState[ startLen ];
        for (int i = startLen; --i >= 0;)
        {
            this.startStates[i] = new InnerState( startStates[i],
                knownStates );
        }
        
        // ensure that all end-states are reachable, somehow
        int endLen = endStates.length;
        for (int i = endLen; --i >= 0;)
        {
            if (!knownStates.contains( endStates[i] ))
            {
                throw new IllegalArgumentException("End state "+
                    endStates[i].getName()+" is unreachable.");
            }
        }
        
        // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
        // TODO:
        // create paths in all known states to the endStates, where possible.
        
    }
    
    
    /**
     * Return the next path in the generator's sequence.
     */
    public IPath getNextPath()
    {
        LOG.debug( "enter getNextPath()" );
        if (this.startNode == null)
        {
            reset();
        }

        // capture the next transition before it is incremented
        InnerTransition it = this.startNode.getCurrentTransition();
        if (it == null)
        {
            throw new IllegalStateException(
                "No known start states." );
        }
        InnerState is = it.getNextState();
        if (is == null)
        {
            throw new IllegalStateException(
                "Transition destination state is null." );
        }
        
        Vector v = new Vector( this.currentDepth );
        LOG.debug( "forming path" );
        if (this.startNode.addPath( this.currentDepth, v ))
        {
            // the node reset itself, so we must increment our depth
            ++this.currentDepth;
        }
        
        ITransition t[] = new ITransition[ v.size() ];
        v.copyInto( t );
        LOG.debug( "creating IPath" );
        IPath p = new PathImpl( is.getState(), t );
        
        LOG.debug( "leaving getNextPath()" );
        return p;
    }
    
    
    /**
     * Reset the generator's sequence.  There is no guarantee that the
     * order of returned IPath instances will be identical as the previous
     * generation sequence.
     */
    public void reset()
    {
        LOG.debug("Entering reset()");
        this.startNode = new InnerStatePath( startStates );
        
        // there needs to be at least one transition.  Just testing the
        // start states doesn't really do anything.
        // Perhaps this should be configurable.
        this.currentDepth = 1;
        
        LOG.debug("Leaving reset()");
    }
}

