/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2000 - 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file 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.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.NodeSPI;
import org.jboss.cache.commands.DataCommand;
import org.jboss.cache.commands.write.PutDataMapCommand;
import org.jboss.cache.commands.write.PutKeyValueCommand;
import org.jboss.cache.config.EvictionAlgorithmConfig;
import org.jboss.cache.eviction.EvictionEvent.Type;

import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import java.util.Iterator;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.BlockingQueue;

/**
 * Eviction algorithm that uses a key in the Node data that indicates the time
 * the node should be evicted.  The key must be a java.lang.Long object, with
 * the time to expire as milliseconds past midnight January 1st, 1970 UTC (the
 * same relative time as provided by {@link
 * java.lang.System#currentTimeMillis()}).
 * <p/>
 * This algorithm also obeys the configuration key {@link
 * ExpirationAlgorithmConfig#getMaxNodes()}, and will evict the soonest to
 * expire entires first to reduce the region size.  If there are not enough
 * nodes with expiration keys set, a warning is logged.
 * <p/>
 * If a node in the eviction region does not have an expiration value, then
 * {@link ExpirationAlgorithmConfig#getTimeToLive()} (if set) will be used.
 * The expiration is updated when a node is added or updated.
 * <p/>
 * If there is no time-to-live set, and a node in the eviction region does not
 * have an expiration value, then that node will never be evicted.  As
 * forgetting to indicate an expiration value is likely a mistake, a warning
 * message is logged by this class. This warning, however, can be disabled
 * through {@link ExpirationAlgorithmConfig#setWarnNoExpirationKey(boolean)}.
 * <p/>
 * A node's expiration time can be changed by setting a new value in the node.
 * <p/>
 * Example usage:
 * <pre>
 * Cache cache;
 * Fqn fqn1 = Fqn.fromString("/node/1");
 * Long future = new Long(System.currentTimeMillis() + 2000);
 * cache.put(fqn1, ExpirationConfiguration.EXPIRATION_KEY, future);
 * cache.put(fqn1, "foo");
 * assertTrue(cache.get(fqn1) != null);
 * <p/>
 * Thread.sleep(5000); // 5 seconds
 * assertTrue(cache.get(fqn1) == null);
 * <p/>
 * </pre>
 */
public class ExpirationAlgorithm extends BaseEvictionAlgorithm
{

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

   private ExpirationAlgorithmConfig config;

   private SortedSet<ExpirationEntry> set;

   /**
    * Constructs a new algorithm with a policy.
    */
   public ExpirationAlgorithm()
   {
      this.set = new TreeSet<ExpirationEntry>();
   }

   private void addEvictionEntry(EvictionEvent node)
   {
      Fqn fqn = node.getFqn();
      addEvictionEntry(fqn, node.getCommand(), node.getTransaction());
   }

   private void addEvictionEntry(Fqn fqn, DataCommand command, Transaction tx)
   {
      Long l = getExpiration(fqn);
      if (l == null) {
         if (command != null)
            l = getExpirationFromCommand(command);

         if (l == null)
            return;
      }

      if (l == -1)
      {
         if (config.isWarnNoExpirationKey() && log.isWarnEnabled())
            log.warn("No expiration key '" + config.getExpirationKeyName() + "' for Node: " + fqn);
         else if (log.isDebugEnabled())
            log.debug("No expiration key for Node: " + fqn);
      }
      else
      {
         setExpiration(fqn, l, tx);
      }
   }

   private Long getExpirationFromCommand(DataCommand command) {
      if (command instanceof PutKeyValueCommand) {
         PutKeyValueCommand putKeyCommand = (PutKeyValueCommand) command;
         if (putKeyCommand.getKey().equals(config.getExpirationKeyName())) {
            return (Long) putKeyCommand.getValue();
         }
      } else if (command instanceof PutDataMapCommand) {
         PutDataMapCommand putDataCommand = (PutDataMapCommand) command;
         Object expiration = putDataCommand.getData().get(config.getExpirationKeyName());
         if (expiration != null) {
            return (Long) expiration;
         }
      }
      return null;
   }

   private void setExpiration(Fqn fqn, Long l, Transaction tx)
   {
      ExpirationEntry ee = new ExpirationEntry(fqn, l, tx);
      if (trace)
         log.trace("adding eviction entry: " + ee);
      set.add(ee);
   }

   @SuppressWarnings("unchecked")
   private Long getExpiration(Fqn fqn)
   {
      NodeSPI n = cache.peek(fqn, false);
      if (n == null)
         return null;
      Long expiration = (Long) n.getDirect(config.getExpirationKeyName());
      if ( expiration == null )
         return -1L;
      return expiration;
   }

   @Override
   protected void processQueues(BlockingQueue<EvictionEvent> queue) throws EvictionException
   {
      EvictionEvent node;
      int count = 0;
      while ((node = getNextInQueue(queue)) != null)
      {
         count++;
         switch (node.getEventType())
         {
            case ADD_NODE_EVENT:
            case ADD_ELEMENT_EVENT:
               addEvictionEntry(node);
               break;
            case REMOVE_ELEMENT_EVENT:
            case REMOVE_NODE_EVENT:
            case UNMARK_USE_EVENT:
               // Removals will be noticed when double-checking expiry time
               // removeEvictionEntry(node);
               break;
            case VISIT_NODE_EVENT:
               // unused
               break;
            case MARK_IN_USE_EVENT:
               markInUse(node);
               break;
            default:
               throw new RuntimeException("Illegal Eviction Event type " + node.getEventType());
         }
      }

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

   private void markInUse(EvictionEvent node)
   {
      long expiration = node.getInUseTimeout() + System.currentTimeMillis();
      setExpiration(node.getFqn(), expiration, node.getTransaction());
   }

   @Override
   protected void prune() throws EvictionException
   {
      if (set.isEmpty())
         return;
      long now = System.currentTimeMillis();
      int max = config.getMaxNodes();
      for (Iterator<ExpirationEntry> i = set.iterator(); i.hasNext();)
      {
         ExpirationEntry ee = i.next();
         if (trace)
            log.trace("attempt to prune: " + ee);

         Long ce = getExpiration(ee.getFqn());
         if (ce == null) {
            if (ee.getTransaction() != null && isTransactionActive(ee.getTransaction())) {
               if (trace)
                  log.trace("transaction active, keep eviction entry: " + ee);
               continue;
            } else {
               // Expiration now older
               i.remove();
               continue;
            }
         }

         if (ce == -1 || ce > ee.getExpiration())
         {
            // Expiration now older
            i.remove();
            continue;
         }
         if (ee.getExpiration() < now || (max != -1 && set.size() > max))
         {
            i.remove();
            evictCacheNode(ee.getFqn());
         }
         else
         {
            break;
         }
      }
      if (max != -1 && set.size() > max )
         log.warn("Unable to remove nodes to reduce region size below " +
               config.getMaxNodes() + ".  " +
               "Set expiration for nodes in this region");
   }

   private boolean isTransactionActive(Transaction transaction) {
      try {
         switch (transaction.getStatus()) {
            case Status.STATUS_UNKNOWN:
            case Status.STATUS_ROLLEDBACK:
            case Status.STATUS_COMMITTED:
            case Status.STATUS_NO_TRANSACTION:
               return false;
            default:
               return true;
         }
      } catch (SystemException e) {
         return false;
      }
   }

   @Override
   public void resetEvictionQueue()
   {
      for (ExpirationEntry ee : set)
      {
         addEvictionEntry(ee.getFqn(), null, null);
      }
   }

   @Override
   protected EvictionQueue setupEvictionQueue() throws EvictionException
   {
      this.config = (ExpirationAlgorithmConfig) evictionAlgorithmConfig;
      return new DummyEvictionQueue();
   }

   @Override
   protected boolean shouldEvictNode(NodeEntry ne)
   {
      throw new UnsupportedOperationException();
   }

   @Override
   public boolean canIgnoreEvent(Type eventType)
   {
      return (eventType == EvictionEvent.Type.VISIT_NODE_EVENT);
   }

   public Class<? extends EvictionAlgorithmConfig> getConfigurationClass()
   {
      return ExpirationAlgorithmConfig.class;
   }

   /**
    * Ordered list of FQN, with the expiration taken from the Map at the time
    * of processing.
    */
   static class ExpirationEntry implements Comparable<ExpirationEntry>
   {

      private long expiration;

      private Fqn fqn;
      
      private Transaction transaction;

      public ExpirationEntry(Fqn fqn)
      {
         this.fqn = fqn;
      }

      public ExpirationEntry(Fqn fqn, long expiration, Transaction transaction)
      {
         this.fqn = fqn;
         this.expiration = expiration;
         this.transaction = transaction;
      }

      /**
       * Compares expiration, then FQN order.
       */
      public int compareTo(ExpirationEntry ee)
      {
         long n = expiration - ee.expiration;
         if (n < 0)
            return -1;
         if (n > 0)
            return 1;
         return fqn.compareTo(ee.fqn);
      }

      /**
       * @return the expiration
       */
      public long getExpiration()
      {
         return expiration;
      }

      /**
       * @return the fqn
       */
      public Fqn getFqn()
      {
         return fqn;
      }

      public Transaction getTransaction()
      {
         return transaction;
      }

      @Override
      public boolean equals(Object o)
      {
         if (!(o instanceof ExpirationEntry))
            return false;
         ExpirationEntry ee = (ExpirationEntry) o;
         return expiration == ee.expiration && fqn.equals(ee.fqn);
      }

      @Override
      public int hashCode()
      {
         return (int) expiration ^ fqn.hashCode();
      }

      @Override
      public String toString()
      {
         long now = System.currentTimeMillis();
         long ttl = expiration - now;
         String sttl;
         if (ttl > 1000 * 60)
            sttl = (ttl / (1000 * 60)) + "min";
         else if (ttl > 1000)
            sttl = (ttl / 1000) + "s";
         else
            sttl = ttl + "ms";
         return "EE fqn=" + fqn + " ttl=" + sttl;
      }
   }

   class DummyEvictionQueue implements EvictionQueue
   {

      public void addNodeEntry(NodeEntry entry)
      {
         throw new UnsupportedOperationException();
      }

      public void clear()
      {
         set.clear();
      }

      public boolean containsNodeEntry(NodeEntry entry)
      {
         return false;
      }

      public NodeEntry getFirstNodeEntry()
      {
         return null;
      }

      public NodeEntry getNodeEntry(Fqn fqn)
      {
         return null;
      }

      public NodeEntry getNodeEntry(String fqn)
      {
         return null;
      }

      public int getNumberOfElements()
      {
         return set.size();
      }

      public int getNumberOfNodes()
      {
         return set.size();
      }

      public Iterator<NodeEntry> iterator()
      {
         return null;
      }

      public void modifyElementCount(int difference)
      {
      }

      public void removeNodeEntry(NodeEntry entry)
      {
         throw new UnsupportedOperationException();
      }
   }

}
