/**
 *
 */
package org.jboss.cache.loader;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.CacheException;
import org.jboss.cache.Fqn;
import org.jboss.cache.Modification;
import org.jboss.cache.config.CacheLoaderConfig.IndividualCacheLoaderConfig;
import org.jboss.cache.util.MapCopy;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * The AsyncCacheLoader is a delegating cache loader that extends
 * AbstractDelegatingCacheLoader overriding methods to that should not
 * just delegate the operation to the underlying cache loader.
 * <p/>
 * Read operations are done synchronously, while write (CRUD - Create, Remove,
 * Update, Delete) operations are done asynchronously.  There is no provision
 * for exception handling at the moment for problems encountered with the
 * underlying CacheLoader during a CRUD operation, and the exception is just
 * logged.
 * <p/>
 * When configuring the CacheLoader, use the following attribute:
 * <p/>
 * <code>
 * &lt;attribute name="CacheLoaderAsynchronous"&gt;true&lt;/attribute&gt;
 * </code>
 * <p/>
 * to define whether cache loader operations are to be asynchronous.  If not
 * specified, a cache loader operation is assumed synchronous.
 * <p/>
 * <p/>
 * The following additional parameters are available:
 * <dl>
 * <dt>cache.async.batchSize</dt>
 * <dd>Number of modifications to commit in one transaction, default is
 * 100. The minimum batch size is 1.</dd>
 * <dt>cache.async.pollWait</dt>
 * <dd>How long to wait before processing an incomplete batch, in
 * milliseconds.  Default is 100.  Set this to 0 to not wait before processing
 * available records.</dd>
 * <dt>cache.async.returnOld</dt>
 * <dd>If <code>true</code>, this loader returns the old values from {@link
 * #put} and {@link #remove} methods.  Otherwise, these methods always return
 * null.  Default is true.  <code>false</code> improves the performance of these
 * operations.</dd>
 * <dt>cache.async.queueSize</dt>
 * <dd>Maximum number of entries to enqueue for asynchronous processing.
 * Lowering this size may help prevent out-of-memory conditions.  It also may
 * help to prevent less records lost in the case of JVM failure.  Default is
 * 10,000 operations.</dd>
 * <dt>cache.async.put</dt>
 * <dd>If set to false, all {@link #put} operations will be processed
 * synchronously, and then only the {@link #remove} operations will be
 * processed asynchronously. This mode may be useful for processing
 * expiration of messages within a separate thread and keeping other
 * operations synchronous for reliability.
 * </dd>
 * </dl>
 * For increased performance for many smaller transactions, use higher values
 * for <code>cache.async.batchSize</code> and
 * <code>cache.async.pollWait</code>.  For larger sized records, use a smaller
 * value for <code>cache.async.queueSize</code>.
 *
 * @author Manik Surtani (manik.surtani@jboss.com)
 */
public class AsyncCacheLoader extends AbstractDelegatingCacheLoader
{

   private static final Log log = LogFactory.getLog(AsyncCacheLoader.class);
   private static final boolean trace = log.isTraceEnabled();

   private static AtomicInteger threadId = new AtomicInteger(0);

   /**
    * Default limit on entries to process asynchronously.
    */
   private static final int DEFAULT_QUEUE_SIZE = 10000;

   private AsyncCacheLoaderConfig config;
   private AsyncProcessor processor;
   private AtomicBoolean stopped = new AtomicBoolean(true);
   private BlockingQueue<Modification> queue = new ArrayBlockingQueue<Modification>(DEFAULT_QUEUE_SIZE);

   public AsyncCacheLoader()
   {
      super(null);
   }

   public AsyncCacheLoader(CacheLoader cacheLoader)
   {
      super(cacheLoader);
   }

   @Override
   public void setConfig(IndividualCacheLoaderConfig base)
   {
      if (base instanceof AsyncCacheLoaderConfig)
      {
         config = (AsyncCacheLoaderConfig) base;
      }
      else
      {
         config = new AsyncCacheLoaderConfig(base);
      }

      if (config.getQueueSize() > 0)
      {
         queue = new ArrayBlockingQueue<Modification>(config.getQueueSize());
      }

      super.setConfig(base);
   }

   @Override
   public Map get(Fqn name) throws Exception
   {
      try
      {
         return super.get(name);
      }
      catch (IOException e)
      {
         // FileCacheLoader sometimes does this apparently
         log.trace(e);
         return new HashMap(); // ?
      }
   }

   Object get(Fqn name, Object key) throws Exception
   {
      if (config.getReturnOld())
      {
         try
         {
            Map map = super.get(name);
            if (map != null)
            {
               return map.get(key);
            }
         }
         catch (IOException e)
         {
            // FileCacheLoader sometimes does this apparently
            log.trace(e);
         }
      }
      return null;
   }

   @Override
   public Object put(Fqn name, Object key, Object value) throws Exception
   {
      if (config.getUseAsyncPut())
      {
         Object oldValue = get(name, key);
         Modification mod = new Modification(Modification.ModificationType.PUT_KEY_VALUE, name, key, value);
         enqueue(mod);
         return oldValue;
      }
      else
      {
         return super.put(name, key, value);
      }
   }

   @Override
   public void put(Fqn name, Map attributes) throws Exception
   {
      if (config.getUseAsyncPut())
      {
         // JBCACHE-769 -- make a defensive copy
         Map attrs = (attributes == null ? null : new MapCopy(attributes));
         Modification mod = new Modification(Modification.ModificationType.PUT_DATA, name, attrs);
         enqueue(mod);
      }
      else
      {
         super.put(name, attributes); // Let delegate make its own defensive copy
      }
   }

   @Override
   public void put(List<Modification> modifications) throws Exception
   {
      if (config.getUseAsyncPut())
      {
         for (Modification modification : modifications)
         {
            enqueue(modification);
         }
      }
      else
      {
         super.put(modifications);
      }
   }

   @Override
   public Object remove(Fqn name, Object key) throws Exception
   {
      Object oldValue = get(name, key);
      Modification mod = new Modification(Modification.ModificationType.REMOVE_KEY_VALUE, name, key);
      enqueue(mod);
      return oldValue;
   }

   @Override
   public void remove(Fqn name) throws Exception
   {
      Modification mod = new Modification(Modification.ModificationType.REMOVE_NODE, name);
      enqueue(mod);
   }

   @Override
   public void removeData(Fqn name) throws Exception
   {
      Modification mod = new Modification(Modification.ModificationType.REMOVE_DATA, name);
      enqueue(mod);
   }

   @Override
   public void start() throws Exception
   {
      if (log.isInfoEnabled()) log.info("Async cache loader starting: " + this);
      stopped.set(false);
      super.start();
      processor = new AsyncProcessor();
      processor.start();
   }

   @Override
   public void stop()
   {
      stopped.set(true);
      if (processor != null)
      {
         processor.stop();
      }
      super.stop();
   }

   private void enqueue(Modification mod)
         throws CacheException, InterruptedException
   {
      if (stopped.get())
      {
         throw new CacheException("AsyncCacheLoader stopped; no longer accepting more entries.");
      }
      if (trace)
      {
         log.trace("Enqueuing modification " + mod);
      }
      queue.put(mod);
   }

   /**
    * Processes (by batch if possible) a queue of {@link Modification}s.
    *
    * @author manik surtani
    */
   private class AsyncProcessor implements Runnable
   {
      private Thread t;

      // Modifications to invoke as a single put
      private final List<Modification> mods = new ArrayList<Modification>(config.getBatchSize());

      public void start()
      {
         if (t == null || !t.isAlive())
         {
            t = new Thread(this, "AsyncCacheLoader-" + threadId.getAndIncrement());
            t.setDaemon(true);
            t.start();
         }
      }

      public void stop()
      {
         if (t != null)
         {
            t.interrupt();
            try
            {
               t.join();
            }
            catch (InterruptedException e)
            {
            }
         }
         if (!queue.isEmpty())
         {
            log.warn("Async queue not yet empty, possibly interrupted");
         }
      }

      public void run()
      {
         while (!Thread.interrupted())
         {
            try
            {
               run0();
            }
            catch (InterruptedException e)
            {
               break;
            }
         }

         try
         {
            if (trace) log.trace("process remaining batch " + mods.size());
            put(mods);
            if (trace) log.trace("process remaining queued " + queue.size());
            while (!queue.isEmpty())
            {
               run0();
            }
         }
         catch (InterruptedException e)
         {
            log.trace("remaining interrupted");
         }
      }

      private void run0() throws InterruptedException
      {
         log.trace("Checking for modifications");
         int i = queue.drainTo(mods, config.getBatchSize());
         if (i == 0)
         {
            Modification m = queue.take();
            mods.add(m);
         }

         if (trace)
         {
            log.trace("Calling put(List) with " + mods.size() + " modifications");
         }
         put(mods);
         mods.clear();
      }

      private void put(List<Modification> mods)
      {
         try
         {
            AsyncCacheLoader.super.put(mods);
         }
         catch (Exception e)
         {
            if (log.isWarnEnabled()) log.warn("Failed to process async modifications: " + e);
            log.debug("Exception: ", e);
         }
      }

      @Override
      public String toString()
      {
         return "TQ t=" + t;
      }

   }

   @Override
   public String toString()
   {
      return super.toString() +
            " delegate=[" + super.getCacheLoader() + "]" +
            " processor=" + processor +
            " stopped=" + stopped +
            " batchSize=" + config.getBatchSize() +
            " returnOld=" + config.getReturnOld() +
            " asyncPut=" + config.getUseAsyncPut() +
            " queue.remainingCapacity()=" + queue.remainingCapacity() +
            " queue.peek()=" + queue.peek();
   }

}
