/*
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.cache.pojo.impl;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.aop.Advised;
import org.jboss.aop.Advisor;
import org.jboss.aop.InstanceAdvisor;
import org.jboss.aop.advice.Interceptor;
import org.jboss.aop.proxy.ClassProxy;
import org.jboss.cache.Cache;
import org.jboss.cache.CacheException;
import org.jboss.cache.CacheSPI;
import org.jboss.cache.Fqn;
import org.jboss.cache.Node;
import org.jboss.cache.Region;
import org.jboss.cache.pojo.PojoCacheException;
import org.jboss.cache.pojo.collection.CollectionInterceptorUtil;
import org.jboss.cache.pojo.interceptors.dynamic.AbstractCollectionInterceptor;
import org.jboss.cache.pojo.interceptors.dynamic.BaseInterceptor;
import org.jboss.cache.pojo.memory.FieldPersistentReference;
import org.jboss.cache.pojo.util.AopUtil;

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Delegate class for PojoCache, the real implementation code happens here.
 *
 * @author Ben Wang
 */
public class PojoCacheDelegate
{
   private PojoCacheImpl pojoCache;
   private Cache<Object, Object> cache;
   private final static Log log = LogFactory.getLog(PojoCacheDelegate.class);
   private InternalHelper internal_;
   private AdvisedPojoHandler advisedHandler_;
   private ObjectGraphHandler graphHandler_;
   private CollectionClassHandler collectionHandler_;
   private SerializableObjectHandler serializableHandler_;
   // Use ThreadLocal to hold a boolean isBulkRemove
   private ThreadLocal<Boolean> bulkRemove_ = new ThreadLocal<Boolean>();
   private final String DETACH = "DETACH";
   private PojoUtil util_ = new PojoUtil();

   public PojoCacheDelegate(PojoCacheImpl cache)
   {
      pojoCache = cache;
      this.cache = pojoCache.getCache();
      internal_ = new InternalHelper(this.cache);
      graphHandler_ = new ObjectGraphHandler(pojoCache, internal_);
      collectionHandler_ = new CollectionClassHandler(pojoCache, internal_);
      serializableHandler_ = new SerializableObjectHandler(pojoCache, internal_);
      advisedHandler_ = new AdvisedPojoHandler(pojoCache, internal_, util_);
   }

   public void setBulkRemove(boolean bulk)
   {
      bulkRemove_.set(bulk);
   }

   private boolean getBulkRemove()
   {
      return bulkRemove_.get();
   }

   public Object getObject(Fqn fqn, String field) throws CacheException
   {
      // TODO Must we really to couple with BR? JBCACHE-669
      Object pojo = internal_.getPojo(fqn, field);
      if (pojo != null)
      {
         // we already have an advised instance
         if (log.isDebugEnabled())
         {
            log.debug("getObject(): id: " + fqn + " retrieved from existing instance directly. ");
         }
         return pojo;
      }

      // OK. So we are here meaning that this is a failover or passivation since the transient
      // pojo instance is not around. Let's also make sure the right classloader is used
      // as well.
      ClassLoader prevCL = Thread.currentThread().getContextClassLoader();
      try
      {
         Region region = cache.getRegion(fqn, false);
         if (region != null && region.getClassLoader() != null)
            Thread.currentThread().setContextClassLoader(region.getClassLoader());

         return getObjectInternal(fqn, field);
      }
      finally
      {
         Thread.currentThread().setContextClassLoader(prevCL);
      }
   }

   public Object putObjectI(Fqn fqn, Object obj, String field) throws CacheException
   {
      // Skip some un-necessary update if obj is the same class as the old one
      Object oldValue = internal_.getPojo(fqn, field);
      if (oldValue == obj && (obj instanceof Advised || obj instanceof ClassProxy))
      {
         if (log.isDebugEnabled())
         {
            log.debug("putObject(): id: " + fqn + " pojo is already in the cache. Return right away.");
         }
         return obj;
      }
      return null;
   }

   /**
    * Note that caller of this method will take care of synchronization within the <code>fqn</code> sub-tree.
    */
   public Object putObjectII(Fqn fqn, Object obj, String field) throws CacheException
   {
      // Skip some un-necessary update if obj is the same class as the old one
      Object oldValue = internal_.getPojo(fqn, field);
      if (oldValue == obj)
      {
         if (log.isDebugEnabled())
         {
            log.debug("putObject(): id: " + fqn + " pojo is already in the cache. Return right away.");
         }
         return obj;
      }

      // remove old value before overwriting it. This is necessary to detach any interceptor.
      // TODO Or can we simply walk thru that somewhere? Well, there is also implication of Collection though
      pojoCache.detach(fqn, field);

      if (obj == null)
      {
         return oldValue;// we are done
      }

      // creates the internal node first without going thru the interceptor.
      // This way we don't block on __JBossInternal__ node.
      //createChildNodeFirstWithoutLocking(internalFqn);

      if ((obj instanceof Advised || obj instanceof ClassProxy) && isMultipleReferencedPut(obj))
      {
         // we pass in the originating fqn intentionaly
         graphHandler_.put(fqn, obj, field);
      }
      else
      {
         Fqn internalFqn = createInternalFqn(fqn, obj);
         if (log.isDebugEnabled())
         {
            log.debug("putObject(): id: " + fqn + " will store the pojo in the internal area: "
                      + internalFqn);
         }

         if (obj instanceof Advised)
         {
            advisedHandler_.put(internalFqn, fqn, obj);
         }
         else if (isCollection(obj))
         {
            collectionHandler_.put(internalFqn, fqn, obj);
            //
         }
         else
         {
            // must be Serializable, including primitive types
            serializableHandler_.put(internalFqn, obj);
         }

         // Used by notification sub-system
         cache.put(internalFqn, InternalConstant.POJOCACHE_STATUS, "ATTACHED");

         setPojoReference(fqn, obj, field, internalFqn);
      }

      return oldValue;
   }

   Fqn createInternalFqn(Fqn fqn, Object obj) throws CacheException
   {
      // Create an internal Fqn name
      return AopUtil.createInternalFqn(fqn, cache);
   }

   Fqn setPojoReference(Fqn fqn, Object obj, String field, Fqn internalFqn) throws CacheException
   {
      // Create PojoReference
      CachedType type = pojoCache.getCachedType(obj.getClass());
      PojoReference pojoReference = new PojoReference();
      pojoReference.setPojoClass(type.getType());

      // store PojoReference
      pojoReference.setFqn(internalFqn);
      internal_.putPojoReference(fqn, pojoReference, field);
      if (log.isDebugEnabled())
      {
         log.debug("put(): inserting PojoReference with id: " + fqn);
      }
      // store obj in the internal fqn
      return internalFqn;
   }

   private void createChildNodeFirstWithoutLocking(Fqn internalFqn)
   {
      int size = internalFqn.size();
      Fqn f = internalFqn.getSubFqn(0, size - 1);
      Fqn child = internalFqn.getSubFqn(size - 1, size);

      Node base = cache.getRoot().getChild(f);
      if (base == null)
      {
         log.debug("The node retrieved is null from fqn: " + f);
         return;
      }
      base.addChild(child);
   }

   /**
    * Note that caller of this method will take care of synchronization within the <code>fqn</code> sub-tree.
    *
    * @param fqn
    * @return
    * @throws CacheException
    */
   public Object removeObject(Fqn fqn, String field) throws CacheException
   {
      // the class attribute is implicitly stored as an immutable read-only attribute
      PojoReference pojoReference = internal_.getPojoReference(fqn, field);
      if (pojoReference == null)
      {
         //  clazz and pojoReference can be not null if this node is the replicated brother node.
         if (log.isTraceEnabled())
         {
            log.trace("removeObject(): clazz is null. id: " + fqn + " No need to remove.");
         }
         return null;
      }

      Class clazz = pojoReference.getPojoClass();
      Fqn internalFqn = pojoReference.getFqn();

      if (log.isDebugEnabled())
      {
         log.debug("removeObject(): removing object from id: " + fqn
                   + " with the corresponding internal id: " + internalFqn);
      }

      Object result = pojoCache.getObject(internalFqn);
      if (result == null)
      {
         return null;
      }

      if (graphHandler_.isMultipleReferenced(internalFqn))
      {
         graphHandler_.remove(fqn, internalFqn, result);
      }
      else
      {
         cache.put(internalFqn, InternalConstant.POJOCACHE_STATUS, "DETACHING");
         if (Advised.class.isAssignableFrom(clazz))
         {
            advisedHandler_.remove(internalFqn, result, clazz);
            internal_.cleanUp(internalFqn, null);
         }
         else if (isCollectionGet(clazz))
         {
            // We need to return the original reference
            result = collectionHandler_.remove(internalFqn, result);
            internal_.cleanUp(internalFqn, null);
         }
         else
         {// Just Serializable objects. Do a brute force remove is ok.
            serializableHandler_.remove();
            internal_.cleanUp(internalFqn, null);
         }
      }

      internal_.cleanUp(fqn, field);
      // remove the interceptor as well.
      return result;
   }

   public Map findObjects(Fqn fqn) throws CacheException
   {

      // Traverse from fqn to do getObject, if it return a pojo we then stop.
      Map map = new HashMap();
      Object pojo = getObject(fqn, null);
      if (pojo != null)
      {
         map.put(fqn, pojo);// we are done!
         return map;
      }

      findChildObjects(fqn, map);
      if (log.isDebugEnabled())
      {
         log.debug("_findObjects(): id: " + fqn + " size of pojos found: " + map.size());
      }
      return map;
   }

   private Object getObjectInternal(Fqn fqn, String field) throws CacheException
   {
      Fqn internalFqn = fqn;
      PojoReference pojoReference = internal_.getPojoReference(fqn, field);
      if (pojoReference != null)
      {
         internalFqn = pojoReference.getFqn();
      }
      else if (field != null)
      {
         return null;
      }

      if (log.isDebugEnabled())
         log.debug("getObject(): id: " + fqn + " with a corresponding internal id: " + internalFqn);

      /**
       * Reconstruct the managed POJO
       */
      Object obj;

      PojoInstance pojoInstance = internal_.getPojoInstance(internalFqn);

      if (pojoInstance == null)
         return null;
         //throw new PojoCacheException("PojoCacheDelegate.getObjectInternal(): null PojoInstance for fqn: " + internalFqn);

      Class clazz = pojoInstance.getPojoClass();

      // Check for both Advised and Collection classes for object graph.
      // Note: no need to worry about multiple referencing here. If there is a graph, we won't come this far.
      if (Advised.class.isAssignableFrom(clazz))
      {
         obj = advisedHandler_.get(internalFqn, clazz, pojoInstance);
      }
      else if (isCollectionGet(clazz))
      {// Must be Collection classes. We will use aop.ClassProxy instance instead.
         obj = collectionHandler_.get(internalFqn, clazz, pojoInstance);
      }
      else
      {
         // Maybe it is just a serialized object.
         obj = serializableHandler_.get(internalFqn, clazz, pojoInstance);
      }

      InternalHelper.setPojo(pojoInstance, obj);
      return obj;
   }

   private boolean isCollectionGet(Class clazz)
   {
      if (Map.class.isAssignableFrom(clazz) || Collection.class.isAssignableFrom(clazz))
      {
         return true;
      }

      return false;
   }


   private boolean isMultipleReferencedPut(Object obj)
   {
      Interceptor interceptor = null;
      if (obj instanceof Advised)
      {
         InstanceAdvisor advisor = ((Advised) obj)._getInstanceAdvisor();
         if (advisor == null)
         {
            throw new PojoCacheException("_putObject(): InstanceAdvisor is null for: " + obj);
         }

         // Step Check for cross references
         interceptor = AopUtil.findCacheInterceptor(advisor);
      }
      else
      {
         interceptor = CollectionInterceptorUtil.getInterceptor((ClassProxy) obj);
      }
      if (interceptor == null) return false;

      Fqn originalFqn = null;

      // ah, found something. So this will be multiple referenced.
      originalFqn = ((BaseInterceptor) interceptor).getFqn();

      return originalFqn != null;

   }

   private boolean isCollection(Object obj)
   {
      return obj instanceof Collection || obj instanceof Map;

   }

   private void detachInterceptor(InstanceAdvisor advisor, Interceptor interceptor,
                                  boolean detachOnly, Map undoMap)
   {
      if (!detachOnly)
      {
         util_.detachInterceptor(advisor, interceptor);
         undoMap.put(advisor, interceptor);
      }
      else
      {
         undoMap.put(DETACH, interceptor);
      }
   }

   private static void undoInterceptorDetach(Map undoMap)
   {
      for (Iterator it = undoMap.keySet().iterator(); it.hasNext();)
      {
         Object obj = it.next();

         if (obj instanceof InstanceAdvisor)
         {
            InstanceAdvisor advisor = (InstanceAdvisor) obj;
            BaseInterceptor interceptor = (BaseInterceptor) undoMap.get(advisor);

            if (interceptor == null)
            {
               throw new IllegalStateException("PojoCacheDelegate.undoInterceptorDetach(): null interceptor");
            }

            advisor.appendInterceptor(interceptor);
         }
         else
         {
            BaseInterceptor interceptor = (BaseInterceptor) undoMap.get(obj);
            boolean copyToCache = false;
            ((AbstractCollectionInterceptor) interceptor).attach(null, copyToCache);
         }
      }
   }

   private void findChildObjects(Fqn fqn, Map map) throws CacheException
   {
      // We need to traverse then
      Node root = cache.getRoot();
      Node current = root.getChild(fqn);

      if (current == null) return;

      Collection<Node> col = current.getChildren();
      if (col == null) return;
      for (Node n : col)
      {
         Fqn newFqn = n.getFqn();
         if (InternalHelper.isInternalNode(newFqn)) continue;// skip

         Object pojo = getObject(newFqn, null);
         if (pojo != null)
         {
            map.put(newFqn, pojo);
         }
         else
         {
            findChildObjects(newFqn, map);
         }
      }
   }

   public boolean exists(Fqn<?> id)
   {
      return internal_.getPojoReference(id, null) != null || internal_.getPojoInstance(id) != null;
   }
}
