/*
 * 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 org.jboss.cache.Fqn;
import org.jboss.cache.TreeCache;
import org.jboss.cache.TreeCacheListener;
import org.jboss.logging.Logger;
import org.jgroups.View;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

import java.util.Timer;


/** Provider to provide eviction policy. This one is based on LRU algorithm that a user
 * can specify either maximum number of nodes or the idle time of a node to be evicted.
 *
 * @author Ben Wang 02-2004
 */
public class LRUPolicy extends BaseEvictionPolicy implements TreeCacheListener
{
   protected Logger log_ = Logger.getLogger(LRUPolicy.class);
   static final int WAKEUP_DEFAULT = 5;
   final static String ATTR = "attribute";
   static final String NAME = "name";
   static final Fqn ROOT =new Fqn("/");
   private int wakeUpIntervalSeconds_;
   private RegionValue[] regionValues_;
   private Timer timer_;

   public LRUPolicy() {
      super();
   }

   /**--- Implementation of TreeCacheListener interface ---*/

   public void nodeAdded(Fqn fqn)
   {
      // ignore root
      if(fqn.equals(ROOT)) return;

      if(log_.isTraceEnabled()) {
         log_.trace("nodeAdded(): fqn- " +fqn);
      }

      Region region = regionManager_.getRegion(fqn.toString());
      region.setAddedNode(fqn);
   }

   /**
    * Note that this removes all children nodes as well. So we will need to put the children nodes
    * into removed queue.
    * @param fqn
    */
   public void nodeRemoved(Fqn fqn)
   {
      // Once we get this node event, the children node has all been disassociated
      // There is no way to remove them from the eviction list. We will count on
      // the items to age out by itself.
      if(log_.isTraceEnabled()) {
         log_.trace("nodeRemoved(): fqn- " +fqn );
      }
      Region region = regionManager_.getRegion(fqn.toString());
      region.setRemovedNode(fqn);
   }

   public void nodeEvicted(Fqn fqn) {
      ;
   }

   public void nodeModified(Fqn fqn)
   {
      if(log_.isTraceEnabled()) {
         log_.trace("nodeModified(): redirecting to node visited. fqn- " +fqn);
      }

      nodeVisited(fqn);
   }

   public void nodeLoaded(Fqn fqn) {
      // TODO. What should we do?
   }

   public void nodeCreated(Fqn fqn) {
      nodeAdded(fqn);
   }

   public void nodeVisited(Fqn fqn)
   {
      if(log_.isTraceEnabled()) {
         log_.trace("nodeVisited(): fqn- " +fqn);
      }
      Region region = regionManager_.getRegion(fqn.toString());
      // TODO May need to optimize to check if this fqn is already on the queue. No need to process it twice.
//      log_.error("nodeVisited(): before fqn- " +fqn);
      region.setVisitedNode(fqn);
//      log_.error("nodeVisited(): leaving fqn- " +fqn);
   }

   public void cacheStarted(TreeCache cache)
   {
      log_.info("Starting eviction policy using the provider: " +this.getClass().getName());

      log_.info("Starting a eviction timer with wake up interval of (secs) "
              +wakeUpIntervalSeconds_);
      // Step x. Start the timer thread
      timer_ = new Timer();
      timer_.schedule( new EvictionTimerTask(this), wakeUpIntervalSeconds_ *1000,
            wakeUpIntervalSeconds_ *1000);
   }

   protected EvictionAlgorithm getEvictionAlgorithm() {
      return new LRUAlgorithm();
   }

   private void parseConfig(Element elem)
   {
      String REGION = "region";
      wakeUpIntervalSeconds_ = Integer.parseInt(getAttr(elem, "wakeUpIntervalSeconds"));
      if( wakeUpIntervalSeconds_ <= 0 ) wakeUpIntervalSeconds_ = WAKEUP_DEFAULT;

      NodeList list=elem.getElementsByTagName(REGION);
      regionValues_ = new RegionValue[list.getLength()];
      for(int i=0; i < list.getLength(); i++) {
         RegionValue val = new RegionValue();
         org.w3c.dom.Node node = list.item(i);
         if(node.getNodeType() != org.w3c.dom.Node.ELEMENT_NODE)
            continue;
         Element element=(Element)node;
         String name=element.getAttribute(NAME);
         val.name_ = name;
         val.maxNodes_ = Integer.parseInt(getAttr(element, "maxNodes"));
         String timeToLive = getAttr(element, "timeToIdleSeconds");
         if(timeToLive == null) {
            timeToLive = getAttr(element, "timeToLiveSeconds");
            if(timeToLive == null)
               throw new RuntimeException("LRUPolicy.parseConfig(): Null timeToLiveSeconds element");
         }
         val.timeToLiveSeconds_ = Integer.parseInt(timeToLive);
         if(log_.isDebugEnabled()) {
            log_.debug("parseConfig: name -- " +name + " maxNodes -- "
                  +val.maxNodes_ + " timeToLiveSeconds -- " +val.timeToLiveSeconds_ );
         }
         regionValues_[i] = val;
      }
   }

   private String getAttr(Element elem, String myName)
   {
      NodeList list=elem.getElementsByTagName(ATTR);

      for(int s=0; s < list.getLength(); s++) {
         org.w3c.dom.Node node=list.item(s);
         if(node.getNodeType() != org.w3c.dom.Node.ELEMENT_NODE)
            continue;

         Element element=(Element)node;
         String name=element.getAttribute(NAME);
         if( name.equals(myName)) {
            String valueStr=getElementContent(element, true);
            return valueStr;
         }
      }
      return null;
   }

   private String getElementContent(Element element, boolean trim)
   {
      NodeList nl=element.getChildNodes();
      String attributeText="";
      for(int i=0; i < nl.getLength(); i++) {
         Node n=nl.item(i);
         if(n instanceof Text) {
            attributeText+=((Text)n).getData();
         }
      } // end of for ()
      if(trim)
         attributeText=attributeText.trim();
      return attributeText;
   }

   public void cacheStopped(TreeCache cache)
   {
      // Stop the thread before exiting
      log_.info("Stopping eviction policy timer ... ");
      timer_.cancel();
   }

   public void viewChange(View new_view)  // might be MergeView after merging
   {
      // no op
   }

   public int getWakeupIntervalSeconds()
   {
      return wakeUpIntervalSeconds_;
   }

   public void configure(TreeCache cache)
   {
      cache_ = cache;
      // Step x. Get the config elem
      Element elem = cache.getEvictionPolicyConfig();
      parseConfig(elem);
      // Step x. Create LRUAlgorithm and regions
      // Step x. Create regions
      regionManager_ = new RegionManager(this);
      for(int i=0; i< regionValues_.length; i++) {
         EvictionAlgorithm algo = getEvictionAlgorithm();
         try {
            Region region = regionManager_.createRegion(regionValues_[i].name_, algo);
            region.setMaxNodes(regionValues_[i].maxNodes_ );
            region.setTimeToLiveSeconds(regionValues_[i].timeToLiveSeconds_);
         } catch (RegionNameConflictException e) {
            throw new RuntimeException(
                  "LRUPolicy.cacheStarted(): illegal region name specified for eviction policy "
               + " exception: " +e);
         }
      }
   }

   static class RegionValue {
      String name_;
      int maxNodes_;
      int timeToLiveSeconds_;
   }
}
