/*
 * 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 org.neo4j.graphdb.Node;
import org.neo4j.graphdb.NotFoundException;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.kernel.impl.nioneo.store.PropertyData;
import org.neo4j.kernel.impl.transaction.LockException;
import org.neo4j.kernel.impl.transaction.LockType;
import org.neo4j.kernel.impl.util.ArrayMap;

class RelationshipImpl extends Primitive implements Relationship,
    Comparable<Relationship>
{
    private final int startNodeId;
    private final int endNodeId;
    private final RelationshipType type;

    // Dummy constructor for NodeManager to acquire read lock on relationship
    // when loading from PL.
    RelationshipImpl( int id, NodeManager nodeManager )
    {
        super( id, nodeManager );
        this.startNodeId = -1;
        this.endNodeId = -1;
        this.type = null;
    }

    RelationshipImpl( int id, int startNodeId, int endNodeId,
        RelationshipType type, boolean newRel, NodeManager nodeManager )
    {
        super( id, newRel, nodeManager );
        if ( type == null )
        {
            throw new IllegalArgumentException( "Null type" );
        }
        if ( startNodeId == endNodeId )
        {
            throw new IllegalArgumentException( "Start node equals end node" );
        }

        this.startNodeId = startNodeId;
        this.endNodeId = endNodeId;
        this.type = type;
    }

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

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

    protected void removeProperty( int propertyId )
    {
        nodeManager.relRemoveProperty( this, propertyId );
    }

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

    public Node[] getNodes()
    {
        return new Node[] { new NodeProxy( startNodeId, nodeManager ),
            new NodeProxy( endNodeId, nodeManager ) };
    }

    public Node getOtherNode( Node node )
    {
        if ( startNodeId == (int) node.getId() )
        {
            return new NodeProxy( endNodeId, nodeManager );
        }
        if ( endNodeId == (int) node.getId() )
        {
            return new NodeProxy( startNodeId, nodeManager );
        }
        throw new NotFoundException( "Node[" + node.getId()
            + "] not connected to this relationship[" + getId() + "]" );
    }

    public Node getStartNode()
    {
        return new NodeProxy( startNodeId, nodeManager );
    }

    int getStartNodeId()
    {
        return startNodeId;
    }

    public Node getEndNode()
    {
        return new NodeProxy( endNodeId, nodeManager );
    }

    int getEndNodeId()
    {
        return endNodeId;
    }

    public RelationshipType getType()
    {
        return type;
    }

    public boolean isType( RelationshipType otherType )
    {
        return otherType != null
            && otherType.name().equals( this.getType().name() );
    }

    public void delete()
    {
        NodeImpl startNode = null;
        NodeImpl endNode = null;
        boolean startNodeLocked = false;
        boolean endNodeLocked = false;
        nodeManager.acquireLock( this, LockType.WRITE );
        boolean success = false;
        try
        {
            startNode = nodeManager.getLightNode( startNodeId );
            if ( startNode != null )
            {
                nodeManager.acquireLock( startNode, LockType.WRITE );
                startNodeLocked = true;
            }
            endNode = nodeManager.getLightNode( endNodeId );
            if ( endNode != null )
            {
                nodeManager.acquireLock( endNode, LockType.WRITE );
                endNodeLocked = true;
            }
            // no need to load full relationship, all properties will be
            // deleted when relationship is deleted

            ArrayMap<Integer,PropertyData> skipMap = 
                nodeManager.getCowPropertyRemoveMap( this, true );
            ArrayMap<Integer,PropertyData> removedProps = 
                nodeManager.deleteRelationship( this );
            if ( removedProps.size() > 0 )
            {
                for ( int index : removedProps.keySet() )
                {
                    skipMap.put( index, removedProps.get( index ) );
                }
            }
            success = true;
            if ( startNode != null )
            {
                startNode.removeRelationship( type, id );
            }
            if ( endNode != null )
            {
                endNode.removeRelationship( type, id );
            }
            success = true;
        }
        finally
        {
            boolean releaseFailed = false;
            try
            {
                if ( startNodeLocked )
                {
                    nodeManager.releaseLock( startNode, LockType.WRITE );
                }
            }
            catch ( Exception e )
            {
                releaseFailed = true;
                e.printStackTrace();
            }
            try
            {
                if ( endNodeLocked )
                {
                    nodeManager.releaseLock( endNode, LockType.WRITE );
                }
            }
            catch ( Exception e )
            {
                releaseFailed = true;
                e.printStackTrace();
            }
            nodeManager.releaseLock( this, LockType.WRITE );
            if ( !success )
            {
                setRollbackOnly();
            }
            if ( releaseFailed )
            {
                throw new LockException( "Unable to release locks ["
                    + startNode + "," + endNode + "] in relationship delete->"
                    + this );
            }
        }
    }

    public String toString()
    {
        return "RelationshipImpl #" + this.getId() + " of type " + type
            + " between Node[" + startNodeId + "] and Node[" + endNodeId + "]";
    }

    public int compareTo( Relationship r )
    {
        int ourId = (int) this.getId(), theirId = (int) r.getId();

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

    /**
     * Returns true if object <CODE>o</CODE> is a relationship with the same
     * id as <CODE>this</CODE>.
     * 
     * @param o
     *            the object to compare
     * @return true if equal, else false
     */
    public boolean equals( Object o )
    {
        // verify type and not null, should use Node inteface
        if ( !(o instanceof Relationship) )
        {
            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() == ((Relationship) o).getId();

    }

    public int hashCode()
    {
        return id;
    }
}