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

import org.jboss.aop.advice.Interceptor;
import org.jboss.aop.joinpoint.FieldReadInvocation;
import org.jboss.aop.joinpoint.FieldWriteInvocation;
import org.jboss.aop.joinpoint.Invocation;
import org.jboss.aop.joinpoint.MethodInvocation;
import org.jboss.cache.Fqn;
import org.jboss.logging.Logger;

import java.io.Externalizable;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Iterator;

/**
 * Created: Sat Apr 26 10:35:01 2003
 *
 * @author Harald Gliebe
 * @author Ben Wang
 */

public class CacheInterceptor implements Interceptor
{
   protected static Logger log_ = Logger.getLogger(CacheInterceptor.class);
   TreeCacheAop cache;
   Fqn fqn;
   CachedType type;
   boolean checkSerialization;

   static Method writeExternal, readExternal;

   static
   {
      try {
         writeExternal =
               Externalizable.class.getMethod("writeExternal",
                     new Class[]{ObjectOutput.class});
         readExternal =
               Externalizable.class.getMethod("readExternal",
                     new Class[]{ObjectInput.class});
      } catch (Exception e) {
         e.printStackTrace();
      }
   }

   public CacheInterceptor(TreeCacheAop cache, Fqn fqn, CachedType type)
   {
      this.cache = cache;
      this.fqn = fqn;
      this.type = type;
      checkSerialization =
            !WriteReplaceable.class.isAssignableFrom(type.getType());
   }

   public String getName()
   {
      return "CacheInterceptor on [" + fqn + "]";
   }

   public Object invoke(Invocation invocation) throws Throwable
   {
      if (invocation instanceof FieldWriteInvocation) {
         checkCacheConsistency();
         FieldWriteInvocation fieldInvocation =
               (FieldWriteInvocation) invocation;
         Field field = fieldInvocation.getField();

         // Only if this field is replicatable
         if( !CachedType.isNonReplicatable(field)) {

            CachedType fieldType = cache.getCachedType(field.getType());

            Object value = fieldInvocation.getValue();
            if (fieldType.isImmediate()) {
               cache.put(fqn, field.getName(), value);
            } else {
               //cache.putObject(((Fqn)fqn.clone()).add(field.getName()), value);
               cache.putObject(new Fqn(fqn, field.getName()), value);
            }
         }

      } else if (invocation instanceof FieldReadInvocation) {
         checkCacheConsistency();
         FieldReadInvocation fieldInvocation =
               (FieldReadInvocation) invocation;
         Field field = fieldInvocation.getField();
         // Only if this field is replicatable
         if( !CachedType.isNonReplicatable(field)) {

            CachedType fieldType = cache.getCachedType(field.getType());
            Object result;
            if (fieldType.isImmediate()) {
               result = cache.get(fqn, field.getName());
            } else {
               //result = cache.getObject(((Fqn)fqn.clone()).add(field.getName()));
               result = cache.getObject(new Fqn(fqn, field.getName()));
            }

            // if result is null, we need to make sure the in-memory reference is null
            // as well. If it is not, then we know this one is null because it has
            // been evicted. Will need to reconstruct it
            if(result != null)
               return result;
            else {
               // TODO There is a chance of recursive loop here if caller tries to print out obj that will trigger the fieldRead interception.
               Object value = invocation.getTargetObject();
               // TODO Need to handle the modifiers transient, final, and static fields here
               if(value == null || field.get(value) == null)   // if both are null, we know this is null as well.
                  return null;
               else {
                  if(log_.isDebugEnabled()) {
                     log_.debug("invoke(): Node on fqn: " +fqn + " has obviously been evicted. Will need to reconstruct it");
                  }

                  cache.putObject(fqn, value);
               }
            }
         }
      } else if (checkSerialization) {
         MethodInvocation methodInvocation = (MethodInvocation) invocation;
         Method method = methodInvocation.getMethod();

         if (method != null
               && method.getName().equals("writeReplace")
               && method.getReturnType().equals(Object.class)
               && method.getParameterTypes().length == 0) {

            beforeSerialization(invocation.getTargetObject());
         } else if (method == writeExternal) {
            Object target = methodInvocation.getTargetObject();
            beforeSerialization(target);
         }
      }

      return invocation.invokeNext();

   }

   protected void checkCacheConsistency() throws Exception
   {
      if (this != cache.peek(fqn, AOPInstance.KEY)) {
         new RuntimeException("Cache inconsistency: Outdated AOPInstance");
      }
   }

   public void beforeSerialization(Object target) throws Exception
   {

      // fill objects
      for (Iterator i = type.getFields().iterator(); i.hasNext();) {
         Field field = (Field) i.next();
         CachedType fieldType = cache.getCachedType(field.getType());
         Object value = null;
         if (fieldType.isImmediate()) {
            value = cache.get(fqn, field.getName());
         } else {
            //      value = removeObject(fqn+TreeCache.SEPARATOR+field.getName());
            //value = cache.getObject(((Fqn)fqn.clone()).add(field.getName()));
            value = cache.getObject(new Fqn(fqn, field.getName()));
         }
         //     System.out.println("Setting field " + field.getName() + "[" + field.getDeclaringClass() + "] of "+ target.getClass() + " to " + value);
         field.set(target, value);
      }
   }

   boolean isChildOf(Fqn parentFqn)
   {
      return fqn.isChildOf(parentFqn);
   }

   Fqn getFqn()
   {
      return fqn;
   }

   void setFqn(Fqn fqn)
   {
      this.fqn = fqn;
   }

}