/***************************************
 *                                     *
 *  JBoss: The OpenSource J2EE WebOS   *
 *                                     *
 *  Distributable under LGPL license.  *
 *  See terms of license at gnu.org.   *
 *                                     *
 ***************************************/
package org.jboss.remoting;

import EDU.oswego.cs.dl.util.concurrent.Executor;
import EDU.oswego.cs.dl.util.concurrent.PooledExecutor;
import EDU.oswego.cs.dl.util.concurrent.ThreadFactory;
import org.jboss.logging.Logger;
import org.jboss.remoting.invocation.InternalInvocation;
import org.jboss.remoting.invocation.OnewayInvocation;
import org.jboss.remoting.marshal.Marshaller;
import org.jboss.remoting.marshal.UnMarshaller;
import org.jboss.remoting.transport.ClientInvoker;
import org.jboss.util.id.GUID;

import java.util.List;
import java.util.Map;

/**
 * Client is a convience method for invoking remote methods for a given subsystem
 *
 * @author <a href="mailto:jhaynie@vocalocity.net">Jeff Haynie</a>
 * @author <a href="mailto:telrod@e2technologies.net">Tom Elrod</a>
 * @version $Revision: 1.10.8.2 $
 */
public class Client
{

   /**
    * Indicated the max number of threads used within oneway thread pool.
    */
   /**
    * Specifies the number of work threads in the pool for
    * executing one way invocations on the client.
    * Value is 10.
    */
   public static final int MAX_NUM_ONEWAY_THREADS = 10;

   private static final Logger log = Logger.getLogger(Client.class);
   private ClientInvoker invoker;
   private ClassLoader classloader;
   private String subsystem;
   private String sessionId = new GUID().toString();
   private static PooledExecutor onewayExecutor;
   private static int onewayThreadCounter = 0;
   public static final String RAW = "RAW_PAYLOAD";

   public Client(InvokerLocator locator) throws Exception
   {
      this(locator, null);
   }
   public Client(InvokerLocator locator, String subsystem)
         throws Exception
   {
      this(Thread.currentThread().getContextClassLoader(), locator, subsystem);
   }

   public Client(ClassLoader cl, InvokerLocator locator, String subsystem)
         throws Exception
   {
      this(cl, InvokerRegistry.createClientInvoker(locator), subsystem);
   }

   public Client(ClassLoader cl, ClientInvoker invoker, String subsystem)
         throws Exception
   {
      this.classloader = cl;
      this.subsystem = subsystem == null ? null : subsystem.toUpperCase();
      this.invoker = invoker;
   }

   /**
    * This will set the session id used when making invocations on
    * server invokers.  There is a default unique id automatically
    * generated for each Client instance, so unless you have a good reason to set
    * this, do not set this.
    *
    * @param sessionId
    */
   public void setSessionId(String sessionId)
   {
      this.sessionId = sessionId;
   }

   public String getSessionId()
   {
      return this.sessionId;
   }

   public boolean isConnected()
   {
      return (this.invoker != null && this.invoker.isConnected());
   }

   public void connect() throws Exception
   {
      this.invoker.connect();
   }

   public void disconnect()
   {
      this.invoker.disconnect();
   }

   public ClientInvoker getInvoker()
   {
      return invoker;
   }

   public void setInvoker(ClientInvoker invoker)
   {
      this.invoker = invoker;
   }

   public String getSubsystem()
   {
      return subsystem;
   }

   public void setSubsystem(String subsystem)
   {
      this.subsystem = subsystem;
   }

   /**
    * This should be set when want to override the default behavior of automatically
    * getting s suitable locator.  This should be used want want to control what type
    * of callbacks to receive (pull or push).  Set to null to poll for callback messages.
    * This can also be used to receive callbacks using another transport and subsystem,
    * if desired.
    *
    * @param locator
    */
   private void setClientLocator(InvokerLocator locator) throws Exception
   {
      if (invoker != null)
      {
         invoker.setClientLocator(locator);
      }
      else
      {
         throw new Exception("Can not set client locator because client invoker is null.");
      }
   }

   /**
    * Invokes the server invoker handler with the payload parameter passed.
    * @param param
    * @return
    * @throws Throwable
    */
   public Object invoke(Object param) throws Throwable
   {
      return invoke(param, null);
   }

   /**
    * invoke the method remotely
    *
    * @param param - payload for the server invoker handler
    * @param metadata - any extra metadata that may be needed by the transport (i.e. GET or POST if using
    * http invoker) or if need to pass along extra data to the server invoker handler.
    * @return
    * @throws Throwable
    */
   public Object invoke(Object param, Map metadata)
         throws Throwable
   {
      /**
       * Using a local variable for the invoker as work around so don't have
       * to sync method (and take performance hit)
       * Although this may cause having multiple instances of invoker in existance at
       * one time, should avoid having reference changed by another thread while in
       * execution path for method.
       */
      ClientInvoker localInvoker = invoker;

      if (localInvoker != null)
      {
         if (localInvoker.isConnected() == false)
         {
            if (log.isDebugEnabled())
            {
               log.debug("invoke called, but our invoker is disconnected, discarding and fetching another fresh invoker for: " + invoker.getLocator());
            }
            localInvoker = InvokerRegistry.createClientInvoker(localInvoker.getLocator());
            connect();
         }
      }
      else
      {
         throw new Exception("Can not perform invoke because invoker is null.");
      }

      Object ret = localInvoker.invoke(new InvocationRequest(sessionId, subsystem, param, metadata, null, null));
      this.invoker = localInvoker;
      return ret;
   }

   /**
    * Will invoke a oneway call to server without a return object.  This should be used when not expecting a
    * return value from the server and wish to achieve higher performance, since the client will not wait for
    * a return.
    * <b>
    * This is done one of two ways.  The first is to pass true as the clientSide param.  This will cause the
    * execution of the remote call to be excuted in a new thread on the client side and will return the calling thread
    * before making call to server side.  Although, this is optimal for performance, will not know about any problems
    * contacting server.
    * <p/>
    * The second, is to pass false as the clientSide param.  This will allow the current calling thread to make
    * the call to the remote server, at which point, the server side processing of the thread will be executed on
    * the remote server in a new executing thread and the client thread will return.  This is a little slower, but
    * will know that the call made it to the server.
    *
    * @param param
    * @param sendPayload
    * @param clientSide
    */
   public void invokeOneway(final Object param, final Map sendPayload, boolean clientSide) throws Throwable
   {
      if (clientSide)
      {
         Executor executor = getOnewayExecutor();
         Runnable onewayRun = new Runnable()
         {
            public void run()
            {
               try
               {
                  invoke(param, sendPayload);
               }
               catch (Throwable e)
               {
                  // throw away exception since can't get it back to original caller
                  log.error("Error executing client oneway invocation request: " + param, e);
               }
            }
         };
         executor.execute(onewayRun);
      }
      else
      {
         OnewayInvocation invocation = new OnewayInvocation(param);
         invoke(invocation, sendPayload);
      }
   }

   private synchronized static Executor getOnewayExecutor()
   {
      if (onewayExecutor == null)
      {
         onewayExecutor = new PooledExecutor(MAX_NUM_ONEWAY_THREADS);
         onewayExecutor.setKeepAliveTime(3000);
         onewayExecutor.waitWhenBlocked();
         onewayExecutor.setThreadFactory(new ThreadFactory()
         {
            public Thread newThread(Runnable runnable)
            {
               return new Thread(runnable, "Remoting client oneway " + onewayThreadCounter++);
            }
         });
      }
      return onewayExecutor;
   }


   /**
    * Same as calling invokeOneway(Object param, Map sendPayload, boolean clientSide) with
    * clientSide param being false.  Therefore, client thread will not return till it has made
    * remote call.
    *
    * @param param
    * @param sendPayload
    */
   public void invokeOneway(Object param, Map sendPayload) throws Throwable
   {
      invokeOneway(param, sendPayload, false);
   }

   /**
    * Adds the specified handler as a callback listener for pull (sync) callbacks.
    * The invoker server will then collect the callbacks for this specific handler.
    * The callbacks can be retrieved by calling the getCallbacks() method.
    * Note: this will cause the client invoker's client locator to be set to null.
    *
    * @param callbackHandler
    * @throws Throwable
    */
   public void addListener(InvokerCallbackHandler callbackHandler) throws Throwable
   {
      addListener(callbackHandler, null);
   }

   /**
    * Adds the specified handler as a callback listener for push (async) callbacks.
    * The invoker server will then callback on this handler (via the server invoker
    * specified by the clientLocator) when it gets a callback from the server handler.
    * Note: passing a null clientLocator will cause the client invoker's client
    * locator to be set to null.
    *
    * @param callbackHandler
    * @param clientLocator
    * @throws Throwable
    */
   public void addListener(InvokerCallbackHandler callbackHandler,
                           InvokerLocator clientLocator) throws Throwable
   {
      addListener(callbackHandler, clientLocator, null);
   }

   /**
    * Adds the specified handler as a callback listener for push (async) callbacks.
    * The invoker server will then callback on this handler (via the server invoker
    * specified by the clientLocator) when it gets a callback from the server handler.
    * Note: passing a null clientLocator will cause the client invoker's client
    * locator to be set to null.
    *
    * @param callbackHandler interface to call on with callback
    * @param clientLocator locator for callback server to callback on
    * @param callbackHandlerObject will be included in the callback object passed upon callback
    * @throws Throwable
    */
   public void addListener(InvokerCallbackHandler callbackHandler,
                           InvokerLocator clientLocator, Object callbackHandlerObject) throws Throwable
   {
      invoker.setClientLocator(clientLocator);
      if (clientLocator != null)
      {
         Client client = new Client(clientLocator, subsystem);
         client.setSessionId(getSessionId());
         client.connect();

         client.invoke(new InternalInvocation(InternalInvocation.ADDCLIENTLISTENER,
               new Object[]{callbackHandler, callbackHandlerObject}),
               null);
         client.disconnect();
      }
      // now call server to add listener
      invoke(new InternalInvocation(InternalInvocation.ADDLISTENER, null), null);
   }

   /**
    * Removes callback handler as a callback listener from the server (and client in
    * the case that it was setup to receive async callbacks). See addListener().
    *
    * @param callbackHandler
    * @throws Throwable
    */
   public void removeListener(InvokerCallbackHandler callbackHandler) throws Throwable
   {
      // connect to the given client locator and remove handler as listener
      InvokerLocator locator = invoker.getClientLocator();
      if (locator != null) // async callback
      {
         Client client = new Client(locator, subsystem);
         client.setSessionId(getSessionId());
         client.connect();
         client.invoke(new InternalInvocation(InternalInvocation.REMOVECLIENTLISTENER,
               new Object[]{callbackHandler}),
               null);
         client.disconnect();
      }
      // now call server to remove listener
      invoke(new InternalInvocation(InternalInvocation.REMOVELISTENER, null), null);
   }

   public List getCallbacks() throws Throwable
   {
      return (List) invoke(new InternalInvocation(InternalInvocation.GETCALLBACKS, null), null);
   }

   public void setMarshaller(Marshaller marshaller)
   {
      if (invoker != null && marshaller != null)
      {
         invoker.setMarshaller(marshaller);
      }
   }

   public void setUnMarshaller(UnMarshaller unmarshaller)
   {
      if (invoker != null && unmarshaller != null)
      {
         invoker.setUnMarshaller(unmarshaller);
      }
   }
}