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

import org.apache.commons.logging.LogFactory;
import org.jboss.cache.CacheException;
import org.jboss.cache.DataContainer;
import org.jboss.cache.Fqn;
import org.jboss.cache.InvocationContext;
import org.jboss.cache.NodeFactory;
import org.jboss.cache.NodeNotExistsException;
import org.jboss.cache.NodeSPI;
import org.jboss.cache.commands.ReversibleCommand;
import org.jboss.cache.commands.read.GetChildrenNamesCommand;
import org.jboss.cache.commands.read.GetDataMapCommand;
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.write.ClearDataCommand;
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.factories.annotations.Start;
import org.jboss.cache.notifications.Notifier;
import static org.jboss.cache.notifications.event.NodeModifiedEvent.ModificationType.*;
import org.jboss.cache.optimistic.DataVersion;
import org.jboss.cache.optimistic.DefaultDataVersion;
import org.jboss.cache.optimistic.TransactionWorkspace;
import org.jboss.cache.optimistic.WorkspaceNode;
import org.jboss.cache.transaction.GlobalTransaction;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;

/**
 * Operations on nodes are done on the copies that exist in the workspace rather than passed down
 * to the {@link org.jboss.cache.interceptors.CallInterceptor}.  These operations happen in this interceptor.
 *
 * @author <a href="mailto:manik@jboss.org">Manik Surtani (manik@jboss.org)</a>
 * @author <a href="mailto:stevew@jofti.com">Steve Woodcock (stevew@jofti.com)</a>
 */
public class OptimisticNodeInterceptor extends OptimisticInterceptor
{
   /**
    * Needed for the creation of workspace nodes based on underlying nodes in the cache.
    */
   private NodeFactory nodeFactory;
   private Notifier notifier;
   private DataContainer dataContainer;
   private long lockAcquisitionTimeout;

   @Inject
   protected void injectDependencies(Notifier notifier, NodeFactory nodeFactory, DataContainer dataContainer)
   {
      this.notifier = notifier;
      this.nodeFactory = nodeFactory;
      this.dataContainer = dataContainer;
   }

   @Start
   private void init()
   {
      lockAcquisitionTimeout = configuration.getLockAcquisitionTimeout();
   }

   public OptimisticNodeInterceptor()
   {
      log = LogFactory.getLog(getClass());
      trace = log.isTraceEnabled();
   }

   @Override
   public Object visitRemoveNodeCommand(InvocationContext ctx, RemoveNodeCommand command) throws Throwable
   {
      TransactionWorkspace workspace = getTransactionWorkspace(ctx);
      WorkspaceNode workspaceNode = fetchWorkspaceNode(ctx, command.getFqn(), workspace, false, true);
      if (workspaceNode != null)
      {
         setVersioning(ctx, workspace, workspaceNode);
      }
      Object result = removeNode(workspace, workspaceNode, true, ctx);
      addToModificationList(command, ctx);
      return result;
   }

   @Override
   public Object visitPutForExternalReadCommand(InvocationContext ctx, PutForExternalReadCommand command) throws Throwable
   {
      return visitPutKeyValueCommand(ctx, command);
   }

   @Override
   public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable
   {
      TransactionWorkspace workspace = getTransactionWorkspace(ctx);
      WorkspaceNode workspaceNode = fetchWorkspaceNode(ctx, command.getFqn(), workspace, true, true);
      if (workspaceNode != null)
      {
         setVersioning(ctx, workspace, workspaceNode);
      }
      else
      {
         // "fail-more-silently" patch thanks to Owen Taylor - JBCACHE-767
         if ((ctx.getOptionOverrides() == null || !ctx.getOptionOverrides().isFailSilently()))
         {
            throw new CacheException("Unable to set node version for " + command.getFqn() + ", node is null.");
         }
      }
      Object result = putDataKeyValueAndNotify(command.getKey(), command.getValue(), workspace, workspaceNode, ctx);
      addToModificationList(command, ctx);
      return result;
   }

   @Override
   public Object visitPutDataMapCommand(InvocationContext ctx, PutDataMapCommand command) throws Throwable
   {
      TransactionWorkspace workspace = getTransactionWorkspace(ctx);
      WorkspaceNode workspaceNode = fetchWorkspaceNode(ctx, command.getFqn(), workspace, true, true);
      if (workspaceNode != null)
      {
         setVersioning(ctx, workspace, workspaceNode);
      }
      else
      {
         // "fail-more-silently" patch thanks to Owen Taylor - JBCACHE-767
         if ((ctx.getOptionOverrides() == null || !ctx.getOptionOverrides().isFailSilently()))
         {
            throw new CacheException("Unable to set node version for " + command.getFqn() + ", node is null.");
         }
      }
      putDataMapAndNotify(command.getData(), workspace, workspaceNode, ctx);
      addToModificationList(command, ctx);
      return null;
   }


   @Override
   public Object visitMoveCommand(InvocationContext ctx, MoveCommand command) throws Throwable
   {
      TransactionWorkspace workspace = getTransactionWorkspace(ctx);
      WorkspaceNode workspaceNode = fetchWorkspaceNode(ctx, command.getFqn(), workspace, true, true);
      if (ctx.isOriginLocal() && ctx.getOptionOverrides() != null && ctx.getOptionOverrides().getDataVersion() != null)
      {
         throw new CacheException("Setting a data version while performing a move() is not supported!!");
      }
      if (workspaceNode != null)
      {
         setVersioning(ctx, workspace, workspaceNode);
      }
      moveNodeAndNotify(command.getTo(), workspaceNode, workspace, ctx);
      addToModificationList(command, ctx);
      return null;
   }

   @Override
   public Object visitRemoveKeyCommand(InvocationContext ctx, RemoveKeyCommand command) throws Throwable
   {
      TransactionWorkspace workspace = getTransactionWorkspace(ctx);
      WorkspaceNode workspaceNode = fetchWorkspaceNode(ctx, command.getFqn(), workspace, true, true);

      if (workspaceNode != null)
      {
         setVersioning(ctx, workspace, workspaceNode);
      }
      Object result = removeKeyAndNotify(command.getKey(), workspace, workspaceNode, ctx);
      addToModificationList(command, ctx);
      return result;
   }

   @Override
   public Object visitClearDataCommand(InvocationContext ctx, ClearDataCommand command) throws Throwable
   {
      TransactionWorkspace workspace = getTransactionWorkspace(ctx);
      WorkspaceNode workspaceNode = fetchWorkspaceNode(ctx, command.getFqn(), workspace, true, true);
      if (workspaceNode != null)
      {
         setVersioning(ctx, workspace, workspaceNode);
      }
      removeDataAndNotify(workspace, workspaceNode, ctx);
      addToModificationList(command, ctx);
      return null;
   }

   @Override
   public Object visitGetKeyValueCommand(InvocationContext ctx, GetKeyValueCommand command) throws Throwable
   {
      TransactionWorkspace workspace = getTransactionWorkspace(ctx);
      Object result;
      WorkspaceNode workspaceNode = fetchWorkspaceNode(ctx, command.getFqn(), workspace, false, false);

      if (workspaceNode == null)
      {
         if (trace) log.debug("Unable to find node " + command.getFqn() + " in workspace.");
         result = null;
      }
      else
      {
         //add this node into the wrokspace
         notifier.notifyNodeVisited(command.getFqn(), true, ctx);
         Object val = workspaceNode.get(command.getKey());
         workspace.addNode(workspaceNode);
         notifier.notifyNodeVisited(command.getFqn(), false, ctx);
         result = val;
      }
      return result;
   }

   @Override
   public Object visitGetKeysCommand(InvocationContext ctx, GetKeysCommand command) throws Throwable
   {
      TransactionWorkspace workspace = getTransactionWorkspace(ctx);
      Object result;
      Fqn fqn = command.getFqn();
      WorkspaceNode workspaceNode = fetchWorkspaceNode(ctx, fqn, workspace, false, false);
      if (workspaceNode == null)
      {
         if (trace) log.trace("unable to find node " + fqn + " in workspace.");
         result = null;
      }
      else
      {
         notifier.notifyNodeVisited(fqn, true, ctx);
         Object keySet = workspaceNode.getKeys();
         workspace.addNode(workspaceNode);
         notifier.notifyNodeVisited(fqn, false, ctx);
         result = keySet;
      }
      return result;
   }

   @Override
   public Object visitGetDataMapCommand(InvocationContext ctx, GetDataMapCommand command) throws Throwable
   {
      TransactionWorkspace workspace = getTransactionWorkspace(ctx);
      Object result;
      WorkspaceNode workspaceNode = fetchWorkspaceNode(ctx, command.getFqn(), workspace, false, false);
      if (workspaceNode == null)
      {
         if (trace) log.trace("unable to find node " + command.getFqn() + " in workspace.");
         result = null;
      }
      else
      {
         notifier.notifyNodeVisited(command.getFqn(), true, ctx);
         Object data = workspaceNode.getData();
         workspace.addNode(workspaceNode);
         notifier.notifyNodeVisited(command.getFqn(), false, ctx);
         result = data;
      }
      return result;
   }

   @Override
   public Object visitGetChildrenNamesCommand(InvocationContext ctx, GetChildrenNamesCommand command) throws Throwable
   {
      TransactionWorkspace workspace = getTransactionWorkspace(ctx);
      Object result;
      WorkspaceNode workspaceNode = fetchWorkspaceNode(ctx, command.getFqn(), workspace, false, false);
      if (workspaceNode == null)
      {
         if (trace) log.trace("Unable to find node " + command.getFqn() + " in workspace.");
         result = null;
      }
      else
      {
         notifier.notifyNodeVisited(command.getFqn(), true, ctx);
         Object nameSet = workspaceNode.getChildrenNames();
         workspace.addNode(workspaceNode);
         notifier.notifyNodeVisited(command.getFqn(), false, ctx);
         result = nameSet;
      }
      return result;
   }

   @Override
   public Object visitGetNodeCommand(InvocationContext ctx, GetNodeCommand command) throws Throwable
   {
      TransactionWorkspace workspace = getTransactionWorkspace(ctx);
      Object result;
      WorkspaceNode workspaceNode = fetchWorkspaceNode(ctx, command.getFqn(), workspace, false, false);
      if (workspaceNode == null)
      {
         if (trace) log.trace("Unable to find node " + command.getFqn() + " in workspace.");
         result = null;
      }
      else if (workspaceNode.isDeleted())
      {
         if (trace) log.trace("Attempted to retrieve node " + command.getFqn() + " but it has been deleted!");
         result = null;
      }
      else
      {
         notifier.notifyNodeVisited(command.getFqn(), true, ctx);
         workspace.addNode(workspaceNode);
         notifier.notifyNodeVisited(command.getFqn(), false, ctx);
         result = workspaceNode.getNode();
      }
      return result;
   }

   private void setVersioning(InvocationContext ctx, TransactionWorkspace workspace, WorkspaceNode workspaceNode)
   {
      // use explicit versioning
      if (ctx.getOptionOverrides() != null && ctx.getOptionOverrides().getDataVersion() != null)
      {
         workspace.setVersioningImplicit(false);
         DataVersion version = ctx.getOptionOverrides().getDataVersion();

         workspaceNode.setVersion(version);
         if (trace) log.trace("Setting versioning for node " + workspaceNode.getFqn() + " to explicit");

         workspaceNode.setVersioningImplicit(false);
      }
      else
      {
         if (trace) log.trace("Setting versioning for node " + workspaceNode.getFqn() + " to implicit");
         workspaceNode.setVersioningImplicit(true);
      }
   }

   /**
    * Retrieves a backup fqn in an array of arguments.  This is typically used to parse arguments from a data gravitation cleanup method.
    *
    * @param args array of arguments to parse
    * @return an Fqn
    */
   private Fqn getBackupFqn(Object[] args)
   {
      return (Fqn) args[1];
   }

   /**
    * Adds a method call to the modification list of a given transaction's transaction entry
    */
   private void addToModificationList(ReversibleCommand command, InvocationContext ctx)
   {
      ctx.getTransactionEntry().addModification(command);
      if (log.isDebugEnabled()) log.debug("Adding command " + command + " to modification list");
   }

   // -----------------------------------------------------------------

   // Methods that mimic core functionality in the cache, except that they work on WorkspaceNodes in the TransactionWorkspace.

   // -----------------------------------------------------------------


   /**
    * Performs a move within the workspace.
    *
    * @param parentFqn parent under which the node is to be moved
    * @param node      node to move
    * @param ws        transaction workspace
    * @param ctx
    */
   private void moveNodeAndNotify(Fqn parentFqn, WorkspaceNode node, TransactionWorkspace ws, InvocationContext ctx)
   {
      Fqn nodeFqn = node.getFqn();
      if (nodeFqn.isRoot())
      {
         log.warn("Attempting to move the root node.  Not taking any action, treating this as a no-op.");
         return;
      }

      WorkspaceNode oldParent = fetchWorkspaceNode(ctx, nodeFqn.getParent(), ws, false, true);
      if (oldParent == null) throw new NodeNotExistsException("Node " + nodeFqn.getParent() + " does not exist!");

      if (parentFqn.equals(oldParent.getFqn()))
      {
         log.warn("Attempting to move a node in same place.  Not taking any action, treating this as a no-op.");
         return;
      }
      // retrieve parent
      WorkspaceNode parent = fetchWorkspaceNode(ctx, parentFqn, ws, false, true);
      if (parent == null) throw new NodeNotExistsException("Node " + parentFqn + " does not exist!");

      Object nodeName = nodeFqn.getLastElement();

      // now that we have the parent and target nodes:
      // first correct the pointers at the pruning point
      oldParent.removeChild(nodeName);

      // parent pointer is calculated on the fly using Fqns.
      // now adjust Fqns of node and all children.
      Fqn nodeNewFqn = Fqn.fromRelativeElements(parent.getFqn(), nodeFqn.getLastElement());

      // pre-notify
      notifier.notifyNodeMoved(nodeFqn, nodeNewFqn, true, ctx);
      recursiveMoveNode(ctx, node, parent.getFqn(), ws);

      // remove old nodes. this may mark some nodes which have already been moved as deleted
      removeNode(ws, node, false, ctx);

      // post-notify
      notifier.notifyNodeMoved(nodeFqn, nodeNewFqn, false, ctx);
   }

   /**
    * Moves a node to a new base.
    *
    * @param node    node to move
    * @param newBase new base Fqn under which the given node will now exist
    * @param ws      transaction workspace
    */
   private void recursiveMoveNode(InvocationContext ctx, WorkspaceNode node, Fqn newBase, TransactionWorkspace ws)
   {
      Fqn newFqn = Fqn.fromRelativeElements(newBase, node.getFqn().getLastElement());
      WorkspaceNode movedNode = fetchWorkspaceNode(ctx, newFqn, ws, true, true);
      movedNode.putAll(node.getData());

      // invoke children
      for (Object n : node.getChildrenNames())
      {
         WorkspaceNode child = fetchWorkspaceNode(ctx, Fqn.fromRelativeElements(node.getFqn(), n), ws, false, true);
         if (child != null) recursiveMoveNode(ctx, child, newFqn, ws);
      }
   }

   private void putDataMapAndNotify(Map<Object, Object> data, TransactionWorkspace workspace, WorkspaceNode workspaceNode, InvocationContext ctx)
   {
      if (workspaceNode == null)
         throw new NodeNotExistsException("optimisticCreateIfNotExistsInterceptor should have created this node!");
      // pre-notify
      notifier.notifyNodeModified(workspaceNode.getFqn(), true, PUT_MAP, workspaceNode.getData(), ctx);
      workspaceNode.putAll(data);
      workspace.addNode(workspaceNode);
      // post-notify
      notifier.notifyNodeModified(workspaceNode.getFqn(), false, PUT_MAP, workspaceNode.getData(), ctx);
   }

   private Object putDataKeyValueAndNotify(Object key, Object value, TransactionWorkspace workspace, WorkspaceNode workspaceNode, InvocationContext ctx)
   {
      if (workspaceNode == null)
         throw new NodeNotExistsException("optimisticCreateIfNotExistsInterceptor should have created this node!");

      if (notifier.shouldNotifyOnNodeModified())// pre-notify
      {
         notifier.notifyNodeModified(workspaceNode.getFqn(), true, PUT_DATA, workspaceNode.getData(), ctx);
      }

      Object old = workspaceNode.put(key, value);
      workspace.addNode(workspaceNode);

      if (notifier.shouldNotifyOnNodeModified())// post-notify
      {
         Map addedData = Collections.singletonMap(key, value);
         notifier.notifyNodeModified(workspaceNode.getFqn(), false, PUT_DATA, addedData, ctx);
      }

      return old;
   }

   private boolean removeNode(TransactionWorkspace workspace, WorkspaceNode workspaceNode, boolean notify, InvocationContext ctx) throws CacheException
   {
      // it is already removed - we can ignore it
      if (workspaceNode == null) return false;

      Fqn parentFqn = workspaceNode.getFqn().getParent();
      WorkspaceNode parentNode = fetchWorkspaceNode(ctx, parentFqn, workspace, false, true);
      if (parentNode == null) throw new NodeNotExistsException("Unable to find parent node with fqn " + parentFqn);

      // pre-notify
      if (notify) notifier.notifyNodeRemoved(workspaceNode.getFqn(), true, workspaceNode.getData(), ctx);

      Fqn nodeFqn = workspaceNode.getFqn();
      parentNode.removeChild(nodeFqn.getLastElement());

      SortedMap<Fqn, WorkspaceNode> tailMap = workspace.getNodesAfter(workspaceNode.getFqn());

      for (WorkspaceNode toDelete : tailMap.values())
      {
         if (toDelete.getFqn().isChildOrEquals(nodeFqn))
         {
            if (trace) log.trace("marking node " + toDelete.getFqn() + " as deleted");
            toDelete.markAsDeleted(true);
         }
         else
         {
            break;// no more children, we came to the end
         }
      }

      // post-notify
      if (notify) notifier.notifyNodeRemoved(workspaceNode.getFqn(), false, null, ctx);
      return workspaceNode.getNode().isValid();
   }

   private Object removeKeyAndNotify(Object removeKey, TransactionWorkspace workspace, WorkspaceNode workspaceNode, InvocationContext ctx)
   {
      if (workspaceNode == null) return null;

      if (notifier.shouldNotifyOnNodeModified())// pre-notify
      {
         notifier.notifyNodeModified(workspaceNode.getFqn(), true, REMOVE_DATA, workspaceNode.getData(), ctx);
      }

      Object old = workspaceNode.remove(removeKey);
      workspace.addNode(workspaceNode);

      if (notifier.shouldNotifyOnNodeModified())
      {
         Map removedData = Collections.singletonMap(removeKey, old);
         // post-notify
         notifier.notifyNodeModified(workspaceNode.getFqn(), false, REMOVE_DATA, removedData, ctx);
      }

      return old;
   }

   private void removeDataAndNotify(TransactionWorkspace workspace, WorkspaceNode workspaceNode, InvocationContext ctx)
   {
      if (workspaceNode == null) return;

      Map data = new HashMap(workspaceNode.getData());

      // pre-notify
      notifier.notifyNodeModified(workspaceNode.getFqn(), true, REMOVE_DATA, data, ctx);

      workspaceNode.clearData();
      workspace.addNode(workspaceNode);

      // post-notify
      notifier.notifyNodeModified(workspaceNode.getFqn(), false, REMOVE_DATA, data, ctx);
   }

   // -----------------------------------------------------------------

   // Methods to help retrieval of nodes from the transaction workspace.

   // -----------------------------------------------------------------

   /**
    * Retrieves a node for a given Fqn from the workspace.  If the node does not exist in the workspace it is retrieved
    * from the cache's data structure.  Note that at no point is a NEW node created in the underlying data structure.
    * That is up to the {@link org.jboss.cache.interceptors.OptimisticCreateIfNotExistsInterceptor}.
    * <p/>
    * If the node requested is in the workspace but marked as deleted, this method will NOT retrieve it, unless undeleteIfNecessary
    * is true, in which case the node's <tt>deleted</tt> property is set to false first before being retrieved.
    *
    * @param fqn                 Fqn of the node to retrieve
    * @param workspace           transaction workspace to look in
    * @param undeleteIfNecessary if the node is in the workspace but marked as deleted, this meth
    * @param includeInvalidNodes
    * @return a node, if found, or null if not.
    */
   private WorkspaceNode fetchWorkspaceNode(InvocationContext ctx, Fqn fqn, TransactionWorkspace workspace, boolean undeleteIfNecessary, boolean includeInvalidNodes)
   {
      WorkspaceNode workspaceNode = workspace.getNode(fqn);
      // if we do not have the node then we need to add it to the workspace
      if (workspaceNode == null)
      {
         NodeSPI node = dataContainer.peek(fqn, true, includeInvalidNodes);
         if (node == null) return null;
         GlobalTransaction gtx = ctx.getGlobalTransaction();
         workspaceNode = lockAndCreateWorkspaceNode(nodeFactory, node, workspace, gtx, lockAcquisitionTimeout);

         // and add the node to the workspace.
         workspace.addNode(workspaceNode);
      }

      // Check that the workspace node has been marked as deleted.
      if (workspaceNode.isDeleted())
      {
         if (trace) log.trace("Node " + fqn + " has been deleted in the workspace.");
         if (undeleteIfNecessary)
         {
            undeleteWorkspaceNode(workspaceNode, fetchWorkspaceNode(ctx, fqn.getParent(), workspace, undeleteIfNecessary, includeInvalidNodes));
         }
         else if (!includeInvalidNodes)
         {
            // don't return deleted nodes if undeleteIfNecessary is false!
            workspaceNode = null;
         }
      }

      // set implicit node versioning flag.
      if (workspaceNode != null && !(workspaceNode.getVersion() instanceof DefaultDataVersion))
      {
         workspaceNode.setVersioningImplicit(false);
      }

      // now make sure all parents are in the wsp as well
      if (workspaceNode != null && !fqn.isRoot())
         fetchWorkspaceNode(ctx, fqn.getParent(), workspace, false, includeInvalidNodes);

      return workspaceNode;
   }
}
