package org.neo4j.kernel;

import java.lang.reflect.Array;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.Expander;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.NotFoundException;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipExpander;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.helpers.Pair;
import org.neo4j.helpers.Predicate;
import org.neo4j.helpers.collection.FilteringIterator;
import org.neo4j.helpers.collection.IteratorWrapper;

public abstract class StandardExpander implements Expander
{
    private StandardExpander()
    {
    }

    static abstract class StandardExpansion<T> implements Expansion<T>
    {
        final StandardExpander expander;
        final Node start;

        StandardExpansion( StandardExpander expander, Node start )
        {
            this.expander = expander;
            this.start = start;
        }

        String stringRepresentation( String nodesORrelationships )
        {
            return "Expansion[" + start + ".expand( " + expander + " )."
                   + nodesORrelationships + "()]";
        }

        abstract StandardExpansion<T> createNew(
                @SuppressWarnings( "hiding" ) StandardExpander expander );

        public StandardExpansion<T> including( RelationshipType type )
        {
            return createNew( expander.add( type ) );
        }

        public StandardExpansion<T> including( RelationshipType type,
                Direction direction )
        {
            return createNew( expander.add( type, direction ) );
        }

        public StandardExpansion<T> excluding( RelationshipType type )
        {
            return createNew( expander.remove( type ) );
        }

        public StandardExpander expander()
        {
            return expander;
        }

        public StandardExpansion<T> filterNodes( Predicate<? super Node> filter )
        {
            return createNew( expander.addNodeFilter( filter ) );
        }

        public StandardExpansion<T> filterRelationships(
                Predicate<? super Relationship> filter )
        {
            return createNew( expander.addRelationsipFilter( filter ) );
        }

        public T getSingle()
        {
            final Iterator<T> expanded = iterator();
            if ( expanded.hasNext() )
            {
                final T result = expanded.next();
                if ( expanded.hasNext() )
                {
                    throw new NotFoundException(
                            "More than one relationship found for " + this );
                }
                return result;
            }
            return null;
        }

        public boolean isEmpty()
        {
            return !expander.doExpand( start ).hasNext();
        }

        public StandardExpansion<Node> nodes()
        {
            return new NodeExpansion( expander, start );
        }

        public StandardExpansion<Relationship> relationships()
        {
            return new RelationsipExpansion( expander, start );
        }

        public StandardExpansion<Pair<Relationship, Node>> pairs()
        {
            return new PairExpansion( expander, start );
        }
    }

    private static final class RelationsipExpansion extends
            StandardExpansion<Relationship>
    {
        RelationsipExpansion( StandardExpander expander, Node start )
        {
            super( expander, start );
        }

        @Override
        public String toString()
        {
            return stringRepresentation( "relationships" );
        }

        @Override
        StandardExpansion<Relationship> createNew(
                @SuppressWarnings( "hiding" ) StandardExpander expander )
        {
            return new RelationsipExpansion( expander, start );
        }

        @Override
        public StandardExpansion<Relationship> relationships()
        {
            return this;
        }

        public Iterator<Relationship> iterator()
        {
            return expander.doExpand( start );
        }
    }

    private static final class NodeExpansion extends StandardExpansion<Node>
    {
        NodeExpansion( StandardExpander expander, Node start )
        {
            super( expander, start );
        }

        @Override
        public String toString()
        {
            return stringRepresentation( "nodes" );
        }

        @Override
        StandardExpansion<Node> createNew(
                @SuppressWarnings( "hiding" ) StandardExpander expander )
        {
            return new NodeExpansion( expander, start );
        }

        @Override
        public StandardExpansion<Node> nodes()
        {
            return this;
        }

        public Iterator<Node> iterator()
        {
            return new IteratorWrapper<Node, Relationship>(
                    expander.doExpand( start ) )
            {
                @Override
                protected Node underlyingObjectToObject( Relationship rel )
                {
                    return rel.getOtherNode( start );
                }
            };
        }
    }

    private static final class PairExpansion extends
            StandardExpansion<Pair<Relationship, Node>>
    {
        PairExpansion( StandardExpander expander, Node start )
        {
            super( expander, start );
        }

        @Override
        public String toString()
        {
            return stringRepresentation( "pairs" );
        }

        @Override
        StandardExpansion<Pair<Relationship, Node>> createNew(
                @SuppressWarnings( "hiding" ) StandardExpander expander )
        {
            return new PairExpansion( expander, start );
        }

        @Override
        public StandardExpansion<Pair<Relationship, Node>> pairs()
        {
            return this;
        }

        public Iterator<Pair<Relationship, Node>> iterator()
        {
            return new IteratorWrapper<Pair<Relationship, Node>, Relationship>(
                    expander.doExpand( start ) )
            {
                @Override
                protected Pair<Relationship, Node> underlyingObjectToObject(
                        Relationship rel )
                {
                    return new Pair<Relationship, Node>( rel,
                            rel.getOtherNode( start ) );
                }
            };
        }
    }

    private static class AllExpander extends StandardExpander
    {
        private final Direction direction;

        AllExpander( Direction direction )
        {
            this.direction = direction;
        }

        @Override
        void buildString( StringBuilder result )
        {
            if ( direction != Direction.BOTH )
            {
                result.append( direction );
                result.append( ":" );
            }
            result.append( "*" );
        }

        @Override
        Iterator<Relationship> doExpand( Node start )
        {
            if ( direction == Direction.BOTH )
            {
                return start.getRelationships().iterator();
            }
            else
            {
                return start.getRelationships( direction ).iterator();
            }
        }

        @Override
        public StandardExpander add( RelationshipType type, Direction dir )
        {
            return this;
        }

        @Override
        public StandardExpander remove( RelationshipType type )
        {
            Map<String, Exclusion> exclude = new HashMap<String, Exclusion>();
            exclude.put( type.name(), Exclusion.ALL );
            return new ExcludingExpander( Exclusion.include( direction ),
                    exclude );
        }

        @Override
        public StandardExpander reversed()
        {
            return this;
        }
    }

    private enum Exclusion
    {
        ALL( null, "!" )
        {
            @Override
            public boolean accept( Node start, Relationship rel )
            {
                return false;
            }
        },
        INCOMING( Direction.OUTGOING )
        {
            @Override
            Exclusion reversed()
            {
                return OUTGOING;
            }
        },
        OUTGOING( Direction.INCOMING )
        {
            @Override
            Exclusion reversed()
            {
                return INCOMING;
            }
        },
        NONE( Direction.BOTH, "" )
        {
            @Override
            boolean includes( Direction direction )
            {
                return true;
            }
        };
        private final String string;
        private final Direction direction;

        private Exclusion( Direction direction, String string )
        {
            this.direction = direction;
            this.string = string;
        }

        private Exclusion( Direction direction )
        {
            this.direction = direction;
            this.string = "!" + name() + ":";
        }

        @Override
        public final String toString()
        {
            return string;
        }

        boolean accept( Node start, Relationship rel )
        {
            return matchDirection( direction, start, rel );
        }

        Exclusion reversed()
        {
            return this;
        }

        boolean includes( Direction dir )
        {
            return this.direction == dir;
        }

        static Exclusion include( Direction direction )
        {
            switch ( direction )
            {
            case INCOMING:
                return OUTGOING;
            case OUTGOING:
                return INCOMING;
            default:
                return NONE;
            }
        }
    }

    private static final class ExcludingExpander extends StandardExpander
    {
        private final Exclusion defaultExclusion;
        private final Map<String, Exclusion> exclusion;

        ExcludingExpander( Exclusion defaultExclusion,
                Map<String, Exclusion> exclusion )
        {
            this.defaultExclusion = defaultExclusion;
            this.exclusion = exclusion;
        }

        @Override
        void buildString( StringBuilder result )
        {
            // FIXME: not really correct
            result.append( defaultExclusion );
            result.append( "*" );
            for ( Map.Entry<String, Exclusion> entry : exclusion.entrySet() )
            {
                result.append( "," );
                result.append( entry.getValue() );
                result.append( entry.getKey() );
            }
        }

        @Override
        Iterator<Relationship> doExpand( final Node start )
        {
            return new FilteringIterator<Relationship>(
                    start.getRelationships().iterator(),
                    new Predicate<Relationship>()
                    {
                        public boolean accept( Relationship rel )
                        {
                            Exclusion exclude = exclusion.get( rel.getType().name() );
                            exclude = ( exclude == null ) ? defaultExclusion
                                    : exclude;
                            return exclude.accept( start, rel );
                        }
                    } );
        }

        @Override
        public StandardExpander add( RelationshipType type, Direction direction )
        {
            Exclusion excluded = exclusion.get( type.name() );
            final Map<String, Exclusion> newExclusion;
            if ( ( ( excluded == null ) ? defaultExclusion : excluded ).includes( direction ) )
            {
                return this;
            }
            else
            {
                excluded = Exclusion.include( direction );
                if ( excluded == defaultExclusion )
                {
                    if ( exclusion.size() == 1 )
                    {
                        return new AllExpander( defaultExclusion.direction );
                    }
                    else
                    {
                        newExclusion = new HashMap<String, Exclusion>(
                                exclusion );
                        newExclusion.remove( type.name() );
                    }
                }
                else
                {
                    newExclusion = new HashMap<String, Exclusion>( exclusion );
                    newExclusion.put( type.name(), excluded );
                }
            }
            return new ExcludingExpander( defaultExclusion, newExclusion );
        }

        @Override
        public StandardExpander remove( RelationshipType type )
        {
            Exclusion excluded = exclusion.get( type.name() );
            if ( excluded == Exclusion.ALL )
            {
                return this;
            }
            Map<String, Exclusion> newExclusion = new HashMap<String, Exclusion>(
                    exclusion );
            newExclusion.put( type.name(), Exclusion.ALL );
            return new ExcludingExpander( defaultExclusion, newExclusion );
        }

        @Override
        public StandardExpander reversed()
        {
            Map<String, Exclusion> newExclusion = new HashMap<String, Exclusion>();
            for ( Map.Entry<String, Exclusion> entry : exclusion.entrySet() )
            {
                newExclusion.put( entry.getKey(), entry.getValue().reversed() );
            }
            return new ExcludingExpander( defaultExclusion.reversed(), newExclusion );
        }
    }

    public static final StandardExpander DEFAULT = new AllExpander(
            Direction.BOTH )
    {
        @Override
        public StandardExpander add( RelationshipType type, Direction direction )
        {
            return create( type, direction );
        }
    };

    static class RegularExpander extends StandardExpander
    {
        final RelationshipType[] types;
        final Map<String, Direction> directions;

        RegularExpander( RelationshipType[] types, Map<String, Direction> dirs )
        {
            this.types = types;
            this.directions = dirs;
        }

        @Override
        void buildString( StringBuilder result )
        {
            String sep = "";
            for ( RelationshipType type : types )
            {
                result.append( sep );
                sep = ",";
                Direction dir = directions.get( type.name() );
                if ( dir != null )
                {
                    result.append( dir.name() );
                    result.append( ":" );
                }
                result.append( type.name() );
            }
        }

        @Override
        Iterator<Relationship> doExpand( final Node start )
        {
            Iterable<Relationship> relationships = start.getRelationships( types );
            if ( directions.isEmpty() )
            {
                return relationships.iterator();
            }
            else
            {
                return new FilteringIterator<Relationship>(
                        relationships.iterator(), new Predicate<Relationship>()
                        {
                            public boolean accept( Relationship rel )
                            {
                                Direction dir = directions.get( rel.getType().name() );
                                return matchDirection(
                                        dir == null ? Direction.BOTH : dir,
                                        start, rel );
                            }
                        } );
            }
        }

        @Override
        public StandardExpander add( RelationshipType type, Direction direction )
        {
            Map<String, Direction> newDirections = directions;
            if ( direction != Direction.BOTH )
            {
                newDirections = new HashMap<String, Direction>( directions );
                newDirections.put( type.name(), direction );
            }
            return createNew( append( types, type ), newDirections );
        }

        RegularExpander createNew( RelationshipType[] newTypes,
                Map<String, Direction> newDirections )
        {
            return new RegularExpander( newTypes, newDirections );
        }

        @Override
        public StandardExpander remove( RelationshipType type )
        {
            for ( int i = 0; i < types.length; i++ )
            {
                if ( type.name().equals( types[i].name() ) )
                {
                    Map<String, Direction> newDirections = directions;
                    if ( directions.containsKey( type.name() ) )
                    {
                        newDirections = new HashMap<String, Direction>(
                                directions );
                    }
                    RelationshipType[] newTypes = new RelationshipType[types.length - 1];
                    System.arraycopy( types, 0, newTypes, 0, i );
                    System.arraycopy( types, i + 1, newTypes, i, types.length
                                                                 - i - 1 );
                    return createNew( types, newDirections );
                }
            }
            return this;
        }

        @Override
        public StandardExpander reversed()
        {
            if ( directions.isEmpty() )
            {
                return this;
            }
            else
            {
                Map<String, Direction> newDirections = new HashMap<String, Direction>();
                for ( Map.Entry<String, Direction> entry : directions.entrySet() )
                {
                    newDirections.put( entry.getKey(),
                            entry.getValue().reverse() );
                }
                return createNew( types, newDirections );
            }
        }
    }

    private static final class FilteringExpander extends StandardExpander
    {
        private final StandardExpander expander;
        private final Filter[] filters;

        FilteringExpander( StandardExpander expander, Filter... filters )
        {
            this.expander = expander;
            this.filters = filters;
        }

        @Override
        void buildString( StringBuilder result )
        {
            expander.buildString( result );
            result.append( "; filter:" );
            for ( Filter filter : filters )
            {
                result.append( " " );
                result.append( filter );
            }
        }

        @Override
        Iterator<Relationship> doExpand( final Node start )
        {
            return new FilteringIterator<Relationship>(
                    expander.doExpand( start ), new Predicate<Relationship>()
                    {
                        public boolean accept( Relationship item )
                        {
                            for ( Filter filter : filters )
                            {
                                if ( filter.exclude( start, item ) )
                                    return false;
                            }
                            return true;
                        }
                    } );
        }

        @Override
        public StandardExpander addNodeFilter( Predicate<? super Node> filter )
        {
            return new FilteringExpander( expander, append( filters,
                    new NodeFilter( filter ) ) );
        }

        @Override
        public StandardExpander addRelationsipFilter(
                Predicate<? super Relationship> filter )
        {
            return new FilteringExpander( expander, append( filters,
                    new RelationshipFilter( filter ) ) );
        }

        @Override
        public StandardExpander add( RelationshipType type, Direction direction )
        {
            return new FilteringExpander( expander.add( type, direction ),
                    filters );
        }

        @Override
        public StandardExpander remove( RelationshipType type )
        {
            return new FilteringExpander( expander.remove( type ), filters );
        }

        @Override
        public StandardExpander reversed()
        {
            return new FilteringExpander( expander.reversed(), filters );
        }
    }

    private static final class TypeLimitingExpander extends StandardExpander
    {
        private final StandardExpander expander;
        private final Map<String, Direction> exclusion;

        TypeLimitingExpander( StandardExpander expander,
                Map<String, Direction> exclusion )
        {
            this.expander = expander;
            this.exclusion = exclusion;
        }

        @Override
        void buildString( StringBuilder result )
        {
            result.append( "*" );
            for ( Map.Entry<String, Direction> entry : exclusion.entrySet() )
            {
                result.append( ",!" );
                if ( entry.getValue() != Direction.BOTH )
                {
                    result.append( entry.getValue().name() );
                    result.append( ":" );
                }
                result.append( entry.getKey() );
            }
        }

        @Override
        public StandardExpander add( RelationshipType type, Direction direction )
        {
            Direction excluded = exclusion.get( type.name() );
            final Map<String, Direction> newExclusion;
            if ( excluded == null )
            {
                return this;
            }
            else if ( excluded == direction || direction == Direction.BOTH )
            {
                if ( exclusion.size() == 1 )
                {
                    return expander;
                }
                else
                {
                    newExclusion = new HashMap<String, Direction>( exclusion );
                    newExclusion.remove( type.name() );
                }
            }
            else
            {
                newExclusion = new HashMap<String, Direction>( exclusion );
                newExclusion.put( type.name(), direction.reverse() );
            }
            return new TypeLimitingExpander( expander, newExclusion );
        }

        @Override
        Iterator<Relationship> doExpand( final Node start )
        {
            return new FilteringIterator<Relationship>(
                    start.getRelationships().iterator(),
                    new Predicate<Relationship>()
                    {
                        public boolean accept( Relationship item )
                        {
                            Direction dir = exclusion.get( item.getType().name() );
                            return !matchDirection( dir, start, item );
                        }
                    } );
        }

        @Override
        public StandardExpander remove( RelationshipType type )
        {
            Direction excluded = exclusion.get( type.name() );
            if ( excluded == Direction.BOTH )
            {
                return this;
            }
            Map<String, Direction> newExclusion = new HashMap<String, Direction>(
                    exclusion );
            newExclusion.put( type.name(), Direction.BOTH );
            return new TypeLimitingExpander( expander, newExclusion );
        }

        @Override
        public StandardExpander reversed()
        {
            Map<String, Direction> newExclusion = new HashMap<String, Direction>();
            for ( Map.Entry<String, Direction> entry : exclusion.entrySet() )
            {
                newExclusion.put( entry.getKey(), entry.getValue().reverse() );
            }
            return new TypeLimitingExpander( expander, newExclusion );
        }
    }

    private static final class WrappingExpander extends StandardExpander
    {
        private static final String IMMUTABLE = "Immutable Expander ";
        private final RelationshipExpander expander;

        WrappingExpander( RelationshipExpander expander )
        {
            this.expander = expander;
        }

        @Override
        void buildString( StringBuilder result )
        {
            result.append( expander );
        }

        @Override
        Iterator<Relationship> doExpand( Node start )
        {
            return expander.expand( start ).iterator();
        }

        @Override
        public StandardExpander add( RelationshipType type, Direction direction )
        {
            throw new UnsupportedOperationException( IMMUTABLE + expander );
        }

        @Override
        public StandardExpander remove( RelationshipType type )
        {
            throw new UnsupportedOperationException( IMMUTABLE + expander );
        }

        @Override
        public StandardExpander reversed()
        {
            throw new UnsupportedOperationException( IMMUTABLE + expander );
        }
    }

    private static abstract class Filter
    {
        abstract boolean exclude( Node start, Relationship item );
    }

    private static final class NodeFilter extends Filter
    {
        private final Predicate<? super Node> predicate;

        NodeFilter( Predicate<? super Node> predicate )
        {
            this.predicate = predicate;
        }

        @Override
        public String toString()
        {
            return predicate.toString();
        }

        @Override
        boolean exclude( Node start, Relationship item )
        {
            return !predicate.accept( item.getOtherNode( start ) );
        }
    }

    private static final class RelationshipFilter extends Filter
    {
        private final Predicate<? super Relationship> predicate;

        RelationshipFilter( Predicate<? super Relationship> predicate )
        {
            this.predicate = predicate;
        }

        @Override
        public String toString()
        {
            return predicate.toString();
        }

        @Override
        boolean exclude( Node start, Relationship item )
        {
            return !predicate.accept( item );
        }
    }

    public final Expansion<Relationship> expand( Node start )
    {
        return new RelationsipExpansion( this, start );
    }

    static <T> T[] append( T[] array, T item )
    {
        @SuppressWarnings( "unchecked" ) T[] result = (T[]) Array.newInstance(
                array.getClass().getComponentType(), array.length + 1 );
        System.arraycopy( array, 0, result, 0, array.length );
        result[array.length] = item;
        return result;
    }

    private static <T> T[] extract( Class<T[]> type, T obj1, T obj2,
            Object[] more, boolean odd )
    {
        if ( more.length % 2 != 0 )
        {
            throw new IllegalArgumentException();
        }
        Object[] target = (Object[]) Array.newInstance(
                type.getComponentType(), ( more.length / 2 ) + 2 );
        try
        {
            target[0] = obj1;
            target[1] = obj2;
            for ( int i = 2; i < target.length; i++ )
            {
                target[i] = more[( i - 2 ) * 2 + ( odd ? 1 : 0 )];
            }
        }
        catch ( ArrayStoreException cast )
        {
            throw new IllegalArgumentException( cast );
        }
        return type.cast( target );
    }

    static boolean matchDirection( Direction dir, Node start, Relationship rel )
    {
        switch ( dir )
        {
        case INCOMING:
            return rel.getEndNode().equals( start );
        case OUTGOING:
            return rel.getStartNode().equals( start );
        }
        return true;
    }

    abstract Iterator<Relationship> doExpand( Node start );

    @Override
    public final String toString()
    {
        StringBuilder result = new StringBuilder( "Expander[" );
        buildString( result );
        result.append( "]" );
        return result.toString();
    }

    abstract void buildString( StringBuilder result );

    public final StandardExpander add( RelationshipType type )
    {
        return add( type, Direction.BOTH );
    }

    public abstract StandardExpander add( RelationshipType type,
            Direction direction );

    public abstract StandardExpander remove( RelationshipType type );

    public abstract StandardExpander reversed();

    public StandardExpander addNodeFilter( Predicate<? super Node> filter )
    {
        return new FilteringExpander( this, new NodeFilter( filter ) );
    }

    public StandardExpander addRelationsipFilter(
            Predicate<? super Relationship> filter )
    {
        return new FilteringExpander( this, new RelationshipFilter( filter ) );
    }

    static StandardExpander wrap( RelationshipExpander expander )
    {
        return new WrappingExpander( expander );
    }

    static Expander create( Direction direction )
    {
        return new AllExpander( direction );
    }

    static StandardExpander create( RelationshipType type, Direction dir )
    {
        final Map<String, Direction> dirs = new HashMap<String, Direction>();
        if ( dir != Direction.BOTH )
        {
            dirs.put( type.name(), dir );
        }
        return new RegularExpander( new RelationshipType[] { type }, dirs );
    }

    static StandardExpander create( RelationshipType type1, Direction dir1,
            RelationshipType type2, Direction dir2 )
    {
        final Map<String, Direction> dirs = new HashMap<String, Direction>();
        if ( dir1 != Direction.BOTH )
        {
            dirs.put( type1.name(), dir1 );
        }
        if ( dir2 != Direction.BOTH )
        {
            dirs.put( type2.name(), dir2 );
        }
        return new RegularExpander( new RelationshipType[] { type1, type2 },
                dirs );
    }

    static StandardExpander create( RelationshipType type1, Direction dir1,
            RelationshipType type2, Direction dir2, Object... more )
    {
        RelationshipType[] types = extract( RelationshipType[].class, type1,
                type2, more, false );
        Direction[] directions = extract( Direction[].class, dir1, dir2, more,
                true );
        final Map<String, Direction> dirs = new HashMap<String, Direction>();
        for ( int i = 0; i < directions.length; i++ )
        {
            if ( directions[i] != Direction.BOTH )
            {
                dirs.put( types[i].name(), directions[i] );
            }
        }
        return new RegularExpander( types, dirs );
    }
}
