/*
 * Copyright (c) 2002-2009 "Neo Technology,"
 *     Network Engine for Objects in Lund AB [http://neotechnology.com]
 *
 * This file is part of Neo4j.
 *
 * Neo4j is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.neo4j.kernel.impl.batchinsert;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;

import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.NotFoundException;
import org.neo4j.graphdb.NotInTransactionException;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.ReturnableEvaluator;
import org.neo4j.graphdb.StopEvaluator;
import org.neo4j.graphdb.Transaction;
import org.neo4j.graphdb.Traverser;
import org.neo4j.graphdb.Traverser.Order;
import org.neo4j.graphdb.event.KernelEventHandler;
import org.neo4j.graphdb.event.TransactionEventHandler;
import org.neo4j.kernel.impl.cache.LruCache;
import org.neo4j.kernel.impl.nioneo.store.InvalidRecordException;

class BatchGraphDatabaseImpl implements GraphDatabaseService
{
    final BatchInserterImpl batchInserter;

    private final LruCache<Long,NodeBatchImpl> nodes =
        new LruCache<Long,NodeBatchImpl>( "NodeCache", 10000, null )
        {
            @Override
            public void elementCleaned( NodeBatchImpl node )
            {
                Map<String,Object> properties = node.getProperties();
                if ( properties != null )
                {
                    batchInserter.setNodeProperties( node.getId(), properties );
                }
            }
        };

    private final LruCache<Long,RelationshipBatchImpl> rels =
        new LruCache<Long,RelationshipBatchImpl>( "RelCache", 10000, null )
        {
            @Override
            public void elementCleaned( RelationshipBatchImpl rel )
            {
                Map<String,Object> properties = rel.getProperties();
                if ( properties != null )
                {
                    batchInserter.setRelationshipProperties( rel.getId(),
                        properties );
                }
            }
        };

    BatchGraphDatabaseImpl( BatchInserterImpl batchInserter )
    {
        this.batchInserter = batchInserter;
    }

    BatchInserterImpl getBatchInserter()
    {
        return batchInserter;
    }

    public Transaction beginTx()
    {
        return new FakeTransaction();
    }

    public Node createNode()
    {
        long id = batchInserter.createNode( null );
        NodeBatchImpl node = new NodeBatchImpl( id, this, emptyProps() );
        nodes.put( id, node );
        return node;
    }

    static Map<String,Object> emptyProps()
    {
        return new HashMap<String,Object>();
    }

    public boolean enableRemoteShell()
    {
        return false;
    }

    public boolean enableRemoteShell( Map<String,Serializable> initialProperties )
    {
        return false;
    }

    public Iterable<Node> getAllNodes()
    {
        throw new UnsupportedOperationException( "Batch inserter mode" );
    }

    public Node getNodeById( long id )
    {
        NodeBatchImpl node = nodes.get( id );
        if ( node == null )
        {
            try
            {
                node = new NodeBatchImpl( id, this,
                    batchInserter.getNodeProperties( id ) );
                nodes.put( id, node );
            }
            catch ( InvalidRecordException e )
            {
                throw new NotFoundException( e );
            }
        }
        return node;
    }

    public Node getReferenceNode()
    {
        return getNodeById( 0 );
    }

    public Relationship getRelationshipById( long id )
    {
        RelationshipBatchImpl rel = rels.get( id );
        if ( rel == null )
        {
            try
            {
                SimpleRelationship simpleRel =
                    batchInserter.getRelationshipById( id );
                Map<String,Object> props =
                    batchInserter.getRelationshipProperties( id );
                rel = new RelationshipBatchImpl( simpleRel, this, props );
                rels.put( id, rel );
            }
            catch ( InvalidRecordException e )
            {
                throw new NotFoundException( e );
            }
        }
        return rel;
    }

    public Iterable<RelationshipType> getRelationshipTypes()
    {
        throw new UnsupportedOperationException( "Batch inserter mode" );
    }

    public void shutdown()
    {
        batchInserter.shutdown();
    }

    static class FakeTransaction implements Transaction
    {
        public void failure()
        {
            throw new NotInTransactionException( "Batch insert mode, " +
                "failure is not an option." );
        }

        public void finish()
        {
        }

        public void success()
        {
        }
    }

    private static class NodeBatchImpl implements Node
    {
        private final BatchGraphDatabaseImpl graphDbService;

        private final long id;
        private final Map<String,Object> properties;

        NodeBatchImpl( long id, BatchGraphDatabaseImpl graphDbService,
            Map<String,Object> properties )
        {
            this.id = id;
            this.graphDbService = graphDbService;
            this.properties = properties;
        }

        public GraphDatabaseService getGraphDatabase()
        {
            return graphDbService;
        }

        public Relationship createRelationshipTo( Node otherNode,
            RelationshipType type )
        {
            long relId = graphDbService.getBatchInserter().createRelationship( id,
                otherNode.getId(), type, null );
            RelationshipBatchImpl rel = new RelationshipBatchImpl(
                new SimpleRelationship( (int)relId, (int) id,
                    (int) otherNode.getId(), type ), graphDbService, emptyProps() );
            graphDbService.addRelationshipToCache( relId, rel );
            return rel;
        }

        Map<String,Object> getProperties()
        {
            return properties;
        }

        public void delete()
        {
            throw new UnsupportedOperationException();
        }

        public long getId()
        {
            return id;
        }

        private RelIterator newRelIterator( Direction dir,
            RelationshipType[] types )
        {
            Iterable<Long> relIds =
                graphDbService.getBatchInserter().getRelationshipIds( id );
            return new RelIterator( graphDbService, relIds, id, dir, types );
        }

        public Iterable<Relationship> getRelationships()
        {
            return newRelIterator( Direction.BOTH, null );
        }

        public Iterable<Relationship> getRelationships(
            RelationshipType... types )
        {
            return newRelIterator( Direction.BOTH, types );
        }

        public Iterable<Relationship> getRelationships( Direction dir )
        {
            return newRelIterator( dir, null );
        }

        public Iterable<Relationship> getRelationships( RelationshipType type,
            Direction dir )
        {
            return newRelIterator( dir, new RelationshipType[] { type } );
        }

        public Relationship getSingleRelationship( RelationshipType type,
            Direction dir )
        {
            Iterator<Relationship> relItr =
                newRelIterator( dir, new RelationshipType[] { type } );
            if ( relItr.hasNext() )
            {
                Relationship rel = relItr.next();
                if ( relItr.hasNext() )
                {
                    throw new NotFoundException( "More than one relationship[" +
                        type + ", " + dir + "] found for " + this );
                }
                return rel;
            }
            return null;
        }

        public boolean hasRelationship()
        {
            Iterator<Relationship> relItr =
                newRelIterator( Direction.BOTH, null );
            return relItr.hasNext();
        }

        public boolean hasRelationship( RelationshipType... types )
        {
            Iterator<Relationship> relItr =
                newRelIterator( Direction.BOTH, types );
            return relItr.hasNext();
        }

        public boolean hasRelationship( Direction dir )
        {
            Iterator<Relationship> relItr =
                newRelIterator( dir, null );
            return relItr.hasNext();
        }

        public boolean hasRelationship( RelationshipType type, Direction dir )
        {
            Iterator<Relationship> relItr =
                newRelIterator( dir, new RelationshipType[] { type } );
            return relItr.hasNext();
        }

        /* Tentative expansion API
        public Expansion<Relationship> expandAll()
        {
            return Traversal.expanderForAllTypes().expand( this );
        }

        public Expansion<Relationship> expand( RelationshipType type )
        {
            return expand( type, Direction.BOTH );
        }

        public Expansion<Relationship> expand( RelationshipType type,
                Direction direction )
        {
            return Traversal.expanderForTypes( type, direction ).expand(
                    this );
        }

        public Expansion<Relationship> expand( Direction direction )
        {
            return Traversal.expanderForAllTypes( direction ).expand(
                    this );
        }

        public Expansion<Relationship> expand( RelationshipExpander expander )
        {
            return Traversal.expander( expander ).expand( this );
        }
        */

        public Traverser traverse( Order traversalOrder,
            StopEvaluator stopEvaluator,
            ReturnableEvaluator returnableEvaluator,
            RelationshipType relationshipType, Direction direction )
        {
            throw new UnsupportedOperationException( "Batch inserter mode" );
        }

        public Traverser traverse( Order traversalOrder,
            StopEvaluator stopEvaluator,
            ReturnableEvaluator returnableEvaluator,
            RelationshipType firstRelationshipType, Direction firstDirection,
            RelationshipType secondRelationshipType, Direction secondDirection )
        {
            throw new UnsupportedOperationException( "Batch inserter mode" );
        }

        public Traverser traverse( Order traversalOrder,
            StopEvaluator stopEvaluator,
            ReturnableEvaluator returnableEvaluator,
            Object... relationshipTypesAndDirections )
        {
            throw new UnsupportedOperationException( "Batch inserter mode" );
        }

        public Object getProperty( String key )
        {
            Object val = properties.get( key );
            if ( val == null )
            {
                throw new NotFoundException( key );
            }
            return val;
        }

        public Object getProperty( String key, Object defaultValue )
        {
            Object val = properties.get( key );
            if ( val == null )
            {
                return defaultValue;
            }
            return val;
        }

        public Iterable<String> getPropertyKeys()
        {
            return properties.keySet();
        }

        public Iterable<Object> getPropertyValues()
        {
            return properties.values();
        }

        public boolean hasProperty( String key )
        {
            return properties.containsKey( key );
        }

        public Object removeProperty( String key )
        {
            Object val = properties.remove( key );
            if ( val == null )
            {
                throw new NotFoundException( "Property " + key );
            }
            return val;
        }

        public void setProperty( String key, Object value )
        {
            properties.put( key, value );
        }

        @Override
        public boolean equals( Object o )
        {
            if ( !(o instanceof Node) )
            {
                return false;
            }
            return this.getId() == ((Node) o).getId();

        }

        @Override
        public int hashCode()
        {
            return (int) id;
        }
    }

    private static class RelationshipBatchImpl implements Relationship
    {
        private final SimpleRelationship rel;
        private final BatchGraphDatabaseImpl graphDbService;
        private final Map<String,Object> properties;

        RelationshipBatchImpl( SimpleRelationship rel,
            BatchGraphDatabaseImpl graphDbService, Map<String,Object> properties )
        {
            this.rel = rel;
            this.graphDbService = graphDbService;
            this.properties = properties;
        }

        public GraphDatabaseService getGraphDatabase()
        {
            return graphDbService;
        }

        Map<String,Object> getProperties()
        {
            return properties;
        }

        public void delete()
        {
            throw new UnsupportedOperationException( "Batch inserter mode" );
        }

        public Node getEndNode()
        {
            return graphDbService.getNodeById( rel.getEndNode() );
        }

        public long getId()
        {
            return rel.getId();
        }

        public Node[] getNodes()
        {
            return new Node[] { getStartNode(), getEndNode() };
        }

        public Node getOtherNode( Node node )
        {
            Node startNode = getStartNode();
            Node endNode = getEndNode();
            if ( node.equals( endNode ) )
            {
                return startNode;
            }
            if ( node.equals( startNode ) )
            {
                return endNode;
            }
            throw new IllegalArgumentException( "" + node );
        }

        public Node getStartNode()
        {
            return graphDbService.getNodeById( rel.getStartNode() );
        }

        public RelationshipType getType()
        {
            return rel.getType();
        }

        public boolean isType( RelationshipType type )
        {
            return rel.getType().equals( type );
        }

        public Object getProperty( String key )
        {
            Object val = properties.get( key );
            if ( val == null )
            {
                throw new NotFoundException( key );
            }
            return val;
        }

        public Object getProperty( String key, Object defaultValue )
        {
            Object val = properties.get( key );
            if ( val == null )
            {
                return defaultValue;
            }
            return val;
        }

        public Iterable<String> getPropertyKeys()
        {
            return properties.keySet();
        }

        public Iterable<Object> getPropertyValues()
        {
            return properties.values();
        }

        public boolean hasProperty( String key )
        {
            return properties.containsKey( key );
        }

        public Object removeProperty( String key )
        {
            Object val = properties.remove( key );
            if ( val == null )
            {
                throw new NotFoundException( "Property " + key );
            }
            return val;
        }

        public void setProperty( String key, Object value )
        {
            properties.put( key, value );
        }

        @Override
        public boolean equals( Object o )
        {
            if ( !(o instanceof Relationship) )
            {
                return false;
            }
            return this.getId() == ((Relationship) o).getId();

        }

        @Override
        public int hashCode()
        {
            return (int) rel.getId();
        }
    }

    void addRelationshipToCache( long id, RelationshipBatchImpl rel )
    {
        rels.put( id, rel );
    }

    static class RelIterator implements
        Iterable<Relationship>, Iterator<Relationship>
    {

        private final BatchGraphDatabaseImpl graphDbService;
        private final Iterable<Long> relIds;
        private final Iterator<Long> relItr;
        private final long nodeId;
        private final Direction dir;
        private final RelationshipType[] types;

        private Relationship nextElement;

        RelIterator( BatchGraphDatabaseImpl graphDbService, Iterable<Long> relIds,
            long nodeId, Direction dir, RelationshipType[] types )
        {
            this.graphDbService = graphDbService;
            this.relIds = relIds;
            this.relItr = relIds.iterator();
            this.nodeId = nodeId;
            this.dir = dir;
            this.types = types;
        }

        public Iterator<Relationship> iterator()
        {
            return new RelIterator( graphDbService, relIds, nodeId, dir, types );
        }

        public boolean hasNext()
        {
            getNextElement();
            if ( nextElement != null )
            {
                return true;
            }
            return false;
        }

        public Relationship next()
        {
            getNextElement();
            if ( nextElement != null )
            {
                Relationship returnVal = nextElement;
                nextElement = null;
                return returnVal;
            }
            throw new NoSuchElementException();
        }

        private void getNextElement()
        {
            while ( nextElement == null && relItr.hasNext() )
            {
                Relationship possibleRel =
                    graphDbService.getRelationshipById( relItr.next() );
                if ( dir == Direction.OUTGOING &&
                    possibleRel.getEndNode().getId() == nodeId )
                {
                    continue;
                }
                if ( dir == Direction.INCOMING &&
                    possibleRel.getStartNode().getId() == nodeId )
                {
                    continue;
                }
                if ( types != null )
                {
                    for ( RelationshipType type : types )
                    {
                        if ( type.name().equals(
                            possibleRel.getType().name() ) )
                        {
                            nextElement = possibleRel;
                            break;
                        }
                    }
                }
                else
                {
                    nextElement = possibleRel;
                }
            }
        }

        public void remove()
        {
            throw new UnsupportedOperationException();
        }
    }

    void clearCaches()
    {
        nodes.clear();
        rels.clear();
    }

    public KernelEventHandler registerKernelEventHandler(
            KernelEventHandler handler )
    {
        throw new UnsupportedOperationException();
    }

    public <T> TransactionEventHandler<T> registerTransactionEventHandler(
            TransactionEventHandler<T> handler )
    {
        throw new UnsupportedOperationException();
    }

    public KernelEventHandler unregisterKernelEventHandler(
            KernelEventHandler handler )
    {
        throw new UnsupportedOperationException();
    }

    public <T> TransactionEventHandler<T> unregisterTransactionEventHandler(
            TransactionEventHandler<T> handler )
    {
        throw new UnsupportedOperationException();
    }
}