/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.cache.interceptors;

import org.jboss.cache.Fqn;
import org.jboss.cache.Node;
import org.jboss.cache.TreeCache;
import org.jboss.cache.loader.CacheLoader;
import org.jgroups.blocks.MethodCall;

import java.lang.reflect.Method;
import java.util.Iterator;
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,v 1.3.2.5 2005/12/01 19:48:41 dimitris Exp $
 */
public class CacheLoaderInterceptor extends Interceptor {
   private CacheLoader        loader=null;



   public void setCache(TreeCache cache) {
      super.setCache(cache);
      this.loader=cache.getCacheLoader();
   }



   /**
    * 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 m
    * @return
    * @throws Throwable
    */
   public Object invoke(MethodCall m) throws Throwable {
      Fqn          fqn=null;
      Node         n=null;
      Method       meth=m.getMethod();
      Object[]     args=m.getArgs();
      boolean      load_attributes=false; // load all attrs of a given node as well

      if(meth.equals(TreeCache.putDataMethodLocal) || meth.equals(TreeCache.putDataEraseMethodLocal) ||
            meth.equals(TreeCache.putKeyValMethodLocal)) {
         fqn=(Fqn)args[1];
         load_attributes=true;
      }
      else if(meth.equals(TreeCache.removeNodeMethodLocal)) {
         // fqn=(Fqn)args[1]; // we don't need to load a node if it will be removed (bela Dec 3 2004)
      }
      else if(meth.equals(TreeCache.removeKeyMethodLocal) || meth.equals(TreeCache.removeDataMethodLocal)) {
         fqn=(Fqn)args[1];
         load_attributes=true;
      }
      else if(meth.equals(TreeCache.addChildMethodLocal)) {
         fqn=(Fqn)args[1];
      }
      else if(meth.equals(TreeCache.getKeyValueMethodLocal)) {
         fqn=(Fqn)args[0];
         load_attributes=true;
      }
      else if(meth.equals(TreeCache.getNodeMethodLocal)) {
         fqn=(Fqn)args[0];
      }
      else if(meth.equals(TreeCache.getKeysMethodLocal)) {
         fqn=(Fqn)args[0];
         load_attributes=true;
      }
      else if(meth.equals(TreeCache.getChildrenNamesMethodLocal) || meth.equals(TreeCache.releaseAllLocksMethodLocal) ||
            meth.equals(TreeCache.printMethodLocal)) {
         fqn=(Fqn)args[0];
      }


      /* 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 */
      synchronized(this) {
         if(fqn != null && (!cache.exists(fqn) || cache.exists(fqn, TreeCache.UNINITIALIZED))) {
            n=loadNode(fqn);
            if(load_attributes && n != null)
               loadAttributesFromCacheLoader(n);
            lock(fqn, Node.LOCK_TYPE_WRITE, false); // not recursive
         }

         // special case: node is loaded, but children nodes haven't: check with CacheLoader if children nodes are
         // present and load if yes
         if(meth.equals(TreeCache.getChildrenNamesMethodLocal) && n != null && fqn != null) {
            Map children=n.getChildren();
            if(children == null) {
               Set children_names=null;
               try {
                  children_names=loader.getChildrenNames(fqn);
               }
               catch(Exception e) {
                  log.error("failed getting the children names for " + fqn + " from the cache loader", e);
               }
               if(children_names != null) {
                  // create one Node per child, don't load attributes yet (marked as UNINITIALIZED)
                  for(Iterator it=children_names.iterator(); it.hasNext();) {
                     String child_name=(String)it.next();
                     Fqn child_fqn=new Fqn(n.getFqn(), child_name);
                     n.createChild(child_name, child_fqn, n, TreeCache.UNINITIALIZED, null);
                  }
                  lock(fqn, Node.LOCK_TYPE_READ, true); // recursive=true: lock entire subtree
               }
            }
         }
      }

      return super.invoke(m);
   }

   private void lock(Fqn fqn, int lock_type, boolean recursive) throws Throwable {
      MethodCall meth=new MethodCall(TreeCache.lockMethodLocal,
                                     new Object[]{fqn,
                                                  new Integer(lock_type),
                                                  new Boolean(recursive)});
      super.invoke(meth);
   }


   private Node loadNode(Fqn fqn) {
      Node n, child_node=null;
      Object child_name;
      Fqn tmp_fqn=new Fqn();

      if(fqn == null) return null;
      int treeNodeSize=fqn.size();

      n=cache.getRoot();
      for(int i=0; i < treeNodeSize && n != null; i++) {
         child_name=fqn.get(i);
         tmp_fqn=new Fqn(tmp_fqn, child_name);
         child_node=n.getChild(child_name);

         // try CacheLoader if node is not in cache
         if(child_node == null) {
            try {
               if(loader.exists(fqn)) {
                  child_node=n.createChild(child_name, tmp_fqn, n, TreeCache.UNINITIALIZED, null);
                  cache.notifyNodeLoaded(tmp_fqn);
               }
            }
            catch(Throwable t) {
               log.error("failed loading node " + fqn + " from CacheLoader", t);
            }
         }
         n=child_node;
      }
      return n;
   }




   private void loadAttributesFromCacheLoader(Node n) {
      if(n == null || !n.containsKey(TreeCache.UNINITIALIZED))
         return;

      // lazily load attributes before the method is executed
      try {
         Map m=loader.get(n.getFqn());
         n.remove(TreeCache.UNINITIALIZED); // all attributes have been loaded
         n.put(m);
      }
      catch(Throwable t) {
         log.error("failure when lazily loading attributes of " + n.getFqn(), t);
      }
   }


}
