/*
 * 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.mx.remoting;

import org.jboss.remoting.*;
import org.jboss.remoting.ident.Identity;
import org.jboss.remoting.invocation.NameBasedInvocation;
import org.jboss.util.NestedRuntimeException;
import org.jboss.logging.Logger;

import javax.management.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * 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.2.2.6 $
 */
public class JMXSubsystemInvocationHandler implements ServerInvocationHandler
{
    private static final transient Logger log = Logger.getLogger(JMXSubsystemInvocationHandler.class.getName());
    private MBeanServer server;
    private Map notificationCaches = new HashMap();
    private ServerInvoker invoker;
    private Identity identity;
    private boolean destroyed = false;

    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 NestedRuntimeException("Error resolving methods", e);
        }
    }

    public JMXSubsystemInvocationHandler()
    {
        super();
    }

    /**
     * set the invoker that owns this handler
     * 
     * @param invoker 
     */
    public synchronized 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 (destroyed) return;

        if (log.isDebugEnabled())
        {
            log.debug("destroy - notificationCaches=" + notificationCaches);
        }
        // make a copy since destroyed will be called
        Iterator iter = new HashMap(notificationCaches).values().iterator();
        while (iter.hasNext())
        {
            MBeanNotificationCache q = (MBeanNotificationCache) iter.next();
            q.destroy();
            iter.remove();
            q = null;
        }

        destroyed = true;
        notificationCaches.clear();
    }

    /**
     * called when the cache is destroyed
     *
     * @param locator
     * @param cache
     */
    synchronized void destroyed (InvokerLocator locator, MBeanNotificationCache cache)
    {
        if (log.isDebugEnabled())
        {
            log.debug("NotificationCache - "+cache+" has been destroyed for locator: "+locator+", before removal: "+notificationCaches);
        }
        notificationCaches.remove(locator);
        if (log.isDebugEnabled())
        {
            log.debug("NotificationCache - after removal of cache, notificationCaches="+notificationCaches);
        }
    }

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

    /**
     * pull any pending notifications from the queue and place in the return payload
     * 
     * @param locator   
     * @param sessionId 
     * @param payload   
     */
    private void storeNotifications(InvokerLocator locator, String sessionId, Map payload)
    {
        MBeanNotificationCache notificationCache = (MBeanNotificationCache) notificationCaches.get(locator);
        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$"))
            {
                MBeanNotificationCache notificationCache = (MBeanNotificationCache) notificationCaches.get(invocation.getLocator());
                return notificationCache != null && notificationCache.shouldClientPoll() ?
                        Boolean.TRUE : Boolean.FALSE;
            }
            if (methodName.equals("$NOTIFICATIONS$"))
            {
                // we are receiving async notifications from a remote server
                NotificationQueue queues[] = (NotificationQueue[]) args[0];
                if (queues != null && queues.length > 0)
                {
                    for (int c = 0; c < queues.length; c++)
                    {
                        NotificationQueue queue = queues[c];
                        if (queue == null) continue;
                        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 (notificationCaches.isEmpty() == false)
            {
                storeNotifications(invocation.getLocator(), invocation.getSessionId(), invocation.getReturnPayload());
            }
        }
    }

    private synchronized void handleAddNotificationListener(InvokerLocator locator, String sessionId, ObjectName objName, NotificationFilter filter, Object handback)
            throws Throwable
    {
        MBeanNotificationCache cache = (MBeanNotificationCache) notificationCaches.get(locator);
        if (cache == null)
        {
            if (log.isDebugEnabled()) log.debug("This is the first listener for: "+locator+", creating a new notification cache - current list: "+notificationCaches);
            cache = new MBeanNotificationCache(this, invoker, server, locator, sessionId);
            notificationCaches.put(locator, cache);
        }
        cache.addNotificationListener(objName, filter, handback);
    }

    private synchronized void handleRemoveNotificationListener(InvokerLocator locator, String sessionId, ObjectName objName, Object key)
            throws Throwable
    {
        MBeanNotificationCache cache = (MBeanNotificationCache) notificationCaches.get(locator);
        if (cache != null)
        {
            cache.removeNotificationListener(objName, key);
            if (cache.hasListeners() == false)
            {
                if (log.isDebugEnabled()) log.debug("This is the last listener for: "+locator+", destroy the notification cache: "+cache+" - current list (before removal): "+notificationCaches);
                cache.destroy();
                notificationCaches.remove(locator);
                cache = null;
            }
        }
    }

    /**
     * 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
    }
}
