/*
 * 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.core;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.NotFoundException;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.ReturnableEvaluator;
import org.neo4j.graphdb.StopEvaluator;
import org.neo4j.graphdb.Traverser;
import org.neo4j.graphdb.Traverser.Order;
import org.neo4j.kernel.impl.nioneo.store.PropertyData;
import org.neo4j.kernel.impl.nioneo.store.Record;
import org.neo4j.kernel.impl.nioneo.store.RelationshipChainPosition;
import org.neo4j.kernel.impl.transaction.LockType;
import org.neo4j.kernel.impl.traversal.OldTraverserWrapper;
import org.neo4j.kernel.impl.util.ArrayMap;
import org.neo4j.kernel.impl.util.IntArray;

class NodeImpl extends Primitive implements Node, Comparable<Node>
{
    private ArrayMap<String,IntArray> relationshipMap = null;
    // private RelationshipGrabber relationshipGrabber = null;
    private RelationshipChainPosition relChainPosition = null;


    NodeImpl( int id, NodeManager nodeManager )
    {
        super( id, nodeManager );
    }

    // newNode will only be true for NodeManager.createNode
    NodeImpl( int id, boolean newNode, NodeManager nodeManager )
    {
        super( id, newNode, nodeManager );
        if ( newNode )
        {
            relationshipMap = new ArrayMap<String,IntArray>();
            relChainPosition = new RelationshipChainPosition(
                Record.NO_NEXT_RELATIONSHIP.intValue() );
        }
    }

    @Override
    protected void changeProperty( int propertyId, Object value )
    {
        nodeManager.nodeChangeProperty( this, propertyId, value );
    }

    @Override
    protected int addProperty( PropertyIndex index, Object value )
    {
        return nodeManager.nodeAddProperty( this, index, value );
    }

    @Override
    protected void removeProperty( int propertyId )
    {
        nodeManager.nodeRemoveProperty( this, propertyId );
    }

    @Override
    protected ArrayMap<Integer,PropertyData> loadProperties( boolean light )
    {
        return nodeManager.loadProperties( this, light );
    }

    List<RelTypeElementIterator> getAllRelationships()
    {
        ensureFullRelationships();
        List<RelTypeElementIterator> relTypeList =
            new LinkedList<RelTypeElementIterator>();
        ArrayMap<String,IntArray> addMap =
            nodeManager.getCowRelationshipAddMap( this );
        for ( String type : relationshipMap.keySet() )
        {
            IntArray src = relationshipMap.get( type );
            IntArray remove = nodeManager.getCowRelationshipRemoveMap(
                this, type );
            IntArray add = null;
            if ( addMap != null )
            {
                add = addMap.get( type );
            }
            if ( src != null || add != null )
            {
                relTypeList.add( RelTypeElement.create( type, this, src, add,
                    remove ) );
            }
        }
        if ( addMap != null )
        {
            for ( String type : addMap.keySet() )
            {
                if ( relationshipMap.get( type ) == null )
                {
                    IntArray remove = nodeManager.getCowRelationshipRemoveMap(
                        this, type );
                    IntArray add = addMap.get( type );
                    relTypeList.add( RelTypeElement.create( type, this, null,
                        add, remove ) );
                }
            }
        }
        return relTypeList;
    }

    List<RelTypeElementIterator> getAllRelationshipsOfType(
        RelationshipType... types)
    {
        ensureFullRelationships();
        List<RelTypeElementIterator> relTypeList =
            new LinkedList<RelTypeElementIterator>();
        for ( RelationshipType type : types )
        {
            IntArray src = relationshipMap.get( type.name() );
            IntArray remove = nodeManager.getCowRelationshipRemoveMap(
                this, type.name() );
            IntArray add = nodeManager.getCowRelationshipAddMap( this,
                type.name() );
            if ( src != null || add != null )
            {
                relTypeList.add( RelTypeElement.create( type.name(), this, src,
                    add, remove ) );
            }
        }
        return relTypeList;
    }

    public Iterable<Relationship> getRelationships()
    {
        return new IntArrayIterator( getAllRelationships(), this,
            Direction.BOTH, nodeManager, new RelationshipType[0] );
    }

    public Iterable<Relationship> getRelationships( Direction dir )
    {
        return new IntArrayIterator( getAllRelationships(), this, dir,
            nodeManager, new RelationshipType[0] );
    }

    public Iterable<Relationship> getRelationships( RelationshipType type )
    {
        RelationshipType types[] = new RelationshipType[] { type };
        return new IntArrayIterator(
            getAllRelationshipsOfType( types ),
            this, Direction.BOTH, nodeManager, types );
    }

    public Iterable<Relationship> getRelationships( RelationshipType... types )
    {
        return new IntArrayIterator(
            getAllRelationshipsOfType(types ),
            this, Direction.BOTH, nodeManager, types );
    }

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

    public Iterable<Relationship> getRelationships( RelationshipType type,
        Direction dir )
    {
        RelationshipType types[] = new RelationshipType[] { type };
        return new IntArrayIterator(
            getAllRelationshipsOfType( types ),
            this, dir, nodeManager, types );
    }

    public void delete()
    {
        nodeManager.acquireLock( this, LockType.WRITE );
        boolean success = false;
        try
        {
            ArrayMap<Integer,PropertyData> skipMap =
                nodeManager.getCowPropertyRemoveMap( this, true );
            ArrayMap<Integer,PropertyData> removedProps =
                nodeManager.deleteNode( this );
            if ( removedProps.size() > 0 )
            {
                for ( int index : removedProps.keySet() )
                {
                    skipMap.put( index, removedProps.get( index ) );
                }
            }
            success = true;
        }
        finally
        {
            nodeManager.releaseLock( this, LockType.WRITE );
            if ( !success )
            {
                setRollbackOnly();
            }
        }
    }

    /**
     * If object <CODE>node</CODE> is a node, 0 is returned if <CODE>this</CODE>
     * node id equals <CODE>node's</CODE> node id, 1 if <CODE>this</CODE>
     * node id is greater and -1 else.
     * <p>
     * If <CODE>node</CODE> isn't a node a ClassCastException will be thrown.
     *
     * @param node
     *            the node to compare this node with
     * @return 0 if equal id, 1 if this id is greater else -1
     */
    public int compareTo( Node n )
    {
        long ourId = this.getId(), theirId = n.getId();

        if ( ourId < theirId )
        {
            return -1;
        }
        else if ( ourId > theirId )
        {
            return 1;
        }
        else
        {
            return 0;
        }
    }

    /**
     * Returns true if object <CODE>o</CODE> is a node with the same id as
     * <CODE>this</CODE>.
     *
     * @param o
     *            the object to compare
     * @return true if equal, else false
     */
    @Override
    public boolean equals( Object o )
    {
        // verify type and not null, should use Node inteface
        if ( !(o instanceof Node) )
        {
            return false;
        }

        // The equals contract:
        // o reflexive: x.equals(x)
        // o symmetric: x.equals(y) == y.equals(x)
        // o transitive: ( x.equals(y) && y.equals(z) ) == true
        // then x.equals(z) == true
        // o consistent: the nodeId never changes
        return this.getId() == ((Node) o).getId();

    }

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

    /**
     * Returns this node's string representation.
     *
     * @return the string representation of this node
     */
    @Override
    public String toString()
    {
        return "NodeImpl#" + this.getId();
    }

    // caller is responsible for acquiring lock
    // this method is only called when a relationship is created or
    // a relationship delete is undone or when the full node is loaded
    void addRelationship( RelationshipType type, int relId )
    {
        IntArray relationshipSet = nodeManager.getCowRelationshipAddMap(
            this, type.name(), true );
        relationshipSet.add( relId );
    }

    // caller is responsible for acquiring lock
    // this method is only called when a undo create relationship or
    // a relationship delete is invoked.
    void removeRelationship( RelationshipType type, int relId )
    {
        IntArray relationshipSet = nodeManager.getCowRelationshipRemoveMap(
            this, type.name(), true );
        relationshipSet.add( relId );
    }

    private synchronized boolean ensureFullRelationships()
    {
        if ( relationshipMap == null )
        {
            this.relChainPosition =
                nodeManager.getRelationshipChainPosition( this );
            this.relationshipMap = new ArrayMap<String,IntArray>();
            getMoreRelationships();
            return true;
        }
        return false;
    }

    synchronized boolean getMoreRelationships()
    {
        if ( !relChainPosition.hasMore() )
        {
            return false;
        }
        ArrayMap<String,IntArray> addMap = nodeManager.getMoreRelationships(
            this );
        if ( addMap.size() == 0 )
        {
            return false;
        }
        for ( String type : addMap.keySet() )
        {
            IntArray addRels = addMap.get( type );
            IntArray srcRels = relationshipMap.get( type );
            if ( srcRels == null )
            {
                relationshipMap.put( type, addRels );
            }
            else
            {
                srcRels.addAll( addRels );
            }
        }
        return true;
    }

    public Relationship createRelationshipTo( Node otherNode,
        RelationshipType type )
    {
        return nodeManager.createRelationship( this, otherNode, type );
    }

    /* 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 )
    {
        return OldTraverserWrapper.traverse( new NodeProxy( id, nodeManager ),
                traversalOrder, stopEvaluator,
                returnableEvaluator, relationshipType, direction );
    }

    public Traverser traverse( Order traversalOrder,
        StopEvaluator stopEvaluator, ReturnableEvaluator returnableEvaluator,
        RelationshipType firstRelationshipType, Direction firstDirection,
        RelationshipType secondRelationshipType, Direction secondDirection )
    {
        return OldTraverserWrapper.traverse( new NodeProxy( id, nodeManager ),
                traversalOrder, stopEvaluator,
                returnableEvaluator, firstRelationshipType, firstDirection,
                secondRelationshipType, secondDirection );
    }

    public Traverser traverse( Order traversalOrder,
        StopEvaluator stopEvaluator, ReturnableEvaluator returnableEvaluator,
        Object... relationshipTypesAndDirections )
    {
        return OldTraverserWrapper.traverse( new NodeProxy( id, nodeManager ),
                traversalOrder, stopEvaluator,
                returnableEvaluator, relationshipTypesAndDirections );
    }

    public boolean hasRelationship()
    {
        return getRelationships().iterator().hasNext();
    }

    public boolean hasRelationship( RelationshipType... types )
    {
        return getRelationships( types ).iterator().hasNext();
    }

    public boolean hasRelationship( Direction dir )
    {
        return getRelationships( dir ).iterator().hasNext();
    }

    public boolean hasRelationship( RelationshipType type, Direction dir )
    {
        return getRelationships( type, dir ).iterator().hasNext();
    }

    protected void commitRelationshipMaps(
        ArrayMap<String,IntArray> cowRelationshipAddMap,
        ArrayMap<String,IntArray> cowRelationshipRemoveMap )
    {
        if ( relationshipMap == null )
        {
            // we will load full in some other tx
            return;
        }
        if ( cowRelationshipAddMap != null )
        {
            for ( String type : cowRelationshipAddMap.keySet() )
            {
                IntArray add = cowRelationshipAddMap.get( type );
                IntArray remove = null;
                if ( cowRelationshipRemoveMap != null )
                {
                    remove = cowRelationshipRemoveMap.get( type );
                }
                IntArray src = relationshipMap.get( type );
                relationshipMap.put( type, IntArray.composeNew(
                    src, add, remove ) );
            }
        }
        if ( cowRelationshipRemoveMap != null )
        {
            for ( String type : cowRelationshipRemoveMap.keySet() )
            {
                if ( cowRelationshipAddMap != null &&
                    cowRelationshipAddMap.get( type ) != null )
                {
                    continue;
                }
                IntArray src = relationshipMap.get( type );
                IntArray remove = cowRelationshipRemoveMap.get( type );
                relationshipMap.put( type, IntArray.composeNew( src, null,
                     remove ) );
            }
        }
    }

    RelationshipChainPosition getRelChainPosition()
    {
        return relChainPosition;
    }
}