/*
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.cache.lock;

import EDU.oswego.cs.dl.util.concurrent.Sync;
import org.jboss.logging.Logger;

import java.util.Collection;
import java.util.List;

/**
 * Lock object which grants and releases locks, and associates locks with <em>owners</em>.
 * The methods to acquire and release a lock require an owner (Object). When a lock is acquired,
 * we store the current owner with the lock. When the same owner (<em>but possibly
 * running in a different thread</em>) wants to acquire the lock, it is immediately
 * granted. When an owner different from the one currently owning the lock wants to release
 * the lock, we do nothing (no-op).<p>
 * Consider the following example:
 * <pre>
 * FIFOSemaphore lock=new FIFOSemaphore(1);
 * lock.acquire();
 * lock.acquire();
 * lock.release();
 * </pre>
 * This would block on the second <tt>acquire()</tt> (although we currently already hold
 * the lock) because the lock only has 1 permit. So <tt>IdentityLock</tt> will allow the
 * following code to work properly:
 * <pre>
 * IdentityLock lock=new IdentityLock();
 * lock.readLock().acquire(this, timeout);
 * lock.writeLock().acquire(this, timeout);
 * lock.release(this);
 * </pre>
 * If the owner is null, then the current thread is taken by default. This allow the following
 * code to work:
 * <pre>
 * IdentityLock lock=new IdentityLock();
 * lock.readLock().attempt(null, timeout);
 * lock.writeLock().attempt(null, timeout);
 * lock.release(null);
 * </pre>
 * <br/>
 * Note that the Object given as owner is required to implement {@link Object#equals}. For
 * a use case see the examples in the testsuite.
 *
 * @author <a href="mailto:bela@jboss.org">Bela Ban</a> Apr 11, 2003
 * @author Ben Wang July 2003
 * @version $Revision: 1.2.2.5 $
 */
public class IdentityLock
{
   private static Logger log = Logger.getLogger(IdentityLock.class);
   private LockStrategy lock_;
   private LockMap map_;

   public IdentityLock()
   {
      lock_ = LockStrategyFactory.getInstance().getLockStrategy();
      map_ = new LockMap();
   }

   /**
    * Return a copy of the reader lock owner in List. Size is zero is not available. Note that this list
    * is synchronized.
    *
    * @return
    */
   public List getReaderOwners()
   {
      return map_.readerOwners();
   }

   /**
    * Return the writer lock owner object. Null if not available.
    *
    * @return
    */
   public Object getWriterOwner()
   {
      return map_.writerOwner();
   }

   /**
    * Acquire a write lock with a timeout of <code>timeout</code> milliseconds.
    * Note that if the current owner owns a read lock, it will be upgraded
    * automatically. However, if upgrade fails, i.e., timeout, the read lock will
    * be released automatically.
    *
    * @param owner   Can't be null.
    * @param timeout
    * @throws LockingException
    * @throws TimeoutException
    */
   public void acquireWriteLock(Object owner, long timeout)
         throws LockingException, TimeoutException
   {
      boolean rc;

      if (owner == null) {
//            owner = Thread.currentThread();
         throw new IllegalArgumentException("IdentityLock.acquireWriteLock(): null owner object.");
      }

      if (map_.isOwner(owner, LockMap.OWNER_WRITE)) {
         if (log.isDebugEnabled())
            log.debug("acquireWriteLock(): caller already owns lock.");
         return; // owner already has the lock
      }

      try {
         // Check first if we need to upgrade
         if (map_.isOwner(owner, LockMap.OWNER_READ)) {
            // Currently is a reader owner. Obtain the writer ownership then.
            Sync wLock = null;
            try {
//               if (log.isDebugEnabled())
//                  log.debug("attempt to upgrade read lock.");
               wLock = lock_.upgradeLockAttempt(timeout);
//               if (log.isDebugEnabled())
//                  log.debug("return from upgrade read lock.");
            } catch (UpgradeException ue) {
               log.warn("acquireWriteLock: lock upgrade failed. Exception: " +ue);
               throw new LockingException("acquireWriteLock: lock upgrade failed.", ue);
            }
            if (wLock == null) {
               release(owner);   // bug fix: remember to release the read lock before throwing the exception
               map_.removeReader(owner);
               throw new TimeoutException("upgrade lock could not " +
                     "be acquired after " + timeout + " ms." + " Lock map ownership " + map_.printInfo());
            }
            try {
               if (log.isDebugEnabled())
                  log.debug("upgrade lock");
               map_.upgrade(owner);
            } catch (OwnerNotExistedException ex) {
               throw new LockingException("Can't upgrade lock ownership to write." +
                     " Internal error in LockMap.upgrade(). " + ex);
            }
         } else {
            // Not a current reader owner. Obtain the writer ownership then.
//            if (log.isDebugEnabled())
//               log.debug("attempt to acquire lock.");
            rc = lock_.writeLock().attempt(timeout);
//            if (log.isDebugEnabled())
//               log.debug("return from acquire write lock.");

            // we don't need to synchronize from here on because we own the semaphore
            if (rc == false) {
               String errStr = "lock could not be acquired after " + timeout + " ms. " +
                     "Lock map ownership " + map_.printInfo();
               log.error(errStr);
               throw new TimeoutException(errStr);
            }
            map_.addWriter(owner);
         }
      } catch (InterruptedException e) {
         throw new LockingException("IdentityLock.attempt()", e);
      }
   }

   /**
    * Acquire a read lock with a timeout period of <code>timeout</code> milliseconds.
    *
    * @param owner   Can't be null.
    * @param timeout
    * @throws LockingException
    * @throws TimeoutException
    */
   public void acquireReadLock(Object owner, long timeout)
         throws LockingException, TimeoutException
   {
      boolean rc;

      if (owner == null) {
//            owner = Thread.currentThread();
         throw new IllegalArgumentException("null owner object.");
      }

      if (map_.isOwner(owner, LockMap.OWNER_ANY)) {
         if (log.isDebugEnabled())
            log.debug("caller already owns lock.");
         return; // owner already has the lock
      }

      try {
//         if (log.isDebugEnabled())
//            log.debug("attempt to acquire read lock.");
         rc = lock_.readLock().attempt(timeout);
//         if (log.isDebugEnabled())
//            log.debug("return from read lock.");

         // we don't need to synchronize from here on because we own the semaphore
         if (rc == false) {
            log.error("attempt failed. lock map ownership "
                  + map_.printInfo());
            throw new TimeoutException("lock could not be acquired after " + timeout + "ms. " +
                                       "Lock map ownership  " + map_.printInfo());
         }

         map_.addReader(owner);
      } catch (InterruptedException e) {
         throw new LockingException("IdentityLock.attempt()", e);
      }
   }

   /**
    * Release the lock held by the owner.
    *
    * @param owner Can't be null.
    */
   public void release(Object owner)
   {
      if (owner == null) {
//            owner = Thread.currentThread();
         throw new IllegalArgumentException("IdentityLock.release(): null owner object.");
      }

      // Check whether to release reader or writer lock.
      if (map_.isOwner(owner, LockMap.OWNER_READ)) {
         map_.removeReader(owner);
         lock_.readLock().release();
      } else if (map_.isOwner(owner, LockMap.OWNER_WRITE)) {
         map_.removeWriter(owner);
         lock_.writeLock().release();
      } else {
         if (log.isDebugEnabled())
            log.debug("release(): lock map owner " + owner + " not found." +
                  " Could have been released.");
//           throw new RuntimeException("release(): lock map owner " +owner + "not found");
      }
   }

   /**
    * Release all locks associated with this instance.
    */
   public void releaseAll()
   {
      Object obj = null;
      Collection col = null;
      if ((obj = map_.writerOwner()) != null) {
         lock_.readLock().release();
         map_.removeWriter(obj);
      }

      if ((col = map_.readerOwners()).size() != 0) {
         java.util.Iterator it = col.iterator();
         while (it.hasNext()) {
            lock_.readLock().release();
         }
         col.clear();
      }
   }

   /**
    * Same as releaseAll now.
    */
   public void releaseForce()
   {
      releaseAll();
   }

   /**
    * Check if there is a read lock.
    *
    * @return
    */
   public boolean isReadLocked()
   {
      return (map_.readerOwners().size() != 0);
   }

   /**
    * Check if there is a write lock.
    *
    * @return
    */
   public boolean isWriteLocked()
   {
      return (map_.writerOwner() != null);
   }

   /**
    * Check if there is a read or write lock
    * @return
    */
   public boolean isLocked()
   {
      return isReadLocked() || isWriteLocked();
   }

   /**
    * Am I a lock owner?
    *
    * @param o
    * @return
    */
   public boolean isOwner(Object o)
   {
      return map_.isOwner(o, LockMap.OWNER_ANY);
   }

   //    void log(String msg) {
   //        System.out.println("** " + msg);
   //    }


   public String toString()
   {
      StringBuffer sb=new StringBuffer();
      boolean printed_read_owners=false;
      Collection read_owners=lock_ != null ? getReaderOwners() : null;
      if(read_owners != null && read_owners.size() > 0) {
         sb.append("read owners=").append(read_owners);
         printed_read_owners=true;
      }

      Object write_owner=lock_ != null ? getWriterOwner() : null;
      if(write_owner != null) {
         if(printed_read_owners)
            sb.append(", ");
         sb.append("write owner=").append(write_owner);
      }
      return sb.toString();
   }

    public void toString(StringBuffer sb)
   {
      boolean printed_read_owners=false;
      Collection read_owners=lock_ != null ? getReaderOwners() : null;
      if(read_owners != null && read_owners.size() > 0) {
         sb.append("read owners=").append(read_owners);
         printed_read_owners=true;
      }

      Object write_owner=lock_ != null ? getWriterOwner() : null;
      if(write_owner != null) {
         if(printed_read_owners)
            sb.append(", ");
         sb.append("write owner=").append(write_owner);
      }
   }
}
