package org.neo4j.kernel;

import java.util.Iterator;

import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.Expander;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Path;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipExpander;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.traversal.BranchOrderingPolicy;
import org.neo4j.graphdb.traversal.BranchSelector;
import org.neo4j.graphdb.traversal.PruneEvaluator;
import org.neo4j.graphdb.traversal.TraversalBranch;
import org.neo4j.graphdb.traversal.TraversalDescription;
import org.neo4j.helpers.Predicate;
import org.neo4j.kernel.impl.traversal.FinalExpansionSource;
import org.neo4j.kernel.impl.traversal.TraversalDescriptionImpl;

/**
 * A factory for objects regarding traversal of the graph. F.ex. it has a
 * method {@link #description()} for creating a new
 * {@link TraversalDescription}, methods for creating new
 * {@link TraversalBranch} instances and more.
 */
public class Traversal
{
    private static final BranchOrderingPolicy PREORDER_DEPTH_FIRST_SELECTOR =
            new BranchOrderingPolicy()
    {
        public BranchSelector create( TraversalBranch startSource )
        {
            return new PreorderDepthFirstSelector( startSource );
        }
    };
    private static final BranchOrderingPolicy POSTORDER_DEPTH_FIRST_SELECTOR =
            new BranchOrderingPolicy()
    {
        public BranchSelector create( TraversalBranch startSource )
        {
            return new PostorderDepthFirstSelector( startSource );
        }
    };
    private static final BranchOrderingPolicy PREORDER_BREADTH_FIRST_SELECTOR =
            new BranchOrderingPolicy()
    {
        public BranchSelector create( TraversalBranch startSource )
        {
            return new PreorderBreadthFirstSelector( startSource );
        }
    };
    private static final BranchOrderingPolicy POSTORDER_BREADTH_FIRST_SELECTOR =
            new BranchOrderingPolicy()
    {
        public BranchSelector create( TraversalBranch startSource )
        {
            return new PostorderBreadthFirstSelector( startSource );
        }
    };
    private static final Predicate<Path> RETURN_ALL = new Predicate<Path>()
    {
        public boolean accept( Path item )
        {
            return true;
        }
    };
    private static final Predicate<Path> RETURN_ALL_BUT_START_NODE = new Predicate<Path>()
    {
        public boolean accept( Path item )
        {
            return item.length() > 0;
        }
    };

    /**
     * Creates a new {@link TraversalDescription} with default value for
     * everything so that it's OK to call
     * {@link TraversalDescription#traverse(org.neo4j.graphdb.Node)} without
     * modification. But it isn't a very useful traversal, instead you should
     * add rules and behaviours to it before traversing.
     *
     * @return a new {@link TraversalDescription} with default values.
     */
    public static TraversalDescription description()
    {
        return new TraversalDescriptionImpl();
    }

    /**
     * Creates a new {@link RelationshipExpander} which is set to expand
     * relationships with {@code type} and {@code direction}.
     *
     * @param type the {@link RelationshipType} to expand.
     * @param dir the {@link Direction} to expand.
     * @return a new {@link RelationshipExpander}.
     */
    public static Expander expanderForTypes( RelationshipType type,
            Direction dir )
    {
        return StandardExpander.create( type, dir );
    }

    /**
     * Returns an empty {@link Expander} which, if not modified, will expand
     * all relationships when asked to expand a {@link Node}. Criterias
     * can be added to narrow the {@link Expansion}.
     * @return an empty {@link Expander} which, if not modified, will expand
     * all relationship for {@link Node}s.
     */
    public static Expander emptyExpander()
    {
        return StandardExpander.DEFAULT; // TODO: should this be a PROPER empty?
    }

    /**
     * Creates a new {@link RelationshipExpander} which is set to expand
     * relationships with two different types and directions.
     *
     * @param type1 a {@link RelationshipType} to expand.
     * @param dir1 a {@link Direction} to expand.
     * @param type2 another {@link RelationshipType} to expand.
     * @param dir2 another {@link Direction} to expand.
     * @return a new {@link RelationshipExpander}.
     */
    public static Expander expanderForTypes( RelationshipType type1,
            Direction dir1, RelationshipType type2, Direction dir2 )
    {
        return StandardExpander.create( type1, dir1, type2, dir2 );
    }

    /**
     * Creates a new {@link RelationshipExpander} which is set to expand
     * relationships with multiple types and directions.
     *
     * @param type1 a {@link RelationshipType} to expand.
     * @param dir1 a {@link Direction} to expand.
     * @param type2 another {@link RelationshipType} to expand.
     * @param dir2 another {@link Direction} to expand.
     * @param more additional pairs or type/direction to expand.
     * @return a new {@link RelationshipExpander}.
     */
    public static Expander expanderForTypes( RelationshipType type1,
            Direction dir1, RelationshipType type2, Direction dir2,
            Object... more )
    {
        return StandardExpander.create( type1, dir1, type2, dir2, more );
    }

    /**
     * Returns a {@link RelationshipExpander} which expands relationships
     * of all types and directions.
     * @return a relationship expander which expands all relationships.
     */
    public static Expander expanderForAllTypes()
    {
        return expanderForAllTypes( Direction.BOTH );
    }

    /**
     * Returns a {@link RelationshipExpander} which expands relationships
     * of all types in the given {@code direction}.
     * @return a relationship expander which expands all relationships in
     * the given {@code direction}.
     */
    public static Expander expanderForAllTypes( Direction direction )
    {
        return StandardExpander.create( direction );
    }

    /**
     * Returns a {@link RelationshipExpander} wrapped as an {@link Expander}.
     * @param expander {@link RelationshipExpander} to wrap.
     * @return a {@link RelationshipExpander} wrapped as an {@link Expander}.
     */
    public static Expander expander( RelationshipExpander expander )
    {
        if ( expander instanceof Expander )
        {
            return (Expander) expander;
        }
        return StandardExpander.wrap( expander );
    }

    /**
     * Combines two {@link TraversalBranch}s with a common
     * {@link TraversalBranch#node() head node} in order to obtain an
     * {@link TraversalBranch} representing a path from the start node of the
     * <code>source</code> {@link TraversalBranch} to the start node of the
     * <code>target</code> {@link TraversalBranch}. The resulting
     * {@link TraversalBranch} will not {@link TraversalBranch#next() expand
     * further}, and does not provide a {@link TraversalBranch#parent() parent}
     * {@link TraversalBranch}.
     *
     * @param source the {@link TraversalBranch} where the resulting path starts
     * @param target the {@link TraversalBranch} where the resulting path ends
     * @throws IllegalArgumentException if the {@link TraversalBranch#node()
     *             head nodes} of the supplied {@link TraversalBranch}s does not
     *             match
     * @return an {@link TraversalBranch} that represents the path from the
     *         start node of the <code>source</code> {@link TraversalBranch} to
     *         the start node of the <code>target</code> {@link TraversalBranch}
     */
    public static TraversalBranch combineSourcePaths( TraversalBranch source,
            TraversalBranch target )
    {
        if ( !source.node().equals( target.node() ) )
        {
            throw new IllegalArgumentException(
                    "The nodes of the head and tail must match" );
        }
        Path headPath = source.position(), tailPath = target.position();
        Relationship[] relationships = new Relationship[headPath.length()
                                                        + tailPath.length()];
        Iterator<Relationship> iter = headPath.relationships().iterator();
        for ( int i = 0; iter.hasNext(); i++ )
        {
            relationships[i] = iter.next();
        }
        iter = tailPath.relationships().iterator();
        for ( int i = relationships.length - 1; iter.hasNext(); i-- )
        {
            relationships[i] = iter.next();
        }
        return new FinalExpansionSource( tailPath.startNode(), relationships );
    }

    /**
     * A {@link PruneEvaluator} which prunes everything beyond {@code depth}.
     * @param depth the depth to prune beyond (after).
     * @return a {@link PruneEvaluator} which prunes everything after
     * {@code depth}.
     */
    public static PruneEvaluator pruneAfterDepth( final int depth )
    {
        return new PruneEvaluator()
        {
            public boolean pruneAfter( Path position )
            {
                return position.length() >= depth;
            }
        };
    }

    /**
     * A traversal return filter which returns all {@link Path}s it encounters.
     *
     * @return a return filter which returns everything.
     */
    public static Predicate<Path> returnAll()
    {
        return RETURN_ALL;
    }

    /**
     * A traversal return filter which returns all {@link Path}s except the
     * position of the start node.
     *
     * @return a return filter which returns everything except the start node.
     */
    public static Predicate<Path> returnAllButStartNode()
    {
        return RETURN_ALL_BUT_START_NODE;
    }

    /**
     * Returns a "preorder depth first" ordering policy. A depth first selector
     * always tries to select positions (from the current position) which are
     * deeper than the current position.
     * 
     * @return a {@link BranchOrderingPolicy} for a preorder depth first
     *         selector.
     */
    public static BranchOrderingPolicy preorderDepthFirst()
    {
        return PREORDER_DEPTH_FIRST_SELECTOR;
    }

    /**
     * Returns a "postorder depth first" ordering policy. A depth first selector
     * always tries to select positions (from the current position) which are
     * deeper than the current position. A postorder depth first selector
     * selects deeper position before the shallower ones.
     * 
     * @return a {@link BranchOrderingPolicy} for a postorder depth first
     *         selector.
     */
    public static BranchOrderingPolicy postorderDepthFirst()
    {
        return POSTORDER_DEPTH_FIRST_SELECTOR;
    }

    /**
     * Returns a "preorder breadth first" ordering policy. A breadth first
     * selector always selects all positions on the current depth before
     * advancing to the next depth.
     * 
     * @return a {@link BranchOrderingPolicy} for a preorder breadth first
     *         selector.
     */
    public static BranchOrderingPolicy preorderBreadthFirst()
    {
        return PREORDER_BREADTH_FIRST_SELECTOR;
    }

    /**
     * Returns a "postorder breadth first" ordering policy. A breadth first
     * selector always selects all positions on the current depth before
     * advancing to the next depth. A postorder breadth first selector selects
     * the levels in the reversed order, starting with the deepest.
     * 
     * @return a {@link BranchOrderingPolicy} for a postorder breadth first
     *         selector.
     */
    public static BranchOrderingPolicy postorderBreadthFirst()
    {
        return POSTORDER_BREADTH_FIRST_SELECTOR;
    }

    /**
     * Provides hooks to help build a string representation of a {@link Path}.
     * @param <T> the type of {@link Path}.
     */
    public static interface PathDescriptor<T extends Path>
    {
        /**
         * Returns a string representation of a {@link Node}.
         * @param path the {@link Path} we're building a string representation
         * from.
         * @param node the {@link Node} to return a string representation of.
         * @return a string representation of a {@link Node}.
         */
        String nodeRepresentation( T path, Node node );

        /**
         * Returns a string representation of a {@link Relationship}.
         * @param path the {@link Path} we're building a string representation
         * from.
         * @param from the previous {@link Node} in the path.
         * @param relationship the {@link Relationship} to return a string
         * representation of.
         * @return a string representation of a {@link Relationship}.
         */
        String relationshipRepresentation( T path, Node from,
                Relationship relationship );
    }

    /**
     * The default {@link PathDescriptor} used in common toString()
     * representations in classes implementing {@link Path}.
     * @param <T> the type of {@link Path}.
     */
    public static class DefaultPathDescriptor<T extends Path> implements PathDescriptor<T>
    {
        public String nodeRepresentation( Path path, Node node )
        {
            return "(" + node.getId() + ")";
        }

        public String relationshipRepresentation( Path path,
                Node from, Relationship relationship )
        {
            String prefix = "--", suffix = "--";
            if ( from.equals( relationship.getEndNode() ) )
            {
                prefix = "<--";
            }
            else
            {
                suffix = "-->";
            }
            return prefix + "<" + relationship.getType().name() + "," +
                    relationship.getId() + "]" + suffix;
        }
    }

    /**
     * Method for building a string representation of a {@link Path}, using
     * the given {@code builder}.
     * @param <T> the type of {@link Path}.
     * @param path the {@link Path} to build a string representation of.
     * @param builder the {@link PathDescriptor} to get
     * {@link Node} and {@link Relationship} representations from.
     * @return a string representation of a {@link Path}.
     */
    public static <T extends Path> String pathToString( T path, PathDescriptor<T> builder )
    {
        Node current = path.startNode();
        StringBuilder result = new StringBuilder();
        for ( Relationship rel : path.relationships() )
        {
            result.append( builder.nodeRepresentation( path, current ) );
            result.append( builder.relationshipRepresentation( path, current, rel ) );
            current = rel.getOtherNode( current );
        }
        result.append( builder.nodeRepresentation( path, current ) );
        return result.toString();
    }

    /**
     * Returns the default string representation of a {@link Path}. It uses
     * the {@link DefaultPathDescriptor} to get representations.
     * @param path the {@link Path} to build a string representation of.
     * @return the default string representation of a {@link Path}.
     */
    public static String defaultPathToString( Path path )
    {
        return pathToString( path, new DefaultPathDescriptor<Path>() );
    }

    /**
     * Returns a quite simple string representation of a {@link Path}. It
     * doesn't print relationship types or ids, just directions.
     * @param path the {@link Path} to build a string representation of.
     * @return a quite simple representation of a {@link Path}.
     */
    public static String simplePathToString( Path path )
    {
        return pathToString( path, new DefaultPathDescriptor<Path>()
        {
            @Override
            public String relationshipRepresentation( Path path, Node from,
                    Relationship relationship )
            {
                return relationship.getStartNode().equals( from ) ? "-->" : "<--";
            }
        } );
    }

    /**
     * Returns a quite simple string representation of a {@link Path}. It
     * doesn't print relationship types or ids, just directions. it uses the
     * {@code nodePropertyKey} to try to display that property value as in the
     * node representation instead of the node id. If that property doesn't
     * exist, the id is used.
     * @param path the {@link Path} to build a string representation of.
     * @return a quite simple representation of a {@link Path}.
     */
    public static String simplePathToString( Path path, final String nodePropertyKey )
    {
        return pathToString( path, new DefaultPathDescriptor<Path>()
        {
            @Override
            public String nodeRepresentation( Path path, Node node )
            {
                return "(" + node.getProperty( nodePropertyKey, node.getId() ) + ")";
            }

            @Override
            public String relationshipRepresentation( Path path, Node from,
                    Relationship relationship )
            {
                return relationship.getStartNode().equals( from ) ? "-->" : "<--";
            }
        } );
    }
}
