/*
 * 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.CacheSPI;
import org.jboss.cache.DataContainer;
import org.jboss.cache.Fqn;
import org.jboss.cache.InvocationContext;
import org.jboss.cache.NodeFactory;
import org.jboss.cache.NodeSPI;
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.factories.annotations.Inject;
import org.jboss.cache.factories.annotations.Start;
import org.jboss.cache.notifications.Notifier;
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.ArrayList;
import java.util.List;

/**
 * Used to create new {@link NodeSPI} instances in the main data structure and then copy it into the
 * {@link TransactionWorkspace} as {@link WorkspaceNode}s as needed.  This is only invoked if nodes needed do not exist
 * in the underlying structure, they are added and the corresponding {@link org.jboss.cache.optimistic.WorkspaceNode#isCreated()}
 * would return <tt>true</tt> to denote that this node has been freshly created in the underlying structure.
 *
 * @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 OptimisticCreateIfNotExistsInterceptor extends OptimisticInterceptor
{
   /**
    * A reference to the node factory registered with the cache instance, used to create both WorkspaceNodes as well as
    * NodeSPI objects in the underlying data structure.
    */
   private NodeFactory nodeFactory;

   private DataContainer dataContainer;

   private CacheSPI cache;

   private long lockAcquisitionTimeout;

   @Inject
   private void injectDependencies(NodeFactory nodeFactory, DataContainer dataContainer, CacheSPI cacheSPI)
   {
      this.nodeFactory = nodeFactory;
      this.dataContainer = dataContainer;
      this.cache = cacheSPI;
   }

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

   @Override
   public Object visitPutDataMapCommand(InvocationContext ctx, PutDataMapCommand command) throws Throwable
   {
      createNode(ctx, command.getFqn(), false);
      return invokeNextInterceptor(ctx, command);
   }

   @Override
   public Object visitPutForExternalReadCommand(InvocationContext ctx, PutForExternalReadCommand command) throws Throwable
   {
      createNode(ctx, command.getFqn(), false);
      return invokeNextInterceptor(ctx, command);
   }

   @Override
   public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable
   {
      createNode(ctx, command.getFqn(), false);
      return invokeNextInterceptor(ctx, command);
   }

   @Override
   public Object visitMoveCommand(InvocationContext ctx, MoveCommand command) throws Throwable
   {
      List<Fqn> fqns = new ArrayList<Fqn>();
      fqns.add(command.getTo());
      //  peek into Node and get a hold of all child fqns as these need to be in the workspace.
      NodeSPI node = dataContainer.peek(command.getFqn(), true, true);
      greedyGetFqns(fqns, node, command.getTo());
      if (trace) log.trace("Adding Fqns " + fqns + " for a move() operation.");
      for (Fqn f : fqns)
      {
         createNode(ctx, f, true);
      }
      return invokeNextInterceptor(ctx, command);
   }

   /**
    * The only method that should be creating nodes.
    *
    * @param targetFqn
    * @throws CacheException
    */
   private void createNode(InvocationContext ctx, Fqn targetFqn, boolean suppressNotification) throws CacheException
   {
      if (dataContainer.peek(targetFqn, false, false) != null) return;
      // we do nothing if targetFqn is null
      if (targetFqn == null) return;

      boolean debug = log.isDebugEnabled();

      GlobalTransaction gtx = getGlobalTransaction(ctx);
      TransactionWorkspace workspace = getTransactionWorkspace(ctx);

      WorkspaceNode workspaceNode;

      List<Fqn> nodesCreated = new ArrayList<Fqn>();

      DataVersion version = null;
      if (ctx.getOptionOverrides() != null && ctx.getOptionOverrides().getDataVersion() != null)
      {
         version = ctx.getOptionOverrides().getDataVersion();
         workspace.setVersioningImplicit(false);
      }

      // start with the ROOT node and then work our way down to the node necessary, creating nodes along the way.
      workspaceNode = workspace.getNode(Fqn.ROOT);
      if (debug) log.debug("GlobalTransaction: " + gtx + "; Root: " + workspaceNode);

      // we do not have the root in the workspace!  Put it into thr workspace now.
      if (workspaceNode == null)
      {
         NodeSPI node = dataContainer.getRoot();
         workspaceNode = lockAndCreateWorkspaceNode(nodeFactory, node, workspace, gtx, lockAcquisitionTimeout);
         workspace.addNode(workspaceNode);
         log.debug("Created root node in workspace.");
      }
      else
      {
         log.debug("Found root node in workspace.");
      }

      // iterate through the target Fqn's elements.
      int targetFqnSize = targetFqn.size(), currentDepth = 1;
      for (Object childName : targetFqn.peekElements())
      {
         boolean isTargetFqn = (currentDepth == targetFqnSize);
         currentDepth++;

         // current workspace node canot be null.
         // try and get the child of current node

         if (debug) log.debug("Attempting to get child " + childName);
         NodeSPI currentNode = workspaceNode.getNode().getChildDirect(childName);

         if (currentNode == null)
         {
            // first test that it exists in the workspace and has been created in thix tx!
            WorkspaceNode peekInWorkspace = workspace.getNode(Fqn.fromRelativeElements(workspaceNode.getFqn(), childName));
            if (peekInWorkspace != null && peekInWorkspace.isCreated())
            {
               // exists in workspace and has just been created.
               currentNode = peekInWorkspace.getNode();
               if (peekInWorkspace.isDeleted())
               {
                  peekInWorkspace.markAsDeleted(false);
                  // add in parent again
                  workspaceNode.addChild(peekInWorkspace);
               }
            }
         }

         if (currentNode == null)
         {
            // no child exists with this name; create it in the underlying data structure and then add it to the workspace.
            if (trace) log.trace("Creating new child, since it doesn't exist in the cache.");
            // we put the parent node into the workspace as we are changing it's children.
            // at this point "workspaceNode" refers to the parent of the current node.  It should never be null if
            // you got this far!
            if (workspaceNode.isDeleted())
            {
               //add a new one or overwrite an existing one that has been deleted
               if (trace)
                  log.trace("Parent node doesn't exist in workspace or has been deleted.  Adding to workspace.");
               workspace.addNode(workspaceNode);
               if (!(workspaceNode.getVersion() instanceof DefaultDataVersion))
                  workspaceNode.setVersioningImplicit(false);
            }
            else
            {
               if (trace) log.trace("Parent node exists: " + workspaceNode);
            }

            // get the version passed in, if we need to use explicit versioning.
            DataVersion versionToPassIn = null;
            if (isTargetFqn && !workspace.isVersioningImplicit()) versionToPassIn = version;

            NodeSPI newUnderlyingChildNode = workspaceNode.createChild(childName, workspaceNode.getNode(), cache, versionToPassIn);

            // now assign "workspaceNode" to the new child created.
            workspaceNode = lockAndCreateWorkspaceNode(nodeFactory, newUnderlyingChildNode, workspace, gtx, lockAcquisitionTimeout);
            workspaceNode.setVersioningImplicit(versionToPassIn == null || !isTargetFqn);
            if (trace)
               log.trace("setting versioning of " + workspaceNode.getFqn() + " to be " + (workspaceNode.isVersioningImplicit() ? "implicit" : "explicit"));

            // now add the wrapped child node into the transaction space
            workspace.addNode(workspaceNode);
            workspaceNode.markAsCreated();
            // save in list so we can broadcast our created nodes outside
            // the synch block
            nodesCreated.add(workspaceNode.getFqn());
         }
         else
         {
            // node does exist but might not be in the workspace
            workspaceNode = workspace.getNode(currentNode.getFqn());
            // wrap it up so we can put it in later if we need to
            if (workspaceNode == null)
            {
               if (trace)
                  log.trace("Child node " + currentNode.getFqn() + " doesn't exist in workspace or has been deleted.  Adding to workspace in gtx " + gtx);

               workspaceNode = lockAndCreateWorkspaceNode(nodeFactory, currentNode, workspace, gtx, lockAcquisitionTimeout);

               // if the underlying node is a tombstone then mark the workspace node as newly created
               if (!currentNode.isValid()) workspaceNode.markAsCreated();

               if (isTargetFqn && !workspace.isVersioningImplicit())
               {
                  workspaceNode.setVersion(version);
                  workspaceNode.setVersioningImplicit(false);
               }
               else
               {
                  workspaceNode.setVersioningImplicit(true);
               }
               if (trace)
                  log.trace("setting versioning of " + workspaceNode.getFqn() + " to be " + (workspaceNode.isVersioningImplicit() ? "implicit" : "explicit"));
               workspace.addNode(workspaceNode);
            }
            else if (workspaceNode.isDeleted())
            {
               if (trace) log.trace("Found node but it is deleted in this workspace.  Needs resurrecting.");
               undeleteWorkspaceNode(workspaceNode, workspace);
            }
            else
            {
               if (trace) log.trace("Found child node in the workspace: " + currentNode);

            }
         }
      }

      if (!suppressNotification && nodesCreated.size() > 0)
      {
         Notifier n = cache.getNotifier();
         for (Fqn temp : nodesCreated)
         {
            n.notifyNodeCreated(temp, true, ctx);
            n.notifyNodeCreated(temp, false, ctx);
            if (trace) log.trace("Notifying cache of node created in workspace " + temp);
         }
      }
   }

}
