/**
 *  Copyright 2005-2016 Red Hat, Inc.
 *
 *  Red Hat 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 io.fabric8.testkit.jolokia;

import org.jolokia.client.J4pClient;
import org.jolokia.client.exception.J4pException;
import org.jolokia.client.request.AbtractJ4pMBeanRequest;
import org.jolokia.client.request.J4pExecRequest;
import org.jolokia.client.request.J4pReadRequest;
import org.jolokia.client.request.J4pResponse;
import org.jolokia.client.request.J4pWriteRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.management.ObjectName;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.List;

public class JolokiaInvocationHandler implements InvocationHandler {
    private static final transient Logger LOG = LoggerFactory.getLogger(JolokiaInvocationHandler.class);

    private final J4pClient jolokia;
    private final ObjectName objectName;
    private final Class<?> interfaceClass;

    public static <T> T newProxyInstance(J4pClient jolokia, ObjectName objectName, Class<T> interfaceClass) {
        return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[]{interfaceClass}, new JolokiaInvocationHandler(jolokia, objectName, interfaceClass));
    }

    public JolokiaInvocationHandler(J4pClient jolokia, ObjectName objectName, Class<?> interfaceClass) {
        this.jolokia = jolokia;
        this.objectName = objectName;
        this.interfaceClass = interfaceClass;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String name = method.getName();
        String attribute;
        AbtractJ4pMBeanRequest request;
        if ((attribute = getterAttributeName(method)) != null) {
            request = new J4pReadRequest(objectName, attribute);
        } else if ((attribute = setterAttributeName(method)) != null) {
            request = new J4pWriteRequest(objectName, attribute, args[0]);
        } else {
            name = executeMethodName(method);
            if (args == null | method.getParameterTypes().length == 0) {
                request = new J4pExecRequest(objectName, name);
            } else {
                request = new J4pExecRequest(objectName, name, args);
            }
        }
        try {
            request.setPreferredHttpMethod("POST");
            J4pResponse response = jolokia.execute(request);
            Object value = response.getValue();
            return JolokiaClients.convertJolokiaToJavaType(method.getReturnType(), value);
        } catch (J4pException e) {
            List<Object> argsList = args == null ? null : Arrays.asList(args);
            LOG.warn("Failed to invoke " + objectName + " method: " + name + " with arguments: " + argsList + ". " + e, e);
            throw e;
        }
    }

    protected String getterAttributeName(Method method) {
        String name = method.getName();
        int length = name.length();
        Class<?>[] parameterTypes = method.getParameterTypes();
        Class<?> returnType = method.getReturnType();
        if (parameterTypes.length == 0 && !Void.class.equals(returnType)) {
            boolean returnsBool = returnType.equals(Boolean.class) || returnType.equals(boolean.class);
            if (name.startsWith("get") && length > 3) {
                return name.substring(3);
            } else if (returnsBool && name.startsWith("is") && length > 2) {
                return name.substring(2);
            }
        }
        return null;
    }

    protected String setterAttributeName(Method method) {
        Class<?>[] parameterTypes = method.getParameterTypes();
        if (parameterTypes.length == 1) {
            String name = method.getName();
            int nameLength = name.length();
            if (name.startsWith("set") && nameLength > 3) {
                return name.substring(3);
            }
        }
        return null;
    }

    /**
     * Returns the method name with parameter types if there is more than one method with the same name on the interface class
     */
    protected String executeMethodName(Method method) {
        String name = method.getName();
        Class<?>[] parameterTypes = method.getParameterTypes();
        if (methodCount(interfaceClass, name) > 1) {
            StringBuilder buffer = new StringBuilder(name);
            buffer.append("(");
            boolean first = true;
            for (Class<?> parameterType : parameterTypes) {
                if (first) {
                    first = false;
                } else {
                    buffer.append(",");
                }
                buffer.append(parameterType.getCanonicalName());
            }
            buffer.append(")");
            return buffer.toString();
        }
        return name;
    }

    /**
     * Returns the number of declared methods on the given clazz
     */
    protected static int methodCount(Class<?> clazz, String name) {
        int answer = 0;
        Method[] declaredMethods = clazz.getDeclaredMethods();
        for (Method method : declaredMethods) {
            if (name.equals(method.getName())) {
                answer++;
            }
        }
        if (!clazz.equals(Object.class)) {
            Class<?> superclass = clazz.getSuperclass();
            if (superclass != null && !superclass.equals(Object.class)) {
                answer += methodCount(superclass, name);
            }
        }
        return answer;
    }

}
