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

import org.jboss.cache.CacheException;
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.tx.CommitCommand;
import org.jboss.cache.commands.tx.OptimisticPrepareCommand;
import org.jboss.cache.commands.tx.RollbackCommand;
import static org.jboss.cache.config.Configuration.CacheMode;
import org.jboss.cache.factories.annotations.Inject;
import org.jboss.cache.optimistic.DataVersioningException;
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.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Validates the data in the {@link TransactionWorkspace} against data in the underlying data structure
 * (versions only) and then applies changes to the underlying data structure.  This is only triggered when commit,
 * rollback or prepare method calls are encountered.  Other method calls are directly passed up the interceptor chain,
 * untouched.  Note that prepare/commit/rollbacks are <b>not</b> passed up the interceptor chain after being processed.
 * <p/>
 * When preparting, this interceptor does nothing more than validate versions.
 * The validation scheme used is based on the {@link org.jboss.cache.optimistic.DataVersion} implementation used.
 * {@link org.jboss.cache.optimistic.DataVersion#newerThan(org.jboss.cache.optimistic.DataVersion)} is used to determine
 * whether the version of one instance is newer than the version of another.  It is up to the {@link org.jboss.cache.optimistic.DataVersion}
 * implementation to deal with attempting to compare incompatible version types (and potentially throwing {@link org.jboss.cache.optimistic.DataVersioningException}s.
 * <p/>
 * Upon successful commit, changes in the workspace are applied to the underlying data structure in the cache.
 * <p/>
 * On rollback clears the nodes in the workspace and leaves the underlying data structure untouched.
 *
 * @author Manik Surtani (<a href="mailto:manik@jboss.org">manik@jboss.org</a>)
 * @author Steve Woodcock (<a href="mailto:stevew@jofti.com">stevew@jofti.com</a>)
 */
public class OptimisticValidatorInterceptor extends OptimisticInterceptor
{
   private boolean useTombstones;

   private DataContainer dataContainer;

   @Inject
   public void initialize(DataContainer dataContainer)
   {
      this.dataContainer = dataContainer;
   }

   @Inject
   private void init()
   {
      CacheMode mode = configuration.getCacheMode();
      useTombstones = (mode == CacheMode.INVALIDATION_ASYNC) || (mode == CacheMode.INVALIDATION_SYNC);
   }

   @Override
   public Object visitOptimisticPrepareCommand(InvocationContext ctx, OptimisticPrepareCommand command) throws Throwable
   {
      TransactionWorkspace workspace = getTransactionWorkspace(ctx);

      // There is no guarantee that this collection is in any order!
      Collection<WorkspaceNode> nodes = workspace.getNodes().values();

      //we ought to have all necessary locks here so lets try and validate
      if (log.isDebugEnabled()) log.debug("Validating " + nodes.size() + " nodes.");
      for (WorkspaceNode workspaceNode : nodes)
      {
         if (workspaceNode.isDirty())
         {
            Fqn fqn = workspaceNode.getFqn();
            if (trace) log.trace("Validating version for node [" + fqn + "]");

            NodeSPI underlyingNode;
            underlyingNode = dataContainer.peek(fqn, true, true);

            // if this is a newly created node then we expect the underlying node to be null.
            // also, if the node has been deleted in the WS and the underlying node is null, this *may* be ok ... will test again later when comparing versions
            // if not, we have a problem...
            if (underlyingNode == null && !workspaceNode.isCreated() && !workspaceNode.isDeleted())
            {
               throw new DataVersioningException("Underlying node for " + fqn + " is null, and this node wasn't newly created in this transaction!  We have a concurrent deletion event.");
            }

            // needs to have been created AND modified - we allow concurrent creation if no data is put into the node
            if (underlyingNode != null && underlyingNode.isValid() && workspaceNode.isCreated() && workspaceNode.isModified())
            {
               throw new DataVersioningException("Transaction attempted to create " + fqn + " anew.  It has already been created since this transaction started, by another (possibly remote) transaction.  We have a concurrent creation event.");
            }

            if (underlyingNode != null && !underlyingNode.isValid())
            {
               // we havea  tombstone
               if (!workspaceNode.isCreated() && !workspaceNode.isDeleted())
                  throw new DataVersioningException("Underlying node doesn't exist but a tombstone does; workspace node should be marked as created!");
               if (underlyingNode.getVersion().newerThan(workspaceNode.getVersion()))
               {
                  // we have an out of date node here
                  throw new DataVersioningException("Version mismatch for node " + fqn + ": underlying node with version " + workspaceNode.getNode().getVersion() + " is newer than workspace node, with version " + workspaceNode.getVersion());
               }
            }

            if (!workspaceNode.isCreated() && (workspaceNode.isDeleted() || workspaceNode.isModified()))
            {
               // if the real node is null, throw a DVE
               if (underlyingNode == null)
               {
                  // but not if the WSN has also been deleted
                  if (!workspaceNode.isDeleted())
                     throw new DataVersioningException("Unable to compare versions since the underlying node has been deleted by a concurrent transaction!");
                  else if (trace)
                     log.trace("The data node [" + fqn + "] is null, but this is ok since the workspace node is marked as deleted as well");
               }
               // if there is a DataVersion type mismatch here, leave it up to the DataVersion impl to barf if necessary.  - JBCACHE-962
               else if (underlyingNode.getVersion().newerThan(workspaceNode.getVersion()))
               {
                  // we have an out of date node here
                  throw new DataVersioningException("Version mismatch for node " + fqn + ": underlying node with version " + workspaceNode.getNode().getVersion() + " is newer than workspace node, with version " + workspaceNode.getVersion());
               }
            }
         }
         else
         {
            if (trace) log.trace("Node [" + workspaceNode.getFqn() + "] doesn't need validating as it isn't dirty");
         }
      }
      log.debug("Successfully validated nodes");
      return invokeNextInterceptor(ctx, command);
   }

   private void commitTransaction(InvocationContext ctx)
   {
      GlobalTransaction gtx = getGlobalTransaction(ctx);
      TransactionWorkspace workspace;
      workspace = getTransactionWorkspace(ctx);

      if (log.isDebugEnabled()) log.debug("Commiting successfully validated changes for GlobalTransaction " + gtx);
      Collection<WorkspaceNode> workspaceNodes = workspace.getNodes().values();
      for (WorkspaceNode workspaceNode : workspaceNodes)
      {
         NodeSPI underlyingNode = workspaceNode.getNode();
         // short circuit if this node is deleted?
         if (workspaceNode.isDeleted())
         {
            if (trace) log.trace("Workspace node " + workspaceNode.getFqn() + " deleted; removing");

            if (underlyingNode.getFqn().isRoot())
            {
               throw new CacheException("An illegal attempt to delete the root node!");
            }
            else
            {
               // mark it as invalid so any direct references are marked as such
               underlyingNode.setValid(false, true);
               // we need to update versions here, too
               performVersionUpdate(underlyingNode, workspaceNode);

               if (!useTombstones)
               {
                  // don't retain the tombstone
                  NodeSPI parent = underlyingNode.getParent();
                  if (parent == null)
                  {
                     throw new CacheException("Underlying node " + underlyingNode + " has no parent");
                  }

                  parent.removeChildDirect(underlyingNode.getFqn().getLastElement());
               }
            }
         }
         else
         {
            boolean updateVersion = false;
            if (workspaceNode.isChildrenModified() || workspaceNode.isResurrected()) // if it is newly created make sure we remove all underlying children that may exist, to solve a remove-and-create-in-tx condition
            {
               if (trace) log.trace("Updating children since node has modified children");
               // merge children.
               List<Set<Fqn>> deltas = workspaceNode.getMergedChildren();

               if (trace) log.trace("Applying children deltas to parent node " + underlyingNode.getFqn());

               if (workspaceNode.isResurrected())
               {
                  // mark it as invalid so any direct references are marked as such
                  Map childNode = underlyingNode.getChildrenMapDirect();
                  if (childNode != null)
                  {
                     for (Object o : childNode.values())
                     {
                        NodeSPI cn = (NodeSPI) o;
                        cn.setValid(false, true);
                        if (!useTombstones) underlyingNode.removeChildDirect(cn.getFqn().getLastElement());
                     }
                  }
               }

               for (Fqn child : deltas.get(0))
               {
                  NodeSPI childNode = workspace.getNode(child).getNode();
                  underlyingNode.addChildDirect(childNode);
                  childNode.setValid(true, false);
               }

               for (Fqn child : deltas.get(1))
               {
                  // mark it as invalid so any direct references are marked as such
                  NodeSPI childNode = underlyingNode.getChildDirect(child.getLastElement());
                  if (childNode != null) childNode.setValid(false, true);

                  if (!useTombstones) underlyingNode.removeChildDirect(child.getLastElement());
               }

               updateVersion = underlyingNode.isLockForChildInsertRemove();

               // do we need to notify listeners of a modification??  If all we've done is added children then don't
               // notify.
            }

            if (workspaceNode.isModified())
            {
               if (trace) log.trace("Merging data since node is dirty");
               Map mergedData = workspaceNode.getMergedData();
               underlyingNode.clearDataDirect();
               underlyingNode.putAllDirect(mergedData);

               // mark node and any parents as valid- if available.  Versioning parents are tough though - leave as old versions?
               validateNodeAndParents(underlyingNode);
               updateVersion = true;
            }

            if (updateVersion)
            {
               performVersionUpdate(underlyingNode, workspaceNode);
            }
         }
      }
   }

   @Override
   public Object visitCommitCommand(InvocationContext ctx, CommitCommand command) throws Throwable
   {
      commitTransaction(ctx);
      return invokeNextInterceptor(ctx, command);
   }

   @Override
   public Object visitRollbackCommand(InvocationContext ctx, RollbackCommand command) throws Throwable
   {
      TransactionWorkspace workspace;
      workspace = getTransactionWorkspace(ctx);
      workspace.clearNodes();
      return invokeNextInterceptor(ctx, command);
   }


   private void validateNodeAndParents(NodeSPI node)
   {
      node.setValid(true, false);
      if (!node.getFqn().isRoot()) validateNodeAndParents(node.getParent());
   }

   private void performVersionUpdate(NodeSPI underlyingNode, WorkspaceNode workspaceNode)
   {
      if (workspaceNode.isVersioningImplicit())
      {
         if (trace) log.trace("Versioning is implicit; incrementing.");
         underlyingNode.setVersion(((DefaultDataVersion) workspaceNode.getVersion()).increment());
      }
      else
      {
         if (trace) log.trace("Versioning is explicit; not attempting an increment.");
         underlyingNode.setVersion(workspaceNode.getVersion());
      }

      if (trace)
         log.trace("Setting version of node " + underlyingNode.getFqn() + " from " + workspaceNode.getVersion() + " to " + underlyingNode.getVersion());
   }

}
