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

import java.lang.ref.SoftReference;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.HashMap;
import java.util.Map;

import javax.management.Attribute;
import javax.management.AttributeList;
import javax.management.MBeanException;
import javax.management.MBeanServer;
import javax.management.NotificationFilter;
import javax.management.ObjectName;

import org.jboss.logging.Logger;

import org.jboss.remoting.*;
import org.jboss.remoting.invocation.NameBasedInvocation;
import org.jboss.remoting.ident.Identity;

/**
 * JMXSubsystemInvocationHandler is a ServerInvocationHandler that will forward requests to the
 * MBeanServer and return the results from the MBeanServer.
 *
 * @author <a href="mailto:jhaynie@vocalocity.net">Jeff Haynie</a>
 * @version $Revision: 1.3 $
 */
public class JMXSubsystemInvocationHandler implements ServerInvocationHandler
{
   private static final Logger log = Logger.getLogger(JMXSubsystemInvocationHandler.class);
   private MBeanServer server;
   private MBeanNotificationCache notificationCache;
   private ServerInvoker invoker;
   private Identity identity;

   private static Method getObjectInstance;
   private static Method isRegistered;
   private static Method getAttribute;
   private static Method getAttributes;
   private static Method setAttribute;
   private static Method setAttributes;
   private static Method invoke;
   private static Method getMBeanInfo;

   static
   {
      try
      {
         Class LObject = (new Object[0]).getClass();
         Class LString = (new String[0]).getClass();

         Class[] Sig_ObjectName =
            new Class[] { ObjectName.class };
         Class[] Sig_ObjectName_String =
            new Class[] { ObjectName.class, String.class };
         Class[] Sig_ObjectName_LString =
            new Class[] { ObjectName.class, LString };
         Class[] Sig_ObjectName_Attribute =
            new Class[] { ObjectName.class, Attribute.class };
         Class[] Sig_ObjectName_AttributeList =
            new Class[] { ObjectName.class, AttributeList.class };
         Class[] Sig_ObjectName_String_LObject_LString =
            new Class[] { ObjectName.class, String.class, LObject, LString };

         getObjectInstance = MBeanServer.class.getMethod("getObjectInstance", Sig_ObjectName);
         isRegistered = MBeanServer.class.getMethod("isRegistered", Sig_ObjectName);
         getAttribute = MBeanServer.class.getMethod("getAttribute", Sig_ObjectName_String);
         getAttributes = MBeanServer.class.getMethod("getAttributes", Sig_ObjectName_LString);
         setAttribute = MBeanServer.class.getMethod("setAttribute", Sig_ObjectName_Attribute);
         setAttributes = MBeanServer.class.getMethod("setAttributes", Sig_ObjectName_AttributeList);
         invoke = MBeanServer.class.getMethod("invoke", Sig_ObjectName_String_LObject_LString);
         getMBeanInfo = MBeanServer.class.getMethod("getMBeanInfo", Sig_ObjectName);
      }
      catch (Exception e)
      {
         throw new RuntimeException("Error resolving methods", e);
      }
   }

   public JMXSubsystemInvocationHandler ()
   {
      super();
   }

   /**
    * set the invoker that owns this handler
    *
    * @param invoker
    */
   public void setInvoker ( ServerInvoker invoker )
   {
      this.invoker = invoker;
   }

   /**
    * set the mbean server that the handler can reference
    *
    * @param server
    */
   public void setMBeanServer ( MBeanServer server )
   {
      this.server = server;
      identity = Identity.get(server);
      // make sure our local server is set
      MBeanTransportPreference.setLocalServer(server,identity);
      if (log.isDebugEnabled())
      {
         log.debug("setMBeanServer called with: "+server+" with identity: "+identity);
      }
   }

   /**
    * method is called to destroy the handler and remove all pending notifications and listeners
    * from the notification cache
    */
   public synchronized void destroy ()
   {
      if (notificationCache!=null)
      {
         notificationCache.destroy();
         notificationCache=null;
      }
   }

   protected void finalize () throws Throwable
   {
      destroy();
      super.finalize();
   }

   /**
    * pull any pending notifications from the queue and place in the return payload
    *
    * @param sessionId
    * @param payload
    */
   private void storeNotifications (String sessionId, Map payload)
   {
      NotificationQueue q = (notificationCache==null) ? null : notificationCache.getNotifications(sessionId);
      if (q!=null)
      {
         payload.put("notifications",q);
      }
   }

   public Object invoke (InvocationRequest invocation)
      throws Throwable
   {
      if (this.server==null)
      {
         throw new IllegalStateException("invoke called prior to mbean server being set");
      }
      try
      {
         NameBasedInvocation nbi = (NameBasedInvocation)invocation.getParameter();
         String methodName = nbi.getMethodName();
         Object args [] = nbi.getParameters();
         String signature [] = nbi.getSignature();
         String sessionId = invocation.getSessionId();

         // this method is called by a polling client for notifications
         if (methodName.equals("$GetNotifications$"))  //FIXME- JGH: make this a little better
         {
            //                if (notificationCache!=null && notificationCache.isConnectedBidirectionally(invocation.getClientLocator()))
            //                {
            //                    return new Boolean(false);
            //                }
            // just return, since the finally will automatically stick the queue in the
            // return payload for us.
            return new Boolean(true);
         }
         if (methodName.equals("$NOTIFICATIONS$")) //FIXME- JGH: make this a little better
         {
            // we are receiving async notifications from a remote server
            NotificationQueue queue=(NotificationQueue)args[0];
            MBeanServerClientInvokerProxy p = MBeanServerClientInvokerProxy.get(queue.getSessionID());
            if (p!=null)
            {
               if (log.isDebugEnabled())
               {
                   log.debug("received remote notifications for JMX id: "+queue.getSessionID()+", queue: "+queue);
               }
               p.deliverNotifications(queue,true);
            }
             else
            {
               log.warn("couldn't find a client invoker proxy for mbean serverid: "+queue.getSessionID()+", dropping notifications ["+queue+"]");
            }
            return null;
         }
         // add and remove are special cases, handle those accordingly
         if (methodName.equals("addNotificationListener") && signature.length==4)
         {
            // listener field is always null, since we don't send it across
            handleAddNotificationListener(invocation.getLocator(),sessionId,(ObjectName)args[0],(NotificationFilter)args[2],args[3]);
            return null;
         }
         else if (methodName.equals("removeNotificationListener") && signature.length==3)
         {
            // listener field is always null, since we don't send it across
            handleRemoveNotificationListener(invocation.getLocator(),sessionId,(ObjectName)args[0],args[2]);
            return null;
         }
         Object _args[] = (args==null && signature!=null) ? new Object[signature.length] : args;
         // get the mbean server method that's being invoked
         Method method = getMethod(methodName,signature);
         // transport against the mbean server
         return method.invoke(server,_args);
      }
      catch (Throwable ex)
      {
         if (ex instanceof UndeclaredThrowableException)
         {
            UndeclaredThrowableException ut=(UndeclaredThrowableException)ex;
            Throwable ute = ut.getUndeclaredThrowable();
            if (ute instanceof Exception)
            {
               throw new MBeanException((Exception)ute,ut.getUndeclaredThrowable().getMessage());
            }
            else
            {
               throw new MBeanException (new Exception(ute.getMessage()),ute.getMessage());
            }
         }
         if (ex instanceof InvocationTargetException)
            throw ((InvocationTargetException) ex).getTargetException();
         throw ex;
      }
      finally
      {
         // on each invocation, we go ahead and deliver back
         // and pending notifications for this session to the remote
         // end
         if (notificationCache!=null)
         {
            storeNotifications(invocation.getSessionId(),invocation.getReturnPayload());
         }
      }
   }
   private synchronized void handleAddNotificationListener (InvokerLocator locator, String sessionId, ObjectName objName, NotificationFilter filter, Object handback)
      throws Throwable
   {
      if (notificationCache==null)
      {
         notificationCache = new MBeanNotificationCache(invoker,server);
      }
      notificationCache.addNotificationListener(locator,sessionId,objName,filter,handback);
   }
   private synchronized void handleRemoveNotificationListener (InvokerLocator locator, String sessionId, ObjectName objName, Object key)
      throws Throwable
   {
      if (notificationCache==null)
      {
         return;
      }
      notificationCache.removeNotificationListener(locator,sessionId,objName,key);
   }
   /**
    * convenience method to lookup the Method object for a given method and signature
    *
    * @param methodName
    * @param sig
    * @return
    * @throws java.lang.Throwable
    */
   private Method getMethod (String methodName, String sig[])
      throws Throwable
   {
      if (methodName.equals("invoke"))
         return invoke;
      else if (methodName.equals("getAttribute"))
         return getAttribute;
      else if (methodName.equals("setAttribute"))
         return setAttribute;
      else if (methodName.equals("getAttributes"))
         return getAttributes;
      else if (methodName.equals("setAttributes"))
         return setAttributes;
      else if (methodName.equals("setAttributes"))
         return setAttributes;
      else if (methodName.equals("getMBeanInfo"))
         return getMBeanInfo;
      else if (methodName.equals("getObjectInstance"))
         return getObjectInstance;
      else if (methodName.equals("isRegistered"))
         return isRegistered;

      Class[] params = null;
      if (sig != null)
      {
         params = new Class[sig.length];
         for (int i = 0 ; i < sig.length; ++i)
            params[i] = Class.forName(sig[i]);
      }
      return MBeanServer.class.getMethod(methodName, params);
   }

   //NOTE: These were added as part of the new remoting callback,
   // but not yet implemented (need to compile). JMX remoting should
   // still work using the old way. -TME
   public void addListener(InvokerCallbackHandler callbackHandler)
   {
      //TODO: Need to implement -TME
   }

   public void removeListener(InvokerCallbackHandler callbackHandler)
   {
      //TODO: Need to implement -TME
   }
}