/*
 * 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.maven.surefire.api.util;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author Kristian Rosenvold
 */
public final class ReflectionUtils {
    private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0];
    private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];

    private ReflectionUtils() {
        throw new IllegalStateException("no instantiable constructor");
    }

    public static Method getMethod(Object instance, String methodName, Class<?>... parameters) {
        return getMethod(instance.getClass(), methodName, parameters);
    }

    public static Method getMethod(Class<?> clazz, String methodName, Class<?>... parameters) {
        try {
            return clazz.getMethod(methodName, parameters);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("When finding method " + methodName, e);
        }
    }

    public static Method tryGetMethod(Class<?> clazz, String methodName, Class<?>... parameters) {
        try {
            return clazz.getMethod(methodName, parameters);
        } catch (NoSuchMethodException e) {
            return null;
        }
    }

    public static <T> T invokeGetter(Object instance, String methodName) {
        return invokeGetter(instance.getClass(), instance, methodName);
    }

    public static <T> T invokeGetter(Class<?> instanceType, Object instance, String methodName) {
        Method method = getMethod(instanceType, methodName);
        return invokeMethodWithArray(instance, method);
    }

    public static Constructor<?> getConstructor(Class<?> clazz, Class<?>... arguments) {
        try {
            return clazz.getConstructor(arguments);
        } catch (NoSuchMethodException e) {
            throw new SurefireReflectionException(e);
        }
    }

    public static <T> Constructor<T> tryGetConstructor(Class<T> clazz, Class<?>... arguments) {
        try {
            return clazz.getConstructor(arguments);
        } catch (NoSuchMethodException e) {
            return null;
        }
    }

    public static <T> T newInstance(Constructor<?> constructor, Object... params) {
        try {
            //noinspection unchecked
            return (T) constructor.newInstance(params);
        } catch (ReflectiveOperationException e) {
            throw new SurefireReflectionException(e);
        }
    }

    public static <T> T instantiate(ClassLoader classLoader, String classname, Class<T> returnType) {
        try {
            Class<?> clazz = loadClass(classLoader, classname);
            return returnType.cast(clazz.newInstance());
        } catch (ReflectiveOperationException e) {
            throw new SurefireReflectionException(e);
        }
    }

    public static <T> T instantiateOneArg(
            ClassLoader classLoader, String className, Class<?> param1Class, Object param1) {
        try {
            Class<?> aClass = loadClass(classLoader, className);
            Constructor<?> constructor = getConstructor(aClass, param1Class);
            //noinspection unchecked
            return (T) constructor.newInstance(param1);
        } catch (InvocationTargetException e) {
            throw new SurefireReflectionException(e.getTargetException());
        } catch (ReflectiveOperationException e) {
            throw new SurefireReflectionException(e);
        }
    }

    public static void invokeSetter(Object o, String name, Class<?> value1clazz, Object value) {
        Method setter = getMethod(o, name, value1clazz);
        invokeSetter(o, setter, value);
    }

    public static <T> T invokeSetter(Object target, Method method, Object value) {
        return invokeMethodWithArray(target, method, value);
    }

    public static <T> T invokeMethodWithArray(Object target, Method method, Object... args) {
        try {
            //noinspection unchecked
            return (T) method.invoke(target, args);
        } catch (IllegalAccessException e) {
            throw new SurefireReflectionException(e);
        } catch (InvocationTargetException e) {
            throw new SurefireReflectionException(e.getTargetException());
        }
    }

    public static <T> T invokeMethodWithArray2(Object target, Method method, Object... args)
            throws InvocationTargetException {
        try {
            //noinspection unchecked
            return (T) method.invoke(target, args);
        } catch (IllegalAccessException e) {
            throw new SurefireReflectionException(e);
        }
    }

    public static <T> T instantiateObject(String className, Class<?>[] types, Object[] params, ClassLoader cl) {
        Class<?> clazz = loadClass(cl, className);
        final Constructor<?> constructor = getConstructor(clazz, types);
        return newInstance(constructor, params);
    }

    @SuppressWarnings("checkstyle:emptyblock")
    public static Class<?> tryLoadClass(ClassLoader classLoader, String className) {
        try {
            return classLoader.loadClass(className);
        } catch (NoClassDefFoundError | ClassNotFoundException ignore) {
        }
        return null;
    }

    public static Class<?> loadClass(ClassLoader classLoader, String className) {
        try {
            return classLoader.loadClass(className);
        } catch (NoClassDefFoundError | ClassNotFoundException e) {
            throw new SurefireReflectionException(e);
        }
    }

    public static Class<?> reloadClass(ClassLoader classLoader, Object source) throws ReflectiveOperationException {
        return classLoader.loadClass(source.getClass().getName());
    }

    /**
     * Invoker of public static no-argument method.
     *
     * @param clazz         class on which public static no-argument {@code methodName} is invoked
     * @param methodName    public static no-argument method to be called
     * @param parameterTypes    method parameter types
     * @param parameters    method parameters
     * @return value returned by {@code methodName}
     * @throws RuntimeException if no such method found
     * @throws SurefireReflectionException if the method could not be called or threw an exception.
     * It has original cause Exception.
     */
    public static <T> T invokeStaticMethod(
            Class<?> clazz, String methodName, Class<?>[] parameterTypes, Object[] parameters) {
        if (parameterTypes.length != parameters.length) {
            throw new IllegalArgumentException("arguments length do not match");
        }
        Method method = getMethod(clazz, methodName, parameterTypes);
        return invokeMethodWithArray(null, method, parameters);
    }

    /**
     * Method chain invoker.
     *
     * @param classesChain        classes to invoke on method chain
     * @param noArgMethodNames    chain of public methods to call
     * @param fallback            returned value if a chain could not be invoked due to an error
     * @return successfully returned value from the last method call; {@code fallback} otherwise
     * @throws IllegalArgumentException if {@code classes} and {@code noArgMethodNames} have different array length
     */
    public static <T> T invokeMethodChain(Class<?>[] classesChain, String[] noArgMethodNames, Object fallback) {
        if (classesChain.length != noArgMethodNames.length) {
            throw new IllegalArgumentException("arrays must have the same length");
        }
        Object obj = null;
        try {
            for (int i = 0, len = noArgMethodNames.length; i < len; i++) {
                if (i == 0) {
                    obj = invokeStaticMethod(
                            classesChain[i], noArgMethodNames[i], EMPTY_CLASS_ARRAY, EMPTY_OBJECT_ARRAY);
                } else {
                    Method method = getMethod(classesChain[i], noArgMethodNames[i]);
                    obj = invokeMethodWithArray(obj, method);
                }
            }
            //noinspection unchecked
            return (T) obj;
        } catch (RuntimeException e) {
            //noinspection unchecked
            return (T) fallback;
        }
    }
}
