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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.CacheException;
import org.jboss.cache.CacheSPI;
import org.jboss.cache.Fqn;
import org.jboss.cache.NodeSPI;
import org.jboss.cache.RegionEmptyException;
import org.jboss.cache.RegionManager;
import org.jboss.cache.config.Configuration;
import org.jboss.cache.factories.annotations.Inject;
import org.jboss.cache.factories.annotations.NonVolatile;
import org.jboss.cache.loader.CacheLoaderManager;
import org.jboss.cache.lock.LockManager;
import static org.jboss.cache.lock.LockType.READ;
import org.jboss.cache.lock.TimeoutException;
import org.jboss.cache.marshall.InactiveRegionException;
import org.jboss.cache.marshall.Marshaller;
import org.jboss.cache.marshall.NodeData;
import org.jboss.cache.marshall.NodeDataMarker;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

@NonVolatile
public class StateTransferManager
{
   protected final static Log log = LogFactory.getLog(StateTransferManager.class);

   public static final NodeData STREAMING_DELIMITER_NODE = new NodeDataMarker();

   public static final String PARTIAL_STATE_DELIMITER = "_PARTIAL_STATE_DELIMITER";

   private CacheSPI cache;
   private Marshaller marshaller;
   private RegionManager regionManager;
   private Configuration configuration;
   private LockManager lockManager;

   public StateTransferManager()
   {
   }

   @Inject
   public void injectDependencies(CacheSPI cache, Marshaller marshaller, RegionManager regionManager, Configuration configuration, LockManager lockManager)
   {
      this.cache = cache;
      this.regionManager = regionManager;
      this.marshaller = marshaller;
      this.configuration = configuration;
      this.lockManager = lockManager;
   }

   public StateTransferManager(CacheSPI cache)
   {
      this.cache = cache;
   }

   /**
    * Writes the state for the portion of the tree named by <code>fqn</code> to
    * the provided OutputStream.
    * <p/>
    * <p/>
    *
    * @param out            stream to write state to
    * @param fqn            Fqn indicating the uppermost node in the
    *                       portion of the tree whose state should be returned.
    * @param timeout        max number of ms this method should wait to acquire
    *                       a read lock on the nodes being transferred
    * @param force          if a read lock cannot be acquired after
    *                       <code>timeout</code> ms, should the lock acquisition
    *                       be forced, and any existing transactions holding locks
    *                       on the nodes be rolled back? <strong>NOTE:</strong>
    *                       In release 1.2.4, this parameter has no effect.
    * @param suppressErrors should any Throwable thrown be suppressed?
    * @throws Throwable in event of error
    */
   public void getState(ObjectOutputStream out, Fqn fqn, long timeout, boolean force, boolean suppressErrors) throws Throwable
   {
      // can't give state for regions currently being activated/inactivated
      boolean canProvideState = (!regionManager.isInactive(fqn) && cache.peek(fqn, false) != null);

      boolean fetchTransientState = configuration.isFetchInMemoryState();
      CacheLoaderManager cacheLoaderManager = cache.getCacheLoaderManager();
      boolean fetchPersistentState = cacheLoaderManager != null && cacheLoaderManager.isFetchPersistentState();

      if (canProvideState && (fetchPersistentState || fetchTransientState))
      {
         marshaller.objectToObjectStream(true, out);
         StateTransferGenerator generator = getStateTransferGenerator();
         Object owner = getOwnerForLock();
         long startTime = System.currentTimeMillis();
         NodeSPI rootNode = cache.peek(fqn, false, false);

         try
         {
            if (log.isDebugEnabled())
            {
               log.debug("locking the " + fqn + " subtree to return the in-memory (transient) state");
            }
            acquireLocksForStateTransfer(rootNode, owner, timeout, true, force);
            generator.generateState(out, rootNode, fetchTransientState, fetchPersistentState, suppressErrors);
            if (log.isDebugEnabled())
            {
               log.debug("Successfully generated state in " + (System.currentTimeMillis() - startTime) + " msec");
            }
         }
         finally
         {
            releaseStateTransferLocks(rootNode, owner, true);
         }
      }
      else
      {
         marshaller.objectToObjectStream(false, out);
         Exception e = null;
         if (!canProvideState)
         {
            String exceptionMessage = "Cache instance at " + cache.getLocalAddress() + " cannot provide state for fqn " + fqn + ".";

            if (regionManager.isInactive(fqn))
            {
               exceptionMessage += " Region for fqn " + fqn + " is inactive.";
               e = new InactiveRegionException(exceptionMessage);
            }
            // this is not really an exception.  Just provide empty state. The exception is just a signal.  Yes, lousy.  - JBCACHE-1349
            if (cache.peek(fqn, false, false) == null)
            {
               e = new RegionEmptyException();
            }
         }
         if (!fetchPersistentState && !fetchTransientState)
         {
            e = new CacheException("Cache instance at " + cache.getLocalAddress() + " is not configured to provide state");
         }
         marshaller.objectToObjectStream(e, out);
         if (e != null) throw e;
      }
   }

   /**
    * Set the portion of the cache rooted in <code>targetRoot</code>
    * to match the given state. Updates the contents of <code>targetRoot</code>
    * to reflect those in <code>new_state</code>.
    * <p/>
    * <strong>NOTE:</strong> This method performs no locking of nodes; it
    * is up to the caller to lock <code>targetRoot</code> before calling
    * this method.
    * <p/>
    * This method will use any {@link ClassLoader} needed as defined by the active {@link org.jboss.cache.Region}
    * in the {@link org.jboss.cache.RegionManager}, pertaining to the targetRoot passed in.
    *
    * @param in         an input stream containing the state
    * @param targetRoot fqn of the node into which the state should be integrated
    * @throws Exception In event of error
    */
   public void setState(ObjectInputStream in, Fqn targetRoot) throws Exception
   {
      NodeSPI target = cache.peek(targetRoot, false, false);
      if (target == null)
      {
         // Create the integration root, but do not replicate
         cache.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);

         //needed for BR state transfers
         cache.getInvocationContext().getOptionOverrides().setSkipCacheStatusCheck(true);
         cache.put(targetRoot, null);
         target = cache.peek(targetRoot, false, false);
      }
      Object o = marshaller.objectFromObjectStream(in);
      Boolean hasState = (Boolean) o;
      if (hasState)
      {
         setState(in, target);
      }
      else
      {
         throw new CacheException("Cache instance at " + cache.getLocalAddress()
               + " cannot integrate state since state provider could not provide state due to " + marshaller.objectFromObjectStream(in));
      }
   }

   /**
    * Set the portion of the cache rooted in <code>targetRoot</code>
    * to match the given state. Updates the contents of <code>targetRoot</code>
    * to reflect those in <code>new_state</code>.
    * <p/>
    * <strong>NOTE:</strong> This method performs no locking of nodes; it
    * is up to the caller to lock <code>targetRoot</code> before calling
    * this method.
    *
    * @param state      a serialized byte[][] array where element 0 is the
    *                   transient state (or null) , and element 1 is the
    *                   persistent state (or null)
    * @param targetRoot node into which the state should be integrated
    */
   private void setState(ObjectInputStream state, NodeSPI targetRoot) throws Exception
   {
      Object owner = getOwnerForLock();
      long timeout = configuration.getStateRetrievalTimeout();
      long startTime = System.currentTimeMillis();

      try
      {
         // Acquire a lock on the root node
         acquireLocksForStateTransfer(targetRoot, owner, timeout, true, true);

         /*
          * Vladimir/Manik/Brian (Dec 7,2006)
          *
          * integrator.integrateState(in,targetRoot, cl) will call cache.put for each
          * node read from stream. Having option override below allows nodes read
          * to be directly stored into a tree since we bypass interceptor chain.
          *
          */

//         Option option = new Option();
//         option.setBypassInterceptorChain(true);
//         cache.getInvocationContext().setOptionOverrides(option);
//
         StateTransferIntegrator integrator = getStateTransferIntegrator(state, targetRoot.getFqn());
         if (log.isDebugEnabled())
         {
            log.debug("starting state integration at node " + targetRoot);
         }
         integrator.integrateState(state, targetRoot);
         if (log.isDebugEnabled())
         {
            log.debug("successfully integrated state in " + (System.currentTimeMillis() - startTime) + " msec");
         }
      }
      finally
      {
         releaseStateTransferLocks(targetRoot, owner, true);
      }
   }


   /**
    * Acquires locks on a root node for an owner for state transfer.
    */
   protected void acquireLocksForStateTransfer(NodeSPI root,
                                               Object lockOwner,
                                               long timeout,
                                               boolean lockChildren,
                                               boolean force)
         throws Exception
   {
      try
      {
         if (lockChildren)
         {
            lockManager.lockAll(root, READ, lockOwner, timeout, true);
         }
         else
         {
            lockManager.lock(Fqn.ROOT, READ, lockOwner, timeout);
         }
      }
      catch (TimeoutException te)
      {
         log.error("Caught TimeoutException acquiring locks on region " +
               root.getFqn(), te);
         if (force)
         {
            // Until we have FLUSH in place, don't force locks
            //            forceAcquireLock(root, lockOwner, lockChildren);
            throw te;

         }
         else
         {
            throw te;
         }
      }
   }

   /**
    * Releases all state transfer locks acquired.
    *
    * @see #acquireLocksForStateTransfer
    */
   protected void releaseStateTransferLocks(NodeSPI root,
                                            Object lockOwner,
                                            boolean childrenLocked)
   {
      try
      {
         if (childrenLocked)
         {
            lockManager.unlockAll(root, lockOwner);
         }
         else
         {
            lockManager.unlock(Fqn.ROOT, lockOwner);
         }
      }
      catch (Throwable t)
      {
         log.error("failed releasing locks", t);
      }
   }

   protected StateTransferGenerator getStateTransferGenerator()
   {
      return StateTransferFactory.getStateTransferGenerator(cache);
   }

   protected StateTransferIntegrator getStateTransferIntegrator(ObjectInputStream istream, Fqn fqn) throws Exception
   {
      return StateTransferFactory.getStateTransferIntegrator(istream, fqn, cache);
   }

   /**
    * Returns an object suitable for use in node locking, either the current
    * transaction or the current thread if there is no transaction.
    */
   private Object getOwnerForLock()
   {
      Object owner = cache.getCurrentTransaction();
      if (owner == null)
      {
         owner = Thread.currentThread();
      }
      return owner;
   }
}
