package org.jboss.cache.marshall;

import org.jboss.cache.InvocationContext;
import org.jboss.cache.commands.ReplicableCommand;
import org.jboss.cache.commands.VisitableCommand;
import org.jboss.cache.commands.remote.AnnounceBuddyPoolNameCommand;
import org.jboss.cache.commands.remote.AssignToBuddyGroupCommand;
import org.jboss.cache.commands.remote.RemoveFromBuddyGroupCommand;
import org.jboss.cache.factories.ComponentRegistry;
import org.jboss.cache.interceptors.InterceptorChain;
import org.jboss.cache.invocation.InvocationContextContainer;
import org.jgroups.Address;
import org.jgroups.Channel;
import org.jgroups.MembershipListener;
import org.jgroups.Message;
import org.jgroups.MessageListener;
import org.jgroups.blocks.GroupRequest;
import org.jgroups.blocks.RpcDispatcher;
import org.jgroups.blocks.RspFilter;
import org.jgroups.util.Rsp;
import org.jgroups.util.RspList;

import java.io.NotSerializableException;
import java.util.Vector;

/**
 * A JGroups RPC dispatcher that knows how to deal with {@link org.jboss.cache.commands.ReplicableCommand}s.
 *
 * @author Manik Surtani (<a href="mailto:manik@jboss.org">manik@jboss.org</a>)
 * @since 2.2.0
 */
public class CommandAwareRpcDispatcher extends RpcDispatcher
{
   protected InvocationContextContainer invocationContextContainer;
   protected InterceptorChain interceptorChain;
   protected ComponentRegistry componentRegistry;
   protected boolean trace;

   public CommandAwareRpcDispatcher()
   {
   }

   public CommandAwareRpcDispatcher(Channel channel, MessageListener l, MembershipListener l2, Object serverObj,
                                    InvocationContextContainer container, InterceptorChain interceptorChain,
                                    ComponentRegistry componentRegistry)
   {
      super(channel, l, l2, serverObj);
      this.invocationContextContainer = container;
      this.componentRegistry = componentRegistry;
      this.interceptorChain = interceptorChain;
      trace = log.isTraceEnabled();
   }

   protected boolean isValid(Message req)
   {
      if (server_obj == null)
      {
         log.error("no method handler is registered. Discarding request.");
         return false;
      }

      if (req == null || req.getLength() == 0)
      {
         log.error("message or message buffer is null");
         return false;
      }

      return true;
   }

   /**
    * Similar to {@link #callRemoteMethods(java.util.Vector, org.jgroups.blocks.MethodCall, int, long, boolean, boolean, org.jgroups.blocks.RspFilter)} except that this version
    * is aware of {@link org.jboss.cache.commands.ReplicableCommand} objects.
    */
   public RspList invokeRemoteCommands(Vector<Address> dests, ReplicableCommand command, int mode, long timeout,
                                       boolean use_anycasting, boolean oob, RspFilter filter) throws NotSerializableException
   {
      if (dests != null && dests.isEmpty())
      {
         // don't send if dest list is empty
         if (trace) log.trace("Destination list is empty: no need to send message");
         return new RspList();
      }

      if (trace)
         log.trace(new StringBuilder("dests=").append(dests).append(", command=").append(command).
               append(", mode=").append(mode).append(", timeout=").append(timeout));

      byte[] buf;
      try
      {
         buf = getRequestMarshaller().objectToByteBuffer(command);
      }
      catch (Exception e)
      {
         throw new RuntimeException("failure to marshal argument(s)", e);
      }

      Message msg = new Message(null, null, buf);
      if (oob)
         msg.setFlag(Message.OOB);
      RspList retval = super.castMessage(dests, msg, mode, timeout, use_anycasting, filter);
      if (log.isTraceEnabled()) log.trace("responses: " + retval);

      // a null response is 99% likely to be due to a marshalling problem - we throw a NSE, this needs to be changed when
      // JGroups supports http://jira.jboss.com/jira/browse/JGRP-193
      // the serialization problem could be on the remote end and this is why we cannot catch this above, when marshalling.
      if (retval == null)
      {
         throw new NotSerializableException("RpcDispatcher returned a null.  This is most often caused by args for " + command.getClass().getSimpleName() + " not being serializable.");
      }

      if (mode == GroupRequest.GET_NONE || retval.isEmpty() || containsOnlyNulls(retval))
         return null;
      else
         return retval;
   }

   private boolean containsOnlyNulls(RspList l)
   {
      for (Rsp r : l.values())
      {
         if (r.getValue() != null || !r.wasReceived() || r.wasSuspected()) return false;
      }
      return true;
   }

   /**
    * Message contains a Command. Execute it against *this* object and return result.
    */
   @Override
   public Object handle(Message req)
   {
      if (isValid(req))
      {
         try
         {
            return executeCommand((ReplicableCommand) getRequestMarshaller().objectFromByteBuffer(req.getBuffer()), req);
         }
         catch (Throwable x)
         {
            if (trace) log.trace("Problems invoking command.", x);
            return x;
         }
      }
      else
      {
         return null;
      }
   }

   protected Object executeCommand(ReplicableCommand cmd, Message req) throws Throwable
   {
      if (trace) log.trace("Executing command: " + cmd + " [sender=" + req.getSrc() + "]");

      if (cmd instanceof VisitableCommand)
      {
         InvocationContext ctx = invocationContextContainer.get();
         ctx.setOriginLocal(false);
         if (!componentRegistry.invocationsAllowed(false))
         {
            return null;
         }
         return interceptorChain.invoke(ctx, (VisitableCommand) cmd);
      }
      else
      {
         if (trace) log.trace("This is a non-visitable command - so performing directly and not via the invoker.");

         // need to check cache status for all except buddy replication commands.
         if (!(cmd instanceof AnnounceBuddyPoolNameCommand ||
               cmd instanceof AssignToBuddyGroupCommand ||
               cmd instanceof RemoveFromBuddyGroupCommand)
               && !componentRegistry.invocationsAllowed(false))
         {
            return null;
         }
         return cmd.perform(null);
      }
   }

   @Override
   public String toString()
   {
      return getClass().getSimpleName() + "[Outgoing marshaller: " + getRequestMarshaller() + "; incoming marshaller: " + getResponseMarshaller() + "]";
   }
}
