package org.jboss.cache.loader;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.Modification;
import org.jboss.cache.NodeSPI;
import org.jboss.cache.config.CacheLoaderConfig;
import org.jboss.cache.config.CacheLoaderConfig.IndividualCacheLoaderConfig.SingletonStoreConfig;
import org.jboss.cache.notifications.annotation.CacheListener;
import org.jboss.cache.notifications.annotation.CacheStarted;
import org.jboss.cache.notifications.annotation.CacheStopped;
import org.jboss.cache.notifications.annotation.ViewChanged;
import org.jboss.cache.notifications.event.Event;
import org.jboss.cache.notifications.event.ViewChangedEvent;
import org.jgroups.Address;
import org.jgroups.View;

import java.io.ObjectInputStream;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * SingletonStoreCacheLoader is a delegating cache loader used for situations when only one node should interact with
 * the underlying store. The coordinator of the cluster will be responsible for the underlying CacheLoader.
 * SingletonStoreCacheLoader is a simply facade to a real CacheLoader implementation. It always delegates reads to the
 * real CacheLoader.
 * <p/>
 * Writes are forwarded only if this SingletonStoreCacheLoader is currently the cordinator. This avoid having all
 * CacheLoaders in a cluster writing the same data to the same underlying store. Although not incorrect (e.g. a DB
 * will just discard additional INSERTs for the same key, and throw an exception), this will avoid a lot of
 * redundant work.<br/>
 * <p/>
 * Whenever the current coordinator dies (or leaves), the second in line will take over. That SingletonStoreCacheLoader
 * will then pass writes through to its underlying CacheLoader. Optionally, when a new coordinator takes over the
 * Singleton, it can push the in-memory state to the cache cacheLoader, within a time constraint.
 *
 * @author Bela Ban
 * @author <a href="mailto:galder.zamarreno@jboss.com">Galder Zamarreno</a>
 */
public class SingletonStoreCacheLoader extends AbstractDelegatingCacheLoader
{
   /**
    * Log instance.
    */
   private static final Log log = LogFactory.getLog(SingletonStoreCacheLoader.class);
   private static final boolean trace = log.isTraceEnabled();

   /**
    * Name of thread that should pushing in-memory state to cache loader.
    */
   private static final String THREAD_NAME = "InMemoryToCacheLoaderPusher";

   /**
    * Configuration for the SingletonStoreCacheLoader.
    */
   private SingletonStoreDefaultConfig config;

   /**
    * Executor service used to submit tasks to push in-memory state.
    */
   private final ExecutorService executor;

   /**
    * Future result of the in-memory push state task. This allows SingletonStoreCacheLoader to check whether there's any
    * push taks on going.
    */
   private Future<?> pushStateFuture; /* FutureTask guarantess a safe publication of the result */

   /**
    * Address instance that allows SingletonStoreCacheLoader to find out whether it became the coordinator of the
    * cluster, or whether it stopped being it. This dictates whether the SingletonStoreCacheLoader is active or not.
    */
   private Address localAddress;

   /**
    * Whether the the current node is the coordinator and therefore SingletonStoreCacheLoader is active. Being active
    * means delegating calls to the underlying cache loader.
    */
   private boolean active;

   /**
    * Empty constructor so that it can instantiated using reflection.
    */
   public SingletonStoreCacheLoader()
   {
      super(null);

      executor = Executors.newSingleThreadExecutor(new ThreadFactory()
      {
         public Thread newThread(Runnable r)
         {
            return new Thread(r, THREAD_NAME);
         }
      });
   }

   /**
    * Sets the config for SingletonStoreCacheLoader and for the delegating cache loader.
    */
   @Override
   public void setConfig(CacheLoaderConfig.IndividualCacheLoaderConfig config)
   {
      super.setConfig(config);

      SingletonStoreConfig ssc = config.getSingletonStoreConfig();
      if (ssc instanceof SingletonStoreDefaultConfig)
      {
         this.config = (SingletonStoreDefaultConfig) ssc;
      }
      else if (ssc != null)
      {
         this.config = new SingletonStoreDefaultConfig(ssc);
      }
      else
      {
         this.config = new SingletonStoreDefaultConfig();
      }
   }

   @Override
   public void create() throws Exception
   {
      super.create();

      cache.addCacheListener(new SingletonStoreListener());
   }

   /**
    * Protected constructor which should only be used from unit tests. Production code should set
    * pushStateWhenCoordinator using setConfig() method instead.
    *
    * @param SingletonStoreDefaultConfig configuration instance for SingletonStoreCacheLoader
    */
   protected SingletonStoreCacheLoader(SingletonStoreDefaultConfig config)
   {
      this();

      this.config = config;
   }

   /**
    * Returns SingletonStoreCacheLoader's configuration instance. This method has been defined for convenience reasons
    * when unit testing SingletonStoreCacheLoader's configuration.
    *
    * @return instance of SingletonStoreDefaultConfig
    */
   protected SingletonStoreDefaultConfig getSingletonStoreDefaultConfig()
   {
      return config;
   }

   /**
    * Returns the Future instance of a running in-memory to cache loader push task. This method has been defined for
    * convenience reasons when unit testing.
    *
    * @return an instance of Future
    */
   protected Future<?> getPushStateFuture()
   {
      return pushStateFuture;
   }

   /**
    * Method called when the node either becomes the coordinator or stops being the coordinator. If it becomes the
    * coordinator, it can optionally start the in-memory state transfer to the underlying cache store.
    *
    * @param newActiveState true if the node just became the coordinator, false if the nodes stopped being the coordinator.
    */
   protected void activeStatusChanged(boolean newActiveState) throws PushStateException
   {
      active = newActiveState;
      log.debug("changed mode: " + this);
      if (active && config.isPushStateWhenCoordinator())
      {
         doPushState();
      }
   }

   /**
    * Factory method for the creation of a Callable task in charge of pushing in-memory state to cache loader.
    *
    * @return new instance of Callable<?> whose call() method either throws an exception or returns null if the task
    *         was successfull.
    */
   protected Callable<?> createPushStateTask()
   {
      return new Callable()
      {
         public Object call() throws Exception
         {
            final boolean debugEnabled = log.isDebugEnabled();

            if (debugEnabled) log.debug("start pushing in-memory state to cache cacheLoader");
            pushState(cache.getRoot());
            if (debugEnabled) log.debug("in-memory state passed to cache cacheLoader successfully");

            return null;
         }
      };
   }

   /**
    * Pushes the state of a specific node by reading the node's data from the cache and putting in the cache store
    * via the cache loader. This method is call recursively so that it iterates through the whole cache.
    *
    * @param node instance of NodeSPI to push to the cache loader
    * @throws Exception if there's any issues reading the data from the cache or pushing the node's data to the cache
    *                   loader.
    */
   protected void pushState(NodeSPI node) throws Exception
   {
      /* Put the node's data first */
      Set keys = node.getKeysDirect();
      Fqn fqn = node.getFqn();

      for (Object aKey : keys)
      {
         Object value = cache.get(fqn, aKey);
         put(fqn, aKey, value);
      }

      /* Navigates to the children */
      Collection<NodeSPI> children = node.getChildrenDirect();
      for (NodeSPI aChildren : children)
      {
         //Map.Entry entry = (Map.Entry) aChildren;
         pushState(aChildren);
      }
   }

   /**
    * Method that waits for the in-memory to cache loader state to finish. This method's called in case a push state
    * is already in progress and we need to wait for it to finish.
    *
    * @param future  instance of Future representing the on going push task
    * @param timeout time to wait for the push task to finish
    * @param unit    instance of TimeUnit representing the unit of timeout
    */
   protected void awaitForPushToFinish(Future future, int timeout, TimeUnit unit)
   {
      final boolean debugEnabled = log.isDebugEnabled();
      try
      {
         if (debugEnabled) log.debug("wait for state push to cache loader to finish");
         future.get(timeout, unit);
      }
      catch (TimeoutException e)
      {
         if (debugEnabled) log.debug("timed out waiting for state push to cache loader to finish");
      }
      catch (ExecutionException e)
      {
         if (debugEnabled) log.debug("exception reported waiting for state push to cache loader to finish");
      }
      catch (InterruptedException ie)
      {
         /* Re-assert the thread's interrupted status */
         Thread.currentThread().interrupt();
         if (trace) log.trace("wait for state push to cache loader to finish was interrupted");
      }
   }

   /**
    * Called when the SingletonStoreCacheLoader discovers that the node has become the coordinator and push in memory
    * state has been enabled. It might not actually push the state if there's an ongoing push task running, in which
    * case will wait for the push task to finish.
    *
    * @throws PushStateException when the push state task reports an issue.
    */
   private void doPushState() throws PushStateException
   {
      if (pushStateFuture == null || pushStateFuture.isDone())
      {
         Callable<?> task = createPushStateTask();
         pushStateFuture = executor.submit(task);
         try
         {
            waitForTaskToFinish(pushStateFuture, config.getPushStateWhenCoordinatorTimeout(), TimeUnit.MILLISECONDS);
         }
         catch (Exception e)
         {
            throw new PushStateException("unable to complete in memory state push to cache loader", e);
         }
      }
      else
      {
         /* at the most, we wait for push state timeout value. if it push task finishes earlier, this call
         * will stop when the push task finishes, otherwise a timeout exception will be reported */
         awaitForPushToFinish(pushStateFuture, config.getPushStateWhenCoordinatorTimeout(), TimeUnit.MILLISECONDS);
      }
   }

   /**
    * Waits, within a time constraint, for a task to finish.
    *
    * @param future  represents the task waiting to finish.
    * @param timeout maximum time to wait for the time to finish.
    * @param unit    instance of TimeUnit representing the unit of timeout
    * @throws Exception if any issues are reported while waiting for the task to finish
    */
   private void waitForTaskToFinish(Future future, int timeout, TimeUnit unit) throws Exception
   {
      try
      {
         future.get(timeout, unit);
      }
      catch (TimeoutException e)
      {
         throw new Exception("task timed out", e);
      }
      catch (InterruptedException e)
      {
         /* Re-assert the thread's interrupted status */
         Thread.currentThread().interrupt();
         if (trace) log.trace("task was interrupted");
      }
      finally
      {
         /* no-op if task is completed */
         future.cancel(true); /* interrupt if running */
      }
   }

   /**
    * Indicates whether the current nodes is the coordinator of the cluster.
    *
    * @param newView View instance containing the new view of the cluster
    * @return whether the current node is the coordinator or not.
    */
   private boolean isCoordinator(View newView)
   {
      if (newView != null && localAddress != null)
      {
         Vector mbrs = newView.getMembers();
         if (mbrs != null && mbrs.size() > 0 && localAddress.equals(mbrs.firstElement()))
         {
            /* This node is the coordinator */
            return true;
         }

         return false;
      }

      /* Invalid new view, so previous value returned */
      return active;
   }

   /**
    * Calls the underlying cache loader's operation if the current node is the coordinator.
    */
   @Override
   public Object put(Fqn name, Object key, Object value) throws Exception
   {
      if (active)
      {
         return super.put(name, key, value);
      }

      return null;
   }

   /**
    * Calls the underlying cache loader's operation if the current node is the coordinator.
    */
   @Override
   public void put(Fqn name, Map attributes) throws Exception
   {
      if (active)
      {
         super.put(name, attributes);
      }
   }

   /**
    * Calls the underlying cache loader's operation if the current node is the coordinator.
    */
   @Override
   public void put(List<Modification> modifications) throws Exception
   {
      if (active)
      {
         super.put(modifications);
      }
   }

   /**
    * Calls the underlying cache loader's operation if the current node is the coordinator.
    */
   @Override
   public Object remove(Fqn fqn, Object key) throws Exception
   {
      if (active)
      {
         return super.remove(fqn, key);
      }

      return null;
   }

   /**
    * Calls the underlying cache loader's operation if the current node is the coordinator.
    */
   @Override
   public void remove(Fqn fqn) throws Exception
   {
      if (active)
      {
         super.remove(fqn);
      }
   }

   /**
    * Calls the underlying cache loader's operation if the current node is the coordinator.
    */
   @Override
   public void removeData(Fqn fqn) throws Exception
   {
      if (active)
      {
         super.removeData(fqn);
      }
   }

   /**
    * Calls the underlying cache loader's operation if the current node is the coordinator.
    */
   @Override
   public void prepare(Object tx, List<Modification> modifications, boolean one_phase) throws Exception
   {
      if (active)
      {
         super.prepare(tx, modifications, one_phase);
      }
   }

   /**
    * Calls the underlying cache loader's operation if the current node is the coordinator.
    */
   @Override
   public void commit(Object tx) throws Exception
   {
      if (active)
      {
         super.commit(tx);
      }
   }

   /**
    * Calls the underlying cache loader's operation if the current node is the coordinator.
    */
   @Override
   public void rollback(Object tx)
   {
      if (active)
      {
         super.rollback(tx);
      }
   }

   /**
    * Calls the underlying cache loader's operation if the current node is the coordinator.
    */
   @Override
   public void storeEntireState(ObjectInputStream is) throws Exception
   {
      if (active)
      {
         super.storeEntireState(is);
      }
   }

   /**
    * Calls the underlying cache loader's operation if the current node is the coordinator.
    */
   @Override
   public void storeState(Fqn subtree, ObjectInputStream is) throws Exception
   {
      if (active)
      {
         super.storeState(subtree, is);
      }
   }

   /**
    * Calls the underlying cache loader's operation if the current node is the coordinator.
    */
   @Override
   public String toString()
   {
      return "loc_addr=" + localAddress + ", active=" + active;
   }

   /**
    * Cache listener that reacts to cluster topology changes to find out whether a new coordinator is elected.
    * SingletonStoreCacheLoader reacts to these changes in order to decide which node should interact with the
    * underlying cache store.
    */
   @CacheListener
   public class SingletonStoreListener
   {
      /**
       * Cache started, check whether the node is the coordinator and set the singleton store cache loader's active
       * status.
       */
      @CacheStarted
      public void cacheStarted(Event e)
      {
         localAddress = cache.getLocalAddress();
         active = cache.getRPCManager().isCoordinator();
         if (log.isDebugEnabled()) log.debug("cache started: " + this);
      }

      @CacheStopped
      public void cacheStopped(Event e)
      {
         if (log.isDebugEnabled()) log.debug("cache stopped: " + this);
      }

      /**
       * The cluster formation changed, so determine whether the current node stopped being the coordinator or became
       * the coordinator. This method can lead to an optional in memory to cache loader state push, if the current node
       * became the coordinator. This method will report any issues that could potentially arise from this push.
       */
      @ViewChanged
      public void viewChange(ViewChangedEvent event)
      {
         boolean tmp = isCoordinator(event.getNewView());

         if (active != tmp)
         {
            try
            {
               activeStatusChanged(tmp);
            }
            catch (PushStateException e)
            {
               log.error("exception reported changing nodes active status", e);
            }

         }
      }
   }

   /**
    * Exception representing any issues that arise from pushing the in-memory state to the cache loader.
    */
   public static class PushStateException extends Exception
   {
      private static final long serialVersionUID = 5542893943730200886L;

      public PushStateException(String message, Throwable cause)
      {
         super(message, cause);
      }

      public PushStateException(Throwable cause)
      {
         super(cause);
      }
   }
}