/*
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.cache.eviction;

import EDU.oswego.cs.dl.util.concurrent.BoundedBuffer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.lock.TimeoutException;

/**
 * Abstract Event Processing Eviction Algorithm.
 * This class is used to implement basic event processing for Eviction Algorithms.
 * To extend this abstract class to make an Eviction Algorithm, implement the
 * abstract methods and a policy.
 *
 * @author Daniel Huang - dhuang@jboss.org 10/2005
 * @author <a href="mailto:galder.zamarreno@jboss.com">Galder Zamarreno</a>
 * @version $Revision: 5559 $
 */
public abstract class BaseEvictionAlgorithm implements EvictionAlgorithm
{
   private static final Log log = LogFactory.getLog(BaseEvictionAlgorithm.class);

   /**
    * Mapped region.
    */
   protected Region region;

   /**
    * Contains Fqn instances.
    */
   protected BoundedBuffer recycleQueue;

   /**
    * Contains NodeEntry instances.
    */
   protected EvictionQueue evictionQueue;

   /**
    * This method will create an EvictionQueue implementation and prepare it for use.
    *
    * @param region Region to setup an eviction queue for.
    * @return The created EvictionQueue to be used as the eviction queue for this algorithm.
    * @throws EvictionException
    * @see EvictionQueue
    */
   protected abstract EvictionQueue setupEvictionQueue(Region region) throws EvictionException;

   /**
    * This method will check whether the given node should be evicted or not.
    *
    * @param ne NodeEntry to test eviction for.
    * @return True if the given node should be evicted. False if the given node should not be evicted.
    */
   protected abstract boolean shouldEvictNode(NodeEntry ne);

   protected BaseEvictionAlgorithm()
   {
      recycleQueue = new BoundedBuffer(RegionManager.CAPACITY);
   }

   protected void initialize(Region region) throws EvictionException
   {
      this.region = region;
      evictionQueue = setupEvictionQueue(region);
   }

   /**
    * Process the given region.
    * <p/>
    * Eviction Processing encompasses the following:
    * <p/>
    * - Add/Remove/Visit Nodes
    * - Prune according to Eviction Algorithm
    * - Empty/Retry the recycle queue of previously evicted but locked (during actual cache eviction) nodes.
    *
    * @param region Cache region to process for eviction.
    * @throws EvictionException
    */
   public void process(Region region) throws EvictionException
   {
      if (this.region == null)
      {
         this.initialize(region);
      }

      if (log.isTraceEnabled())
      {
         log.trace("process(): region: " + region.getFqn());
      }

      this.processQueues(region);
      this.emptyRecycleQueue();
      this.prune();
   }

   public void resetEvictionQueue(Region region)
   {
   }

   /**
    * Get the underlying EvictionQueue implementation.
    *
    * @return the EvictionQueue used by this algorithm
    * @see EvictionQueue
    */
   public EvictionQueue getEvictionQueue()
   {
      return this.evictionQueue;
   }

   /**
    * Event processing for Evict/Add/Visiting of nodes.
    * <p/>
    * - On AddEvents a new element is added into the eviction queue
    * - On RemoveEvents, the removed element is removed from the eviction queue.
    * - On VisitEvents, the visited node has its eviction statistics updated (idleTime, numberOfNodeVisists, etc..)
    *
    * @param region Cache region to process for eviction.
    * @throws EvictionException
    */
   protected void processQueues(Region region) throws EvictionException
   {
      EvictedEventNode node;
      int count = 0;
      while ((node = region.takeLastEventNode()) != null)
      {
         int eventType = node.getEvent();
         Fqn fqn = node.getFqn();

         count++;
         switch (eventType)
         {
            case EvictedEventNode.ADD_NODE_EVENT:
               this.processAddedNodes(fqn,
                     node.getElementDifference(),
                     node.isResetElementCount());
               break;
            case EvictedEventNode.REMOVE_NODE_EVENT:
               this.processRemovedNodes(fqn);
               break;
            case EvictedEventNode.VISIT_NODE_EVENT:
               this.processVisitedNodes(fqn);
               break;
            case EvictedEventNode.ADD_ELEMENT_EVENT:
               this.processAddedElement(fqn);
               break;
            case EvictedEventNode.REMOVE_ELEMENT_EVENT:
               this.processRemovedElement(fqn);
               break;
            case EvictedEventNode.MARK_IN_USE_EVENT:
               this.processMarkInUseNodes(fqn, node.getInUseTimeout());
               break;
            case EvictedEventNode.UNMARK_USE_EVENT:
               this.processUnmarkInUseNodes(fqn);
               break;
            default:
               throw new RuntimeException("Illegal Eviction Event type " + eventType);
         }
      }

      if (log.isTraceEnabled())
      {
         log.trace("processed " + count + " node events in region: " + region.getFqn());
      }

   }

   protected void evict(NodeEntry ne)
   {
//      NodeEntry ne = evictionQueue.getNodeEntry(fqn);
      if (ne != null)
      {
         evictionQueue.removeNodeEntry(ne);
         if (!this.evictCacheNode(ne.getFqn()))
         {
            try
            {
               boolean result = recycleQueue.offer(ne.getFqn(), 5000);
               if (!result)
               {
                  log.warn("Unable to add Fqn[" + ne.getFqn() + "] to recycle " +
                        "queue because it's full. This is often sign that " +
                        "evictions are not occurring and nodes that should be " +
                        "evicted are piling up waiting to be evicted.");
               }
            }
            catch (InterruptedException e)
            {
               log.debug("InterruptedException", e);
            }
         }
      }
   }

   /**
    * Evict a node from cache.
    *
    * @param fqn node corresponds to this fqn
    * @return True if successful
    */
   protected boolean evictCacheNode(Fqn fqn)
   {
      if (log.isTraceEnabled())
      {
         log.trace("Attempting to evict cache node with fqn of " + fqn);
      }
      EvictionPolicy policy = region.getEvictionPolicy();
      // Do an eviction of this node

      try
      {
         policy.evict(fqn);
      }
      catch (Exception e)
      {
         if (e instanceof TimeoutException)
         {
            log.warn("eviction of " + fqn + " timed out. Will retry later.");
            return false;
         }
         e.printStackTrace();
         return false;
      }

      if (log.isTraceEnabled())
      {
         log.trace("Eviction of cache node with fqn of " + fqn + " successful");
      }

      return true;
   }

   protected void processMarkInUseNodes(Fqn fqn, long inUseTimeout) throws EvictionException
   {
      if (log.isTraceEnabled())
      {
         log.trace("Marking node " + fqn + " as in use with a usage timeout of " + inUseTimeout);
      }

      NodeEntry ne = evictionQueue.getNodeEntry(fqn);
      if (ne != null)
      {
         ne.setCurrentlyInUse(true, inUseTimeout);
      }
   }

   protected void processUnmarkInUseNodes(Fqn fqn) throws EvictionException
   {
      if (log.isTraceEnabled())
      {
         log.trace("Unmarking node " + fqn + " as in use");
      }

      NodeEntry ne = evictionQueue.getNodeEntry(fqn);
      if (ne != null)
      {
         ne.setCurrentlyInUse(false, 0);
      }
   }

   protected void processAddedNodes(Fqn fqn, int numAddedElements, boolean resetElementCount) throws EvictionException
   {
      if (log.isTraceEnabled())
      {
         log.trace("Adding node " + fqn + " with " + numAddedElements + " elements to eviction queue");
      }

      NodeEntry ne = evictionQueue.getNodeEntry(fqn);
      if (ne != null)
      {
         ne.setModifiedTimeStamp(System.currentTimeMillis());
         ne.setNumberOfNodeVisits(ne.getNumberOfNodeVisits() + 1);
         if (resetElementCount)
         {
            ne.setNumberOfElements(numAddedElements);
         }
         else
         {
            ne.setNumberOfElements(ne.getNumberOfElements() + numAddedElements);
         }
         return;
      }

      long stamp = System.currentTimeMillis();
      ne = new NodeEntry(fqn);
      ne.setModifiedTimeStamp(stamp);
      ne.setNumberOfNodeVisits(1);
      ne.setNumberOfElements(numAddedElements);
      // add it to the node map and eviction queue
      if (evictionQueue.containsNodeEntry(ne))
      {
         if (log.isTraceEnabled())
         {
            log.trace("Queue already contains " + ne.getFqn() + " processing it as visited");
         }
         this.processVisitedNodes(ne.getFqn());
         return;
      }

      evictionQueue.addNodeEntry(ne);

      if (log.isTraceEnabled())
      {
         log.trace(ne.getFqn() + " added successfully to eviction queue");
      }
   }

   /**
    * Remove a node from cache.
    * <p/>
    * This method will remove the node from the eviction queue as well as
    * evict the node from cache.
    * <p/>
    * If a node cannot be removed from cache, this method will remove it from the eviction queue
    * and place the element into the recycleQueue. Each node in the recycle queue will get retried until
    * proper cache eviction has taken place.
    * <p/>
    * Because EvictionQueues are collections, when iterating them from an iterator, use iterator.remove()
    * to avoid ConcurrentModificationExceptions. Use the boolean parameter to indicate the calling context.
    *
    * @param fqn FQN of the removed node
    * @throws EvictionException
    */
   protected void processRemovedNodes(Fqn fqn) throws EvictionException
   {
      if (log.isTraceEnabled())
      {
         log.trace("Removing node " + fqn + " from eviction queue and attempting eviction");
      }

      NodeEntry ne = evictionQueue.getNodeEntry(fqn);
      if (ne != null)
      {
         evictionQueue.removeNodeEntry(ne);
      }
      else
      {
         if (log.isDebugEnabled())
         {
            log.debug("processRemoveNodes(): Can't find node associated with fqn: " + fqn
                  + "Could have been evicted earlier. Will just continue.");
         }
         return;
      }

      if (log.isTraceEnabled())
      {
         log.trace(fqn + " removed from eviction queue");
      }
   }

   /**
    * Visit a node in cache.
    * <p/>
    * This method will update the numVisits and modifiedTimestamp properties of the Node.
    * These properties are used as statistics to determine eviction (LRU, LFU, MRU, etc..)
    * <p/>
    * *Note* that this method updates Node Entries by reference and does not put them back
    * into the queue. For some sorted collections, a remove, and a re-add is required to
    * maintain the sorted order of the elements.
    *
    * @param fqn FQN of the visited node.
    * @throws EvictionException
    */
   protected void processVisitedNodes(Fqn fqn) throws EvictionException
   {
      NodeEntry ne = evictionQueue.getNodeEntry(fqn);
      if (ne == null)
      {
         if (log.isDebugEnabled())
         {
            log.debug("Visiting node that was not added to eviction queues. Assuming that it has 1 element.");
         }
         this.processAddedNodes(fqn, 1, false);
         return;
      }
      // note this method will visit and modify the node statistics by reference!
      // if a collection is only guaranteed sort order by adding to the collection,
      // this implementation will not guarantee sort order.
      ne.setNumberOfNodeVisits(ne.getNumberOfNodeVisits() + 1);
      ne.setModifiedTimeStamp(System.currentTimeMillis());
   }

   protected void processRemovedElement(Fqn fqn) throws EvictionException
   {
      NodeEntry ne = evictionQueue.getNodeEntry(fqn);

      if (ne == null)
      {
         if (log.isDebugEnabled())
         {
            log.debug("Removing element from " + fqn + " but eviction queue does not contain this node. " +
                  "Ignoring removeElement event.");
         }
         return;
      }

      ne.setNumberOfElements(ne.getNumberOfElements() - 1);
      // also treat it as a node visit.
      ne.setNumberOfNodeVisits(ne.getNumberOfNodeVisits() + 1);
      ne.setModifiedTimeStamp(System.currentTimeMillis());
   }

   protected void processAddedElement(Fqn fqn) throws EvictionException
   {
      NodeEntry ne = evictionQueue.getNodeEntry(fqn);
      if (ne == null)
      {
         if (log.isDebugEnabled())
         {
            log.debug("Adding element " + fqn + " for a node that doesn't exist yet. Process as an add.");
         }
         this.processAddedNodes(fqn, 1, false);
         return;
      }

      ne.setNumberOfElements(ne.getNumberOfElements() + 1);

      // also treat it as a node visit.
      ne.setNumberOfNodeVisits(ne.getNumberOfNodeVisits() + 1);
      ne.setModifiedTimeStamp(System.currentTimeMillis());
   }


   /**
    * Empty the Recycle Queue.
    * <p/>
    * This method will go through the recycle queue and retry to evict the nodes from cache.
    *
    * @throws EvictionException
    */
   protected void emptyRecycleQueue() throws EvictionException
   {
      while (true)
      {
         Fqn fqn;

         try
         {
            fqn = (Fqn) recycleQueue.poll(0);
         }
         catch (InterruptedException e)
         {
            e.printStackTrace();
            break;
         }

         if (fqn == null)
         {
            if (log.isTraceEnabled())
            {
               log.trace("Recycle queue is empty");
            }
            break;
         }

         if (log.isTraceEnabled())
         {
            log.trace("emptying recycle bin. Evict node " + fqn);
         }

         // Still doesn't work
         if (!evictCacheNode(fqn))
         {
            try
            {
               recycleQueue.put(fqn);
            }
            catch (InterruptedException e)
            {
               e.printStackTrace();
            }
            break;
         }
      }
   }

   protected boolean isNodeInUseAndNotTimedOut(NodeEntry ne)
   {
      if (ne.isCurrentlyInUse())
      {
         if (ne.getInUseTimeoutTimestamp() == 0)
         {
            return true;
         }

         if (System.currentTimeMillis() < ne.getInUseTimeoutTimestamp())
         {
            return true;
         }
      }
      return false;
   }


   protected void prune() throws EvictionException
   {
      NodeEntry entry;
      while ((entry = evictionQueue.getFirstNodeEntry()) != null)
      {
         if (this.shouldEvictNode(entry))
         {
            this.evict(entry);
         }
         else
         {
            break;
         }
      }
   }

}
