/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.cache.eviction;

import EDU.oswego.cs.dl.util.concurrent.BoundedBuffer;
import org.jboss.cache.Fqn;
import org.jboss.cache.lock.TimeoutException;
import org.jboss.logging.Logger;

import java.util.HashMap;
import java.util.Map;

/** Least recently Used algorithm to purge old data.
 * Note that this algorithm is not thread-safe.
 * @author Ben Wang 02-2004
 */
public class LRUAlgorithm implements EvictionAlgorithm
{
   protected Logger log_ = Logger.getLogger(LRUAlgorithm.class);
   // Map of (fqn, nodeEntry). Fast search.
   protected Map nodeMap_ = new HashMap();
   // linked list
   protected NodeEntry head_ = null;
   protected NodeEntry tail_ = null;
   protected Region region_;
   private static final int CAPACITY = 50000;
   // These queues can do put and take concurrently.
   protected BoundedBuffer recycleQueue_;

   public LRUAlgorithm() {
      recycleQueue_ = new BoundedBuffer(CAPACITY);
   }

   /**
    * No lock is used here since we assume it is single-threaded access by the timer only.
    * @param region
    */
   public void process(Region region) throws EvictionException
   {
      region_ = region;
      if(log_.isDebugEnabled()) {
         log_.debug("process(): process the sitting node events in region: " +region_.toString());
         log_.debug("process(): current eviction queue size is " +evictionQueueSize());
      }
      // First to process the node events
      processQueues();
      // Then to prune, if ncessary
      if(log_.isDebugEnabled()) {
         log_.debug("process(): trimming the nodes if ncessary ...");
      }
      prune();
   }

   public void resetEvictionQueue(Region region)
   {
//      region_.resetEvictionQueues();
   }

   private void processQueues() throws EvictionException
   {
      boolean loop = true;
      int count = 0;
      while(loop) {
         EvictedEventNode node = region_.takeLastEventNode();
         if(node == null) { // Means queue is empty
            break;
         }
         Fqn fqn = node.getFqn();
         Integer event = node.getEvent();

         count++;

         if(event.equals(EvictedEventNode.ADD_EVENT)) {
            processAddedNodes(fqn);
         } else if (event.equals(EvictedEventNode.REMOVE_EVENT)) {
            processRemovedNodes(fqn);
         } else if (event.equals(EvictedEventNode.VISIT_EVENT) ) {
            processVisitedNodes(fqn);
         } else {
            throw new RuntimeException("LRUAlgorithm.processQueues(): Illegal event type " +event);
         }
      }

      if(log_.isDebugEnabled()) {
         log_.debug("processQueues(): process " +count + " node events");
      }
   }

   private void processAddedNodes(Fqn fqn) throws EvictionException
   {
      // Create time stamp (there is a fuzzy logic involved here...)
      long stamp =  System.currentTimeMillis();
      NodeEntry ne = new NodeEntry(fqn);
      ne.setModifiedTimeStamp(stamp);
      // add it to the node map
      add(ne);

   }

   private void processRemovedNodes(Fqn fqn)  throws EvictionException
   {
      // Obtain the NodeEntry from nade map
      removeFromQueue(fqn);
   }

   private void processVisitedNodes(Fqn fqn) throws EvictionException
   {
      // Obtain NodeEntry from nodeMap
      NodeEntry ne = (NodeEntry)nodeMap_.get(fqn);
      if(ne==null) { // can be null since node itself exists but value is gone.
         ne = new NodeEntry(fqn);
         add(ne);
      }
      // Update the time stamp
      long stamp =  System.currentTimeMillis();
      ne.setModifiedTimeStamp(stamp);
      // call demote
      demote(fqn);
   }

   private void add(NodeEntry ne) throws EvictionException
   {
      if( nodeMap_.containsKey(ne.getFqn()) ) {
         demote(ne.getFqn()); // node already existed, treat it as visit event.
         return;
      }

      if(head_ == null && tail_ == null) {
         head_ = ne;
         tail_ = ne;
         nodeMap_.put(ne.getFqn(), ne);
         return; // first timer
      }

      tail_.setNext(ne);
      ne.setPrevious(tail_);
      tail_ = ne;
      nodeMap_.put(ne.getFqn(), ne);
   }

   /*
   private void promote(Fqn fqn) throws EvictionException
   {
      NodeEntry ne = (NodeEntry)nodeMap_.get(fqn);
      if( ne == null )
         throw new EvictionException("LRUAlgorithm.prune(): internal error. Can't find fqn in nodeMap: " +fqn);

      ne.getPrevious().setNext(ne.getPrevious());
      ne.getNext().setPrevious(ne.getNext());

      head_ = ne;
      ne.setPrevious(null);
   }
   */

   protected void demote(Fqn fqn) throws EvictionException
   {
      NodeEntry ne = (NodeEntry)nodeMap_.get(fqn);
      if( ne == null )
         throw new EvictionException("LRUAlgorithm.demote(): internal error. Can't find fqn in nodeMap: " +fqn);

      if(head_ == ne && tail_ == ne) { // single node
         return;
      }

      if(tail_ == ne) return; // last node, do nothing.

      if(head_ == ne) { // first node
         head_ = head_.getNext();
         head_.setPrevious(null);
      } else {
         ne.getPrevious().setNext(ne.getNext());
         ne.getNext().setPrevious(ne.getPrevious());
      }

      tail_.setNext(ne);
      ne.setPrevious(tail_);
      tail_ = ne;
      ne.setNext(null);
   }

   /**
    * Evict a node from cache.
    * @param fqn node corresponds to this fqn
    * @return True if successful
    */
   private boolean evictCacheNode(Fqn fqn)
   {
      EvictionPolicy policy = region_.getEvictionPolicy();
      // Do an eviction of this node
      // TODO Will need a tryLock api first
      try {
         policy.evict(fqn);
      } catch (Exception e) {
         if( e instanceof TimeoutException) {
            log_.warn("evictCacheNode(): evictCacheNode time out. Will try later.");
            return false;
         }
         e.printStackTrace();
         return false;
      }

      return true;
   }

   private void removeFromQueue(Fqn fqn) throws EvictionException
   {
      NodeEntry ne = (NodeEntry)nodeMap_.remove(fqn);
      if( ne == null ) {
         log_.warn("evict(): Node not found with this fqn: " +fqn + " Could have been evicted earlier already");
         throw new EvictionException("LRUAlgorithm.removeFromQueue(): internal error. Can't find fqn in nodeMap. fqn: " +
               fqn);
      }

      if(head_ == ne && tail_ == ne) { // single node
         head_ = tail_ = null;
         return;
      }

      if(head_ == ne) { // first node
         head_ = head_.getNext();
         head_.setPrevious(null);
         ne.setNext(null);
         return;
      }

      if(tail_ == ne) { // last node
         tail_ = tail_.getPrevious();
         tail_.setNext(null);
         ne.setPrevious(null);
         return;
      }

      ne.getPrevious().setNext(ne.getNext());
      ne.getNext().setPrevious(ne.getPrevious());
   }

   /**
    *
    * @throws EvictionException
    */
   protected void prune() throws EvictionException
   {
      // Step x. Run thru the internal queues to weed out aged nodes, if necessary.
      // Empty recycle bin
      boolean loop = true;
      while(loop) {
         NodeEntry ne = null;

         try {
            ne = (NodeEntry)recycleQueue_.poll(0);
         } catch (InterruptedException e) {
            e.printStackTrace();
            break;
         }

         if(ne==null) break;
         if(log_.isDebugEnabled()) {
            log_.debug("prune(): emptying recycle bin. Evict node " +ne.getFqn());
         }

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

      // prune size
      // if it is 0, means no size limit
      while( region_.getMaxNodes() != 0 && region_.getMaxNodes() < nodeMap_.size() ) {
         Fqn fqn = head_.getFqn();
         if(log_.isDebugEnabled()) {
            log_.debug("prune(): over-capacity. Evict node " +fqn);
         }

         evict(fqn);

         if( head_ == null )
            throw new EvictionException("LRUAlgorithm.prune(): internal error. head is null");
      }

      // prune ageout items
      if(head_ == null) return;  // no item to prune
      if(region_.getTimeToLiveSeconds() == 0) return; // no time limit
      NodeEntry cursor = head_;
      long currentTime = System.currentTimeMillis();
      while(cursor != null) {
         Fqn fqn = cursor.getFqn();
         long idleTime = currentTime -cursor.getModifiedTimeStamp();
         cursor = cursor.getNext();
         if(idleTime >= (region_.getTimeToLiveSeconds() *1000) ) {
            if(log_.isTraceEnabled()) {
               log_.trace("prune(): over-aged. Evict node " +fqn);
            }

            evict(fqn);
         }
      }
   }

   protected void evict(Fqn fqn) throws EvictionException
   {
      NodeEntry ne = (NodeEntry)nodeMap_.get(fqn);
      try {
         removeFromQueue(fqn);
      } catch (EvictionException e) {
         // NodeNotExisted
         e.printStackTrace();
         return;
      }

      if( !evictCacheNode(fqn) ) {
         try {
            recycleQueue_.put(ne);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }

   public int evictionQueueSize() { return nodeMap_.size(); }
}
