/*
 * JBoss, Home of Professional Open Source
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.cache;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.commands.VisitableCommand;
import org.jboss.cache.config.Option;
import org.jboss.cache.lock.NodeLock;
import org.jboss.cache.marshall.MethodCall;
import org.jboss.cache.transaction.GlobalTransaction;
import org.jboss.cache.transaction.TransactionEntry;
import org.jboss.cache.transaction.TransactionTable;

import javax.transaction.Transaction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * This context holds information specific to a method invocation.
 *
 * @author <a href="mailto:manik@jboss.org">Manik Surtani (manik@jboss.org)</a>
 */
@SuppressWarnings("deprecation")
public class InvocationContext implements Cloneable
{
   private static final Log log = LogFactory.getLog(InvocationContext.class);
   private static final boolean trace = log.isTraceEnabled();

   private Transaction transaction;
   private GlobalTransaction globalTransaction;
   private TransactionEntry transactionEntry;
   private Option optionOverrides;
   // defaults to true.
   private boolean originLocal = true;
   private boolean txHasMods;
   private boolean localRollbackOnly;
   @Deprecated
   private MethodCall methodCall;
   @Deprecated
   private VisitableCommand command;
   List<NodeLock> invocationLocks;

   // used to store cache peeks within the scope of a single context. Performing a cache peek can be a huge bottle neck.
   // See JBCACHE-811
   // backing out for now
   //private Map<Fqn, NodeSPI> peekedNodes = new HashMap<Fqn, NodeSPI>();

   /**
    * Retrieves a node that may have previously been "peeked" within the scope of the same invocation.
    *
    * @param f fqn of node to find
    * @return node, if previously peeked, or null if not.
    * @since 2.1.0
    */
//   public NodeSPI getPeekedNode(Fqn f)
//   {
//      return peekedNodes.get(f);
//   }

   /**
    * Adds a node to the previously peeked list.
    *
    * @param n node to add
    * @param f fqn of node
    * @since 2.1.0
    */
//   public void savePeekedNode(NodeSPI n, Fqn f)
//   {
//      peekedNodes.put(f, n);
//   }

   /**
    * Wipe list of previously peeked nodes.
    *
    * @since 2.1.0
    */
//   public void wipePeekedNodes()
//   {
//      peekedNodes.clear();
//   }
   public void setLocalRollbackOnly(boolean localRollbackOnly)
   {
      this.localRollbackOnly = localRollbackOnly;
   }

   /**
    * Retrieves the transaction associated with this invocation
    *
    * @return The transaction associated with this invocation
    */
   public Transaction getTransaction()
   {
      return transaction;
   }

   /**
    * Sets the transaction associated with this invocation
    *
    * @param transaction
    */
   public void setTransaction(Transaction transaction)
   {
      this.transaction = transaction;
   }

   /**
    * @return the transaction entry associated with the current transaction, or null if the current thread is not associated with a transaction.
    * @since 2.2.0
    */
   public TransactionEntry getTransactionEntry()
   {
      return transactionEntry;
   }

   /**
    * Sets the transaction entry to be associated with the current thread.
    *
    * @param transactionEntry transaction entry to set
    * @since 2.2.0
    */
   public void setTransactionEntry(TransactionEntry transactionEntry)
   {
      this.transactionEntry = transactionEntry;
   }

   /**
    * Retrieves the global transaction associated with this invocation
    *
    * @return the global transaction associated with this invocation
    */
   public GlobalTransaction getGlobalTransaction()
   {
      return globalTransaction;
   }

   /**
    * Sets the global transaction associated with this invocation
    *
    * @param globalTransaction
    */
   public void setGlobalTransaction(GlobalTransaction globalTransaction)
   {
      this.globalTransaction = globalTransaction;
   }

   /**
    * Retrieves the option overrides associated with this invocation
    *
    * @return the option overrides associated with this invocation
    */
   public Option getOptionOverrides()
   {
      if (optionOverrides == null)
      {
         optionOverrides = new Option();
      }
      return optionOverrides;
   }

   public boolean isOptionsUninitialised()
   {
      return optionOverrides == null;
   }

   /**
    * Sets the option overrides associated with this invocation
    *
    * @param optionOverrides
    */
   public void setOptionOverrides(Option optionOverrides)
   {
      this.optionOverrides = optionOverrides;
   }

   /**
    * Tests if this invocation originated locally or from a remote cache.
    *
    * @return true if the invocation originated locally.
    */
   public boolean isOriginLocal()
   {
      return originLocal;
   }

   public List<NodeLock> getInvocationLocksAcquired()
   {
      return invocationLocks;
   }

   public void addInvocationLocksAcquired(Collection<NodeLock> locks)
   {
      // no need to worry about concurrency here - a context is only valid for a single thread.
      if (invocationLocks == null) invocationLocks = new ArrayList<NodeLock>(5);
      invocationLocks.addAll(locks);
   }

   public void addInvocationLockAcquired(NodeLock l)
   {
      // no need to worry about concurrency here - a context is only valid for a single thread.
      if (invocationLocks == null) invocationLocks = new ArrayList<NodeLock>(5);
      invocationLocks.add(l);
   }

   /**
    * @return true if options exist to suppress locking - false otherwise.  Note that this is only used by the {@link org.jboss.cache.interceptors.PessimisticLockInterceptor}.
    */
   public boolean isLockingSuppressed()
   {
      return getOptionOverrides() != null && getOptionOverrides().isSuppressLocking();
   }

   /**
    * If set to true, the invocation is assumed to have originated locally.  If set to false,
    * assumed to have originated from a remote cache.
    *
    * @param originLocal
    */
   public void setOriginLocal(boolean originLocal)
   {
      this.originLocal = originLocal;
   }

   @Override
   public String toString()
   {
      return "InvocationContext{" +
            "transaction=" + transaction +
            ", globalTransaction=" + globalTransaction +
            ", optionOverrides=" + optionOverrides +
            ", originLocal=" + originLocal +
            ", txHasMods=" + txHasMods +
            '}';
   }

   public boolean isTxHasMods()
   {
      return txHasMods;
   }

   public void setTxHasMods(boolean b)
   {
      txHasMods = b;
   }

   public boolean isLocalRollbackOnly()
   {
      return localRollbackOnly;
   }

   /**
    * Resets this to the defaults used when constructing an invocation context object
    */
   public void reset()
   {
      transaction = null;
      globalTransaction = null;
      optionOverrides = null;
      originLocal = true;
      txHasMods = false;
      invocationLocks = null;
      methodCall = null;
      command = null;
   }

   @Override
   public InvocationContext clone() throws CloneNotSupportedException
   {
      InvocationContext clone = (InvocationContext) super.clone();
      clone.setOptionOverrides(getOptionOverrides().clone());
      return clone;
   }

   /**
    * Sets the state of the InvocationContext based on the template context passed in
    *
    * @param template
    */
   public void setState(InvocationContext template)
   {
      if (template == null)
      {
         throw new NullPointerException("Template InvocationContext passed in to InvocationContext.setState() passed in is null");
      }

      this.setGlobalTransaction(template.getGlobalTransaction());
      this.setLocalRollbackOnly(template.isLocalRollbackOnly());
      this.setOptionOverrides(template.getOptionOverrides());
      this.setOriginLocal(template.isOriginLocal());
      this.setTransaction(template.getTransaction());
      this.setTxHasMods(template.isTxHasMods());
   }

   @Override
   public boolean equals(Object o)
   {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      final InvocationContext that = (InvocationContext) o;

      if (localRollbackOnly != that.localRollbackOnly) return false;
      if (originLocal != that.originLocal) return false;
      if (txHasMods != that.txHasMods) return false;
      if (globalTransaction != null ? !globalTransaction.equals(that.globalTransaction) : that.globalTransaction != null)
      {
         return false;
      }
      if (optionOverrides != null ? !optionOverrides.equals(that.optionOverrides) : that.optionOverrides != null)
      {
         return false;
      }
      if (transaction != null ? !transaction.equals(that.transaction) : that.transaction != null) return false;

      return true;
   }

   @Override
   public int hashCode()
   {
      int result;
      result = (transaction != null ? transaction.hashCode() : 0);
      result = 29 * result + (globalTransaction != null ? globalTransaction.hashCode() : 0);
      result = 29 * result + (optionOverrides != null ? optionOverrides.hashCode() : 0);
      result = 29 * result + (originLocal ? 1 : 0);
      result = 29 * result + (txHasMods ? 1 : 0);
      result = 29 * result + (localRollbackOnly ? 1 : 0);
      return result;
   }

   /**
    * @return the method call associated with this invocation
    */
   @Deprecated
   @SuppressWarnings("deprecation")
   public MethodCall getMethodCall()
   {
      if (methodCall == null)
      {
         methodCall = createMethodCall();
      }
      return methodCall;
   }

   @SuppressWarnings("deprecation")
   private MethodCall createMethodCall()
   {
      if (command == null) return null;
      MethodCall call = new MethodCall();
      call.setMethodId(command.getCommandId());
      call.setArgs(command.getParameters());
      return call;
   }


   /**
    * Sets the method call associated with this invocation.
    *
    * @param methodCall methodcall to set
    * @deprecated not used anymore.  Interceptors will get a {@link org.jboss.cache.commands.ReplicableCommand} instance passed in along with an InvocationContext.
    */
   @Deprecated
   public void setMethodCall(MethodCall methodCall)
   {
      this.methodCall = methodCall;
   }

   /**
    * Factory method that creates a context with a given method call.
    *
    * @param methodCall methodcall to use
    * @return invocation context
    */
   public static InvocationContext fromMethodCall(MethodCall methodCall)
   {
      InvocationContext ctx = new InvocationContext();
      ctx.methodCall = methodCall;
      return ctx;
   }

   /**
    * If the acq timeout if overwritten for current call, then return that one.
    * If not overwritten return default value.
    */
   public long getContextLockAcquisitionTimeout(long timeout)
   {
      if (getOptionOverrides() != null
            && getOptionOverrides().getLockAcquisitionTimeout() >= 0)
      {
         timeout = getOptionOverrides().getLockAcquisitionTimeout();
      }
      return timeout;
   }

   /**
    * This is only used for backward compatibility with old interceptors implementation and should <b>NOT</b> be
    * use by any new custom interceptors. The commands is now passed in as the second param in each implementing
    * handlers (handler = method in ChainedInterceptor class)
    */
   @Deprecated
   @SuppressWarnings("deprecation")
   public void setCommand(VisitableCommand cacheCommand)
   {
      this.command = cacheCommand;
   }

   /**
    * @see #setCommand(org.jboss.cache.commands.VisitableCommand)
    */
   @Deprecated
   @SuppressWarnings("deprecation")
   public VisitableCommand getCommand()
   {
      return command;
   }

   public boolean isValidTransaction()
   {
      return transaction != null && TransactionTable.isValid(transaction);
   }

   public void throwIfNeeded(Throwable e) throws Throwable
   {
      Option optionOverride = getOptionOverrides();
      boolean shouldRethtrow = optionOverride == null || !optionOverride.isFailSilently();
      if (!shouldRethtrow)
      {
         if (trace)
            log.trace("There was a problem handling this request, but failSilently was set, so suppressing exception", e);
         return;
      }
      throw e;
   }
}
