/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt 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.remoting;

import org.jboss.logging.Logger;
import org.jboss.remoting.transport.ClientInvoker;
import org.jboss.remoting.invocation.InternalInvocation;
import org.jboss.remoting.invocation.OnewayInvocation;
import org.jboss.util.id.GUID;

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

import EDU.oswego.cs.dl.util.concurrent.PooledExecutor;
import EDU.oswego.cs.dl.util.concurrent.Executor;
import EDU.oswego.cs.dl.util.concurrent.ThreadFactory;

/**
 * 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.2.2 $
 */
public class Client
{

    /**
     * Indicated the max number of threads used within oneway thread pool.
     */
    //TODO: figure out reasonable max -TME
    public static final int MAX_NUM_ONEWAY_THREADS = 500;

    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 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.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();
    }

    /**
     * destroy the client and the underlying invoker (if not already destroyed)
     */
    public void destroy ()
    {
        if (this.invoker!=null)
        {
            InvokerRegistry.destroyClientInvoker(this.invoker.getLocator());
            this.invoker = null;
        }
    }

    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.");
        }
    }


   /**
    * invoke the method remotely
    *
    * @param param
    * @param sendPayload
    * @return
    * @throws Throwable
    */
   public Object invoke(Object param, Map sendPayload)
         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 (!localInvoker.isDestroyed())
             {
                 connect();
             }
             else
             {
                 throw new ConnectionFailedException(localInvoker.getLocator()+" has been destroyed");
             }
             connect();
         }
      }
      else
      {
         throw new Exception("Can not perform invoke because invoker is null.");
      }

      Object ret = localInvoker.invoke(new InvocationRequest(sessionId, subsystem, param, sendPayload, 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
    {
        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}),
                          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);
   }

}
