package org.jboss.cache.interceptors;

import org.jboss.cache.GlobalTransaction;
import org.jboss.cache.Replicatable;
import org.jboss.cache.TransactionEntry;
import org.jboss.cache.TreeCache;
import org.jboss.util.NestedRuntimeException;
import org.jgroups.Address;
import org.jgroups.blocks.MethodCall;

import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import java.util.*;

/**
 * Takes care of replicating modifications to other nodes in a cluster. Also listens for prepare(),
 * commit() and rollback() messages which are received 'side-ways' (see docs/design/Refactoring.txt).
 * @author Bela Ban
 * @version $Id: ReplicationInterceptor.java,v 1.6.2.2 2004/09/23 18:42:34 bwang00 Exp $
 */
public class ReplicationInterceptor extends Interceptor implements Replicatable {
   TransactionManager tx_mgr=null;

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



   public ReplicationInterceptor() {

   }

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


   public Object invoke(MethodCall m) throws Throwable {
      Transaction tx=null;
      Object retval=super.invoke(m); // invoke first

      // 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)) {
            GlobalTransaction gtx=cache.getGlobalTransactionFromLocalTransaction(tx);
            if(gtx == null)
               throw new Exception("failed to get global transaction");
            // tx.registerSynchronization(new SynchronizationHandler(gtx, tx, cache));

            OrderedSynchronizationHandler handler=OrderedSynchronizationHandler.getInstance(tx);
            SynchronizationHandler myHandler=new SynchronizationHandler(gtx, tx, cache);
            handler.registerAtHead(myHandler); // needs to be invoked first on TX commit

            transactions.add(tx);  // todo: remove transaction again !
         }
         return retval;
      }
      // NON-TRANSACTIONAL and CRUD method; we simply discard non-modifying methods
      else if(TreeCache.isCrudMethod(m.getMethod())) {
         if(log.isTraceEnabled()) {
            log.trace("invoking method " + m.getName() + "(" + m.getArgs() + ")" + ", members=" + cache.getMembers() +
                      ", mode=" + cache.getCacheMode() + ", exclude_self=" + true + ", timeout=" + cache.getSyncReplTimeout());
         }
         switch(cache.getCacheModeInternal()) {
            case TreeCache.REPL_ASYNC:

               // 2. Replicate change to all *other* members (exclude self !)
               if(cache.getUseReplQueue() && cache.getReplQueue() != null) {
                  cache.getReplQueue().add(m);
               }
               else
                  cache.callRemoteMethods(cache.getMembers(), TreeCache.replicate_method,
                                          new Object[]{m},
                                          false, // synchronous ?
                                          true,  // exclude self ?
                                          cache.getSyncReplTimeout());
               break;
            case TreeCache.REPL_SYNC:
               // REVISIT Needs to exclude itself and apply the local change manually.
               // This is needed such that transient field is modified properly in-VM.
               List rsps=cache.callRemoteMethods(cache.getMembers(), TreeCache.replicate_method,
                                                 new Object[]{m}, true, true, cache.getSyncReplTimeout());
               if(log.isTraceEnabled())
                  log.trace("responses=" + rsps);
               checkResponses(rsps);
               break;
         }
      }
      return retval;
   }


   /**
    * Checks whether any of the responses are exceptions. If yes, re-throws
    * them (as exceptions or runtime exceptions).
    *
    * @param rsps
    * @throws Exception
    */
   private void checkResponses(List rsps) throws Exception {
      Object rsp;
      if(rsps != null) {
         for(Iterator it=rsps.iterator(); it.hasNext();) {
            rsp=it.next();
            if(rsp != null) {
               if(rsp instanceof RuntimeException)
                  throw (RuntimeException)rsp;
               if(rsp instanceof Exception)
                  throw (Exception)rsp;
            }
         }
      }
   }




   /**
    * Received as result of replication sent from another node to this node
    * @param method_call
    * @throws Throwable
    */
   public Object replicate(MethodCall method_call) throws Throwable {
      if(method_call == null)
         throw new NullPointerException("method call is null");
      //Method meth=method_call.getMethod();

      // 2PC method have to be invoked against the ReplicationInterceptor itself, whereas...

      // **** doesn't work: we delegate to TreeCache.prepare() !! ****** REVISIT

      //if(meth == TreeCache.prepare_method || meth == TreeCache.commit_method || meth == TreeCache.rollback_method) {
       //  return method_call.invoke(this);
      //}
      //else { // ... all other methods are passed to the interceptor chain
         return super.invoke(method_call);
      // }
   }

   public void replicate(List method_calls) throws Throwable {
      if(method_calls == null) return;
      for(Iterator it=method_calls.iterator(); it.hasNext();) {
         MethodCall c=(MethodCall)it.next();
         try {
            replicate(c);
         }
         catch(Throwable t) {
            log.error("failed executing method call", t);
         }
      }
   }










   class SynchronizationHandler implements Synchronization {
      Transaction       tx=null;
      GlobalTransaction gtx=null;
      TreeCache         cache=null;
      List              modifications=null;


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


      /**
       * Multicast a PREPARE to all nodes in the cluster (except myself), and collect the responses (only if synchronous).
       */
      public void beforeCompletion() {

         // fetch the modifications before the transaction is committed (and thus removed from the tx_table)
         TransactionEntry entry=cache.getTransactionEntry(gtx);
         if(entry == null)
            throw new IllegalStateException("cannot find transaction entry for " + gtx);
         modifications=new LinkedList(entry.getModifications());

         if(cache.getCacheModeInternal() != TreeCache.REPL_SYNC) // we only handle sync repl here
            return;

         // REPL_SYNC only from now on
         try {
            int status=tx.getStatus();
            if(modifications.size() == 0) // we don't replicate if there are no modifications in the current transaction
               return;

            switch(status) {
               case Status.STATUS_ACTIVE: // added Feb 2 2004 (bela)
               case Status.STATUS_COMMITTING:
                  try {
                     runPreparePhase(gtx,
                                     (Address)cache.getLocalAddress(),
                                     modifications,
                                     false,   // synchronous
                                     false);  // don't commit or rollback - wait for call
                  }
                  catch(Throwable t) {
                     log.warn("beforeCompletion(): runPreparePhase failed. Transaction is now rolled back", t);
                     tx.setRollbackOnly();
                     // change Bela Nov 5 2003: we need to rethrow the exception so the Transaction
                     // knows it has to set status of TX to rolled back
                     throw t;
                  }
                  break;
            }
         }
         catch(Throwable t) {
            throw new NestedRuntimeException("", t);
         }
      }



      public void afterCompletion(int status) {
          int cache_mode=cache.getCacheModeInternal();

          switch(status) {
             case Status.STATUS_COMMITTED:
                if(modifications != null && modifications.size() > 0) {
                   switch(cache_mode) {
                      case TreeCache.REPL_ASYNC:
                         replicateAsynchronously(gtx, modifications);
                         break;
                      case TreeCache.REPL_SYNC:
                         runCommitPhase(gtx); // commit locally, then send async commit() message to other mbrs
                         break;
                   }
                }
                break;

             case Status.STATUS_MARKED_ROLLBACK: // this one is probably not needed
             case Status.STATUS_ROLLEDBACK:
                if(log.isDebugEnabled())
                   log.debug("afterCompletion(): rolling back transaction");
                if(modifications != null && modifications.size() > 0) {
                   switch(cache_mode) {
                      case TreeCache.REPL_SYNC:
                         runRollbackPhase(gtx);
                         break;
                   }
                }
                break;
             default:
                throw new IllegalStateException("illegal status: " + status);
          }
      }
   }



   /**
    * Calls {@link #prepare(GlobalTransaction,List,org.jgroups.Address,boolean))} in all members except self.
    * Waits for all responses. If one of the members failed to prepare, its return value
    * will be an exception. If there is one exception we rethrow it. This will mark the
    * current transaction as rolled back, which will cause the
    * {@link #afterCompletion(int)} callback to have a status
    * of <tt>MARKED_ROLLBACK</tt>. When we get that call, we simply roll back the
    * transaction.<br/>
    * If everything runs okay, the {@link #afterCompletion(int)}
    * callback will trigger the {@link #runCommitPhase(GlobalTransaction))}.
    * <br/>
    *
    * @param tx
    * @param coordinator
    * @param async       Run this asynchronously
    * @param commit      Commit immediately after prepare() is successful (true if asynchronous)
    * @throws Exception
    */
   protected void runPreparePhase(GlobalTransaction tx,
                                  Address coordinator,
                                  List modifications,
                                  boolean async,
                                  boolean commit) throws Exception {
      List       rsps;
      MethodCall prepare_method_call;

      // this method will return immediately if we're the only member (because exclude_self=true)
      if(log.isDebugEnabled())
         log.debug("runPreparePhase(): run remote prepare with async mode: " + async +
                   " commit: " + commit + " and coordinator: " + coordinator);

      prepare_method_call=new MethodCall(TreeCache.prepare_method,
                                         new Object[]{tx, modifications, (Address)cache.getLocalAddress(),
                                                      new Boolean(commit)});
      rsps=cache.callRemoteMethods(cache.getMembers(),
                                   TreeCache.replicate_method,
                                   new Object[]{prepare_method_call},
                                   !async, // sync or async call ?
                                   true,   // exclude self
                                   cache.getSyncReplTimeout());
      if(!async && rsps != null)
         checkResponses(rsps); // throws an exception if one of the rsps is an exception
   }



   /**
     * Asynchronously calls {@link #commit(GlobalTransaction)} in all members
     *
     * @param tx
     */
    protected void runCommitPhase(GlobalTransaction gtx) {
      boolean sync_commit_phase=cache.getSyncCommitPhase();

      // 1. Multicast commit() to all members (exclude myself though)
      try {
         MethodCall commit_method=new MethodCall(TreeCache.commit_method, new Object[]{gtx});
         cache.callRemoteMethods(cache.getMembers(), TreeCache.replicate_method, new Object[]{commit_method},
                                 sync_commit_phase, // this is async by default, can be changed in TreeCache
                                 true,  // exclude self
                                 cache.getSyncReplTimeout());
      }
      catch(Throwable e) {
         log.error("runCommitPhase(): failed", e);
      }
   }


   /**
     * Asynchronously calls {@link #rollback(GlobalTransaction)} in all members
     * @param tx
     */
    protected void runRollbackPhase(GlobalTransaction gtx) {
      boolean sync_rollback_phase=cache.getSyncRollbackPhase();

      // 1. Multicast rollback() to all other members (excluding myself)
      try {
         MethodCall rollback_method=new MethodCall(TreeCache.rollback_method, new Object[]{gtx});
         cache.callRemoteMethods(cache.getMembers(), TreeCache.replicate_method, new Object[]{rollback_method},
                                 sync_rollback_phase, // this is by default an async call; can be changed in TreeCache
                                 true, // exclude self
                                 cache.getSyncReplTimeout());
      }
      catch(Throwable e) {
         log.error("runRollbackPhase(): failed", e);
      }
   }



   void replicateAsynchronously(GlobalTransaction gtx, List modifications) {
      MethodCall mc;
      try {
         mc=new MethodCall(TreeCache.prepare_method,
                           new Object[]{gtx, modifications,
                                        (Address)cache.getLocalAddress(),
                                        Boolean.TRUE});
         if(cache.getUseReplQueue() && cache.getReplQueue() != null) {
            cache.getReplQueue().add(new MethodCall(TreeCache.replicate_method, new Object[]{mc}));
         }
         else {
            // asynchronous PREPARE multicast, excluding self
            runPreparePhase(gtx,
                            (Address)cache.getLocalAddress(),
                            modifications,
                            true,   // async
                            true);  // do commit or rollback after call
         }
      }
      catch(Throwable t) {
         log.warn("failed to replicate asynchronously", t);
      }
   }

}
