package org.jboss.cache.interceptors;

import org.jgroups.blocks.MethodCall;
import org.jgroups.Address;
import org.jboss.cache.loader.CacheLoader;
import org.jboss.cache.*;
import org.jboss.util.NestedRuntimeException;

import javax.transaction.*;
import java.lang.reflect.Method;
import java.util.*;

/**
 * Takes care of writing modifications to a CacheLoader. Currently does <em>not</em> handle cache loading, only
 * cache storing. Cache loading is still done in the JbossCache (POJO) itself (will be moved to this interceptor
 * in the near future).
 * @author Bela Ban
 * @version $Id: CacheLoaderInterceptor.java,v 1.3.2.2 2004/09/23 18:42:33 bwang00 Exp $
 */
public class CacheLoaderInterceptor extends Interceptor {
   CacheLoader        loader=null;
   TransactionManager tx_mgr=null;

   /** List<Transaction> that we have registered for */
   List               transactions=Collections.synchronizedList(new ArrayList());


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


   public Object invoke(MethodCall m) throws Throwable {
      Fqn fqn=null;
      Map attributes=null;
      Object key, value;
      Method meth=m.getMethod();
      Object[] args=m.getArgs();
      Transaction tx;
      Object retval=null;

      if(loader == null && getLoader() == null) {
         log.error("cache loader is not set, cannot handle call");
         return super.invoke(m);
      }

      // could be potentially TRANSACTIONAL. If so, we register for transaction completion callbacks (if we
      // have not yet done so
      if(tx_mgr != null && (tx=tx_mgr.getTransaction()) != null) {
         if(!transactions.contains(tx)) {
            transactions.add(tx);
            GlobalTransaction gtx=cache.getGlobalTransactionFromLocalTransaction(tx);
            if(gtx == null)
               throw new Exception("failed to get global transaction");
            tx.registerSynchronization(new SynchronizationHandler(gtx, cache));
         }
         return super.invoke(m);
      }

      // if we're here we never run in a transaction
      if(meth.equals(TreeCache.put_data_method) || meth.equals(TreeCache.put_data_erase_method)) {
         fqn=(Fqn)args[1];
         attributes=(Map)args[2];
         loader.put(fqn, attributes);
      }
      else if(meth.equals(TreeCache.put_key_val_method)) {
         fqn=(Fqn)args[1];
         key=args[2];
         value=args[3];
         retval=loader.put(fqn, key, value);
      }
      else if(meth.equals(TreeCache.remove_node_method)) {
         fqn=(Fqn)args[1];
         loader.remove(fqn);
      }
      else if(meth.equals(TreeCache.remove_key_method)) {
         fqn=(Fqn)args[1];
         key=args[2];
         loader.remove(fqn, key);
      }
      else if (meth.equals(TreeCache.remove_data_method)) {
         fqn=(Fqn)args[1];
         loader.removeData(fqn);
      }
      else if(meth.equals(TreeCache.prepare_method)) {
         GlobalTransaction gtx=(GlobalTransaction)args[0];
         List modifications=(List)args[1];
         Address coordinator=(Address)args[2];
         boolean one_way=((Boolean)args[4]).booleanValue();
         prepare(gtx, modifications, coordinator, one_way);
      }
      else if(meth.equals(TreeCache.commit_method)) {
         commit((GlobalTransaction)args[0]);
      }
      else if(meth.equals(TreeCache.rollback_method)) {
         rollback((GlobalTransaction)args[0]);
      }
      Object tmp=super.invoke(m);
      return retval != null? retval : tmp;
   }




   /**
    * This call is received as a result of the ReplicationInterceptor underneath us receiving a prepare(), and propagating
    * the call up the interceptor chain
    * @param tx
    * @param modifications
    * @param coordinator
    * @param commit
    * @throws Exception
    */
   void prepare(GlobalTransaction gtx, List modifications, Address coordinator, boolean commit) throws Exception {
      getLoader().prepare(gtx, modifications, commit);
   }


   void commit(GlobalTransaction gtx) {
      try {
         getLoader().commit(gtx);
      }
      catch(Exception e) {
         log.error("cache loader was unable to commit the transaction", e);
      }
   }

   void rollback(GlobalTransaction gtx) {
      getLoader().rollback(gtx);
   }

   CacheLoader getLoader() {
      return loader != null? loader : (loader=cache.getCacheLoader());
   }

   private void prepareCacheLoader(GlobalTransaction gtx) throws Exception {
      List modifications;
      TransactionEntry entry;

      entry=cache.getTransactionEntry(gtx);
      if(entry == null)
         throw new Exception("entry for transaction " + gtx + " not found in transaction table");
      modifications=entry.getModifications();
      if(modifications.size() == 0)
         return;
      List cache_loader_modifications=new ArrayList();
      for(Iterator it=modifications.iterator(); it.hasNext();) {
         MethodCall methodCall=(MethodCall)it.next();
         Modification mod=convertMethodCallToModification(methodCall);
         cache_loader_modifications.add(mod);
      }
      if(cache_loader_modifications.size() > 0) {
         loader.prepare(gtx, cache_loader_modifications, false);
      }
   }



   Modification convertMethodCallToModification(MethodCall methodCall) throws Exception {
      Method method=methodCall.getMethod();
      Object[] args;
      if(method == null)
         throw new Exception("method call has no method: " + methodCall);

      args=methodCall.getArgs();
      if(method == TreeCache.put_data_method) {
         return new Modification(Modification.PUT_DATA,
                                 (Fqn)args[1],      // fqn
                                 (Map)args[2]);     // data
      }
      else if(method == TreeCache.put_data_erase_method) {
         return new Modification(Modification.PUT_DATA_ERASE,
                                 (Fqn)args[1],      // fqn
                                 (Map)args[2]);     // data
      }
      else if(method == TreeCache.put_key_val_method) {
         return new Modification(Modification.PUT_KEY_VALUE,
                                 (Fqn)args[1],      // fqn
                                 args[2],           // key
                                 args[3]);          // value
      }
      else if(method == TreeCache.remove_node_method) {
         return new Modification(Modification.REMOVE_NODE,
                                 (Fqn)args[1]);     // fqn
      }
      else if(method == TreeCache.remove_key_method) {
         return new Modification(Modification.REMOVE_KEY_VALUE,
                                 (Fqn)args[1],      // fqn
                                 args[2]);          // key

      }
      else if(method == TreeCache.remove_data_method) {
         return new Modification(Modification.REMOVE_DATA,
                                 (Fqn)args[1]);     // fqn
      }
      else
         throw new Exception("method call " + method.getName() + " cannot be converted to a modification");
   }




   class SynchronizationHandler implements Synchronization {
      GlobalTransaction gtx=null;
      TreeCache cache=null;


      SynchronizationHandler(GlobalTransaction gtx, TreeCache cache) {
         this.gtx=gtx;
         this.cache=cache;
      }


      /**
       * Needs to call prepare() in the CacheLoader
       */
      public void beforeCompletion() {
         Transaction tx;

         try {
            tx=tx_mgr.getTransaction();
         }
         catch(Exception e) {
            log.error("failed fetching the current transaction", e);
            throw new NestedRuntimeException(e);
         }

         try {
            int status=tx.getStatus();
            switch(status) {
               case Status.STATUS_ACTIVE: // added Feb 2 2004 (bela)
               case Status.STATUS_COMMITTING:
                  prepareCacheLoader(gtx);
                  break;
               case Status.STATUS_MARKED_ROLLBACK:
               case Status.STATUS_ROLLING_BACK:
                  // rollback(gtx); // changed by Bela Feb 5 2004: we can do this in the afterCompletion()
                  // new note: beforeCompletion() will not be called when a TX is rolled back, but we are called
                  // afterCompletion(false) directly
                  break;
               default:
                  log.error("beforeCompletion(). Illegal tx status: " + status);
                  throw new IllegalStateException("Illegal status: " + status);
            }
         }
         catch(Throwable t) {
            throw new NestedRuntimeException("", t);
         }
      }


      /**
       * Depending on the status (OK or FAIL), call commit() or rollback() on the CacheLoader
       *
       * @param status
       */
      public void afterCompletion(int status) {
         switch(status) {
            case Status.STATUS_COMMITTED:
               try {
                  getLoader().commit(gtx);
               }
               catch(Exception e) {
                  log.error("failed committing transaction to cache loader", e);
               }
               break;

            case Status.STATUS_MARKED_ROLLBACK: // this one is probably not needed
            case Status.STATUS_ROLLEDBACK:
               getLoader().rollback(gtx);
               break;
         }
      }
   }

}
