/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2000 - 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.cache.interceptors;

import org.jboss.cache.DataContainer;
import org.jboss.cache.Fqn;
import org.jboss.cache.InvocationContext;
import org.jboss.cache.NodeSPI;
import org.jboss.cache.commands.CommandsFactory;
import org.jboss.cache.commands.DataCommand;
import org.jboss.cache.commands.VisitableCommand;
import org.jboss.cache.commands.WriteCommand;
import org.jboss.cache.commands.legacy.ReversibleCommand;
import org.jboss.cache.commands.read.GetChildrenNamesCommand;
import org.jboss.cache.commands.read.GetKeyValueCommand;
import org.jboss.cache.commands.read.GetKeysCommand;
import org.jboss.cache.commands.read.GetNodeCommand;
import org.jboss.cache.commands.tx.CommitCommand;
import org.jboss.cache.commands.tx.PrepareCommand;
import org.jboss.cache.commands.tx.RollbackCommand;
import org.jboss.cache.commands.write.ClearDataCommand;
import org.jboss.cache.commands.write.EvictCommand;
import org.jboss.cache.commands.write.MoveCommand;
import org.jboss.cache.commands.write.PutDataMapCommand;
import org.jboss.cache.commands.write.PutForExternalReadCommand;
import org.jboss.cache.commands.write.PutKeyValueCommand;
import org.jboss.cache.commands.write.RemoveKeyCommand;
import org.jboss.cache.commands.write.RemoveNodeCommand;
import org.jboss.cache.factories.annotations.Inject;
import org.jboss.cache.interceptors.base.PrePostProcessingCommandInterceptor;
import org.jboss.cache.lock.IsolationLevel;
import org.jboss.cache.lock.LockManager;
import static org.jboss.cache.lock.LockType.READ;
import static org.jboss.cache.lock.LockType.WRITE;
import org.jboss.cache.lock.LockUtil;
import org.jboss.cache.lock.PessimisticNodeBasedLockManager;
import org.jboss.cache.transaction.GlobalTransaction;
import org.jboss.cache.transaction.PessimisticTransactionContext;
import org.jboss.cache.transaction.TransactionContext;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

/**
 * An interceptor that handles locking. When a TX is associated, we register
 * for TX completion and unlock the locks acquired within the scope of the TX.
 * When no TX is present, we keep track of the locks acquired during the
 * current method and unlock when the method returns.
 *
 * @author Bela Ban
 * @version $Id: PessimisticLockInterceptor.java 7168 2008-11-19 17:37:20Z jason.greene@jboss.com $
 * @deprecated will be removed along with optimistic and pessimistic locking.
 */
@Deprecated
public class PessimisticLockInterceptor extends PrePostProcessingCommandInterceptor
{
   private DataContainer dataContainer;
   private PessimisticNodeBasedLockManager lockManager;
   private CommandsFactory commandsFactory;

   @Inject
   public void injectDependencies(DataContainer dataContainer, LockManager lockManager, CommandsFactory commandsFactory)
   {
      this.dataContainer = dataContainer;
      this.lockManager = (PessimisticNodeBasedLockManager) lockManager;
      this.commandsFactory = commandsFactory;
   }

   @Override
   protected Object handlePutDataMapCommand(InvocationContext ctx, PutDataMapCommand command) throws Throwable
   {
      return handlePutCommand(ctx, command, false);
   }

   @Override
   protected Object handlePutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable
   {
      return handlePutCommand(ctx, command, false);
   }

   @Override
   protected Object handlePutForExternalReadCommand(InvocationContext ctx, PutForExternalReadCommand command) throws Throwable
   {
      return handlePutCommand(ctx, command, true);
   }

   private Object handlePutCommand(InvocationContext ctx, DataCommand command, boolean zeroAcquisitionTimeout)
         throws Throwable
   {
      if ((ctx.isLockingSuppressed()) || configuration.getIsolationLevel() == IsolationLevel.NONE)
      {
         if (trace) log.trace("Suppressing locking, creating nodes if necessary");
         int treeNodeSize = command.getFqn().size();
         NodeSPI n = dataContainer.getRoot();
         for (int i = 0; i < treeNodeSize; i++)
         {
            Object childName = command.getFqn().get(i);
            Fqn childFqn = Fqn.fromElements(childName);
            NodeSPI childNode = n.getChildDirect(childFqn);
            if (childNode == null) childNode = n.addChildDirect(childFqn);
            LockUtil.manageReverseRemove(ctx, childNode, true, null, commandsFactory);
            n = childNode;
         }
      }
      else
      {
         lockManager.lockPessimistically(ctx, command.getFqn(), WRITE, true,
               zeroAcquisitionTimeout, false, true, null, false);
      }
      return invokeNextInterceptor(ctx, command);
   }

   @Override
   protected Object handlePrepareCommand(InvocationContext ctx, PrepareCommand command) throws Throwable
   {
      // 2-phase commit prepares are no-ops here.
      if (!command.isOnePhaseCommit()) return invokeNextInterceptor(ctx, command);

      // commit propagated up from the tx interceptor
      commit(ctx.getTransactionContext(), ctx.getGlobalTransaction());
      Object retVal = invokeNextInterceptor(ctx, command);
      lockManager.unlock(ctx);
      return retVal;
   }

   @Override
   protected Object handleCommitCommand(InvocationContext ctx, CommitCommand command) throws Throwable
   {
      commit(ctx.getTransactionContext(), command.getGlobalTransaction());
      if (trace) log.trace("bypassed locking as method commit() doesn't require locking");
      Object retVal = invokeNextInterceptor(ctx, command);
      lockManager.unlock(ctx);
      return retVal;
   }

   @Override
   protected Object handleRollbackCommand(InvocationContext ctx, RollbackCommand command) throws Throwable
   {
      TransactionContext transactionContext = ctx.getTransactionContext();
      if (trace)
      {
         log.trace("called to rollback cache with GlobalTransaction=" + command.getGlobalTransaction());
      }
      if (transactionContext == null)
      {
         log.error("transactionContext for transaction " + command.getGlobalTransaction() + " not found (transaction has possibly already been rolled back)");
      }
      else
      {
         for (Fqn fqn : transactionContext.getRemovedNodes())
         {
            dataContainer.removeFromDataStructure(fqn, false);
         }
         // 1. Revert the modifications by running the undo-op list in reverse. This *cannot* throw any exceptions !

         undoOperations((PessimisticTransactionContext) transactionContext);
      }
      if (trace)
      {
         log.trace("bypassed locking as method rollback() doesn't require locking");
      }
      Object retVal = invokeNextInterceptor(ctx, command);
      lockManager.unlock(ctx);
      return retVal;
   }

   private void undoOperations(PessimisticTransactionContext transactionContext)
   {
      List<WriteCommand> modificationList = transactionContext.getAllModifications();

      if (modificationList.isEmpty())
      {
         if (trace) log.trace("Modification list is null, no modifications in this transaction!");
         return;
      }

      if (trace) log.trace("undoOperations " + modificationList);

      ArrayList<WriteCommand> copy;
      copy = new ArrayList<WriteCommand>(modificationList);
      RuntimeException exc = null;
      for (ListIterator<WriteCommand> i = copy.listIterator(copy.size()); i.hasPrevious();)
      {
         WriteCommand undoOp = i.previous();
         // since we are using pessimistic locking, all pessimistic WriteCommands implement ReversibleCommand.
         ReversibleCommand txCommand = (ReversibleCommand) undoOp;
         if (log.isDebugEnabled()) log.debug("Calling rollback() on command " + undoOp);
         try
         {
            txCommand.rollback();
         }
         catch (RuntimeException re)
         {
            exc = re;
         }
      }
      if (exc != null) throw exc;
   }

   @Override
   protected Object handleMoveCommand(InvocationContext ctx, MoveCommand command) throws Throwable
   {
      if (ctx.isLockingSuppressed()) return invokeNextInterceptor(ctx, command);

      // this call will ensure the node gets a WL and its current parent gets RL.
      if (trace) log.trace("Attempting to get WL on node to be moved [" + command.getFqn() + "]");
      if (command.getFqn() != null && !(configuration.getIsolationLevel() == IsolationLevel.NONE))
      {
         lockManager.lockPessimistically(ctx, command.getFqn(), WRITE, false, false, true, false, null, false);
         if (ctx.getGlobalTransaction() != null)
         {
            ctx.getTransactionContext().addRemovedNode(command.getFqn());
         }
         lockManager.lockAllAndRecord(dataContainer.peek(command.getFqn(), true, false), WRITE, ctx);
      }
      if (command.getTo() != null && !(configuration.getIsolationLevel() == IsolationLevel.NONE))
      {
         //now for an RL for the new parent.
         if (trace) log.trace("Attempting to get RL on new parent [" + command.getTo() + "]");
         lockManager.lockPessimistically(ctx, command.getTo(), READ, false, false, false, false, null, false);
         lockManager.lockAllAndRecord(dataContainer.peek(command.getTo(), true, false), READ, ctx);
      }
      Object retValue = invokeNextInterceptor(ctx, command);

      if (ctx.getTransaction() == null) // not transactional
      {
         // do a REAL remove here.
         NodeSPI n = dataContainer.peek(command.getFqn(), true, false);
         if (n != null) lockManager.unlockAll(n, Thread.currentThread());
      }

      return retValue;
   }

   @Override
   protected Object handleRemoveNodeCommand(InvocationContext ctx, RemoveNodeCommand command) throws Throwable
   {
      if (ctx.isLockingSuppressed()) return invokeNextInterceptor(ctx, command);
      // need to make a note of ALL nodes created here!!
      List<NodeSPI> createdNodes = new LinkedList<NodeSPI>();
      // we need to mark new nodes created as deleted since they are only created to form a path to the node being removed, to
      // create a lock.
      boolean created = lockManager.lockPessimistically(ctx, command.getFqn(), WRITE, true, false, true, true, createdNodes, true);
      TransactionContext transactionContext = null;
      if (ctx.getGlobalTransaction() != null)
      {
         transactionContext = ctx.getTransactionContext();
         transactionContext.addRemovedNode(command.getFqn());
         for (NodeSPI nodeSPI : createdNodes)
         {
            transactionContext.addRemovedNode(nodeSPI.getFqn());
            nodeSPI.markAsDeleted(true);
         }
      }

      lockAllForRemoval(dataContainer.peek(command.getFqn(), false, false), ctx, transactionContext);

      if (!createdNodes.isEmpty())
      {
         if (trace) log.trace("There were new nodes created, skipping notification on delete");
         if (trace)
            log.trace("Changing 'skipNotification' for command 'remove' from " + command.isSkipSendingNodeEvents() + " to true");
         command.setSkipSendingNodeEvents(true);
      }

      Object retVal = invokeNextInterceptor(ctx, command);
      // and make sure we remove all nodes we've created for the sake of later removal.
      if (ctx.getGlobalTransaction() == null)
      {
         for (NodeSPI nodeSPI : createdNodes) dataContainer.removeFromDataStructure(nodeSPI.getFqn(), true);
         dataContainer.removeFromDataStructure(command.getFqn(), true);

         NodeSPI n = dataContainer.peek(command.getFqn(), true, false);
         if (n != null) lockManager.unlockAll(n, Thread.currentThread());
      }
      // if this is a delete op and we had to create the node, return a FALSE as nothing *really* was deleted!
      return created ? false : retVal;
   }

   /**
    * Acquires write locks on the node and all child nodes, adding children to the list of removed nodes in the context.
    *
    * @param node               node to inspect
    * @param ctx                invocation context
    * @param transactionContext transaction entry
    * @throws InterruptedException in the event of interruption
    */
   @SuppressWarnings("unchecked")
   public void lockAllForRemoval(NodeSPI node, InvocationContext ctx, TransactionContext transactionContext) throws InterruptedException
   {
      if (node == null) return;
      // lock node
      lockManager.lockAndRecord(node, WRITE, ctx);

      // add to deleted list
      if (transactionContext != null) transactionContext.addRemovedNode(node.getFqn());

      // now children.
      Map<Object, NodeSPI> children = node.getChildrenMapDirect();
      for (NodeSPI child : children.values())
      {
         // lock child.
         lockAllForRemoval(child, ctx, transactionContext);
      }
   }

   @Override
   protected Object handleRemoveKeyCommand(InvocationContext ctx, RemoveKeyCommand command) throws Throwable
   {
      lockManager.lockPessimistically(ctx, command.getFqn(), WRITE, false, false, false, false, null, false);
      return invokeNextInterceptor(ctx, command);
   }

   @Override
   protected Object handleClearDataCommand(InvocationContext ctx, ClearDataCommand command) throws Throwable
   {
      lockManager.lockPessimistically(ctx, command.getFqn(), WRITE, false, false, false, false, null, false);
      return invokeNextInterceptor(ctx, command);
   }

   @Override
   protected Object handleEvictFqnCommand(InvocationContext ctx, EvictCommand command) throws Throwable
   {
      lockManager.lockPessimistically(ctx, command.getFqn(), WRITE, false, true, false, false, null, false);
      return invokeNextInterceptor(ctx, command);
   }

   @Override
   protected Object handleGetKeyValueCommand(InvocationContext ctx, GetKeyValueCommand command) throws Throwable
   {
      lockManager.lockPessimistically(ctx, command.getFqn(), READ, false, false, false, false, null, false);
      return invokeNextInterceptor(ctx, command);
   }

   @Override
   protected Object handleGetNodeCommand(InvocationContext ctx, GetNodeCommand command) throws Throwable
   {
      lockManager.lockPessimistically(ctx, command.getFqn(), READ, false, false, false, false, null, false);
      return invokeNextInterceptor(ctx, command);
   }

   @Override
   protected Object handleGetKeysCommand(InvocationContext ctx, GetKeysCommand command) throws Throwable
   {
      lockManager.lockPessimistically(ctx, command.getFqn(), READ, false, false, false, false, null, false);
      return invokeNextInterceptor(ctx, command);
   }

   @Override
   protected Object handleGetChildrenNamesCommand(InvocationContext ctx, GetChildrenNamesCommand command) throws Throwable
   {
      lockManager.lockPessimistically(ctx, command.getFqn(), READ, false, false, false, false, null, false);
      return invokeNextInterceptor(ctx, command);
   }

   @Override
   public void doAfterCall(InvocationContext ctx, VisitableCommand command)
   {
      if (!ctx.isValidTransaction()) lockManager.unlock(ctx);
   }

   /**
    * Remove all locks held by <tt>tx</tt>, remove the transaction from the transaction table
    */
   private void commit(TransactionContext transactionContext, GlobalTransaction gtx)
   {
      if (trace) log.trace("committing cache with gtx " + gtx);
      if (transactionContext == null)
      {
         log.error("transactionContext for transaction " + gtx + " not found (maybe already committed)");
         return;
      }
      // first remove nodes that should be deleted.
      for (Fqn fqn : transactionContext.getRemovedNodes())
      {
         dataContainer.removeFromDataStructure(fqn, false);
      }
   }

}
