/*
 * Copyright (c) 2006 JMockit developers
 * This file is subject to the terms of the MIT license (see LICENSE.txt).
 */

package org.apache.doris.common.jmockit;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

/**
 * Modify from mockit.internal.util.MethodReflection JMockit v1.13
 * Util class to get and invoke method from specified class.
 * <p>
 * Copied from Apache Doris
 */
public final class MethodReflection {
    private MethodReflection() {
    }

    public static <T> T invoke(Class<?> theClass, Object targetInstance, String methodName, Object... methodArgs) {
        if (theClass == null || methodName == null) {
            throw new IllegalArgumentException();
        }
        boolean staticMethod = targetInstance == null;
        Class<?>[] argTypes = ParameterReflection.getArgumentTypesFromArgumentValues(methodArgs);
        Method method = staticMethod ? findCompatibleStaticMethod(theClass, methodName, argTypes) :
                findCompatibleMethod(theClass, methodName, argTypes);
        if (staticMethod && !Modifier.isStatic(method.getModifiers())) {
            throw new IllegalArgumentException(
                    "Attempted to invoke non-static method without an instance to invoke it on");
        } else {
            T result = invoke(targetInstance, method, methodArgs);
            return result;
        }
    }

    public static <T> T invoke(Object targetInstance, Method method, Object... methodArgs) {
        if (method == null || methodArgs == null) {
            throw new IllegalArgumentException();
        }
        makeAccessible(method);

        try {
            return (T) method.invoke(targetInstance, methodArgs);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (IllegalArgumentException e) {
            throw new IllegalArgumentException("Failure to invoke method: " + method, e);
        } catch (InvocationTargetException e) {
            Throwable cause = e.getCause();
            if (cause instanceof Error) {
                throw (Error) cause;
            } else if (cause instanceof RuntimeException) {
                throw (RuntimeException) cause;
            } else {
                ThrowOfCheckedException.doThrow((Exception) cause);
                return null;
            }
        }
    }

    /**
     * Get a static method with {@methodName String} and {@argTypes Class<?>[]}.
     * If no method was found, a IllegalArgumentException will be thrown.
     */
    private static Method findCompatibleStaticMethod(Class<?> theClass, String methodName, Class<?>[] argTypes) {
        if (theClass == null || methodName == null || argTypes == null) {
            throw new IllegalArgumentException();
        }
        Method methodFound = findCompatibleMethodInClass(theClass, methodName, argTypes);
        if (methodFound != null) {
            return methodFound;
        } else {
            String argTypesDesc = ParameterReflection.getParameterTypesDescription(argTypes);
            throw new IllegalArgumentException("No compatible static method found: " + methodName + argTypesDesc);
        }
    }

    /**
     * Get a non-static method with {@methodName String} and {@argTypes Class<?>[]}.
     */
    public static Method findCompatibleMethod(Class<?> theClass, String methodName, Class<?>[] argTypes) {
        if (theClass == null || methodName == null || argTypes == null) {
            throw new IllegalArgumentException();
        }
        Method methodFound = findCompatibleMethodIfAvailable(theClass, methodName, argTypes);
        if (methodFound != null) {
            return methodFound;
        } else {
            String argTypesDesc = ParameterReflection.getParameterTypesDescription(argTypes);
            throw new IllegalArgumentException("No compatible method found: " + methodName + argTypesDesc);
        }
    }

    /**
     * Get method with {@methodName String} and {@argTypes Class<?>[]} from {@theClass Class<?>}.
     * If more than one method is found, choose the more specific one. (i.e. method with parameters that have more concrete types is more specific)
     */
    private static Method findCompatibleMethodInClass(Class<?> theClass, String methodName, Class<?>[] argTypes) {
        if (theClass == null || methodName == null || argTypes == null) {
            throw new IllegalArgumentException();
        }
        Method found = null;
        Class<?>[] foundParamTypes = null;
        Method[] methods = theClass.getDeclaredMethods();

        for (Method declaredMethod : methods) {
            if (declaredMethod.getName().equals(methodName)) {
                Class<?>[] declaredParamTypes = declaredMethod.getParameterTypes();
                int gap = declaredParamTypes.length - argTypes.length;
                if (gap == 0 && (ParameterReflection.matchesParameterTypes(declaredParamTypes, argTypes)
                        || ParameterReflection.acceptsArgumentTypes(declaredParamTypes, argTypes))
                        && (foundParamTypes == null
                        || ParameterReflection.hasMoreSpecificTypes(declaredParamTypes, foundParamTypes))) {
                    found = declaredMethod;
                    foundParamTypes = declaredParamTypes;
                }
            }
        }

        return found;
    }

    /**
     * Get method with {@methodName String} and {@argTypes Class<?>[]} from {@theClass Class<?>} as well as its super class.
     * If more than one method is found, choose the more specify one. (i.e. choose the method with parameters that have more concrete types)
     */
    private static Method findCompatibleMethodIfAvailable(Class<?> theClass, String methodName, Class<?>[] argTypes) {
        if (theClass == null || methodName == null || argTypes == null) {
            throw new IllegalArgumentException();
        }
        Method methodFound = null;

        while (true) {
            Method compatibleMethod = findCompatibleMethodInClass(theClass, methodName, argTypes);
            if (compatibleMethod != null && (methodFound == null ||
                    ParameterReflection.hasMoreSpecificTypes(compatibleMethod.getParameterTypes(),
                            methodFound.getParameterTypes()))) {
                methodFound = compatibleMethod;
            }

            Class<?> superClass = theClass.getSuperclass();
            if (superClass == null || superClass == Object.class) {
                return methodFound;
            }

            theClass = superClass;
        }
    }


    // ensure that field is accessible
    public static void makeAccessible(AccessibleObject classMember) {
        if (!classMember.isAccessible()) {
            classMember.setAccessible(true);
        }
    }

    // return true if the two types are same type.
    private static boolean isSameType(Class<?> firstType, Class<?> secondType) {
        return firstType == secondType
                || firstType.isPrimitive() && firstType == AutoType.getPrimitiveType(secondType)
                || secondType.isPrimitive() && secondType == AutoType.getPrimitiveType(firstType);
    }
}
