package org.infinispan.atomic.container;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;

import org.infinispan.Cache;
import org.infinispan.atomic.Updatable;
import org.infinispan.commons.marshall.jboss.GenericJBossMarshaller;
import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified;
import org.infinispan.notifications.cachelistener.event.CacheEntryEvent;
import org.infinispan.util.concurrent.TimeoutException;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;

import javassist.util.proxy.MethodFilter;
import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.ProxyFactory;
import javassist.util.proxy.ProxyObject;

/**
 * @author Pierre Sutra
 * @since 7.2
 */
@Listener(sync = false, clustered = true, primaryOnly = true)
public class Container {

   //
   // CLASS FIELDS
   //

   private static final MethodFilter methodFilter = new MethodFilter() {
      @Override
      public boolean isHandled(Method m) {
         // ignore finalize() and externalization related methods.
         return !m.getName().equals("finalize")
               && !m.getName().equals("readExternal")
               && !m.getName().equals("writeExternal");
      }
   };
   private static Log log = LogFactory.getLog(Container.class);
   private static final int CALL_TTIMEOUT_TIME = 3000;
   private static final int RETRIEVE_TTIMEOUT_TIME = 3000;
   private static Executor globalExecutors = Executors.newCachedThreadPool();

   //
   // OBJECT FIELDS
   //

   private Cache cache;
   private Object key;
   private Object object;
   private Class clazz;
   private Object proxy;
   private List<String> updateMethods;
   private Executor callExecutor = new SerialExecutor(globalExecutors);

   private Boolean withReadOptimization; // serialize operation on the object copy
   private volatile int listenerState; // 0 = not installed, 1 = installed, -1 = disposed
   private final Container listener = this;

   private Map<Long,CallFuture> registeredCalls;
   private CallFuture retrieve_future;
   private ArrayList<CallInvoke> pending_calls;
   private CallRetrieve retrieve_call;

   //
   // PUBLIC METHODS
   //

   public Container(final Cache c, final Class cl, final Object k, final boolean readOptimization,
         final boolean forceNew, final List<String> methods, final Object... initArgs)
         throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, InterruptedException, ExecutionException, NoSuchMethodException, InvocationTargetException {

      cache = c;
      clazz = cl;
      key = clazz.getSimpleName()+"#"+k.toString(); // to avoid collisions

      withReadOptimization = readOptimization;
      listenerState = 0;
      updateMethods = methods;

      registeredCalls = new ConcurrentHashMap<Long, CallFuture>();

      // build the proxy
      MethodHandler handler = new MethodHandler() {

         public Object invoke(Object self, Method m, Method proceed, Object[] args) throws Throwable {

            GenericJBossMarshaller marshaller = new GenericJBossMarshaller();

            // 1 - local operation
            if ( withReadOptimization
                  && ! updateMethods.contains(m.getName()) ) {
               if (log.isDebugEnabled()) log.debugf("Executing %s locally", m.getName());
               return callObject(object, m.getName(), args);
            }

            // 2 - remote operation

            // 2.1 - if necessary, listener installation and object re-creation
            initObject(true, forceNew, initArgs);

            // 2.2 - call creation
            long callID = nextCallID();
            CallInvoke invoke = new CallInvoke(callID,m.getName(),args);
            byte[] bb = marshaller.objectToByteBuffer(invoke);
            CallFuture future = new CallFuture();
            registeredCalls.put(callID, future);

            // 2.3 - call execution
            cache.put(Container.this.key, bb);
            if (log.isDebugEnabled()) log.debugf("Waiting on %s", future.toString());
            Object ret = future.get(CALL_TTIMEOUT_TIME,TimeUnit.MILLISECONDS);
            registeredCalls.remove(callID);
            if(!future.isDone()){
               throw new TimeoutException("Unable to execute "+invoke+" on "+clazz+ " @ "+ Container.this.key);
            }
            if (log.isDebugEnabled()) log.debugf("Return %s %s ", invoke.toString(), (ret==null ? "null" : ret.toString()));
            return ret;
         }
      };

      ProxyFactory fact = new ProxyFactory();
      fact.setSuperclass(clazz);
      fact.setFilter(methodFilter);
      proxy = fact.createClass().newInstance();
      ((ProxyObject)proxy).setHandler(handler);
      initObject(false, forceNew, initArgs);

   }

   /**
    * Internal use of the listener API.
    *
    * @param event of class CacheEntryModifiedEvent
    */
   @SuppressWarnings({ "rawtypes", "unchecked" })
   @CacheEntryModified
   @CacheEntryCreated
   @Deprecated
   public void onCacheModification(CacheEntryEvent event){

      if( !event.getKey().equals(key) )
         return;

      if(event.isPre())
         return;

      try {

         GenericJBossMarshaller marshaller = new GenericJBossMarshaller();
         byte[] bb = (byte[]) event.getValue();
         Call call = (Call) marshaller.objectFromByteBuffer(bb);
         if (log.isDebugEnabled()) log.debugf("Receive %s (isOriginLocal=%s) ", call, event.isOriginLocal());
         callExecutor.execute(new AtomicObjectContainerTask(call));

      } catch (Exception e) {
         e.printStackTrace();
      }

   }

   public synchronized void dispose(boolean keepPersistent) throws IOException, InterruptedException{

      if (log.isDebugEnabled()) log.debugf("Disposing ");

      if (!registeredCalls.isEmpty()){
         log.warnf("Cannot dispose - registeredCalls non-empty");
         return;
      }

      if (listenerState==1){
         cache.removeListener(listener);
         if ( keepPersistent ) {
            if (log.isDebugEnabled()) log.debugf("Persisted");
            GenericJBossMarshaller marshaller = new GenericJBossMarshaller();
            CallPersist persist = new CallPersist(0,object);
            byte[] bb = marshaller.objectToByteBuffer(persist);
            cache.put(key, bb);
         }
      }
      listenerState = -1;

   }


   public Object getProxy(){
      return proxy;
   }

   public Class getClazz(){
      return clazz;
   }

   @Override
   public String toString(){
      return "Container ["+this.key.toString()+"]";
   }

   //
   // PRIVATE METHODS
   //

   /**
    *
    * @param invocation
    * @return true if the operation is local
    * @throws InvocationTargetException
    * @throws IllegalAccessException
    */
   private boolean handleInvocation(CallInvoke invocation)
         throws InvocationTargetException, IllegalAccessException {
      Object ret = callObject(object, invocation.method, invocation.arguments);
      CallFuture future = registeredCalls.get(invocation.callID);
      if(future!=null){
         if (log.isDebugEnabled()) log.debugf("Updating %s",future.toString());
         future.setReturnValue(ret);
         return true;
      }else{
         log.debugf("No future for %s", invocation.callID);
      }
      return false;
   }

   private void initObject(boolean installListener, boolean forceNew, Object... initArgs) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {

      if (object!=null && (!installListener || listenerState==1))
         return;

      if (installListener)
         installListener();

      if( !forceNew){
         GenericJBossMarshaller marshaller = new GenericJBossMarshaller();
         try {
            Call persist = (Call) marshaller.objectFromByteBuffer((byte[]) cache.get(key));
            if(persist instanceof CallPersist){
               if (log.isDebugEnabled()) log.debugf("Persisted %s",key.toString());
               object = ((CallPersist)persist).object;
            }else{
               installListener();
               if (log.isDebugEnabled()) log.debugf("Retrieving object %s",key.toString());
               if (installListener==false)
                  throw new IllegalAccessException();
               retrieve_future = new CallFuture();
               retrieve_call = new CallRetrieve(nextCallID());
               marshaller = new GenericJBossMarshaller();
               cache.put(key,marshaller.objectToByteBuffer(retrieve_call));
               retrieve_future.get(RETRIEVE_TTIMEOUT_TIME,TimeUnit.MILLISECONDS);
               if(!retrieve_future.isDone()) throw new TimeoutException();
               if (log.isDebugEnabled()) log.debugf("Object %s retrieved", key.toString());
               assert object!=null;
            }
            if (object instanceof Updatable){
               ((Updatable)object).setCache(this.cache);
               ((Updatable)object).setKey(this.key);
            }
            return;
         } catch (Exception e) {
            if (log.isDebugEnabled()) log.debugf("Unable to retrieve object %s from the cache.", key.toString());
         }
      }

      boolean found=false;
      Constructor[] allConstructors = clazz.getDeclaredConstructors();
      for (Constructor ctor : allConstructors) {
         Class<?>[] pType  = ctor.getParameterTypes();
         if(pType.length==initArgs.length){
            found=true;
            for (int i = 0; i < pType.length; i++) {
               if(!pType[i].isAssignableFrom(initArgs[i].getClass())){
                  found=false;
                  break;
               }
            }
            if(found){
               object = ctor.newInstance(initArgs);
               break;
            }
         }
      }

      if (object instanceof Updatable){
         ((Updatable)object).setCache(this.cache);
         ((Updatable)object).setKey(this.key);
      }

      if(found) {
         if (log.isDebugEnabled()) {
            log.debugf("Object %s[%s] is created (AtomicObject=%s)",
                  key.toString(),
                  clazz.getSimpleName(),
                  Boolean.toString(object instanceof Updatable));
         }
      } else {
         throw new IllegalArgumentException("Unable to find constructor for " + clazz.toString() + " with " + initArgs);
      }

   }

   //
   // HELPERS
   //

   private void installListener(){
      if (listenerState==1)
         return;
      if (log.isDebugEnabled()) log.debugf("Installing listener %s",key);
      cache.addListener(listener);
      listenerState = 1;
   }

   private long nextCallID(){
      Random random = new Random(System.nanoTime());
      return Thread.currentThread().getId()*random.nextLong();
   }

   private synchronized Object callObject(Object obj, String method, Object[] args) throws InvocationTargetException, IllegalAccessException {
      if (log.isDebugEnabled()) log.debugf("Calling %s()", method.toString());
      boolean isFound = false;
      Object ret = null;
      for (Method m : obj .getClass().getMethods()) { // only public methods (inherited and not)
         if (method.equals(m.getName())) {
            boolean isAssignable = true;
            Class[] argsTypes = m.getParameterTypes();
            if(argsTypes.length == args.length){
               for(int i=0; i<argsTypes.length; i++){
                  if( !argsTypes[i].isAssignableFrom(args[i].getClass()) ){
                     isAssignable = false;
                     break;
                  }
               }
            }else{
               isAssignable = false;
            }
            if(!isAssignable)
               continue;

            ret = m.invoke(obj, args);
            isFound = true;
            break;
         }
      }

      if(!isFound)
         throw new IllegalStateException("Method "+method+" not found.");

      return ret;
   }

   //
   // INNER CLASSES
   //

   private class AtomicObjectContainerTask implements Runnable{

      public Call call;

      public AtomicObjectContainerTask(Call c){
         call = c;
      }

      @Override
      public void run() {

         try {

            if (call instanceof CallInvoke) {

               if(object != null){

                  CallInvoke invocation = (CallInvoke) call;
                  handleInvocation(invocation);

               }else if (pending_calls != null) {

                  pending_calls.add((CallInvoke) call);

               }

            } else if (call instanceof CallRetrieve) {

               if (object != null ) {

                  if (log.isDebugEnabled()) log.debugf("Sending persistent state");

                  CallPersist persist = new CallPersist(0,object);
                  GenericJBossMarshaller marshaller = new GenericJBossMarshaller();
                  cache.put(key, marshaller.objectToByteBuffer(persist));

               }else if (retrieve_call != null && retrieve_call.callID == ((CallRetrieve)call).callID) {

                  assert pending_calls == null;
                  pending_calls = new ArrayList<CallInvoke>();

               }

            } else { // AtomicObjectCallPersist

               if (log.isDebugEnabled()) log.debugf("Persistent state received");

               if (object == null && pending_calls != null)  {
                  object = ((CallPersist)call).object;
                  for(CallInvoke invocation : pending_calls){
                     handleInvocation(invocation);
                  }
                  pending_calls = null;
                  retrieve_future.setReturnValue(null);
               }
            }

         } catch (Exception e) {
            e.printStackTrace();
         }

      }
   }

}
