package org.jboss.cache.interceptors;

import org.jboss.cache.CacheException;
import org.jboss.cache.DataNode;
import org.jboss.cache.Fqn;
import org.jboss.cache.GlobalTransaction;
import org.jboss.cache.InvocationContext;
import org.jboss.cache.TransactionEntry;
import org.jboss.cache.TransactionTable;
import org.jboss.cache.TreeCache;
import org.jboss.cache.TreeNode;
import org.jboss.cache.config.Option;
import org.jboss.cache.loader.AsyncCacheLoader;
import org.jboss.cache.loader.CacheLoader;
import org.jboss.cache.loader.ChainingCacheLoader;
import org.jboss.cache.marshall.JBCMethodCall;
import org.jboss.cache.marshall.MethodCallFactory;
import org.jboss.cache.marshall.MethodDeclarations;
import org.jgroups.blocks.MethodCall;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
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
 * @author <a href="mailto:galder.zamarreno@jboss.com">Galder Zamarreno</a>
 * @version $Id: CacheLoaderInterceptor.java 6187 2008-07-07 18:12:35Z galder.zamarreno@jboss.com $
 */
public class CacheLoaderInterceptor extends Interceptor implements CacheLoaderInterceptorMBean
{
   private boolean isCustomCacheLoader;
   private long m_cacheLoads = 0;
   private long m_cacheMisses = 0;
   private TransactionTable txTable = null;
   protected CacheLoader loader;

   /**
    * 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;

   public void setCache(TreeCache cache)
   {
      super.setCache(cache);
      txTable = cache.getTransactionTable();
      this.loader = cache.getCacheLoaderManager().getCacheLoader();
      isCustomCacheLoader = isCustomCacheLoaderConfigured(loader);
   }

   private boolean isCustomCacheLoaderConfigured(CacheLoader cl)
   {
      if (cl instanceof ChainingCacheLoader)
      {
         // test all loaders in the chain.
         ChainingCacheLoader ccl = (ChainingCacheLoader) cl;
         Iterator it = ccl.getCacheLoaders().iterator();
         boolean isCustom = false;
         while (it.hasNext())
         {
            CacheLoader nextCacheLoader = (CacheLoader) it.next();
            isCustom = isCustom || isCustomCacheLoaderConfigured(nextCacheLoader);
         }
         return isCustom;
      }
      else if (cl instanceof AsyncCacheLoader)
      {
         // test the underlying cache loader
         CacheLoader underlying = ((AsyncCacheLoader) cl).getCacheLoader();
         return isCustomCacheLoaderConfigured(underlying);
      }
      else
      {
         // tests for org.jboss.cache.loader.*
         Package pkg = cl.getClass().getPackage(); // may be null if this is an inner class?  In which case it is certainly a custom cache loader instance.
         return pkg == null || !pkg.getName().startsWith("org.jboss.cache");
      }
   }

   /**
    * Makes sure a node is loaded into memory before a call executes (no-op if node is already loaded). If attributes
    * of a node are to be accessed by the method, the attributes are also loaded.
    *
    * @param call
    * @return
    * @throws Throwable
    */
   public Object invoke(MethodCall call) throws Throwable
   {
      JBCMethodCall m = (JBCMethodCall) call;
      Fqn fqn = null; // if set, load the data
      Method meth = m.getMethod();
      Object[] args = m.getArgs();
      boolean acquireLock = false; // do we need to acquire a lock if we load this node from cloader?
      Map nodeData = null;
      boolean initNode = false; // keep uninitialized
      Object key = null;
      InvocationContext ctx = getInvocationContext();
      TransactionEntry entry = null;
      GlobalTransaction gtx = null;
      if ((gtx = ctx.getGlobalTransaction()) != null)
      {
         entry = txTable.get(gtx);
      }

      if (log.isTraceEnabled())
         log.trace("invoke " + m);
      switch (m.getMethodId())
      {
         case MethodDeclarations.putDataEraseMethodLocal_id:
         case MethodDeclarations.putDataMethodLocal_id:
            fqn = (Fqn) args[1];
            initNode = true;
            break;
         case MethodDeclarations.putKeyValMethodLocal_id:
            fqn = (Fqn) args[1];
            if (useCacheStore)
               initNode = true;
            else
               acquireLock = true;
            break;
         case MethodDeclarations.addChildMethodLocal_id:
            fqn = (Fqn) args[1];
            break;
         case MethodDeclarations.getKeyValueMethodLocal_id:
            fqn = (Fqn) args[0];
            key = args[1];
            acquireLock = true;
            break;
         case MethodDeclarations.getNodeMethodLocal_id:
         case MethodDeclarations.getKeysMethodLocal_id:
         case MethodDeclarations.getChildrenNamesMethodLocal_id:
         case MethodDeclarations.releaseAllLocksMethodLocal_id:
         case MethodDeclarations.printMethodLocal_id:
            fqn = (Fqn) args[0];
            acquireLock = true;
            break;
         case MethodDeclarations.rollbackMethod_id:
            // clean up nodesCreated map
            cleanupNodesCreated(entry);
            break;
         default:
            if (!useCacheStore)
            {
               if (m.getMethodId() == MethodDeclarations.removeKeyMethodLocal_id)
               {
                  fqn = (Fqn) args[1];
               }
               else if (m.getMethodId() == MethodDeclarations.removeDataMethodLocal_id)
               {
                  fqn = (Fqn) args[1];
                  initNode = true;
               }
            }
            break;
      }

      /* On the way in: load elements into cache from the CacheLoader if not yet in the cache. We need to synchronize
      this so only 1 thread attempts to load a given element */

      if (fqn != null)
      {

         DataNode n = cache.peek(fqn);
         boolean mustLoad = mustLoad(n, key);
         if (log.isTraceEnabled())
            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, DataNode.LOCK_TYPE_WRITE, false); // not recursive

            if (!initNode && !wasRemovedInTx(fqn))
            {
               n = loadNode(fqn, n, entry);
            }
         }

         // The complete list of children aren't known without loading them
         if (m.getMethodId() == MethodDeclarations.getChildrenNamesMethodLocal_id)
         {
            loadChildren(fqn, n);
         }

      }

      return super.invoke(m);
   }

   /**
    * Load the children.
    *
    * @param n may be null if the node was not found.
    */
   private void loadChildren(Fqn fqn, DataNode n) throws Throwable
   {

      if (n != null && n.getChildrenLoaded())
         return;
      Set children_names = loader.getChildrenNames(fqn);

      if (log.isTraceEnabled())
         log.trace("load children " + fqn + " children=" + children_names);

      // For getChildrenNames null means no children
      if (children_names == null)
      {
         if (n != null)
         {
            if (useCacheStore)
               n.setChildren(null);
            n.setChildrenLoaded(true);
         }
         return;
      }

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

      // Create one DataNode per child, mark as UNINITIALIZED
      for (Iterator i = children_names.iterator(); i.hasNext();)
      {
         String child_name = (String) i.next();
         Fqn child_fqn = new Fqn(fqn, child_name);
         // create child if it didn't exist
         n.createChild(child_name, child_fqn, n, TreeCache.UNINITIALIZED, null);
      }
      lock(fqn, DataNode.LOCK_TYPE_READ, true); // recursive=true: lock entire subtree
      n.setChildrenLoaded(true);
   }

   private boolean mustLoad(DataNode n, Object key)
   {
      if (log.isTraceEnabled())
      {
         log.trace("mustLoad called with key=" + key + " and datanode=" + n);
      }
      
      return n == null ||
              (n.containsKey(TreeCache.UNINITIALIZED) && (key == null || !n.containsKey(key)));
   }

   public long getCacheLoaderLoads()
   {
      return m_cacheLoads;
   }

   public long getCacheLoaderMisses()
   {
      return m_cacheMisses;
   }

   public void resetStatistics()
   {
      m_cacheLoads = 0;
      m_cacheMisses = 0;
   }

   public Map dumpStatistics()
   {
      Map retval = new HashMap();
      retval.put("CacheLoaderLoads", new Long(m_cacheLoads));
      retval.put("CacheLoaderMisses", new Long(m_cacheMisses));
      return retval;
   }

   protected void lock(Fqn fqn, int lock_type, boolean recursive) throws Throwable
   {

      if (cache.isNodeLockingOptimistic()) return;

      MethodCall meth = MethodCallFactory.create(MethodDeclarations.lockMethodLocal,
              new Object[]{fqn,
                      new Integer(lock_type),
                      Boolean.valueOf(recursive)});
      //super.invoke(meth);
      // let's force this to go thru the whole chain, not just from here on.
      // TxInterceptor will scrub the InvocationContext of any Option, so
      // cache it so we can restore it
      InvocationContext ctx = getInvocationContext();
      Option opt = ctx.getOptionOverrides();
      try
      {
         ((Interceptor) cache.getInterceptors().get(0)).invoke(meth); // need a better way to do this
      }
      finally
      {
         if (opt != null)
            ctx.setOptionOverrides(opt);
      }
   }

   /**
    * Retrieves a node from memory; doesn't access the cache loader
    *
    * @param fqn
    */
   protected DataNode getNode(Fqn fqn)
   {
      int treeNodeSize = fqn.size();

      TreeNode n = cache.getRoot();
      TreeNode child_node;
      Object child_name;
      for (int i = 0; i < treeNodeSize && n != null; i++)
      {
         child_name = fqn.get(i);
         child_node = n.getChild(child_name);
         n = child_node;
      }
      return (DataNode) n;
   }

   /**
    * 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)
   {
      GlobalTransaction t = getInvocationContext().getGlobalTransaction();
      if (t == null)
         return false;
      TransactionEntry entry = txTable.get(t);
      Iterator i = entry.getCacheLoaderModifications().iterator();
      while (i.hasNext())
      {
         JBCMethodCall m = (JBCMethodCall) i.next();
         if (m.getMethodId() == MethodDeclarations.removeNodeMethodLocal_id
                 && fqn.isChildOrEquals((Fqn) m.getArgs()[1]))
            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 DataNode loadNode(Fqn fqn, DataNode n, TransactionEntry entry) throws Exception
   {
      if (log.isTraceEnabled()) log.trace("loadNode " + fqn);
      Map nodeData = loadData(fqn);
      if (nodeData != null)
      {
         n = createNodes(fqn, entry);
         n.put(nodeData, true);
      }
      else if (n != null && n.containsKey(TreeCache.UNINITIALIZED))
      {
         n.remove(TreeCache.UNINITIALIZED);
      }
      return n;
   }

   /**
    * Creates a new memory node in preparation for storage.
    */
   private DataNode createTempNode(Fqn fqn, TransactionEntry entry) throws Exception
   {
      DataNode n = createNodes(fqn, entry);
      n.put(TreeCache.UNINITIALIZED, null);
      if (log.isTraceEnabled())
         log.trace("createTempNode n " + n);
      return n;
   }

   private DataNode createNodes(Fqn fqn, TransactionEntry entry) throws Exception
   {
      Fqn tmp_fqn = Fqn.ROOT;

      int size = fqn.size();

      TreeNode n = cache.getRoot();
      for (int i = 0; i < size; i++)
      {
         Object child_name = fqn.get(i);
         tmp_fqn = new Fqn(tmp_fqn, child_name);
         TreeNode child_node = n.getChild(child_name);
         boolean last = (i == size - 1);

         if (child_node == null)
         {
            if (last)
            {
               child_node = n.createChild(child_name, tmp_fqn, n);
            }
            else
            {
               child_node = n.createChild(child_name, tmp_fqn, n, TreeCache.UNINITIALIZED, null);
            }

            if (entry != null)
            {
               entry.loadUninitialisedNode(tmp_fqn);
            }
         }

         n = child_node;
      }

      return (DataNode) n;
   }

   private void cleanupNodesCreated(TransactionEntry entry)
   {
      boolean traceEnabled = log.isTraceEnabled();
      log.trace("Removing temporarily created nodes from treecache");

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


   private Map loadData(Fqn fqn) throws Exception
   {
      Map nodeData = loader.get(fqn);
      boolean nodeExists = (nodeData != null);
      if (log.isTraceEnabled()) log.trace("nodeExists " + nodeExists);

      if (cache.getUseInterceptorMbeans() && statsEnabled)
      {
         if (nodeExists)
            m_cacheLoads++;
         else
            m_cacheMisses++;
      }

      if (!nodeExists && isCustomCacheLoader)
      {
         warnCustom();
      }

      if (nodeExists)
      {
         cache.notifyNodeLoaded(fqn);
      }

      return nodeData;
   }

   private void warnCustom()
   {
      log.info("CacheLoader.get(Fqn) returned a null; assuming the node does not exist.");
      log.info("The CacheLoader interface has changed since JBossCache 1.3.x");
      log.info("Please see http://jira.jboss.com/jira/browse/JBCACHE-118");
      log.info("CacheLoader.get() should return an empty Map if the node does exist but doesn't have any attributes.");
   }

}
