/*
 * 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.nifi.util;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ReflectionUtils {

    private final static Logger LOG = LoggerFactory.getLogger(ReflectionUtils.class);

    /**
     * Invokes all methods on the given instance that have been annotated with
     * the given Annotation. If the signature of the method that is defined in
     * <code>instance</code> uses 1 or more parameters, those parameters must be
     * specified by the <code>args</code> parameter. However, if more arguments
     * are supplied by the <code>args</code> parameter than needed, the extra
     * arguments will be ignored.
     *
     * @param annotation the annotation to look for
     * @param instance to invoke a method of
     * @param args to supply in a method call
     * @throws InvocationTargetException ite
     * @throws IllegalArgumentException iae
     * @throws IllegalAccessException if not allowed to invoke that method
     */
    public static void invokeMethodsWithAnnotation(final Class<? extends Annotation> annotation, final Object instance, final Object... args)
            throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        for (final Method method : instance.getClass().getMethods()) {
            if (method.isAnnotationPresent(annotation)) {
                final boolean isAccessible = method.isAccessible();
                method.setAccessible(true);

                try {
                    final Class<?>[] argumentTypes = method.getParameterTypes();
                    if (argumentTypes.length > args.length) {
                        throw new IllegalArgumentException(String.format("Unable to invoke method %1$s on %2$s because method expects %3$s parameters but only %4$s were given",
                                method.getName(), instance, argumentTypes.length, args.length));
                    }

                    for (int i = 0; i < argumentTypes.length; i++) {
                        final Class<?> argType = argumentTypes[i];
                        if (!argType.isAssignableFrom(args[i].getClass())) {
                            throw new IllegalArgumentException(String.format(
                                    "Unable to invoke method %1$s on %2$s because method parameter %3$s is expected to be of type %4$s but argument passed was of type %5$s",
                                    method.getName(), instance, i, argType, args[i].getClass()));
                        }
                    }

                    if (argumentTypes.length == args.length) {
                        method.invoke(instance, args);
                    } else {
                        final Object[] argsToPass = new Object[argumentTypes.length];
                        for (int i = 0; i < argsToPass.length; i++) {
                            argsToPass[i] = args[i];
                        }

                        method.invoke(instance, argsToPass);
                    }
                } finally {
                    if (!isAccessible) {
                        method.setAccessible(false);
                    }
                }
            }
        }
    }

    /**
     * Invokes all methods on the given instance that have been annotated with
     * the given Annotation. If the signature of the method that is defined in
     * <code>instance</code> uses 1 or more parameters, those parameters must be
     * specified by the <code>args</code> parameter. However, if more arguments
     * are supplied by the <code>args</code> parameter than needed, the extra
     * arguments will be ignored.
     *
     * @param annotation the annotation to look for
     * @param instance to invoke a method of
     * @param args to supply in a method call
     * @return <code>true</code> if all appropriate methods were invoked and
     * returned without throwing an Exception, <code>false</code> if one of the
     * methods threw an Exception or could not be invoked; if <code>false</code>
     * is returned, an error will have been logged.
     */
    public static boolean quietlyInvokeMethodsWithAnnotation(final Class<? extends Annotation> annotation, final Object instance, final Object... args) {
        for (final Method method : instance.getClass().getMethods()) {
            if (method.isAnnotationPresent(annotation)) {
                final boolean isAccessible = method.isAccessible();
                method.setAccessible(true);

                try {
                    final Class<?>[] argumentTypes = method.getParameterTypes();
                    if (argumentTypes.length > args.length) {
                        LOG.error("Unable to invoke method {} on {} because method expects {} parameters but only {} were given",
                                new Object[]{method.getName(), instance, argumentTypes.length, args.length});
                        return false;
                    }

                    for (int i = 0; i < argumentTypes.length; i++) {
                        final Class<?> argType = argumentTypes[i];
                        if (!argType.isAssignableFrom(args[i].getClass())) {
                            LOG.error("Unable to invoke method {} on {} because method parameter {} is expected to be of type {} but argument passed was of type {}",
                                    new Object[]{method.getName(), instance, i, argType, args[i].getClass()});
                            return false;
                        }
                    }

                    try {
                        if (argumentTypes.length == args.length) {
                            method.invoke(instance, args);
                        } else {
                            final Object[] argsToPass = new Object[argumentTypes.length];
                            for (int i = 0; i < argsToPass.length; i++) {
                                argsToPass[i] = args[i];
                            }

                            method.invoke(instance, argsToPass);
                        }
                    } catch (final IllegalAccessException | IllegalArgumentException | InvocationTargetException t) {
                        LOG.error("Unable to invoke method {} on {} due to {}", new Object[]{method.getName(), instance, t});
                        LOG.error("", t);
                        return false;
                    }
                } finally {
                    if (!isAccessible) {
                        method.setAccessible(false);
                    }
                }
            }
        }
        return true;
    }
}
