/*
 * 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.interceptors;

import org.jboss.cache.CacheException;
import org.jboss.cache.CacheSPI;
import org.jboss.cache.DataContainer;
import org.jboss.cache.Fqn;
import org.jboss.cache.InvocationContext;
import org.jboss.cache.buddyreplication.BuddyFqnTransformer;
import org.jboss.cache.buddyreplication.BuddyManager;
import org.jboss.cache.buddyreplication.GravitateResult;
import org.jboss.cache.commands.CommandsFactory;
import org.jboss.cache.commands.DataCommand;
import org.jboss.cache.commands.ReplicableCommand;
import org.jboss.cache.commands.VisitableCommand;
import org.jboss.cache.commands.read.GetChildrenNamesCommand;
import org.jboss.cache.commands.read.GetDataMapCommand;
import org.jboss.cache.commands.read.GetKeyValueCommand;
import org.jboss.cache.commands.read.GetKeysCommand;
import org.jboss.cache.commands.read.GetNodeCommand;
import org.jboss.cache.commands.read.GravitateDataCommand;
import org.jboss.cache.commands.remote.DataGravitationCleanupCommand;
import org.jboss.cache.commands.tx.CommitCommand;
import org.jboss.cache.commands.tx.OptimisticPrepareCommand;
import org.jboss.cache.commands.tx.PrepareCommand;
import org.jboss.cache.commands.tx.RollbackCommand;
import org.jboss.cache.factories.annotations.Inject;
import org.jboss.cache.marshall.NodeData;
import org.jboss.cache.marshall.RequestIgnoredResponse;
import org.jboss.cache.transaction.GlobalTransaction;
import org.jgroups.Address;
import org.jgroups.blocks.GroupRequest;
import org.jgroups.blocks.RspFilter;

import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * The Data Gravitator interceptor intercepts cache misses and attempts to
 * gravitate data from other parts of the cluster.
 * <p/>
 * Only used if Buddy Replication is enabled.  Also, the interceptor only kicks
 * in if an {@link org.jboss.cache.config.Option} is passed in to force Data
 * Gravitation for a specific invocation or if <b>autoDataGravitation</b> is
 * set to <b>true</b> when configuring Buddy Replication.
 * <p/>
 * See the JBoss Cache User Guide for more details on configuration options.
 * There is a section dedicated to Buddy Replication in the Replication
 * chapter.
 * <p/>
 * In terms of functionality, if a gravitation call has occured and a cleanup call is needed (based on
 * how BR is configured), a cleanup call will be broadcast immediately after the gravitation call (no txs)
 * or if txs are used, an <i>asynchronous</i> call is made to perform the cleanup <i>outside</i> the scope
 * of the tx that caused the gravitation event.
 * <p/>
 *
 * @author <a href="mailto:manik AT jboss DOT org">Manik Surtani (manik AT jboss DOT org)</a>
 * @deprecated will be removed with optimistic and pessimistic locking.
 */
@Deprecated
public class LegacyDataGravitatorInterceptor extends BaseRpcInterceptor
{
   private BuddyManager buddyManager;
   /**
    * Map that contains commands that need cleaning up.  This is keyed on global transaction, and contains a list of
    * cleanup commands corresponding to all gravitate calls made during the course of the transaction in question.
    */
   private Map<GlobalTransaction, List<ReplicableCommand>> cleanupCommands = new ConcurrentHashMap<GlobalTransaction, List<ReplicableCommand>>();
   private DataContainer dataContainer;
   private CommandsFactory commandsFactory;
   private CacheSPI cacheSPI;
   private BuddyFqnTransformer buddyFqnTransformer;

   @Inject
   public void injectComponents(BuddyManager buddyManager, DataContainer dataContainer, CommandsFactory commandsFactory, CacheSPI cacheSPI, BuddyFqnTransformer transformer)
   {
      this.buddyManager = buddyManager;
      this.dataContainer = dataContainer;
      this.commandsFactory = commandsFactory;
      this.cacheSPI = cacheSPI;
      buddyFqnTransformer = transformer;
   }

   @Override
   public Object visitGetChildrenNamesCommand(InvocationContext ctx, GetChildrenNamesCommand command) throws Throwable
   {
      return handleGetMethod(ctx, command);
   }

   @Override
   public Object visitGetDataMapCommand(InvocationContext ctx, GetDataMapCommand command) throws Throwable
   {
      return handleGetMethod(ctx, command);
   }

   @Override
   public Object visitGetKeysCommand(InvocationContext ctx, GetKeysCommand command) throws Throwable
   {
      return handleGetMethod(ctx, command);
   }

   @Override
   public Object visitGetKeyValueCommand(InvocationContext ctx, GetKeyValueCommand command) throws Throwable
   {
      return handleGetMethod(ctx, command);
   }

   @Override
   public Object visitGetNodeCommand(InvocationContext ctx, GetNodeCommand command) throws Throwable
   {
      return handleGetMethod(ctx, command);
   }

   @Override
   public Object visitRollbackCommand(InvocationContext ctx, RollbackCommand command) throws Throwable
   {
      try
      {
         return invokeNextInterceptor(ctx, command);
      }
      finally
      {
         cleanupCommands.remove(ctx.getGlobalTransaction());
      }
   }


   /**
    * Make sure you also run a cleanup if we have an 1pc.
    */
   @Override
   public Object visitPrepareCommand(InvocationContext ctx, PrepareCommand command) throws Throwable
   {
      if (command.isOnePhaseCommit())
      {
         return dataGravitationCleanupOnCommit(ctx, command);
      }
      return invokeNextInterceptor(ctx, command);
   }

   @Override
   public Object visitCommitCommand(InvocationContext ctx, CommitCommand command) throws Throwable
   {
      return dataGravitationCleanupOnCommit(ctx, command);
   }

   @Override
   public Object visitOptimisticPrepareCommand(InvocationContext ctx, OptimisticPrepareCommand command) throws Throwable
   {
      return visitPrepareCommand(ctx, command);
   }

   private Object dataGravitationCleanupOnCommit(InvocationContext ctx, VisitableCommand command)
         throws Throwable
   {
      GlobalTransaction gtx = ctx.getGlobalTransaction();
      try
      {
         doCommit(gtx);
         return invokeNextInterceptor(ctx, command);
      }
      finally
      {
         cleanupCommands.remove(gtx);
      }
   }

   /**
    * @param ctx invocation context
    * @param fqn fqn to test
    * @return true if the node does not exist; false otherwise.
    */
   protected boolean nodeDoesNotExist(InvocationContext ctx, Fqn fqn)
   {
      return !dataContainer.exists(fqn);
   }

   private Object handleGetMethod(InvocationContext ctx, DataCommand command) throws Throwable
   {
      if (isGravitationEnabled(ctx))
      {
         // test that the Fqn being requested exists locally in the cache.
         if (trace) log.trace("Checking local existence of requested fqn " + command.getFqn());
         if (buddyFqnTransformer.isBackupFqn(command.getFqn()))
         {
            log.info("Is call for a backup Fqn, not performing any gravitation.  Direct calls on internal backup nodes are *not* supported.");
         }
         else
         {
            if (nodeDoesNotExist(ctx, command.getFqn()))
            {
               // gravitation is necessary.

               if (trace) log.trace("Gravitating from local backup tree");
               BackupData data = localBackupGet(command.getFqn(), ctx);

               if (data == null)
               {
                  if (trace) log.trace("Gravitating from remote backup tree");
                  // gravitate remotely.
                  data = remoteBackupGet(command.getFqn());
               }

               if (data != null)
               {
                  if (trace)
                     log.trace("Passing the put call locally to make sure state is persisted and ownership is correctly established.");
                  // store the gravitated node locally.  This will cause it being backed up in the current instance's buddy.
                  createNode(data.backupData);
                  cleanBackupData(data, ctx);
                  wrapIfNeeded(ctx, data.primaryFqn);
               }
            }
            else
            {
               if (trace) log.trace("No need to gravitate; have this already.");
            }
         }
      }
      else
      {
         if (trace)
         {
            log.trace("Suppressing data gravitation for this call.");
         }
      }
      return invokeNextInterceptor(ctx, command);
   }

   protected void wrapIfNeeded(InvocationContext ctx, Fqn fqnToWrap) throws InterruptedException
   {
      // no op
   }

   private boolean isGravitationEnabled(InvocationContext ctx)
   {
      boolean enabled = ctx.isOriginLocal();
      if (enabled && !buddyManager.isAutoDataGravitation())
      {
         enabled = ctx.getOptionOverrides().getForceDataGravitation();
      }

      return enabled;
   }

   private void doCommit(GlobalTransaction gtx) throws Throwable
   {
      if (cleanupCommands.containsKey(gtx))
      {
         if (trace) log.trace("Broadcasting cleanup commands for gtx " + gtx);

         for (ReplicableCommand command : cleanupCommands.get(gtx))
         {
            try
            {
               doCleanup(command);
            }
            catch (Throwable th)
            {
               log.warn("Problems performing gravitation cleanup.  Cleanup command: " + command, th);
            }
         }
      }
      else
      {
         if (trace) log.trace("No cleanups to broadcast in commit phase for gtx " + gtx);
      }
   }

   private BackupData remoteBackupGet(Fqn name) throws Exception
   {
      BackupData result = null;
      GravitateResult gr = gravitateData(name);
      if (gr.isDataFound())
      {
         if (trace)
         {
            log.trace("Got response " + gr);
         }

         result = new BackupData(name, gr);
      }
      return result;
   }

   private void cleanBackupData(BackupData backup, InvocationContext ctx) throws Throwable
   {

      DataGravitationCleanupCommand cleanupCommand = commandsFactory.buildDataGravitationCleanupCommand(backup.primaryFqn, backup.backupFqn);
      GlobalTransaction gtx = ctx.getGlobalTransaction();

      if (gtx == null)
      {
         // broadcast removes
         // remove main Fqn
         if (trace) log.trace("Performing cleanup on [" + backup.primaryFqn + "]");
         // remove backup Fqn
         doCleanup(cleanupCommand);
      }
      else
      {
         if (trace)
            log.trace("Data gravitation performed under global transaction " + gtx + ".  Not broadcasting cleanups until the tx commits.  Recording cleanup command for later use.");
         List<ReplicableCommand> commands;
         if (cleanupCommands.containsKey(gtx))
         {
            commands = cleanupCommands.get(gtx);
         }
         else
         {
            commands = new LinkedList<ReplicableCommand>();
         }

         commands.add(cleanupCommand);
         cleanupCommands.put(gtx, commands);
      }
   }

   private void doCleanup(ReplicableCommand cleanupCommand) throws Throwable
   {
      // cleanup commands are always ASYNCHRONOUS and is broadcast to *everyone* (even members of the current buddy
      // group as they may be members of > 1 buddy group)
      replicateCall(null, cleanupCommand, false, false, false, true, -1);
   }

   @SuppressWarnings("deprecation")
   private GravitateResult gravitateData(Fqn fqn) throws Exception
   {
      if (trace) log.trace("Requesting data gravitation for Fqn " + fqn);

      List<Address> mbrs = rpcManager.getMembers();
      Boolean searchSubtrees = buddyManager.isDataGravitationSearchBackupTrees();
      GravitateDataCommand command = commandsFactory.buildGravitateDataCommand(fqn, searchSubtrees);
      // doing a GET_ALL is crappy but necessary since JGroups' GET_FIRST could return null results from nodes that do
      // not have either the primary OR backup, and stop polling other valid nodes.
      List resps = rpcManager.callRemoteMethods(null, command, GroupRequest.GET_ALL, buddyManager.getBuddyCommunicationTimeout(), new ResponseValidityFilter(rpcManager.getMembers().size()), false);

      if (trace) log.trace("got responses " + resps);

      if (resps == null)
      {
         if (mbrs.size() > 1) log.error("No replies to call " + command);
         return GravitateResult.noDataFound();
      }

      // test for and remove exceptions
      GravitateResult result = GravitateResult.noDataFound();
      for (Object o : resps)
      {
         if (o instanceof Throwable)
         {
            if (log.isDebugEnabled())
            {
               log.debug("Found remote Throwable among responses - removing from responses list", (Exception) o);
            }
         }
         else if (o instanceof GravitateResult)
         {
            GravitateResult candidate = (GravitateResult) o;
            if (isPreferable(candidate, result))
            {
               result = candidate;
            }
         } 
         else if (o == null)
         {
            // Null is OK if we are using region based marshalling; it
            // is what is returned if a region is inactive. Otherwise
            // getting a null is an error condition
            if (!configuration.isUseRegionBasedMarshalling())
            {
               log.error("Unexpected null response to call " + command + ".");
            }
         }
         else if ((o instanceof RequestIgnoredResponse) == false)
         {
            log.error("Unexpected response " + o + " to call " + command + ".");
         } 

      }

      return result;
   }

   /**
    * JBCACHE-1530. Compares GravitateResults in order to prefer those not
    * from dead owner trees.
    * 
    * @param candidate a result that might be preferable to <code>existing</code.  
    *                  Cannot be <code>null</code>
    * @param existing the currently preferred result. Cannot be <code>null</code>
    * 
    * @return <code>true</code> if <code>candidate</code> is preferable.
    */
   private boolean isPreferable(GravitateResult candidate, GravitateResult existing)
   {
      if (existing.isDataFound() == false)
      {
         return true;
      }
      else if (candidate.isDataFound() == false)
      {
         return false;
      }
      else 
      {         
         int ownerIndex = BuddyManager.BUDDY_BACKUP_SUBTREE_FQN.size();
         
         Fqn<?> existingFqn = existing.getBuddyBackupFqn();
         String existingOwner = (existingFqn.size() > ownerIndex ? (String) existingFqn.get(ownerIndex) : null);
         if (existingOwner == null)
         {
            return true;
         }
         
         boolean existingDead = existingOwner.endsWith(":DEAD");
         
         Fqn<?> candidateFqn = candidate.getBuddyBackupFqn();
         String candidateOwner = (candidateFqn.size() > ownerIndex ? (String) candidateFqn.get(ownerIndex) : null);
         if (candidateOwner == null)
         {
            log.warn("Could not find a data owner in backupFqn from " + candidate);
            return false;
         }
         else if (candidateOwner.endsWith(":DEAD"))
         {
            if (existingDead)
            {
               // Both are dead. See if generational match matters
               if (candidateOwner.equals(existingOwner))
               {
                  int genIndex = ownerIndex + 1;
                  Object existGen = (existingFqn.size() > genIndex ? existingFqn.get(genIndex) : null);
                  if ((existGen instanceof Integer) == false)
                  {
                     return true;                     
                  }
                  Object candGen = (candidateFqn.size() > genIndex ? candidateFqn.get(genIndex) : null);
                  if ((candGen instanceof Integer) == false)
                  {
                     log.warn("Could not find a dead data owner generation in backupFqn from " + candidate);
                     return false;                     
                  }
                  return ((Integer) candGen).intValue() > ((Integer) existGen).intValue();
               }
               else
               {
                  // No poing comparing generations of different dead owners
                  // Just prefer what we already have
                  return false;
               }
            }
            else               
            {
               // Prefer non-dead existing
               return false;
            }
         }
         else
         {
            // Candidate isn't dead but we prefer what we already have if it's not dead
            return existingDead;
         }         
      }
   }

   @SuppressWarnings("unchecked")
   private void createNode(List<NodeData> nodeData) throws CacheException
   {
      for (NodeData data : nodeData)
      {
         cacheSPI.put(data.getFqn(), data.getAttributes());
      }
   }

   private BackupData localBackupGet(Fqn fqn, InvocationContext ctx) throws CacheException
   {
      GravitateResult result = cacheSPI.gravitateData(fqn, true, ctx);// a "local" gravitation
      boolean found = result.isDataFound();
      BackupData data = null;

      if (found)
      {
         Fqn backupFqn = result.getBuddyBackupFqn();
         data = new BackupData(fqn, result);
         // now the cleanup
         if (buddyManager.isDataGravitationRemoveOnFind())
         {
            // Remove locally only; the remote call will
            // be broadcast later
            ctx.getOptionOverrides().setCacheModeLocal(true);
            cacheSPI.removeNode(backupFqn);
         }
         else
         {
            cacheSPI.evict(backupFqn, true);
         }
      }

      if (trace) log.trace("Retrieved data " + data + " found = " + found);
      return data;
   }

   private static class BackupData
   {
      Fqn primaryFqn;
      Fqn backupFqn;
      List<NodeData> backupData;

      public BackupData(Fqn primaryFqn, GravitateResult gr)
      {
         this.primaryFqn = primaryFqn;
         this.backupFqn = gr.getBuddyBackupFqn();
         this.backupData = gr.getNodeData();
      }

      public String toString()
      {
         return "BackupData{" +
               "primaryFqn=" + primaryFqn +
               ", backupFqn=" + backupFqn +
               ", backupData=" + backupData +
               '}';
      }
   }

   public static class ResponseValidityFilter implements RspFilter
   {
      int memberCount;

      public ResponseValidityFilter(int memberCount)
      {
         this.memberCount = memberCount;
      }

      public boolean isAcceptable(Object object, Address address)
      {
         if (object instanceof GravitateResult)
         {
            memberCount--;
         }
         // always return true to make sure a response is logged by the JGroups RpcDispatcher.
         return true;
      }

      public boolean needMoreResponses()
      {
         return memberCount > 1;
      }
   }
   
   private static class GravitationResponseComparator implements Comparator<Object>
   {
      public int compare(Object o1, Object o2)
      {
         if (o1 instanceof GravitateResult)
         {
            if (o2 instanceof GravitateResult)
            {
               GravitateResult g2 = (GravitateResult) o2;
               if (g2.isDataFound() == false)
               {
                  return -1;
               }
               else
               {
                  return comparePositiveResults((GravitateResult) o1, g2);
               }
            }
            else
            {
               return -1; // put GR before non-GR
            }
         }
         else
         {
            return 1; // put misc stuff at the end
         }
      }

      private int comparePositiveResults(GravitateResult g1, GravitateResult g2)
      {
         Fqn<?> f1 = g1.getBuddyBackupFqn();
         Fqn<?> f2 = g2.getBuddyBackupFqn();
         return -1;
      }
      
   }
}
