/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.superbiz.dynamic.mbean;

import javax.annotation.PreDestroy;
import javax.management.Attribute;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanInfo;
import javax.management.MBeanServer;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Need a @PreDestroy method to disconnect the remote host when used in remote mode.
 */
public class DynamicMBeanHandler implements InvocationHandler {

    private final Map<Method, ConnectionInfo> infos = new ConcurrentHashMap<Method, ConnectionInfo>();

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        final String methodName = method.getName();
        if (method.getDeclaringClass().equals(Object.class) && "toString".equals(methodName)) {
            return getClass().getSimpleName() + " Proxy";
        }
        if (method.getAnnotation(PreDestroy.class) != null) {
            return destroy();
        }

        final ConnectionInfo info = getConnectionInfo(method);
        final MBeanInfo infos = info.getMBeanInfo();
        if (methodName.startsWith("set") && methodName.length() > 3 && args != null && args.length == 1
            && (Void.TYPE.equals(method.getReturnType()) || Void.class.equals(method.getReturnType()))) {
            final String attributeName = attributeName(infos, methodName, method.getParameterTypes()[0]);
            info.setAttribute(new Attribute(attributeName, args[0]));
            return null;
        } else if (methodName.startsWith("get") && (args == null || args.length == 0) && methodName.length() > 3) {
            final String attributeName = attributeName(infos, methodName, method.getReturnType());
            return info.getAttribute(attributeName);
        }
        // operation
        return info.invoke(methodName, args, getSignature(method));
    }

    public Object destroy() {
        for (ConnectionInfo info : infos.values()) {
            info.clean();
        }
        infos.clear();
        return null;
    }

    private String[] getSignature(Method method) {
        String[] args = new String[method.getParameterTypes().length];
        for (int i = 0; i < method.getParameterTypes().length; i++) {
            args[i] = method.getParameterTypes()[i].getName();
        }
        return args; // note: null should often work...
    }

    private String attributeName(MBeanInfo infos, String methodName, Class<?> type) {
        String found = null;
        String foundBackUp = null; // without checking the type
        final String attributeName = methodName.substring(3, methodName.length());
        final String lowerName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4, methodName.length());

        for (MBeanAttributeInfo attribute : infos.getAttributes()) {
            final String name = attribute.getName();
            if (attributeName.equals(name)) {
                foundBackUp = attributeName;
                if (attribute.getType().equals(type.getName())) {
                    found = name;
                }
            } else if (found == null && ((lowerName.equals(name) && !attributeName.equals(name))
                                         || lowerName.equalsIgnoreCase(name))) {
                foundBackUp = name;
                if (attribute.getType().equals(type.getName())) {
                    found = name;
                }
            }
        }

        if (found == null && foundBackUp == null) {
            throw new UnsupportedOperationException("cannot find attribute " + attributeName);
        }

        if (found != null) {
            return found;
        }
        return foundBackUp;
    }

    private synchronized ConnectionInfo getConnectionInfo(Method method) throws Exception {
        if (!infos.containsKey(method)) {
            synchronized (infos) {
                if (!infos.containsKey(method)) { // double check for synchro
                    org.superbiz.dynamic.mbean.ObjectName on = method.getAnnotation(org.superbiz.dynamic.mbean.ObjectName.class);
                    if (on == null) {
                        Class<?> current = method.getDeclaringClass();
                        do {
                            on = method.getDeclaringClass().getAnnotation(org.superbiz.dynamic.mbean.ObjectName.class);
                            current = current.getSuperclass();
                        } while (on == null && current != null);
                        if (on == null) {
                            throw new UnsupportedOperationException("class or method should define the objectName to use for invocation: " + method.toGenericString());
                        }
                    }
                    final ConnectionInfo info;
                    if (on.url().isEmpty()) {
                        info = new LocalConnectionInfo();
                        ((LocalConnectionInfo) info).server = ManagementFactory.getPlatformMBeanServer(); // could use an id...
                    } else {
                        info = new RemoteConnectionInfo();
                        final Map<String, String[]> environment = new HashMap<String, String[]>();
                        if (!on.user().isEmpty()) {
                            environment.put(JMXConnector.CREDENTIALS, new String[]{on.user(), on.password()});
                        }
                        // ((RemoteConnectionInfo) info).connector = JMXConnectorFactory.newJMXConnector(new JMXServiceURL(on.url()), environment);
                        ((RemoteConnectionInfo) info).connector = JMXConnectorFactory.connect(new JMXServiceURL(on.url()), environment);

                    }
                    info.objectName = new ObjectName(on.value());

                    infos.put(method, info);
                }
            }
        }
        return infos.get(method);
    }

    private abstract static class ConnectionInfo {

        protected ObjectName objectName;

        public abstract void setAttribute(Attribute attribute) throws Exception;

        public abstract Object getAttribute(String attribute) throws Exception;

        public abstract Object invoke(String operationName, Object params[], String signature[]) throws Exception;

        public abstract MBeanInfo getMBeanInfo() throws Exception;

        public abstract void clean();
    }

    private static class LocalConnectionInfo extends ConnectionInfo {

        private MBeanServer server;

        @Override
        public void setAttribute(Attribute attribute) throws Exception {
            server.setAttribute(objectName, attribute);
        }

        @Override
        public Object getAttribute(String attribute) throws Exception {
            return server.getAttribute(objectName, attribute);
        }

        @Override
        public Object invoke(String operationName, Object[] params, String[] signature) throws Exception {
            return server.invoke(objectName, operationName, params, signature);
        }

        @Override
        public MBeanInfo getMBeanInfo() throws Exception {
            return server.getMBeanInfo(objectName);
        }

        @Override
        public void clean() {
            // no-op
        }
    }

    private static class RemoteConnectionInfo extends ConnectionInfo {

        private JMXConnector connector;
        private MBeanServerConnection connection;

        private void before() throws IOException {
            connection = connector.getMBeanServerConnection();
        }

        private void after() throws IOException {
            // no-op
        }

        @Override
        public void setAttribute(Attribute attribute) throws Exception {
            before();
            connection.setAttribute(objectName, attribute);
            after();
        }

        @Override
        public Object getAttribute(String attribute) throws Exception {
            before();
            try {
                return connection.getAttribute(objectName, attribute);
            } finally {
                after();
            }
        }

        @Override
        public Object invoke(String operationName, Object[] params, String[] signature) throws Exception {
            before();
            try {
                return connection.invoke(objectName, operationName, params, signature);
            } finally {
                after();
            }
        }

        @Override
        public MBeanInfo getMBeanInfo() throws Exception {
            before();
            try {
                return connection.getMBeanInfo(objectName);
            } finally {
                after();
            }
        }

        @Override
        public void clean() {
            try {
                connector.close();
            } catch (IOException e) {
                // no-op
            }
        }
    }
}
