/*
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 * Created on March 25 2003
 */
package org.jboss.cache.interceptors;

import org.jboss.cache.DataContainer;
import org.jboss.cache.Fqn;
import org.jboss.cache.InvocationContext;
import org.jboss.cache.NodeSPI;
import org.jboss.cache.Region;
import org.jboss.cache.RegionManager;
import org.jboss.cache.commands.read.GetDataMapCommand;
import org.jboss.cache.commands.read.GetKeyValueCommand;
import org.jboss.cache.commands.read.GetNodeCommand;
import org.jboss.cache.commands.write.ClearDataCommand;
import org.jboss.cache.commands.write.EvictCommand;
import org.jboss.cache.commands.write.PutDataMapCommand;
import org.jboss.cache.commands.write.PutForExternalReadCommand;
import org.jboss.cache.commands.write.PutKeyValueCommand;
import org.jboss.cache.commands.write.RemoveKeyCommand;
import org.jboss.cache.commands.write.RemoveNodeCommand;
import org.jboss.cache.eviction.EvictedEventNode;
import org.jboss.cache.eviction.NodeEventType;
import org.jboss.cache.factories.annotations.Inject;
import org.jboss.cache.interceptors.base.CommandInterceptor;

/**
 * Eviction Interceptor.
 * <p/>
 * This interceptor is used to handle eviction events.
 *
 * @author Daniel Huang
 * @author Mircea.Markus@jboss.com
 * @version $Revision: 6120 $
 */
public class EvictionInterceptor extends CommandInterceptor
{
   protected RegionManager regionManager;

   private DataContainer dataContainer;

   @Inject
   public void initialize(DataContainer dataContainer)
   {
      this.dataContainer = dataContainer;
   }

   /**
    * this method is for ease of unit testing. thus package access.
    * <p/>
    * Not to be attempted to be used anywhere else.
    */
   @Inject
   void setRegionManager(RegionManager regionManager)
   {
      this.regionManager = regionManager;
   }

   @Override
   public Object visitEvictFqnCommand(InvocationContext ctx, EvictCommand command) throws Throwable
   {
      Fqn fqn = command.getFqn();
      Object retVal = invokeNextInterceptor(ctx, command);
      // See if the node still exists; i.e. was only data removed
      // because it still has children.
      // If yes, put an ADD event in the queue so the node gets revisited
      boolean complete = (retVal != null && (Boolean) retVal);
      if (!complete)
      {
         Region r;
         if (fqn != null && (r = getRegion(fqn, NodeEventType.ADD_NODE_EVENT)) != null)
         {
            registerEvictionEventToRegionManager(new EvictedEventNode(fqn, NodeEventType.ADD_NODE_EVENT, 0), r);
         }
      }
      return retVal;
   }

   @Override
   public Object visitPutForExternalReadCommand(InvocationContext ctx, PutForExternalReadCommand command) throws Throwable
   {
      return visitPutKeyValueCommand(ctx, command);
   }

   @Override
   public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable
   {
      Object retVal = invokeNextInterceptor(ctx, command);
      Region r;
      if (command.getFqn() != null && command.getKey() != null && (r = getRegion(command.getFqn(), NodeEventType.ADD_ELEMENT_EVENT)) != null)
      {
         registerEvictionEventToRegionManager(new EvictedEventNode(command.getFqn(), NodeEventType.ADD_ELEMENT_EVENT, 1), r);
      }
      return retVal;
   }

   @Override
   public Object visitPutDataMapCommand(InvocationContext ctx, PutDataMapCommand command) throws Throwable
   {
      Object retVal = invokeNextInterceptor(ctx, command);
      Fqn fqn = command.getFqn();
      Region r;
      if (fqn != null && (r = getRegion(fqn, NodeEventType.ADD_NODE_EVENT)) != null)
      {
         if (command.getData() == null)
         {
            if (trace)
            {
               log.trace("Putting null data under fqn " + fqn + ".");
            }
         }
         else
         {
            int size;
            synchronized (command.getData())
            {
               size = command.getData().size();
            }
            EvictedEventNode event = new EvictedEventNode(fqn, NodeEventType.ADD_NODE_EVENT, size);
            registerEvictionEventToRegionManager(event, r);
         }
      }
      return retVal;
   }

   @Override
   public Object visitRemoveKeyCommand(InvocationContext ctx, RemoveKeyCommand command) throws Throwable
   {
      Object retVal = invokeNextInterceptor(ctx, command);
      if (retVal == null)
      {
         if (trace)
         {
            log.trace("No event added. Element does not exist");
         }

      }
      else
      {
         Fqn fqn = command.getFqn();
         Region r;
         if (fqn != null && command.getKey() != null && (r = getRegion(fqn, NodeEventType.REMOVE_ELEMENT_EVENT)) != null)
         {
            registerEvictionEventToRegionManager(new EvictedEventNode(fqn, NodeEventType.REMOVE_ELEMENT_EVENT, 1), r);
         }
      }
      return retVal;
   }

   @Override
   public Object visitGetNodeCommand(InvocationContext ctx, GetNodeCommand command) throws Throwable
   {
      Object retVal = invokeNextInterceptor(ctx, command);
      return handleGetNodeOrDataCommands(retVal, command.getFqn());
   }

   private Object handleGetNodeOrDataCommands(Object retVal, Fqn fqn)
   {
      if (retVal == null)
      {
         if (trace)
         {
            log.trace("No event added. Node does not exist");
         }
      }
      else
      {
         Region r;
         if (fqn != null && (r = getRegion(fqn, NodeEventType.VISIT_NODE_EVENT)) != null)
         {
            registerEvictionEventToRegionManager(new EvictedEventNode(fqn, NodeEventType.VISIT_NODE_EVENT), r);
         }
      }
      return retVal;
   }

   @Override
   public Object visitGetDataMapCommand(InvocationContext ctx, GetDataMapCommand command) throws Throwable
   {
      Object retVal = invokeNextInterceptor(ctx, command);
      return handleGetNodeOrDataCommands(retVal, command.getFqn());
   }

   @Override
   public Object visitGetKeyValueCommand(InvocationContext ctx, GetKeyValueCommand command) throws Throwable
   {
      Object retVal = invokeNextInterceptor(ctx, command);
      Fqn fqn = command.getFqn();
      Region r;
      if (retVal == null)
      {
         if (trace)
         {
            log.trace("No event added. Element does not exist");
         }
      }
      else if (fqn != null && command.getKey() != null && (r = getRegion(fqn, NodeEventType.VISIT_NODE_EVENT)) != null)
      {
         registerEvictionEventToRegionManager(new EvictedEventNode(fqn, NodeEventType.VISIT_NODE_EVENT), r);
      }
      return retVal;
   }

   @Override
   public Object visitRemoveNodeCommand(InvocationContext ctx, RemoveNodeCommand command) throws Throwable
   {
      Object retVal = invokeNextInterceptor(ctx, command);
      Region r;
      if (command.getFqn() != null && (r = getRegion(command.getFqn(), NodeEventType.REMOVE_NODE_EVENT)) != null)
      {
         registerEvictionEventToRegionManager(new EvictedEventNode(command.getFqn(), NodeEventType.REMOVE_NODE_EVENT), r);
      }
      return retVal;
   }

   @Override
   public Object visitClearDataCommand(InvocationContext ctx, ClearDataCommand command) throws Throwable
   {
      Object retVal = invokeNextInterceptor(ctx, command);
      Region r;
      if (command.getFqn() != null && (r = getRegion(command.getFqn(), NodeEventType.REMOVE_NODE_EVENT)) != null)
      {
         registerEvictionEventToRegionManager(new EvictedEventNode(command.getFqn(), NodeEventType.REMOVE_NODE_EVENT), r);
      }
      return retVal;
   }

   private void registerEvictionEventToRegionManager(EvictedEventNode event, Region region)
   {
      if (event == null)
      {
         // no node modifications.
         return;
      }

      NodeSPI<?, ?> nodeSPI = dataContainer.peek(event.getFqn(), false, false);
      //we do not trigger eviction events for resident nodes
      if (nodeSPI != null && nodeSPI.isResident())
      {
         return;
      }

      region.putNodeEvent(event);

      if (trace)
      {
         log.trace("Adding event " + event + " to region at " + region.getFqn());
      }

      if (trace)
      {
         log.trace("Finished updating node");
      }
   }

   protected Region getRegion(Fqn fqn, NodeEventType type)
   {
      Region r = regionManager.getRegion(fqn, Region.Type.EVICTION, false);
      if (r != null && r.getEvictionPolicy().canIgnoreEvent(fqn, type)) return null;
      return r;
   }
}
