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.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.tx.RollbackCommand;
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.config.Configuration;
import static org.jboss.cache.config.Configuration.CacheMode;
import org.jboss.cache.factories.annotations.Inject;
import org.jboss.cache.factories.annotations.Start;
import org.jboss.cache.interceptors.base.CommandInterceptor;
import org.jboss.cache.loader.CacheLoader;
import org.jboss.cache.loader.CacheLoaderManager;
import org.jboss.cache.lock.LockManager;
import org.jboss.cache.lock.LockType;
import org.jboss.cache.lock.TimeoutException;
import org.jboss.cache.notifications.Notifier;
import org.jboss.cache.transaction.TransactionEntry;
import org.jboss.cache.transaction.TransactionTable;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

/**
 * Loads nodes that don't exist at the time of the call into memory from the CacheLoader
 *
 * @author Bela Ban
 * @version $Id: CacheLoaderInterceptor.java 6909 2008-10-13 14:02:29Z bstansberry@jboss.com $
 */
public class CacheLoaderInterceptor extends CommandInterceptor implements CacheLoaderInterceptorMBean
{
   private long cacheLoads = 0;
   private long cacheMisses = 0;
   private CacheLoaderManager clm;
   private LockManager lockManager;

   protected TransactionTable txTable = null;
   protected CacheLoader loader;
   protected DataContainer dataContainer;
   protected Notifier notifier;

   protected boolean isActivation = false;
   protected boolean usingOptimisticInvalidation = false;


   /**
    * True if CacheStoreInterceptor is in place.
    * This allows us to skip loading keys for remove(Fqn, key) and put(Fqn, key).
    * It also affects removal of node data and listing children.
    */
   protected boolean useCacheStore = true;

   @Inject
   protected void injectDependencies(TransactionTable txTable, CacheLoaderManager clm, Configuration configuration,
                                     DataContainer dataContainer, LockManager lockManager, Notifier notifier)
   {
      this.txTable = txTable;
      this.clm = clm;
      CacheMode mode = configuration.getCacheMode();
      usingOptimisticInvalidation = configuration.isNodeLockingOptimistic() && mode.isInvalidation();
      this.dataContainer = dataContainer;
      this.lockManager = lockManager;
      this.notifier = notifier;
   }

   @Start
   protected void startInterceptor()
   {
      loader = clm.getCacheLoader();
   }

   @Override
   public Object visitPutDataMapCommand(InvocationContext ctx, PutDataMapCommand command) throws Throwable
   {
      if (command.getFqn() != null)
      {
         loadIfNeeded(ctx, command.getFqn(), null, true, true, false, ctx.getTransactionEntry(), false, false, false);
      }
      return invokeNextInterceptor(ctx, command);
   }

   @Override
   public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable
   {
      if (command.getFqn() != null)
      {
         loadIfNeeded(ctx, command.getFqn(), command.getKey(), false, useCacheStore, !useCacheStore, ctx.getTransactionEntry(), false, false, false);
      }
      return invokeNextInterceptor(ctx, command);
   }

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

   @Override
   public Object visitMoveCommand(InvocationContext ctx, MoveCommand command) throws Throwable
   {
      if (command.getFqn() != null)
      {
         if (command.getTo() != null)
         {
            loadIfNeeded(ctx, command.getTo(), null, false, false, true, ctx.getTransactionEntry(), false, true, false);
         }
         loadIfNeeded(ctx, command.getFqn(), null, false, false, true, ctx.getTransactionEntry(), true, true, false);
      }
      return invokeNextInterceptor(ctx, command);
   }

   @Override
   public Object visitGetKeyValueCommand(InvocationContext ctx, GetKeyValueCommand command) throws Throwable
   {
      if (command.getFqn() != null)
      {
         loadIfNeeded(ctx, command.getFqn(), command.getKey(), false, false, true, ctx.getTransactionEntry(), false, false, false);
      }
      return invokeNextInterceptor(ctx, command);
   }


   @Override
   public Object visitGetNodeCommand(InvocationContext ctx, GetNodeCommand command) throws Throwable
   {
      if (command.getFqn() != null)
      {
         loadIfNeeded(ctx, command.getFqn(), null, false, false, true, ctx.getTransactionEntry(), false, false, !usingOptimisticInvalidation);
      }
      return invokeNextInterceptor(ctx, command);
   }

   @Override
   public Object visitGetChildrenNamesCommand(InvocationContext ctx, GetChildrenNamesCommand command) throws Throwable
   {
      Fqn fqn = command.getFqn();
      if (fqn != null)
      {
         loadIfNeeded(ctx, fqn, null, false, false, false, ctx.getTransactionEntry(), false, false, true);
         NodeSPI n = dataContainer.peek(fqn, true, true);
         loadChildren(fqn, n, false, false, ctx);
      }
      return invokeNextInterceptor(ctx, command);
   }


   @Override
   public Object visitGetKeysCommand(InvocationContext ctx, GetKeysCommand command) throws Throwable
   {
      if (command.getFqn() != null)
      {
         loadIfNeeded(ctx, command.getFqn(), null, true, false, true, ctx.getTransactionEntry(), false, false, false);
      }
      return invokeNextInterceptor(ctx, command);
   }

   @Override
   public Object visitGetDataMapCommand(InvocationContext ctx, GetDataMapCommand command) throws Throwable
   {
      if (command.getFqn() != null)
      {
         loadIfNeeded(ctx, command.getFqn(), null, true, false, true, ctx.getTransactionEntry(), false, false, false);
      }
      return invokeNextInterceptor(ctx, command);
   }

   @Override
   public Object visitRollbackCommand(InvocationContext ctx, RollbackCommand command) throws Throwable
   {
      // clean up nodesCreated map
      if (trace) log.trace("Removing temporarily created nodes from treecache");

      // this needs to be done in reverse order.
      List list = ctx.getTransactionEntry().getDummyNodesCreatedByCacheLoader();
      if (list != null && list.size() > 0)
      {
         ListIterator i = list.listIterator(list.size());
         while (i.hasPrevious())
         {
            Fqn fqn = (Fqn) i.previous();
            try
            {
               dataContainer.evict(fqn, false);
            }
            catch (CacheException e)
            {
               if (trace) log.trace("Unable to evict node " + fqn, e);
            }
         }
      }
      return invokeNextInterceptor(ctx, command);
   }

   @Override
   public Object visitRemoveNodeCommand(InvocationContext ctx, RemoveNodeCommand command) throws Throwable
   {
      if (configuration.isNodeLockingOptimistic() && command.getFqn() != null)
      {
         loadIfNeeded(ctx, command.getFqn(), null, false, false, false, ctx.getTransactionEntry(), false, false, false);
      }
      return invokeNextInterceptor(ctx, command);
   }

   @Override
   public Object visitRemoveKeyCommand(InvocationContext ctx, RemoveKeyCommand command) throws Throwable
   {
      if (command.getFqn() != null && !useCacheStore)
      {
         loadIfNeeded(ctx, command.getFqn(), command.getKey(), false, false, false, ctx.getTransactionEntry(), false, false, false);
      }
      return invokeNextInterceptor(ctx, command);
   }

   @Override
   public Object visitClearDataCommand(InvocationContext ctx, ClearDataCommand command) throws Throwable
   {
      Fqn fqn = command.getFqn();
      if (fqn != null && !useCacheStore)
      {
         loadIfNeeded(ctx, fqn, null, false, true, false, ctx.getTransactionEntry(), false, false, false);
      }
      return invokeNextInterceptor(ctx, command);
   }

   private void loadIfNeeded(InvocationContext ctx, Fqn fqn, Object key, boolean allKeys, boolean initNode, boolean acquireLock, TransactionEntry entry, boolean recursive, boolean isMove, boolean bypassLoadingData) throws Throwable
   {
      NodeSPI n = dataContainer.peek(fqn, true, true);
      Object lockOwner = lockManager.getLockOwner(ctx);
      boolean needLock = n != null && !lockManager.ownsLock(fqn, lockOwner);
      boolean mustLoad = false;
      try
      {
         if (needLock && !lockManager.lock(n, LockType.READ, lockOwner))
         {
            throw new TimeoutException("Unable to acquire lock on " + fqn + ". Lock info: " + lockManager.printLockInfo(n));
         }
         mustLoad = mustLoad(n, key, allKeys || isMove);
      }
      finally
      {
         if (needLock) lockManager.unlock(n, lockOwner);
      }

      if (trace)
      {
         log.trace("load element " + fqn + " mustLoad=" + mustLoad);
      }
      if (mustLoad)
      {
         if (initNode)
         {
            n = createTempNode(fqn, entry);
         }

         // Only attempt to acquire this lock if we need to - i.e., if
         // the lock hasn't already been acquired by the Lock
         // interceptor.  CRUD methods (put, remove) would have acquired
         // this lock - even if the node is not in memory and needs to be
         // loaded.  Non-CRUD methods (put) would NOT have acquired this
         // lock so if we are to load the node from cache loader, we need
         // to acquire a write lock here.  as a 'catch-all', DO NOT
         // attempt to acquire a lock here *anyway*, even for CRUD
         // methods - this leads to a deadlock when you have threads
         // simultaneously trying to create a node.  See
         // org.jboss.cache.loader.deadlock.ConcurrentCreationDeadlockTest
         // - Manik Surtani (21 March 2006)
         if (acquireLock)
         {
            lock(fqn, LockType.WRITE, false, ctx);// non-recursive for now
         }

//         if (!initNode && !wasRemovedInTx(fqn, ctx.getGlobalTransaction()))
         if (!wasRemovedInTx(fqn, ctx))
         {
            if (bypassLoadingData)
            {
               if (n == null && loader.exists(fqn))
               {
                  // just create a dummy node in memory
                  n = createTempNode(fqn, entry);
               }
            }
            else
            {
               n = loadNode(ctx, fqn, n, entry);
            }
         }
      }

      // The complete list of children aren't known without loading them
      if (recursive)
      {
         loadChildren(fqn, n, recursive, isMove, ctx);
      }
   }

   /**
    * Load the children.
    *
    * @param node may be null if the node was not found.
    * @param ctxt
    */
   private void loadChildren(Fqn fqn, NodeSPI node, boolean recursive, boolean isMove, InvocationContext ctxt) throws Throwable
   {

      if (node != null && node.isChildrenLoaded())
      {
         if (trace) log.trace("Children already loaded!");
         return;
      }
      Set childrenNames;
      try
      {
         childrenNames = loader.getChildrenNames(fqn);
      }
      catch (Exception e)
      {
         if (log.isInfoEnabled()) log.info("Cache loader was unable to load state", e);
         // return!
         return;
      }

      if (trace)
      {
         log.trace("load children " + fqn + " children=" + childrenNames);
      }

      // For getChildrenNames null means no children
      if (childrenNames == null)
      {
         if (node != null)
         {
            if (useCacheStore)
            {
               node.removeChildrenDirect();//getChildrenMapDirect().clear();
            }
            node.setChildrenLoaded(true);
         }
         return;
      }

      // Create if node had not been created already
      if (node == null)
      {
         node = createNodes(fqn, null);// dont care about local transactions
      }

      // Create one DataNode per child, mark as UNINITIALIZED
      for (Object name : childrenNames)
      {
         Fqn childFqn = Fqn.fromElements(name);// this is a RELATIVE Fqn!!

         // create child if it didn't exist
         NodeSPI child = node.addChildDirect(childFqn);
         if ((isMove || isActivation) && recursive)
         {
            // load data for children as well!
            child.setInternalState(loader.get(child.getFqn()));
            child.setDataLoaded(true);
         }

         if (recursive)
         {
            loadChildren(child.getFqn(), child, true, isMove, ctxt);
         }
      }
      lock(fqn, recursive ? LockType.WRITE : LockType.READ, true, ctxt);// recursive=true: lock entire subtree
      node.setChildrenLoaded(true);
   }

   private boolean mustLoad(NodeSPI n, Object key, boolean allKeys)
   {
      if (n == null)
      {
         if (trace) log.trace("must load, node null");
         return true;
      }

      // check this first!!!
      if (!n.isValid() && configuration.isNodeLockingOptimistic())
      {
         // attempt to load again; this only happens if we have tombstones lying around, or we are using invalidation.
         if (trace) log.trace("loading again from cache loader since in-memory node is marked as invalid");
         return true;
      }

      // JBCACHE-1172 Skip single-key optimization if request needs all keys
      if (!allKeys)
      {
         // if we are not looking for a specific key don't bother loading!
         if (key == null)
         {
            if (trace) log.trace("don't load, key requested is null");
            return false;
         }
         if (n.getKeysDirect().contains(key))
         {
            if (trace) log.trace("don't load, already have necessary key in memory");
            return false;
         }
      }
      if (!n.isDataLoaded())
      {
         if (trace) log.trace("must Load, uninitialized");
         return true;
      }
      return false;
   }

   public long getCacheLoaderLoads()
   {
      return cacheLoads;
   }

   public long getCacheLoaderMisses()
   {
      return cacheMisses;
   }

   @Override
   public void resetStatistics()
   {
      cacheLoads = 0;
      cacheMisses = 0;
   }

   @Override
   public Map<String, Object> dumpStatistics()
   {
      Map<String, Object> retval = new HashMap<String, Object>();
      retval.put("CacheLoaderLoads", cacheLoads);
      retval.put("CacheLoaderMisses", cacheMisses);
      return retval;
   }

   protected void lock(Fqn fqn, LockType lockType, boolean recursive, InvocationContext ctx) throws Throwable
   {
      if (configuration.isNodeLockingOptimistic()) return;

      if (recursive)
         lockManager.lockAllAndRecord(fqn, lockType, ctx);
      else
         lockManager.lockAndRecord(fqn, lockType, ctx);
   }

   /**
    * Returns true if the FQN or parent was removed during the current
    * transaction.
    * This is O(N) WRT to the number of modifications so far.
    */
   private boolean wasRemovedInTx(Fqn fqn, InvocationContext ctx)
   {
      TransactionEntry entry = ctx.getTransactionEntry();
      if (entry == null) return false;

      for (ReversibleCommand txCacheCommand : entry.getModifications())
      {
         if (txCacheCommand instanceof RemoveNodeCommand && fqn.isChildOrEquals(txCacheCommand.getFqn())) return true;
      }
      return false;
   }

   /**
    * Loads a node from disk; if it exists creates parent TreeNodes.
    * If it doesn't exist on disk but in memory, clears the
    * uninitialized flag, otherwise returns null.
    */
   private NodeSPI loadNode(InvocationContext ctx, Fqn fqn, NodeSPI n, TransactionEntry entry) throws Exception
   {
      if (trace) log.trace("loadNode " + fqn);
      Map nodeData = loadData(fqn);
      if (nodeData != null)
      {
         if (trace) log.trace("Node data is not null, loading");

         notifier.notifyNodeLoaded(fqn, true, Collections.emptyMap(), ctx);
         if (isActivation)
         {
            notifier.notifyNodeActivated(fqn, true, Collections.emptyMap(), ctx);
         }

         n = createNodes(fqn, entry);
//         n.clearDataDirect();
         n.setInternalState(nodeData);

         // set this node as valid?
         if (usingOptimisticInvalidation) n.setValid(true, false);

         notifier.notifyNodeLoaded(fqn, false, nodeData, ctx);
         if (isActivation)
         {
            notifier.notifyNodeActivated(fqn, false, nodeData, ctx);
         }
      }
      if (n != null && !n.isDataLoaded())
      {
         if (trace) log.trace("Setting dataLoaded to true");
         n.setDataLoaded(true);
      }
      return n;
   }

   /**
    * Creates a new memory node in preparation for storage.
    */
   private NodeSPI createTempNode(Fqn fqn, TransactionEntry entry) throws Exception
   {
      NodeSPI n = createNodes(fqn, entry);
      n.setDataLoaded(false);
      if (trace)
      {
         log.trace("createTempNode n " + n);
      }
      return n;
   }

   @SuppressWarnings("unchecked")
   private NodeSPI createNodes(Fqn fqn, TransactionEntry entry) throws Exception
   {
      Object[] results = dataContainer.createNodes(fqn);
      List<NodeSPI> createdNodes = (List<NodeSPI>) results[0];

      NodeSPI lastCreated = null;
      for (NodeSPI node : createdNodes)
      {
         node.setDataLoaded(false);
         if (entry != null)
         {
            entry.loadUninitialisedNode(node.getFqn());
         }
         lastCreated = node;
      }

      // mark the leaf node as data loaded since that is what we are doing in this interceptor.
      if (lastCreated != null) lastCreated.setDataLoaded(true);

      // regardless of whether the last node was created, return it.
      return (NodeSPI) results[1];
   }

   private Map loadData(Fqn fqn) throws Exception
   {

      Map nodeData = loader.get(fqn);
      boolean nodeExists = (nodeData != null);
      if (trace) log.trace("nodeExists " + nodeExists);

      if (getStatisticsEnabled())
      {
         if (nodeExists)
         {
            cacheLoads++;
         }
         else
         {
            cacheMisses++;
         }
      }
      return nodeData;
   }

}