/*
 *  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.causeway.commons.internal.reflection;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.jspecify.annotations.NonNull;

import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.util.ClassUtils;

import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.commons.internal.assertions._Assert;
import org.apache.causeway.commons.internal.base._NullSafe;
import org.apache.causeway.commons.internal.exceptions._Exceptions;
import org.apache.causeway.commons.semantics.CollectionSemantics;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.experimental.Accessors;
import lombok.experimental.UtilityClass;

/**
 * <h1>- internal use only -</h1>
 * <p>
 * <b>WARNING</b>: Do <b>NOT</b> use any of the classes provided by this package! <br/>
 * These may be changed or removed without notice!
 * @since 2.0
 */
@UtilityClass
public class _GenericResolver {

    // -- MODELS

    /**
     * Represents either a singular or plural type, with generic type arguments (if any) resolved.
     */
    public static interface ResolvedType {
        /**
         * The type either contained or not.
         */
        @NonNull Class<?> elementType();

        /**
         * Optionally the container type, the {@link #elementType()} is contained in,
         * such as {@link List}, {@link Collection}, etc.
         */
        @NonNull Optional<Class<?>> containerType();
        @NonNull Optional<CollectionSemantics> collectionSemantics();

        default boolean isSingular() { return containerType().isEmpty(); }
        default boolean isPlural() { return containerType().isPresent(); }
        default boolean isArray() { return containerType().map(Class::isArray).orElse(false); }
        /**
         * Always <code>true</code> for <i>scalar</i> or <i>array</i>.
         * Otherwise, whether {@link #containerType()} exactly matches
         * the container type from {@link #collectionSemantics()}.
         */
        default boolean isSupportedForActionParameter() {
            return isSingular()
                    || isArray()
                    ? true
                    : Objects.equals(
                            containerType().orElse(null),
                            collectionSemantics().map(CollectionSemantics::getContainerType).orElse(null));
        }

        // -- WITHERS

        default ResolvedType withElementType(final @NonNull Class<?> elementType) {
            return new SimpleTypeOfAnyCardinality(assertSingular(elementType), this.containerType(), this.collectionSemantics());
        }

        // -- FACTORIES

        public static ResolvedType singular(final @NonNull Class<?> singularType) {
            return new SimpleTypeOfAnyCardinality(assertSingular(singularType), Optional.empty(), Optional.empty());
        }
        public static ResolvedType plural(
                final @NonNull Class<?> elementType,
                final @NonNull Class<?> pluralType,
                final @NonNull CollectionSemantics collectionSemantics) {
            if(CollectionSemantics.valueOf(elementType).isPresent()) {
                System.err.printf("nested plural detected %s: will fail later%n", elementType);
            }
            return new SimpleTypeOfAnyCardinality(assertSingular(elementType),
                    Optional.of(assertPlural(pluralType)),
                    Optional.of(collectionSemantics));
        }

        // -- HELPER

        private static Class<?> assertSingular(final @NonNull Class<?> singularType) {
            _Assert.assertEquals(
                    Optional.empty(),
                    CollectionSemantics.valueOf(singularType),
                    ()->String.format("%s should not match any supported plural (collection) types", singularType));
            return singularType;
        }
        private static Class<?> assertPlural(final @NonNull Class<?> pluralType) {
            _Assert.assertTrue(
                    CollectionSemantics.valueOf(pluralType).isPresent(),
                    ()->String.format("%s should match a supported plural (collection) type", pluralType));
            return pluralType;
        }
    }

    /**
     * Represents a {@link Method} that has its generic bounded
     * return type (if any)
     * and parameter types (if any) resolved.
     */
    public static interface ResolvedMethod {
        Method method();
        Stream<Annotation> streamAnnotations();
        Class<?> implementationClass();
        Class<?> returnType();
        Class<?>[] paramTypes();
        default String name() {
            return method().getName();
        }
        default int paramCount() {
            return method().getParameterCount();
        }
        default Class<?> paramType(final int paramIndex) {
            return paramTypes()[paramIndex];
        }
        default ResolvedMethod mostSpecific(final ResolvedMethod other) {
            return _GenericResolver.mostSpecific(this, other);
        }
        default boolean isStatic() {
            return Modifier.isStatic(method().getModifiers());
        }
        default boolean isNoArg() { return paramCount()==0; }
        default boolean isSingleArg() { return paramCount()==1; }
        default boolean isReturnTypeATypeOf(final Class<?> typeOf) {
            return typeOf.isAssignableFrom(returnType());
        }
        default boolean isReturnTypeAnyTypeOf(final Can<Class<?>> allowedReturnTypes) {
            return allowedReturnTypes.stream()
                    .anyMatch(this::isReturnTypeATypeOf);
        }
        Class<?> resolveFirstGenericTypeArgumentOnParameter(int paramIndex);
        Class<?> resolveGenericTypeArgumentOnMethodReturn(int argumentIndex);
        /**
         * Is NOT compliant with the weak-sameness relation
         * {@link ResolvedMethod#methodsWeaklySame(ResolvedMethod, ResolvedMethod)}.
         * Provides a comparator (with an arbitrarily chosen ordering relation) sensitive to any method differences.
         * @apiNote don't depend on the chosen ordering
         */
        public static int methodCompare(final ResolvedMethod a, final ResolvedMethod b) {
            if(a.method().equals(b.method())
                    && a.implementationClass().equals(b.implementationClass()) ) {
                return 0; // exact match
            }
            int c = a.name().compareTo(b.name());
            if(c!=0) {
                return c;
            }
            c = Integer.compare(a.paramCount(), b.paramCount());
            if(c!=0) {
                return c;
            }
            var paramsA = a.paramTypes();
            var paramsB = b.paramTypes();
            for(int i=0; i<a.paramCount(); ++i) {
                c = _Reflect.typesCompare(paramsA[i], paramsB[i]);
                if(c!=0) {
                    return c;
                }
            }
            c = _Reflect.typesCompare(a.returnType(), b.returnType());
            if(c!=0) {
                return _Reflect.shareSameTypeHierarchy(a.returnType(), b.returnType())
                        ? 0 // same
                        : c;
            }
            return 0; // equal
        }
        /**
         * Two {@link Method}s, while one overrides the other, might have different method-keys.
         * We want the cache to only keep the most specific of those methods.
         */
        public static boolean methodsWeaklySame(final ResolvedMethod a, final ResolvedMethod b) {
            if(a.method().equals(b.method())
                    && a.implementationClass().equals(b.implementationClass()) ) {
                return true; // exact match
            }
            return a.name().equals(b.name())
                && _Reflect.methodSignatureWeaklyMatch(a.paramTypes(), b.paramTypes());
        }
    }

    /**
     * Represents a {@link Constructor} that has its generic bounded
     * parameter types (if any) resolved.
     */
    public static interface ResolvedConstructor {
        Constructor<?> constructor();
        Class<?> implementationClass();
        Class<?>[] paramTypes();
        default int paramCount() {
            return constructor().getParameterCount();
        }
        default Class<?> paramType(final int paramIndex) {
            return paramTypes()[paramIndex];
        }
        default boolean isNoArg() { return paramCount()==0; }
        default boolean isSingleArg() { return paramCount()==1; }
        Class<?> resolveFirstGenericTypeArgumentOnParameter(int paramIndex);
    }

    // -- FACTORIES

    /**
     * Resolves a type directly, if there is no {@link Method} to use as its context.
     */
    public ResolvedType forPluralType(
            final @NonNull Class<?> pluralType,
            final @NonNull CollectionSemantics collectionSemantics) {
        var resolvablePluralType = ResolvableType.forClass(pluralType);
        var genericTypeArg = resolvablePluralType.isArray()
                ? resolvablePluralType.getComponentType()
                : resolvablePluralType.getGeneric(0);
        return ResolvedType.plural(
                genericTypeArg.toClass(),
                pluralType,
                collectionSemantics);
    }

    /**
     * Resolves a constructor's parameter type.
     */
    public ResolvedType forConstructorParameter(final ResolvedConstructor resolvedConstructor, final int paramIndex) {
        var paramType = resolvedConstructor.paramType(paramIndex);
        return CollectionSemantics.valueOf(paramType)
            .map(collectionSemantics->
                ResolvedType.plural(
                        resolvedConstructor.resolveFirstGenericTypeArgumentOnParameter(paramIndex),
                        paramType,
                        collectionSemantics)
            )
            .orElseGet(()->ResolvedType.singular(paramType));
    }

    /**
     * Resolves a method's return type.
     */
    public ResolvedType forMethodReturn(final ResolvedMethod resolvedMethod) {
        var methodReturn = resolvedMethod.returnType();
        return CollectionSemantics.valueOf(methodReturn)
            .map(collectionSemantics->
                ResolvedType.plural(
                        resolvedMethod.resolveGenericTypeArgumentOnMethodReturn(collectionSemantics.genericTypeArgumentIndex()),
                        methodReturn,
                        collectionSemantics)
            )
            .orElseGet(()->ResolvedType.singular(methodReturn));
    }

    /**
     * Resolves a method's parameter type.
     */
    public ResolvedType forMethodParameter(final ResolvedMethod resolvedMethod, final int paramIndex) {
        var paramType = resolvedMethod.paramType(paramIndex);
        return CollectionSemantics.valueOf(paramType)
            .map(collectionSemantics->
                ResolvedType.plural(
                        resolvedMethod.resolveFirstGenericTypeArgumentOnParameter(paramIndex),
                        paramType,
                        collectionSemantics)
            )
            .orElseGet(()->ResolvedType.singular(paramType));
    }

    public Optional<ResolvedMethod> resolveMethod(
            final @NonNull Method method,
            final @NonNull Class<?> implementationClass) {
        return new SimpleResolvedMethod(method, implementationClass)
                .guardAgainstCannotResolve();
    }

    public ResolvedConstructor resolveConstructor(
            final @NonNull Constructor<?> constructor,
            final @NonNull Class<?> implementationClass) {
        return new SimpleResolvedConstructor(constructor, implementationClass);
    }

    // -- IMPLEMENTATIONS

    private record SimpleTypeOfAnyCardinality(
            @NonNull Class<?> elementType,
            @NonNull Optional<Class<?>> containerType,
            @NonNull Optional<CollectionSemantics> collectionSemantics) implements ResolvedType {
    }

    private record SimpleResolvedMethod(
            Method method,
            Class<?> implementationClass,
            Class<?>[] paramTypes,
            Class<?> returnType,
            boolean isResolved
            ) implements ResolvedMethod {

        public SimpleResolvedMethod(final Method method, final Class<?> implementationClass) {
            this(method, implementationClass,
                _GenericResolver.resolveParameterTypes(method, implementationClass),
                GenericTypeResolver.resolveReturnType(method, implementationClass),
                // not used in canonical constructor ...
                false);
        }
        // canonical constructor
        public SimpleResolvedMethod(
                final Method method,
                final Class<?> implementationClass,
                final Class<?>[] paramTypes,
                final Class<?> returnType,
                final boolean isResolved) {
            this.method = method;
            this.implementationClass = implementationClass;
            this.paramTypes = paramTypes;
            this.returnType = returnType;
            this.isResolved = isReturnTypeResolved() && areParamsResolved();
        }
        public Optional<ResolvedMethod> guardAgainstCannotResolve() {
            return isResolved ? Optional.of(this) : Optional.empty();
        }
        @Override
        public Class<?> resolveGenericTypeArgumentOnMethodReturn(final int argumentIndex) {
            return genericTypeArg(ResolvableType.forMethodReturnType(method, implementationClass), argumentIndex)
                    .toClass();
        }
        @Override
        public Class<?> resolveFirstGenericTypeArgumentOnParameter(final int paramIndex) {
            return genericTypeArg(ResolvableType.forMethodParameter(method, paramIndex, implementationClass), 0)
                    .toClass();
        }
        @Override
        public String toString() {
            return String.format("ResolvedMethod[%s#%s(%s)]", implementationClass.getName(), name(),
                    Can.ofArray(paramTypes).stream()
                        .map(Class::getSimpleName)
                        .collect(Collectors.joining(",")));
        }
        @Override
        public final boolean equals(final Object obj) {
            return obj instanceof SimpleResolvedMethod other
                    ? Objects.equals(this.method(), other.method())
                            && Objects.equals(this.implementationClass(), other.implementationClass())
                    : false;
        }
        @Override
        public final int hashCode() {
            return Objects.hash(method(), implementationClass());
        }

        //-- HELPER
        private boolean areParamsResolved() {
            if(isNoArg()) return true; // skip check
            final Type[] genericParameterTypes = method.getGenericParameterTypes();
            for(int i=0; i<method.getParameterCount(); ++i) {
                if((genericParameterTypes[i] instanceof TypeVariable<?>)
                        && paramTypes[i].equals(Object.class)) {
                    return false;
                }
            }
            return true;
        }
        private boolean isReturnTypeResolved() {
            if(!_Reflect.hasGenericReturn(method)) return true; // skip check
            return !returnType.equals(Object.class);
        }

        @Override
        public Stream<Annotation> streamAnnotations() {
            return Stream.concat(
                _NullSafe.stream(method().getAnnotatedReturnType().getAnnotations()),
                _NullSafe.stream(method().getAnnotations()));
        }

//        private Try<SimpleResolvedMethod> adopt(final @NonNull ClassLoader classLoader) {
//            return Try.call(()->{
//                var ownerReloaded = Class.forName(implementationClass.getName(), true, classLoader);
//                var methodReloaded = ownerReloaded.getMethod(method.getName(), method.getParameterTypes());
//                return new SimpleResolvedMethod(methodReloaded, ownerReloaded);
//            });
//        }
//        /**[CAUSEWAY-3164] ensures reflection on generic type arguments works in a concurrent introspection setting*/
//        private Try<SimpleResolvedMethod> adoptIntoDefaultClassLoader() {
//            return adopt(_Context.getDefaultClassLoader());
//        }
    }

    @EqualsAndHashCode
    @Getter @Accessors(fluent=true)
    private static class SimpleResolvedConstructor implements ResolvedConstructor {

        private final Constructor<?> constructor;
        private final Class<?> implementationClass;

        @EqualsAndHashCode.Exclude
        private final Class<?>[] paramTypes;

        public SimpleResolvedConstructor(final Constructor<?> constructor, final Class<?> implementationClass) {
            this.constructor = constructor;
            this.implementationClass = implementationClass;
            this.paramTypes = _GenericResolver.resolveParameterTypes(constructor, implementationClass);
        }
        @Override
        public Class<?> resolveFirstGenericTypeArgumentOnParameter(final int paramIndex) {
            return genericTypeArg(ResolvableType.forConstructorParameter(constructor, paramIndex, implementationClass), 0)
                    .toClass();
        }
        @Override
        public String toString() {
            return String.format("ResolvedConstructor[%s(%s)]", implementationClass.getName(),
                    Can.ofArray(paramTypes).stream()
                        .map(Class::getSimpleName)
                        .collect(Collectors.joining(",")));
        }
        // -- HELPER
//        private Try<SimpleResolvedConstructor> adopt(final @NonNull ClassLoader classLoader) {
//            return Try.call(()->{
//                var ownerReloaded = Class.forName(implementationClass.getName(), true, classLoader);
//                var methodReloaded = ownerReloaded.getConstructor(constructor.getParameterTypes());
//                return new SimpleResolvedConstructor(methodReloaded, ownerReloaded);
//            });
//        }
//        /**[CAUSEWAY-3164] ensures reflection on generic type arguments works in a concurrent introspection setting*/
//        private Try<SimpleResolvedConstructor> adoptIntoDefaultClassLoader() {
//            return adopt(_Context.getDefaultClassLoader());
//        }
    }

    // -- HELPER

    @SuppressWarnings("deprecation") // proposed alternative is not publicly visible
    private Class<?>[] resolveParameterTypes(final Executable executable, final Class<?> implementationClass) {
        final var array = new Class<?>[executable.getParameterCount()];
        for (int i = 0; i < array.length; i++) {
            array[i] = GenericTypeResolver.resolveParameterType(methodParameter(executable, i), implementationClass);
        }
        return array;
    }

    private MethodParameter methodParameter(final Executable executable, final int paramIndex) {
        return (executable instanceof Method)
                ? new MethodParameter((Method) executable, paramIndex)
                : new MethodParameter((Constructor<?>) executable, paramIndex);
    }

    /**
     * If a and b are related, such that one overrides the other,
     * that one which is overriding the other is returned.
     * @implNote if both declaring type and return type are the same we (arbitrarily) return b
     *
     */
    private ResolvedMethod mostSpecific(final ResolvedMethod a, final ResolvedMethod b) {
        if(a.equals(b)) return b; // an arbitrary pick

        // if declared types are different chose the mostSpecific type
        var implType = _Reflect.mostSpecificType(a.implementationClass(), b.implementationClass());

        var m = BridgeMethodResolver.findBridgedMethod(
                ClassUtils.getMostSpecificMethod(a.method(), implType));
        if(m.isBridge()) {
            throw _Exceptions.unexpectedCodeReach();
        }
        if(a.method().equals(m)) {
            return a;
        }
        if(b.method().equals(m)) {
            return b;
        }
        return _GenericResolver.resolveMethod(m, implType)
                .orElseThrow(()->_Exceptions.illegalArgument("most specific method\n"
                        + "%s is not resolvable while deciding for methods\n"
                        + "%s or\n"
                        + "%s",
                        m, a.method(), b.method()));
    }

    private ResolvableType genericTypeArg(final ResolvableType pluralType, final int genericTypeArgumentIndex){
        var genericTypeArg = pluralType.isArray()
                ? pluralType.getComponentType()
                : pluralType.getGeneric(genericTypeArgumentIndex);
        return genericTypeArg;
    }

    // -- TESTING

    /**
     * JUnit
     */
    @UtilityClass
    public static class testing {
        @SneakyThrows
        public ResolvedMethod resolveMethod(
                final @NonNull Class<?> implementationClass,
                final @NonNull String methodName,
                final Class<?>... parameterTypes) {

            var candidate = _ClassCache.getInstance().findMethodUniquelyByNameOrFail(implementationClass, methodName);
            var paramTypesFound = Can.ofArray(candidate.paramTypes());
            var paramTypesRequested = Can.ofArray(parameterTypes);
            _Assert.assertEquals(paramTypesFound, paramTypesRequested);
            return candidate;
        }
    }

}
