package org.jboss.resteasy.spi.util;

import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;

import jakarta.annotation.PostConstruct;

import org.jboss.resteasy.resteasy_jaxrs.i18n.Messages;

/**
 * Type conversions and generic type manipulations
 *
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
public final class Types {
    /**
     * Is the genericType of a certain class?
     *
     * @param clazz class
     * @param pType type
     * @return boolean result
     */
    @SuppressWarnings(value = "unchecked")
    public static boolean isA(Class clazz, ParameterizedType pType) {
        return clazz.isAssignableFrom((Class) pType.getRawType());
    }

    /**
     * Gets the index-th type argument.
     *
     * @param pType type
     * @param index index
     * @return the type of argument at index position
     */
    public static Class getArgumentType(ParameterizedType pType, int index) {
        return (Class) pType.getActualTypeArguments()[index];
    }

    public static Class getTemplateParameterOfInterface(Class base, Class desiredInterface) {
        Object rtn = searchForInterfaceTemplateParameter(base, desiredInterface);
        if (rtn != null && rtn instanceof Class)
            return (Class) rtn;
        return null;
    }

    private static Object searchForInterfaceTemplateParameter(Class base, Class desiredInterface) {
        for (int i = 0; i < base.getInterfaces().length; i++) {
            Class intf = base.getInterfaces()[i];
            Type generic = base.getGenericInterfaces()[i];
            if (intf.equals(desiredInterface)) {
                if (generic instanceof ParameterizedType) {
                    ParameterizedType p = (ParameterizedType) generic;
                    Type type = p.getActualTypeArguments()[0];
                    Class rtn = getRawTypeNoException(type);
                    if (rtn != null)
                        return rtn;
                    return type;
                } else {
                    return null;
                }
            }
            Object ret = searchForInterfaceTemplateParameterInSupertype(intf, generic, desiredInterface);
            if (ret != null)
                return ret;
        }
        return searchForInterfaceTemplateParameterInSupertype(base.getSuperclass(), base.getGenericSuperclass(),
                desiredInterface);
    }

    private static Object searchForInterfaceTemplateParameterInSupertype(Class<?> supertype, Type genericSupertype,
            Class<?> desiredInterface) {
        if (supertype == null || supertype.equals(Object.class))
            return null;
        Object rtn = searchForInterfaceTemplateParameter(supertype, desiredInterface);
        if (rtn == null || rtn instanceof Class)
            return rtn;
        if (!(rtn instanceof TypeVariable))
            return null;

        String name = ((TypeVariable) rtn).getName();
        int index = -1;
        TypeVariable[] variables = supertype.getTypeParameters();
        if (variables == null || variables.length < 1)
            return null;

        for (int i = 0; i < variables.length; i++) {
            if (variables[i].getName().equals(name))
                index = i;
        }
        if (index == -1)
            return null;

        if (!(genericSupertype instanceof ParameterizedType))
            return null;

        ParameterizedType pt = (ParameterizedType) genericSupertype;
        Type type = pt.getActualTypeArguments()[index];

        Class clazz = getRawTypeNoException(type);
        if (clazz != null)
            return clazz;
        return type;
    }

    /**
     * See if the two methods are compatible, that is they have the same relative signature.
     *
     * @param method     first method
     * @param intfMethod second method
     * @return boolean result
     */
    @SuppressWarnings(value = "unchecked")
    public static boolean isCompatible(Method method, Method intfMethod) {
        if (method == intfMethod)
            return true;

        if (!method.getName().equals(intfMethod.getName()))
            return false;
        if (method.getParameterCount() != intfMethod.getParameterCount())
            return false;

        for (int i = 0; i < method.getParameterCount(); i++) {
            Class rootParam = method.getParameterTypes()[i];
            Class intfParam = intfMethod.getParameterTypes()[i];
            if (!intfParam.isAssignableFrom(rootParam))
                return false;
        }
        return true;
    }

    /**
     * Given a method and a root class, find the actual method declared in the root that implements the method.
     *
     * @param clazz      class
     * @param intfMethod method
     * @return method in the root class
     */
    @SuppressWarnings(value = "unchecked")
    public static Method getImplementingMethod(Class clazz, Method intfMethod) {
        Class<?> declaringClass = intfMethod.getDeclaringClass();
        if (declaringClass.equals(clazz))
            return intfMethod;

        Class[] paramTypes = intfMethod.getParameterTypes();

        if (declaringClass.getTypeParameters().length > 0 && paramTypes.length > 0) {
            Type[] intfTypes = findParameterizedTypes(clazz, declaringClass);
            Map<String, Type> typeVarMap = new HashMap<String, Type>();
            TypeVariable<? extends Class<?>>[] vars = declaringClass.getTypeParameters();
            for (int i = 0; i < vars.length; i++) {
                if (intfTypes != null && i < intfTypes.length) {
                    typeVarMap.put(vars[i].getName(), intfTypes[i]);
                } else {
                    // Interface type parameters may not have been filled out
                    typeVarMap.put(vars[i].getName(), vars[i].getGenericDeclaration());
                }
            }
            Type[] paramGenericTypes = intfMethod.getGenericParameterTypes();
            paramTypes = new Class[paramTypes.length];

            for (int i = 0; i < paramTypes.length; i++) {
                if (paramGenericTypes[i] instanceof TypeVariable) {
                    TypeVariable tv = (TypeVariable) paramGenericTypes[i];
                    Type t = typeVarMap.get(tv.getName());
                    if (t == null) {
                        throw new RuntimeException(Messages.MESSAGES.unableToResolveTypeVariable());
                    }
                    paramTypes[i] = getRawType(t);
                } else {
                    paramTypes[i] = getRawType(paramGenericTypes[i]);
                }
            }

        }

        try {
            return clazz.getMethod(intfMethod.getName(), paramTypes);
        } catch (NoSuchMethodException e) {
        }

        try {
            Method tmp = clazz.getMethod(intfMethod.getName(), intfMethod.getParameterTypes());
            return tmp;
        } catch (NoSuchMethodException e) {

        }
        return intfMethod;
    }

    /**
     * Find the by a method implementation implemented interface method.
     *
     * @param root           The root class of the class hierarchy.
     * @param iface          The containing interface of the implemented method in the class hierarchy.
     * @param implementation The method implementation.
     * @return The implemented method.
     */
    public static Method getImplementedInterfaceMethod(final Class<?> root, final Class<?> iface,
            final Method implementation) {
        Method overriddenInterfaceMethod = findOverriddenMethod(root, iface, implementation);
        if (overriddenInterfaceMethod != null) {
            return overriddenInterfaceMethod;
        }

        for (Class<?> extended : iface.getInterfaces()) {
            overriddenInterfaceMethod = getImplementedInterfaceMethod(root, extended, implementation);

            if (overriddenInterfaceMethod != null) {
                return overriddenInterfaceMethod;
            }
        }

        return null;
    }

    /**
     * Find the by a method implementation overridden superclass method.
     *
     * @param root           The root class of the class hierarchy.
     * @param superClass     The containing class of the overridden method in the class hierarchy.
     * @param implementation The method implementation.
     * @return The overridden method.
     */
    public static Method findOverriddenMethod(final Class<?> root, final Class<?> superClass,
            final Method implementation) {
        // Check if the overridden method exists without generics
        try {
            final Method method = superClass.getDeclaredMethod(implementation.getName(),
                    implementation.getParameterTypes());
            return method;
        } catch (NoSuchMethodException e) {
            // ignore
        }

        // Check if the overridden method exists with generics
        final Map<TypeVariable<?>, Type> genericTypeMap = populateParameterizedMap(root, superClass);

        for (Method superClassMethod : superClass.getDeclaredMethods()) {
            if (superClassMethod.isSynthetic()) {
                continue;
            }
            if (!implementation.getName().equals(superClassMethod.getName())
                    || implementation.getParameterCount() != superClassMethod.getParameterCount()) {
                continue;
            }

            final Type[] actualMethodParameters = extractTypeVariables(genericTypeMap,
                    superClassMethod.getGenericParameterTypes());
            if (Arrays.equals(implementation.getGenericParameterTypes(), actualMethodParameters)) {
                return superClassMethod;
            }
        }

        return null;
    }

    public static Class<?> getRawType(Type type) {
        if (type instanceof Class<?>) {
            // type is a normal class.
            return (Class<?>) type;

        } else if (type instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) type;
            Type rawType = parameterizedType.getRawType();
            return (Class<?>) rawType;
        } else if (type instanceof GenericArrayType) {
            final GenericArrayType genericArrayType = (GenericArrayType) type;
            final Class<?> componentRawType = getRawType(genericArrayType.getGenericComponentType());
            return Array.newInstance(componentRawType, 0).getClass();
        } else if (type instanceof TypeVariable) {
            final TypeVariable typeVar = (TypeVariable) type;
            if (typeVar.getBounds() != null && typeVar.getBounds().length > 0) {
                return getRawType(typeVar.getBounds()[0]);
            }
        } else if (type instanceof WildcardType) {
            WildcardType wildcardType = (WildcardType) type;
            Type[] upperBounds = wildcardType.getUpperBounds();
            if (upperBounds != null && upperBounds.length > 0) {
                return getRawType(upperBounds[0]);
            }
        }
        throw new RuntimeException(Messages.MESSAGES.unableToDetermineBaseClass());
    }

    public static Class<?> getRawTypeNoException(Type type) {
        if (type instanceof Class<?>) {
            // type is a normal class.
            return (Class<?>) type;

        } else if (type instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) type;
            Type rawType = parameterizedType.getRawType();
            return (Class<?>) rawType;
        } else if (type instanceof GenericArrayType) {
            final GenericArrayType genericArrayType = (GenericArrayType) type;
            final Class<?> componentRawType = getRawType(genericArrayType.getGenericComponentType());
            return Array.newInstance(componentRawType, 0).getClass();
        }
        return null;
    }

    /**
     * Returns the type argument from a parameterized type.
     *
     * @param genericType type
     * @return null if there is no type parameter
     */
    public static Class<?> getTypeArgument(Type genericType) {
        if (!(genericType instanceof ParameterizedType))
            return null;
        ParameterizedType parameterizedType = (ParameterizedType) genericType;
        final Type[] genericTypes = parameterizedType.getActualTypeArguments();
        return genericTypes.length == 0 ? Object.class : getRawType(genericTypes[0]);
    }

    /**
     * Checks that the given class, {@code c}, is the generic type of the given type.
     *
     * @param c    the class the type is checked against
     * @param type the type to check
     *
     * @return {@code true} if the type is not {@code null} and is assignable from the class
     *
     * @see Class#isAssignableFrom(Class)
     */
    public static boolean isGenericTypeInstanceOf(final Class<?> c, final Type type) {
        if (type == null) {
            return false;
        }
        final Class<?> typeArg = getTypeArgument(type);
        return typeArg != null && c.isAssignableFrom(typeArg);
    }

    public static Class getCollectionBaseType(Class type, Type genericType) {
        if (genericType instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) genericType;
            Type componentGenericType = parameterizedType.getActualTypeArguments()[0];
            return getRawType(componentGenericType);
        } else if (genericType instanceof GenericArrayType) {
            final GenericArrayType genericArrayType = (GenericArrayType) genericType;
            Type componentGenericType = genericArrayType.getGenericComponentType();
            return getRawType(componentGenericType);
        } else if (type.isArray()) {
            return type.getComponentType();
        }
        return null;
    }

    public static Class getMapKeyType(Type genericType) {
        if (genericType instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) genericType;
            Type componentGenericType = parameterizedType.getActualTypeArguments()[0];
            return getRawType(componentGenericType);
        }
        return null;
    }

    public static Class getMapValueType(Type genericType) {
        if (genericType instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) genericType;
            Type componentGenericType = parameterizedType.getActualTypeArguments()[1];
            return getRawType(componentGenericType);
        }
        return null;
    }

    public static Type resolveTypeVariables(Class<?> root, Type type) {
        if (type instanceof TypeVariable) {
            Type newType = resolveTypeVariable(root, (TypeVariable) type);
            return (newType == null) ? type : newType;
        } else if (type instanceof ParameterizedType) {
            final ParameterizedType param = (ParameterizedType) type;
            final Type[] actuals = new Type[param.getActualTypeArguments().length];
            for (int i = 0; i < actuals.length; i++) {
                Type newType = resolveTypeVariables(root, param.getActualTypeArguments()[i]);
                actuals[i] = newType == null ? param.getActualTypeArguments()[i] : newType;
            }
            return new ResteasyParameterizedType(actuals, param.getRawType(), param.getOwnerType());
        } else if (type instanceof GenericArrayType) {
            GenericArrayType arrayType = (GenericArrayType) type;
            final Type componentType = resolveTypeVariables(root, arrayType.getGenericComponentType());
            if (componentType == null)
                return type;
            return new GenericArrayType() {
                @Override
                public Type getGenericComponentType() {
                    return componentType;
                }
            };
        } else {
            return type;
        }
    }

    /**
     * Finds an actual value of a type variable. The method looks in a class hierarchy for a class defining the variable
     * and returns the value if present.
     *
     * @param root         root class
     * @param typeVariable type variable
     * @return actual type of the type variable
     */
    public static Type resolveTypeVariable(Class<?> root, TypeVariable<?> typeVariable) {
        if (typeVariable.getGenericDeclaration() instanceof Class<?>) {
            Class<?> classDeclaringTypeVariable = (Class<?>) typeVariable.getGenericDeclaration();
            Type[] types = findParameterizedTypes(root, classDeclaringTypeVariable);
            if (types == null)
                return null;
            for (int i = 0; i < types.length; i++) {
                TypeVariable<?> tv = classDeclaringTypeVariable.getTypeParameters()[i];
                if (tv.equals(typeVariable)) {
                    return types[i];
                }
            }
        }
        return null;
    }

    /**
     * Given a class and an interfaces, go through the class hierarchy to find the interface and return its type arguments.
     *
     * @param classToSearch   class
     * @param interfaceToFind interface to find
     * @return type arguments of the interface
     */
    public static Type[] getActualTypeArgumentsOfAnInterface(Class<?> classToSearch, Class<?> interfaceToFind) {
        Type[] types = findParameterizedTypes(classToSearch, interfaceToFind);
        if (types == null)
            throw new RuntimeException(Messages.MESSAGES.unableToFindTypeArguments(interfaceToFind));
        return types;
    }

    private static final Type[] EMPTY_TYPE_ARRAY = {};

    /**
     * Search for the given interface or class within the root's class/interface hierarchy.
     * If the searched for class/interface is a generic return an array of real types that fill it out.
     *
     * @param root        root class
     * @param searchedFor searched class
     * @return for generic class/interface returns array of real types
     */
    public static Type[] findParameterizedTypes(Class<?> root, Class<?> searchedFor) {
        if (searchedFor.isInterface()) {
            return findInterfaceParameterizedTypes(root, null, searchedFor);
        }
        return findClassParameterizedTypes(root, null, searchedFor);
    }

    public static Type[] findClassParameterizedTypes(Class<?> root, ParameterizedType rootType,
            Class<?> searchedForClass) {
        if (Object.class.equals(root))
            return null;

        Map<TypeVariable<?>, Type> typeVarMap = populateParameterizedMap(root, rootType);

        Class<?> superclass = root.getSuperclass();
        Type genericSuper = root.getGenericSuperclass();

        if (superclass.equals(searchedForClass)) {
            return extractTypes(typeVarMap, genericSuper);
        }

        if (genericSuper instanceof ParameterizedType) {
            ParameterizedType intfParam = (ParameterizedType) genericSuper;
            Type[] types = findClassParameterizedTypes(superclass, intfParam, searchedForClass);
            if (types != null) {
                return extractTypeVariables(typeVarMap, types);
            }
        } else {
            Type[] types = findClassParameterizedTypes(superclass, null, searchedForClass);
            if (types != null) {
                return types;
            }
        }
        return null;
    }

    private static Map<TypeVariable<?>, Type> populateParameterizedMap(Class<?> root, ParameterizedType rootType) {
        Map<TypeVariable<?>, Type> typeVarMap = new HashMap<>();
        if (rootType != null) {
            TypeVariable<? extends Class<?>>[] vars = root.getTypeParameters();
            for (int i = 0; i < vars.length; i++) {
                typeVarMap.put(vars[i], rootType.getActualTypeArguments()[i]);
            }
        }
        return typeVarMap;
    }

    /**
     * Create a mapping for generic types to actual types between two classes / interfaces of a type hierarchy.
     *
     * @param root       The root class / interface of the type hierarchy.
     * @param superClass A superclass / interface of the type hierarchy.
     * @return The Mapping for generic types to actual types.
     */
    public static Map<TypeVariable<?>, Type> populateParameterizedMap(final Class<?> root, final Class<?> superClass) {
        final Type[] types = Types.findParameterizedTypes(root, superClass);
        final TypeVariable<? extends Class<?>>[] typeParameters = superClass.getTypeParameters();

        final Map<TypeVariable<?>, Type> genericTypeMap = new HashMap<>();
        if (typeParameters != null && types != null) {
            int i = 0;
            while (i < types.length) {
                genericTypeMap.put(typeParameters[i], types[i]);
                i++;
            }
            while (i < typeParameters.length) {
                genericTypeMap.put(typeParameters[i], typeParameters[i].getGenericDeclaration());
                i++;
            }
        }

        return genericTypeMap;
    }

    public static Type[] findInterfaceParameterizedTypes(Class<?> root, ParameterizedType rootType,
            Class<?> searchedForInterface) {
        Map<TypeVariable<?>, Type> typeVarMap = populateParameterizedMap(root, rootType);

        for (int i = 0; i < root.getInterfaces().length; i++) {
            Class<?> sub = root.getInterfaces()[i];
            Type genericSub = root.getGenericInterfaces()[i];
            if (sub.equals(searchedForInterface)) {
                return extractTypes(typeVarMap, genericSub);
            }
        }

        for (int i = 0; i < root.getInterfaces().length; i++) {
            Type genericSub = root.getGenericInterfaces()[i];
            Class<?> sub = root.getInterfaces()[i];

            Type[] types = recurseSuperclassForInterface(searchedForInterface, typeVarMap, genericSub, sub);
            if (types != null)
                return types;
        }
        if (root.isInterface())
            return null;

        Class<?> superclass = root.getSuperclass();
        Type genericSuper = root.getGenericSuperclass();

        return recurseSuperclassForInterface(searchedForInterface, typeVarMap, genericSuper, superclass);
    }

    private static Type[] recurseSuperclassForInterface(Class<?> searchedForInterface,
            Map<TypeVariable<?>, Type> typeVarMap, Type genericSub, Class<?> sub) {
        if (genericSub instanceof ParameterizedType) {
            ParameterizedType intfParam = (ParameterizedType) genericSub;
            Type[] types = findInterfaceParameterizedTypes(sub, intfParam, searchedForInterface);
            if (types != null) {
                return extractTypeVariables(typeVarMap, types);
            }
        } else {
            Type[] types = findInterfaceParameterizedTypes(sub, null, searchedForInterface);
            if (types != null) {
                return types;
            }
        }
        return null;
    }

    /**
     * Resolve generic types to actual types.
     *
     * @param typeVarMap The mapping for generic types to actual types.
     * @param types      The types to resolve.
     * @return An array of resolved method parameter types in declaration order.
     */
    private static Type[] extractTypeVariables(final Map<TypeVariable<?>, Type> typeVarMap, final Type[] types) {
        final Type[] resolvedMethodParameterTypes = new Type[types.length];

        for (int i = 0; i < types.length; i++) {
            final Type methodParameterType = types[i];

            if (methodParameterType instanceof TypeVariable<?>) {
                resolvedMethodParameterTypes[i] = typeVarMap.get(methodParameterType);
            } else {
                resolvedMethodParameterTypes[i] = methodParameterType;
            }
        }

        return resolvedMethodParameterTypes;
    }

    private static Type[] extractTypes(Map<TypeVariable<?>, Type> typeVarMap, Type genericSub) {
        if (genericSub instanceof ParameterizedType) {
            ParameterizedType param = (ParameterizedType) genericSub;
            Type[] types = param.getActualTypeArguments();

            Type[] returnTypes = extractTypeVariables(typeVarMap, types);
            return returnTypes;
        } else {
            return EMPTY_TYPE_ARRAY;
        }
    }

    public static class ResteasyParameterizedType implements ParameterizedType {

        private Type[] actuals;

        private Type rawType;

        private Type ownerType;

        public ResteasyParameterizedType(final Type[] actuals, final Type rawType, final Type ownerType) {
            this.actuals = actuals;
            this.rawType = rawType;
            this.ownerType = ownerType;
        }

        @Override
        public Type[] getActualTypeArguments() {
            return actuals;
        }

        @Override
        public Type getRawType() {
            return rawType;
        }

        @Override
        public Type getOwnerType() {
            return ownerType;
        }

        @Override
        public boolean equals(Object other) {
            if (other == null)
                return false;
            if (other instanceof ParameterizedType == false)
                return false;
            ParameterizedType b = (ParameterizedType) other;
            // WARNING: contract defined by ParameterizedType
            return Arrays.equals(actuals, b.getActualTypeArguments()) && Objects.equals(rawType, b.getRawType())
                    && Objects.equals(ownerType, b.getOwnerType());
        }

        @Override
        public int hashCode() {
            // WARNING: stay true to http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/sun/reflect/generics/reflectiveObjects/ParameterizedTypeImpl.java
            return Arrays.hashCode(actuals) ^ Objects.hashCode(ownerType) ^ Objects.hashCode(rawType);
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            if (getOwnerType() != null)
                sb.append(getOwnerType()).append(".");
            sb.append(getRawType());
            if (actuals != null && actuals.length > 0) {
                sb.append("<");
                boolean first = true;
                for (Type actual : actuals) {
                    if (first)
                        first = false;
                    else
                        sb.append(", ");
                    sb.append(actual);
                }
                sb.append(">");
            }
            return sb.toString();
        }

    }

    public static Type boxPrimitives(Type genericType) {
        if (genericType == Boolean.TYPE)
            return Boolean.class;
        if (genericType == Character.TYPE)
            return Character.class;
        if (genericType == Byte.TYPE)
            return Byte.class;
        if (genericType == Short.TYPE)
            return Short.class;
        if (genericType == Integer.TYPE)
            return Integer.class;
        if (genericType == Long.TYPE)
            return Long.class;
        if (genericType == Float.TYPE)
            return Float.class;
        if (genericType == Double.TYPE)
            return Double.class;
        return genericType;
    }

    public static boolean hasPostConstruct(Class<?> clazz) {
        return hasPostConstruct(clazz, (Method mm) -> mm.getParameterCount() == 0);
    }

    public static boolean hasPostConstruct(Class<?> clazz, Function<Method, Boolean> validateParameterCount) {
        for (Method m : clazz.getDeclaredMethods()) {
            if (m.getAnnotation(PostConstruct.class) != null) {
                if (validatePostConstructMethod(m, validateParameterCount)) {
                    return true;
                }
            }
        }

        Class<?> parent = clazz.getSuperclass();
        if (parent != null && parent != Object.class) {
            return hasPostConstruct(parent, validateParameterCount);
        }

        return false;
    }

    private static boolean validatePostConstructMethod(Method m, Function<Method, Boolean> validateParameterCount) {
        if (!validateParameterCount.apply(m)) {
            return false;
        }
        if (!void.class.equals(m.getReturnType())) {
            return false;
        }
        for (Class<?> c : m.getExceptionTypes()) {
            if (!RuntimeException.class.isAssignableFrom(c)) {
                return false;
            }
        }
        if (Modifier.isStatic(m.getModifiers())) {
            return false;
        }
        return true;
    }
}
