/*
 * JBoss, Home of Professional Open Source.
 * Copyright 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.ha.framework.server;

import java.lang.ref.SoftReference;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.InetAddress;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.RemoteStub;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.jboss.ha.client.loadbalance.LoadBalancePolicy;
import org.jboss.ha.framework.interfaces.HAPartition;
import org.jboss.ha.framework.interfaces.HARMIClient;
import org.jboss.ha.framework.interfaces.HARMIProxy;
import org.jboss.ha.framework.interfaces.HARMIResponse;
import org.jboss.ha.framework.interfaces.HARMIServer;
import org.jboss.invocation.MarshalledInvocation;
import org.jboss.logging.Logger;
import org.jboss.net.sockets.DefaultSocketFactory;

/**
 * This class is a <em>server-side</em> proxy for replicated RMI objects.
 *
 * @author bill@jboss.org
 * @author sacha.labourey@jboss.org
 * @author Scott.Stark@jboss.org
 * @version $Revision: 105943 $
 */
public class HARMIServerImpl
   implements HARMIServer
{
   protected Object handler;
   protected Map<Long, Method> invokerMap = new HashMap<Long, Method>();
   protected org.jboss.logging.Logger log;
   protected RemoteStub rmistub;
   protected Object stub;
   protected String key;
   protected Class<?> intf;
   protected RefreshProxiesHATarget target;

   public HARMIServerImpl(HAPartition partition,
                          String replicantName,
                          Class<?> intf,
                          Object handler,
                          int port,
                          RMIClientSocketFactory csf,
                          RMIServerSocketFactory ssf)
      throws Exception
   {
      this(partition,
                      replicantName,
                      intf,
                      handler,
                      port,
                      csf,
                      ssf,
                      null);

   }

   public HARMIServerImpl(HAPartition partition,
                          String replicantName,
                          Class<?> intf,
                          Object handler,
                          int port,
                          RMIClientSocketFactory clientSocketFactory,
                          RMIServerSocketFactory serverSocketFactory,
                          InetAddress bindAddress)
      throws Exception
   {
      this.handler = handler;
      this.log = Logger.getLogger(this.getClass());
      this.intf = intf;
      this.key = partition.getPartitionName() + "/" + replicantName;

      // Obtain the hashes for the supported handler interfaces
      Class<?>[] ifaces = handler.getClass().getInterfaces();
      for (int i = 0; i < ifaces.length; i++)
      {
         @SuppressWarnings("unchecked")
         Map<Long, Method> tmp = MarshalledInvocation.methodToHashesMap(ifaces[i]);
         invokerMap.putAll(tmp);
      }

      if( bindAddress != null )
      {
         // If there is no serverSocketFactory use a default
         if( serverSocketFactory == null )
            serverSocketFactory = new DefaultSocketFactory(bindAddress);
         else
         {
            // See if the server socket supports setBindAddress(String)
            try
            {
               Class<?>[] parameterTypes = {String.class};
               Class<?> ssfClass = serverSocketFactory.getClass();
               Method m = ssfClass.getMethod("setBindAddress", parameterTypes);
               Object[] args = {bindAddress.getHostAddress()};
               m.invoke(serverSocketFactory, args);
            }
            catch (NoSuchMethodException e)
            {
               log.warn("Socket factory does not support setBindAddress(String)");
               // Go with default address
            }
            catch (Exception e)
            {
               log.warn("Failed to setBindAddress="+bindAddress+" on socket factory", e);
               // Go with default address
            }
         }
      }

      this.rmistub = (RemoteStub)UnicastRemoteObject.exportObject(this, port, clientSocketFactory, serverSocketFactory);// casting is necessary because interface has changed in JDK>=1.2
      this.target = new RefreshProxiesHATarget(partition, replicantName, rmistub, HATarget.ENABLE_INVOCATIONS);

      HARMIServer.rmiServers.put(key, this);
   }

   /**
    * Create a new HARMIServer implementation that will act as a RMI end-point for a specific server.
    *
    * @param partition {@link HAPartition} that will determine the cluster member
    * @param replicantName Name of the service using this HARMIServer
    * @param intf Class type under which should appear the RMI server dynamically built
    * @param handler Target object to which calls will be forwarded
    * @throws Exception Thrown if any exception occurs during call forwarding
    */
   public HARMIServerImpl(HAPartition partition, String replicantName, Class<?> intf, Object handler) throws Exception
   {
      this(partition, replicantName, intf, handler, 0, null, null);
   }

   /**
    * Once a HARMIServer implementation exists, it is possible to ask for a stub that can, for example,
    * be bound in JNDI for client use. Each client stub may incorporate a specific load-balancing
    * policy.
    *
    * @param policy {@link org.jboss.ha.client.loadbalance.LoadBalancePolicy} implementation to ues on the client.
    * @return proxy instance object
    */
   public Object createHAStub(LoadBalancePolicy policy)
   {
      HARMIClient client = new HARMIClient(target.getReplicantList(),
         target.getCurrentViewId (), policy, key, handler);
      this.target.addProxy (client);
      return Proxy.newProxyInstance(
      intf.getClassLoader(),
      new Class[]{ intf, HARMIProxy.class },
      client);
   }

   public void destroy()
   {
      try
      {
	 target.destroy();
         HARMIServer.rmiServers.remove(key);
         UnicastRemoteObject.unexportObject(this, true);

      } catch (Exception e)
      {
         log.error("failed to destroy", e);
      }
   }

   // HARMIServer implementation ----------------------------------------------

   public HARMIResponse invoke(long clientViewId, MarshalledInvocation mi)
      throws Exception
   {
      mi.setMethodMap(invokerMap);
      Method method = mi.getMethod();

      try
      {
         HARMIResponse rsp = new HARMIResponse();
         if (clientViewId != target.getCurrentViewId())
         {
            rsp.newReplicants = new ArrayList<Object>(target.getReplicantList());
            rsp.currentViewId = target.getCurrentViewId();
         }

         rsp.response = method.invoke(handler, mi.getArguments());
         return rsp;
      }
      catch (IllegalAccessException iae)
      {
         throw iae;
      }
      catch (IllegalArgumentException iae)
      {
         throw iae;
      }
      catch (java.lang.reflect.InvocationTargetException ite)
      {
         throw (Exception)ite.getTargetException();
      }
   }

   public List<?> getReplicants() throws Exception
   {
      return target.getReplicantList();
   }

   public Object getLocal() throws Exception
   {
      return handler;
   }

   public class RefreshProxiesHATarget extends HATarget
   {
      protected List<SoftReference<HARMIClient>> generatedProxies;

      public RefreshProxiesHATarget(HAPartition partition,
            String replicantName,
            java.io.Serializable target,
            int allowInvocations)
         throws Exception
      {
         super (partition, replicantName, target, allowInvocations);
      }


      public synchronized void addProxy (HARMIClient client)
      {
         initGeneratedProxies();
         generatedProxies.add (new SoftReference<HARMIClient>(client));
      }

      public synchronized void replicantsChanged(String key, List<?> newReplicants, int newReplicantsViewId, boolean merge)
      {
         super.replicantsChanged (key, newReplicants, newReplicantsViewId, merge);

         initGeneratedProxies();
         
         // we now update all generated proxies
         //
         List<SoftReference<HARMIClient>> trash = new ArrayList<SoftReference<HARMIClient>>();
         for (SoftReference<HARMIClient> ref : generatedProxies)
         {
            HARMIClient proxy = ref.get ();
            if (proxy == null)
            {
               trash.add (ref);
            }
            else
            {
               proxy.updateClusterInfo (this.replicants, this.clusterViewId);
            }
         }

         if (trash.size () > 0)
            generatedProxies.removeAll(trash);

      }
      
      private void initGeneratedProxies()
      {
         if (generatedProxies == null)
         {
            generatedProxies = new ArrayList<SoftReference<HARMIClient>>();
         }
      }
   }
}
