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

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.transaction.TransactionManager;

import org.neo4j.kernel.DeadlockDetectedException;

/**
 * The LockManager can lock resources for reading or writing. By doing this one
 * may achieve different transaction isolation levels. A resource can for now be
 * any object (but null).
 * <p>
 * When acquiring a lock you have to release it. Failure to do so will result in
 * the resource being blocked to all other transactions. Put all locks in a try -
 * finally block.
 * <p>
 * Multiple locks on the same resource held by the same transaction requires the
 * transaction to invoke the release lock method multiple times. If a tx has
 * invoked <CODE>getReadLock</CODE> on the same resource x times in a row it
 * must invoke <CODE>releaseReadLock</CODE> x times to release all the locks.
 * <p>
 * LockManager just maps locks to resources and they do all the hard work
 * together with a resource allocation graph.
 */
public class LockManager
{
    private final Map<Object,RWLock> resourceLockMap =
        new HashMap<Object,RWLock>();

    private final RagManager ragManager;

    public LockManager( TransactionManager tm )
    {
        ragManager = new RagManager( tm );
    }

    public long getDetectedDeadlockCount()
    {
        return ragManager.getDeadlockCount();
    }

    /**
     * Tries to acquire read lock on <CODE>resource</CODE> for the current
     * transaction. If read lock can't be acquired the transaction will wait for
     * the lransaction until it can acquire it. If waiting leads to dead lock a
     * {@link DeadlockDetectedException} will be thrown.
     *
     * @param resource
     *            The resource
     * @throws DeadlockDetectedException
     *             If a deadlock is detected
     * @throws IllegalResourceException
     */
    public void getReadLock( Object resource )
        throws DeadlockDetectedException, IllegalResourceException
    {
        if ( resource == null )
        {
            throw new IllegalResourceException( "Null parameter" );
        }

        RWLock lock = null;
        synchronized ( resourceLockMap )
        {
            lock = resourceLockMap.get( resource );
            if ( lock == null )
            {
                lock = new RWLock( resource, ragManager );
                resourceLockMap.put( resource, lock );
            }
            lock.mark();
        }
        lock.acquireReadLock();
    }

    /**
     * Tries to acquire write lock on <CODE>resource</CODE> for the current
     * transaction. If write lock can't be acquired the transaction will wait
     * for the lock until it can acquire it. If waiting leads to dead lock a
     * {@link DeadlockDetectedException} will be thrown.
     *
     * @param resource
     *            The resource
     * @throws DeadlockDetectedException
     *             If a deadlock is detected
     * @throws IllegalResourceException
     */
    public void getWriteLock( Object resource )
        throws DeadlockDetectedException, IllegalResourceException
    {
        if ( resource == null )
        {
            throw new IllegalResourceException( "Null parameter" );
        }

        RWLock lock = null;
        synchronized ( resourceLockMap )
        {
            lock = resourceLockMap.get( resource );
            if ( lock == null )
            {
                lock = new RWLock( resource, ragManager );
                resourceLockMap.put( resource, lock );
            }
            lock.mark();
        }
        lock.acquireWriteLock();
    }

    /**
     * Releases a read lock held by the current transaction on <CODE>resource</CODE>.
     * If current transaction don't have read lock a
     * {@link LockNotFoundException} will be thrown.
     *
     * @param resource
     *            The resource
     * @throws IllegalResourceException
     * @throws LockNotFoundException
     */
    public void releaseReadLock( Object resource )
        throws LockNotFoundException, IllegalResourceException
    {
        if ( resource == null )
        {
            throw new IllegalResourceException( "Null parameter" );
        }

        RWLock lock = null;
        synchronized ( resourceLockMap )
        {
            lock = resourceLockMap.get( resource );
            if ( lock == null )
            {
                throw new LockNotFoundException( "Lock not found for: "
                    + resource );
            }
            if ( !lock.isMarked() && lock.getReadCount() == 1 &&
                lock.getWriteCount() == 0 &&
                lock.getWaitingThreadsCount() == 0 )
            {
                resourceLockMap.remove( resource );
            }
            lock.releaseReadLock();
        }
    }

    /**
     * Releases a read lock held by the current transaction on <CODE>resource</CODE>.
     * If current transaction don't have read lock a
     * {@link LockNotFoundException} will be thrown.
     *
     * @param resource
     *            The resource
     * @throws IllegalResourceException
     * @throws LockNotFoundException
     */
    public void releaseWriteLock( Object resource )
        throws LockNotFoundException, IllegalResourceException
    {
        if ( resource == null )
        {
            throw new IllegalResourceException( "Null parameter" );
        }

        RWLock lock = null;
        synchronized ( resourceLockMap )
        {
            lock = resourceLockMap.get( resource );
            if ( lock == null )
            {
                throw new LockNotFoundException( "Lock not found for: "
                    + resource );
            }
            if ( !lock.isMarked() && lock.getReadCount() == 0 &&
                lock.getWriteCount() == 1 &&
                lock.getWaitingThreadsCount() == 0 )
            {
                resourceLockMap.remove( resource );
            }
            lock.releaseWriteLock();
        }

    }

    /**
     * Utility method for debugging. Dumps info to console of txs having locks
     * on resources.
     *
     * @param resource
     */
    public void dumpLocksOnResource( Object resource )
    {
        RWLock lock = null;
        synchronized ( resourceLockMap )
        {
            if ( !resourceLockMap.containsKey( resource ) )
            {
                System.out.println( "No locks on " + resource );
                return;
            }
            lock = resourceLockMap.get( resource );
        }
        lock.dumpStack();
    }

    /**
     * Utility method for debugging. Dumps the resource allocation graph to
     * console.
     */
    public void dumpRagStack()
    {
        ragManager.dumpStack();
    }

    /**
     * Utility method for debuggin. Dumps info about each lock to console.
     */
    public void dumpAllLocks()
    {
        synchronized ( resourceLockMap )
        {
            Iterator<RWLock> itr = resourceLockMap.values().iterator();
            int emptyLockCount = 0;
            while ( itr.hasNext() )
            {
                RWLock lock = itr.next();
                if ( lock.getWriteCount() > 0 || lock.getReadCount() > 0 )
                {
                    lock.dumpStack();
                }
                else
                {
                    if ( lock.getWaitingThreadsCount() > 0 )
                    {
                        lock.dumpStack();
                    }
                    emptyLockCount++;
                }
            }
            if ( emptyLockCount > 0 )
            {
                System.out.println( "There are " + emptyLockCount
                    + " empty locks" );
            }
            else
            {
                System.out.println( "There are no empty locks" );
            }
        }
    }
}