/*
 * 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.apache.ignite.internal.mxbean;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.HashMap;
import java.util.Map;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
import javax.management.NotCompliantMBeanException;
import javax.management.StandardMBean;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.mxbean.MXBeanDescription;
import org.apache.ignite.mxbean.MXBeanParameter;
import org.apache.ignite.mxbean.MXBeanParametersDescriptions;
import org.apache.ignite.mxbean.MXBeanParametersNames;

/**
 * Extension of standard Java MBean. Overrides some hooks to return
 * annotation based descriptions.
 */
public class IgniteStandardMXBean extends StandardMBean {
    /** */
    private static final String DESC_MUST_START_WITH_UPP_CASE = "Description must start with upper case: ";

    /** */
    private static final String DESC_MUST_END_WITH_PERIOD = "Description must end with period: ";

    /** */
    private static final String DESC_MUST_BE_NOT_NULL = "Description must be not null: ";

    /** */
    private static final String DESC_MUST_BE_NOT_EMPTY = "Description must be not empty: ";

    /** */
    private static final String NAME_MUST_BE_NOT_NULL = "Parameter name must be not null: ";

    /** */
    private static final String NAME_MUST_BE_NOT_EMPTY = "Parameter name must be not empty: ";

    /**
     * Objects maps from primitive classes to primitive object classes.
     */
    private static final Map<String, Class<?>> primCls = new HashMap<>();

    /**
     * Static constructor.
     */
    static {
        primCls.put(Boolean.TYPE.toString().toLowerCase(), Boolean.TYPE);
        primCls.put(Character.TYPE.toString().toLowerCase(), Character.TYPE);
        primCls.put(Byte.TYPE.toString().toLowerCase(), Byte.TYPE);
        primCls.put(Short.TYPE.toString().toLowerCase(), Short.TYPE);
        primCls.put(Integer.TYPE.toString().toLowerCase(), Integer.TYPE);
        primCls.put(Long.TYPE.toString().toLowerCase(), Long.TYPE);
        primCls.put(Float.TYPE.toString().toLowerCase(), Float.TYPE);
        primCls.put(Double.TYPE.toString().toLowerCase(), Double.TYPE);
    }

    /**
     * Make a DynamicMBean out of the object implementation, using the specified
     * mbeanInterface class.
     *
     * @param implementation The implementation of this MBean.
     * @param mbeanInterface The Management Interface exported by this
     *      MBean's implementation. If {@code null}, then this
     *      object will use standard JMX design pattern to determine
     *      the management interface associated with the given
     *      implementation.
     *      If {@code null} value passed then information will be built by
     *      {@link StandardMBean}
     *
     * @exception NotCompliantMBeanException if the {@code mbeanInterface}
     *    does not follow JMX design patterns for Management Interfaces, or
     *    if the given {@code implementation} does not implement the
     *    specified interface.
     */
    public <T> IgniteStandardMXBean(T implementation, Class<T> mbeanInterface)
        throws NotCompliantMBeanException {
        super(implementation, mbeanInterface);
    }

    /** {@inheritDoc} */
    @Override protected String getDescription(MBeanAttributeInfo info) {
        String str = super.getDescription(info);

        String methodName = (info.isIs() ? "is" : "get") + info.getName();

        try {
            // Recursively get method.
            Method mtd = findMethod(getMBeanInterface(), methodName, new Class[]{});

            if (mtd != null) {
                MXBeanDescription desc = mtd.getAnnotation(MXBeanDescription.class);

                if (desc != null) {
                    str = desc.value();

                    assert str != null : "Failed to find method: " + mtd;
                    assert !str.trim().isEmpty() : "Method description cannot be empty: " + mtd;

                    // Enforce proper English.
                    assert Character.isUpperCase(str.charAt(0)) : DESC_MUST_START_WITH_UPP_CASE + str;
                    assert str.charAt(str.length() - 1) == '.' : DESC_MUST_END_WITH_PERIOD + str;
                }
            }
        }
        catch (SecurityException ignored) {
            // No-op. Default value will be returned.
        }

        return str;
    }

    /** {@inheritDoc} */
    @Override protected String getDescription(MBeanInfo info) {
        String str = super.getDescription(info);

        // Return either default one or given by annotation.
        MXBeanDescription desc = U.getAnnotation(getMBeanInterface(), MXBeanDescription.class);

        if (desc != null) {
            str = desc.value();

            assert str != null;
            assert !str.trim().isEmpty();

            // Enforce proper English.
            assert Character.isUpperCase(str.charAt(0)) : DESC_MUST_START_WITH_UPP_CASE + str;
            assert str.charAt(str.length() - 1) == '.' : DESC_MUST_END_WITH_PERIOD + str;
        }

        return str;
    }

    /** {@inheritDoc} */
    @Override protected String getDescription(MBeanOperationInfo info) {
        String str = super.getDescription(info);

        try {
            Method m = getMethod(info);

            MXBeanDescription desc = m.getAnnotation(MXBeanDescription.class);

            if (desc != null) {
                str = desc.value();

                assert str != null;
                assert !str.trim().isEmpty();

                // Enforce proper English.
                assert Character.isUpperCase(str.charAt(0)) : DESC_MUST_START_WITH_UPP_CASE + str;
                assert str.charAt(str.length() - 1) == '.' : DESC_MUST_END_WITH_PERIOD + str;
            }
        }
        catch (SecurityException | ClassNotFoundException ignored) {
            // No-op. Default value will be returned.
        }

        return str;
    }

    /** {@inheritDoc} */
    @Override protected String getDescription(MBeanOperationInfo op, MBeanParameterInfo param, int seq) {
        String str = super.getDescription(op, param, seq);

        try {
            Method m = getMethod(op);

            MXBeanParametersDescriptions decsAnn = m.getAnnotation(MXBeanParametersDescriptions.class);

            if (decsAnn != null) {
                assert decsAnn.value() != null;
                assert seq < decsAnn.value().length;

                str = decsAnn.value()[seq];

                assert str != null : DESC_MUST_BE_NOT_NULL + str;
                assert !str.trim().isEmpty() : DESC_MUST_BE_NOT_EMPTY + str;

                // Enforce proper English.
                assert Character.isUpperCase(str.charAt(0)) : DESC_MUST_START_WITH_UPP_CASE + str;
                assert str.charAt(str.length() - 1) == '.' : DESC_MUST_END_WITH_PERIOD + str;
            }
            else {
                MXBeanParameter argInfoAnnotation = getMXBeanParameterAnnotation(m, seq);

                if (argInfoAnnotation != null) {
                    str = argInfoAnnotation.description();

                    assert str != null : DESC_MUST_BE_NOT_NULL + str;
                    assert !str.trim().isEmpty() : DESC_MUST_BE_NOT_EMPTY + str;

                    // Enforce proper English.
                    assert Character.isUpperCase(str.charAt(0)) : DESC_MUST_START_WITH_UPP_CASE + str;
                    assert str.charAt(str.length() - 1) == '.' : DESC_MUST_END_WITH_PERIOD + str;
                }
            }
        }
        catch (SecurityException | ClassNotFoundException ignored) {
            // No-op. Default value will be returned.
        }

        return str;
    }

    /** {@inheritDoc} */
    @Override protected String getParameterName(MBeanOperationInfo op, MBeanParameterInfo param, int seq) {
        String str = super.getParameterName(op, param, seq);

        try {
            Method m = getMethod(op);

            MXBeanParametersNames namesAnn = m.getAnnotation(MXBeanParametersNames.class);

            if (namesAnn != null) {
                assert namesAnn.value() != null;
                assert seq < namesAnn.value().length;

                str = namesAnn.value()[seq];

                assert str != null : NAME_MUST_BE_NOT_NULL + str;
                assert !str.trim().isEmpty() : NAME_MUST_BE_NOT_EMPTY + str;
            }
            else {
                MXBeanParameter argInfoAnnotation = getMXBeanParameterAnnotation(m, seq);

                if (argInfoAnnotation != null) {
                    str = argInfoAnnotation.name();

                    assert str != null : NAME_MUST_BE_NOT_NULL + str;
                    assert !str.trim().isEmpty() : NAME_MUST_BE_NOT_EMPTY + str;
                }
            }
        }
        catch (SecurityException | ClassNotFoundException ignored) {
            // No-op. Default value will be returned.
        }

        return str;
    }

    /**
     * Gets {@link MXBeanParameter} annotation instance from method if possible, otherwise returns {@code null}.
     *
     * @param m Method instance.
     * @param seq The sequence number of the argument considered ("0" for the first parameter,
     *            "1" for the second parameter, etc...)
     * @return {@link MXBeanParameter} annotation instance.
     */
    private MXBeanParameter getMXBeanParameterAnnotation(Method m, int seq) {
        Parameter[] params = m.getParameters();

        if (seq < params.length)
            return params[seq].getAnnotation(MXBeanParameter.class);

        return null;
    }

    /**
     * Gets method by operation info.
     *
     * @param op MBean operation info.
     * @return Method.
     * @throws ClassNotFoundException Thrown if parameter type is unknown.
     * @throws SecurityException Thrown if method access is not allowed.
     */
    private Method getMethod(MBeanOperationInfo op) throws ClassNotFoundException, SecurityException {
        String mtdName = op.getName();

        MBeanParameterInfo[] signature = op.getSignature();

        Class<?>[] params = new Class<?>[signature.length];

        for (int i = 0; i < signature.length; i++) {
            // Parameter type is either a primitive type or class. Try both.
            Class<?> type = primCls.get(signature[i].getType().toLowerCase());

            if (type == null)
                type = Class.forName(signature[i].getType());

            params[i] = type;
        }

        return findMethod(getMBeanInterface(), mtdName, params);
    }

    /**
     * Finds method for the given interface.
     *
     * @param itf MBean interface.
     * @param methodName Method name.
     * @param params Method parameters.
     * @return Method.
     */
    @SuppressWarnings("unchecked")
    private Method findMethod(Class itf, String methodName, Class[] params) {
        assert itf.isInterface() : itf + " must represent the interface";

        Method res = null;

        // Try to get method from given interface.
        try {
            res = itf.getDeclaredMethod(methodName, params);

            if (res != null)
                return res;
        }
        catch (NoSuchMethodException ignored) {
            // No-op. Default value will be returned.
        }

        // Process recursively super interfaces.
        Class[] superItfs = itf.getInterfaces();

        for (Class superItf: superItfs) {
            res = findMethod(superItf, methodName, params);

            if (res != null)
                return res;
        }

        return res;
    }
}
