package org.infinispan.stats.topK;

import java.util.Map;

import org.infinispan.commands.VisitableCommand;
import org.infinispan.commands.read.GetAllCommand;
import org.infinispan.commands.read.GetKeyValueCommand;
import org.infinispan.commands.tx.PrepareCommand;
import org.infinispan.commands.write.PutKeyValueCommand;
import org.infinispan.context.InvocationContext;
import org.infinispan.context.impl.TxInvocationContext;
import org.infinispan.distribution.DistributionManager;
import org.infinispan.factories.annotations.Inject;
import org.infinispan.factories.impl.BasicComponentRegistry;
import org.infinispan.interceptors.BaseCustomAsyncInterceptor;
import org.infinispan.interceptors.InvocationFinallyAction;
import org.infinispan.jmx.annotations.MBean;
import org.infinispan.jmx.annotations.ManagedAttribute;
import org.infinispan.jmx.annotations.ManagedOperation;
import org.infinispan.jmx.annotations.Parameter;
import org.infinispan.stats.logging.Log;
import org.infinispan.stats.wrappers.TopKeyLockManager;
import org.infinispan.transaction.WriteSkewException;
import org.infinispan.util.concurrent.locks.LockManager;
import org.infinispan.util.logging.LogFactory;

/**
 * Intercepts the VisitableCommands to calculate the corresponding top-key values.
 *
 * @author Pedro Ruivo
 * @since 6.0
 */
@MBean(objectName = "CacheUsageStatistics", description = "Keeps tracks of the accessed keys")
public class CacheUsageInterceptor extends BaseCustomAsyncInterceptor {

   public static final int DEFAULT_TOP_KEY = 10;
   private static final Log log = LogFactory.getLog(CacheUsageInterceptor.class, Log.class);

   @Inject private BasicComponentRegistry componentRegistry;
   @Inject private DistributionManager distributionManager;

   private StreamSummaryContainer streamSummaryContainer;

   private final InvocationFinallyAction writeSkewReturnHandler = new InvocationFinallyAction() {
      @Override
      public void accept(InvocationContext rCtx, VisitableCommand rCommand, Object rv,
            Throwable throwable) throws Throwable {
         if (throwable instanceof WriteSkewException) {
            WriteSkewException wse = (WriteSkewException) throwable;
            Object key = wse.getKey();
            if (streamSummaryContainer.isEnabled() && key != null && rCtx.isOriginLocal()) {
               streamSummaryContainer.addWriteSkewFailed(key);
            }
            throw wse;
         }
      }
   };

   @Override
   public Object visitGetKeyValueCommand(InvocationContext ctx, GetKeyValueCommand command) throws Throwable {
      if (streamSummaryContainer.isEnabled() && ctx.isOriginLocal()) {
         streamSummaryContainer.addGet(command.getKey(), isRemote(command.getKey()));
      }
      return invokeNext(ctx, command);
   }

   @Override
   public Object visitGetAllCommand(InvocationContext ctx, GetAllCommand command) throws Throwable {
      if (streamSummaryContainer.isEnabled() && ctx.isOriginLocal()) {
         for (Object key : command.getKeys()) {
            streamSummaryContainer.addGet(key, isRemote(key));
         }
      }
      return invokeNext(ctx, command);
   }

   // TODO: implement visitPutMapCommand

   @Override
   public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable {
      if (streamSummaryContainer.isEnabled() && ctx.isOriginLocal()) {
         streamSummaryContainer.addPut(command.getKey(), isRemote(command.getKey()));
      }
      return invokeNextAndFinally(ctx, command, writeSkewReturnHandler);
   }

   @Override
   public Object visitPrepareCommand(TxInvocationContext ctx, PrepareCommand command) throws Throwable {
      return invokeNextAndFinally(ctx, command, writeSkewReturnHandler);
   }

   @ManagedOperation(description = "Resets statistics gathered by this component",
                     displayName = "Reset Statistics (Statistics)")
   public void resetStatistics() {
      streamSummaryContainer.resetAll();
   }

   @ManagedOperation(description = "Set K for the top-K values",
                     displayName = "Set capacity")
   public void setTopKValue(@Parameter(name = "n", description = "the n-th top key to collect") int n) {
      streamSummaryContainer.setCapacity(n);
   }

   @ManagedAttribute(description = "Shows the current capacity for top-K values",
                     displayName = "getCapacity")
   public int getCapacity() {
      return streamSummaryContainer.getCapacity();
   }

   @ManagedAttribute(description = "Show the top " + DEFAULT_TOP_KEY + " keys most read remotely by this instance",
                     displayName = "Top Remote Read Keys")
   public Map<String, Long> getRemoteTopGets() {
      return getNRemoteTopGets(DEFAULT_TOP_KEY);
   }

   @ManagedOperation(description = "Show the top n keys most read remotely by this instance",
                     displayName = "Nth Top Remote Read Keys")
   public Map<String, Long> getNRemoteTopGets(@Parameter(name = "n", description = "the n-th top key to return") int n) {
      return streamSummaryContainer.getTopKFromAsKeyString(StreamSummaryContainer.Stat.REMOTE_GET, n);
   }

   @ManagedAttribute(description = "Show the top " + DEFAULT_TOP_KEY + " keys most read locally by this instance",
                     displayName = "Top Local Read Keys")
   public Map<String, Long> getLocalTopGets() {
      return getNLocalTopGets(DEFAULT_TOP_KEY);
   }

   @ManagedOperation(description = "Show the top n keys most read locally by this instance",
                     displayName = "Nth Top Local Read Keys")
   public Map<String, Long> getNLocalTopGets(@Parameter(name = "n", description = "the n-th top key to return") int n) {
      return streamSummaryContainer.getTopKFromAsKeyString(StreamSummaryContainer.Stat.LOCAL_GET, n);
   }

   @ManagedAttribute(description = "Show the top " + DEFAULT_TOP_KEY + " keys most write remotely by this instance",
                     displayName = "Top Remote Write Keys")
   public Map<String, Long> getRemoteTopPuts() {
      return getNRemoteTopPuts(DEFAULT_TOP_KEY);
   }

   @ManagedOperation(description = "Show the top n keys most write remotely by this instance",
                     displayName = "Nth Top Remote Write Keys")
   public Map<String, Long> getNRemoteTopPuts(@Parameter(name = "n", description = "the n-th top key to return") int n) {
      return streamSummaryContainer.getTopKFromAsKeyString(StreamSummaryContainer.Stat.REMOTE_PUT, n);
   }

   @ManagedAttribute(description = "Show the top " + DEFAULT_TOP_KEY + " keys most write locally by this instance",
                     displayName = "Top Local Write Keys")
   public Map<String, Long> getLocalTopPuts() {
      return getNLocalTopPuts(DEFAULT_TOP_KEY);
   }

   @ManagedOperation(description = "Show the top n keys most write locally by this instance",
                     displayName = "Nth Top Local Write Keys")
   public Map<String, Long> getNLocalTopPuts(@Parameter(name = "n", description = "the n-th top key to return") int n) {
      return streamSummaryContainer.getTopKFromAsKeyString(StreamSummaryContainer.Stat.LOCAL_PUT, n);
   }

   @ManagedAttribute(description = "Show the top " + DEFAULT_TOP_KEY + " keys most locked",
                     displayName = "Top Locked Keys")
   public Map<String, Long> getTopLockedKeys() {
      return getNTopLockedKeys(DEFAULT_TOP_KEY);
   }

   @ManagedOperation(description = "Show the top n keys most locked",
                     displayName = "Nth Top Locked Keys")
   public Map<String, Long> getNTopLockedKeys(@Parameter(name = "n", description = "the n-th top key to return") int n) {
      return streamSummaryContainer.getTopKFromAsKeyString(StreamSummaryContainer.Stat.MOST_LOCKED_KEYS, n);
   }

   @ManagedAttribute(description = "Show the top " + DEFAULT_TOP_KEY + " keys most contended",
                     displayName = "Top Contended Keys")
   public Map<String, Long> getTopContendedKeys() {
      return getNTopContendedKeys(DEFAULT_TOP_KEY);
   }

   @ManagedOperation(description = "Show the top n keys most contended",
                     displayName = "Nth Top Contended Keys")
   public Map<String, Long> getNTopContendedKeys(@Parameter(name = "n", description = "the n-th top key to return") int n) {
      return streamSummaryContainer.getTopKFromAsKeyString(StreamSummaryContainer.Stat.MOST_CONTENDED_KEYS, n);
   }

   @ManagedAttribute(description = "Show the top " + DEFAULT_TOP_KEY + " keys whose lock acquisition failed by timeout",
                     displayName = "Top Keys whose Lock Acquisition Failed by Timeout")
   public Map<String, Long> getTopLockFailedKeys() {
      return getNTopLockFailedKeys(DEFAULT_TOP_KEY);
   }

   @ManagedOperation(description = "Show the top n keys whose lock acquisition failed ",
                     displayName = "Nth Top Keys whose Lock Acquisition Failed by Timeout")
   public Map<String, Long> getNTopLockFailedKeys(@Parameter(name = "n", description = "the n-th top key to return") int n) {
      return streamSummaryContainer.getTopKFromAsKeyString(StreamSummaryContainer.Stat.MOST_FAILED_KEYS, n);
   }

   @ManagedAttribute(description = "Show the top " + DEFAULT_TOP_KEY + " keys whose write skew check was failed",
                     displayName = "Top Keys whose Write Skew Check was failed")
   public Map<String, Long> getTopWriteSkewFailedKeys() {
      return getNTopWriteSkewFailedKeys(DEFAULT_TOP_KEY);
   }

   @ManagedOperation(description = "Show the top n keys whose write skew check was failed",
                     displayName = "Nth Top Keys whose Write Skew Check was failed")
   public Map<String, Long> getNTopWriteSkewFailedKeys(@Parameter(name = "n", description = "the n-th top key to return") int n) {
      return streamSummaryContainer.getTopKFromAsKeyString(StreamSummaryContainer.Stat.MOST_WRITE_SKEW_FAILED_KEYS, n);
   }

   @ManagedOperation(description = "Show the top n keys whose write skew check was failed",
                     displayName = "Top Keys whose Write Skew Check was failed")
   public void setStatisticsEnabled(@Parameter(name = "enabled", description = "true to enable the top-k collection") boolean enabled) {
      streamSummaryContainer.setEnabled(enabled);
   }

   @Override
   protected void start() {
      super.start();
      log.startStreamSummaryInterceptor();
      streamSummaryContainer = StreamSummaryContainer.getOrCreateStreamLibContainer(cache);
      streamSummaryContainer.setEnabled(true);

      LockManager oldLockManager = componentRegistry.getComponent(LockManager.class).running();
      LockManager newLockManager = new TopKeyLockManager(oldLockManager, streamSummaryContainer);
      log.replaceComponent("LockManager", oldLockManager, newLockManager);
      componentRegistry.replaceComponent(LockManager.class.getName(), newLockManager, false);
      componentRegistry.rewire();
   }

   @Override
   protected void stop() {
      super.stop();
      log.stopStreamSummaryInterceptor();
      streamSummaryContainer.setEnabled(false);
   }

   private boolean isRemote(Object key) {
      return distributionManager != null && !distributionManager.getCacheTopology().isWriteOwner(key);
   }
}
