/*
 *  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.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.springframework.core.annotation.AnnotationUtils;
import org.jspecify.annotations.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;

import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.commons.functional.Try;
import org.apache.causeway.commons.internal._Constants;
import org.apache.causeway.commons.internal.assertions._Assert;
import org.apache.causeway.commons.internal.base._NullSafe;
import org.apache.causeway.commons.internal.base._Strings;
import org.apache.causeway.commons.internal.collections._Arrays;
import org.apache.causeway.commons.internal.functions._Predicates;

import org.jspecify.annotations.NonNull;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;

/**
 * <h1>- internal use only -</h1>
 * <p>
 * Reflection utilities.
 * </p>
 * <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!
 * </p>
 * @since 2.0
 */
@UtilityClass
public final class _Reflect {

    // -- PREDICATES

    /**
     * Other than checking for Method equality, this function is weaker and only
     * checks for method name, method signature equality
     * and whether a overrides b or vice versa.
     * <p>
     * Example of co-variant overriding:
     * <pre>
     * class Parent {
     *     Object getSomething(){ return 10; }
     * }
     *
     * class Child extends Parent {
     *     &#64;Override Integer getSomething() { return 10; }
     * }
     * </pre>
     * @param a
     * @param b
     * @see Method#equals(Object)
     */
    public boolean methodsSame(final Method a, final Method b) {
        if(!a.getName().equals(b.getName())) {
            return false;
        }
        if(a.getParameterCount()!=b.getParameterCount()) {
            return false;
        }
        if(!_Arrays.testAllMatch(a.getParameters(), b.getParameters(),
                (p1, p2)->p1.getType().equals(p2.getType()))) {
            return false;
        }
        return shareSameTypeHierarchy(a.getReturnType(), b.getReturnType());
    }

    /**
     * Weak variant of {@link #methodsSame(Method, Method)},
     * that relaxes parameter checks,
     * if bridge methods are detected
     */
    public boolean methodsWeaklySame(final Method a, final Method b) {
        if(!a.getName().equals(b.getName())) {
            return false;
        }
        if(a.getParameterCount()!=b.getParameterCount()) {
            return false;
        }
        if(a.getParameterCount()>0) {
            if(a.isBridge()
                    || b.isBridge()) {
                if(!_Arrays.testAllMatch(a.getParameters(), b.getParameters(),
                        (p1, p2)->shareSameTypeHierarchy(p1.getType(), p2.getType()))) {
                    return false;
                }
            } else {
                if(!_Arrays.testAllMatch(a.getParameters(), b.getParameters(),
                        (p1, p2)->p1.getType().equals(p2.getType()))) {
                    return false;
                }
            }
        }
        return shareSameTypeHierarchy(a.getReturnType(), b.getReturnType());
    }

    public boolean shareSameTypeHierarchy(final @NonNull Class<?> a, final @NonNull Class<?> b) {
        return a.isAssignableFrom(b)
                || b.isAssignableFrom(a);
    }

    public boolean shareSameTypeHierarchy(final @NonNull Method a, final @NonNull Method b) {
        return shareSameTypeHierarchy(a.getDeclaringClass(), b.getDeclaringClass());
    }

    // -- COMPARATORS

    /**
     * In compliance with the sameness relation {@link #methodsSame(Method, Method)}
     * provides a comparator (with an arbitrarily chosen ordering relation).
     * @apiNote don't depend on the chosen ordering
     * @param a
     * @param b
     * @see #methodsSame(Method, Method)
     */
    public int methodWeakCompare(final Method a, final Method b) {

        int c = a.getName().compareTo(b.getName());
        if(c!=0) {
            return c;
        }
        c = Integer.compare(a.getParameterCount(), b.getParameterCount());
        if(c!=0) {
            return c;
        }
        var paramsA = a.getParameters();
        var paramsB = b.getParameters();
        for(int i=0; i<a.getParameterCount(); ++i) {
            c = typesCompare(paramsA[i].getType(), paramsB[i].getType());
            if(c!=0) {
                return c;
            }
        }
        c = typesCompare(a.getReturnType(), b.getReturnType());
        if(c!=0) {
            return shareSameTypeHierarchy(a.getReturnType(), b.getReturnType())
                    ? 0 // same
                    : c;
        }
        return 0; // equal
    }

    public int typesCompare(final Class<?> a, final Class<?> b) {
        return a.getName().compareTo(b.getName());
    }

    /**
     * Whether the caller can access this reflected object.
     * (method, field or constructor)
     * @param obj
     */
    public boolean canAccess(
            final @Nullable AccessibleObject member,
            final @Nullable Object obj) {
        return member != null
                && member.canAccess(obj);
    }

    /**
     * Returns whether a {@link Member} is public and not synthetic (can be accessed).
     * @param member Member to check
     */
    public boolean isPublicNonSynthetic(final @Nullable Member member) {
        return member != null
                && Modifier.isPublic(member.getModifiers())
                && !member.isSynthetic();
    }

    /**
     * Whether member name equals given {@code memberName}
     * @param memberName
     */
    public <T extends Member> Predicate<T> withName(final @NonNull String memberName) {
        return m -> m != null && memberName.equals(m.getName());
    }

    /**
     * Whether member name starts with given {@code prefix}
     * @param prefix
     */
    public <T extends Member> Predicate<T> withPrefix(final @NonNull String prefix) {
        return m -> m != null && m.getName().startsWith(prefix);
    }

    /**
     * Whether method parameters count equal to given {@code count}
     * @param count
     */
    public Predicate<Method> withMethodParametersCount(final int count) {
        return (final Method m) -> m != null && m.getParameterTypes().length == count;
    }

    /**
     * Whether field type is assignable to given {@code type}
     * @param type
     */
    public <T> Predicate<Field> withTypeAssignableTo(final @NonNull Class<T> type) {
        return (final Field f) -> f != null && type.isAssignableFrom(f.getType());
    }

    // -- FIELDS

    /**
     * Stream fields of given {@code type}
     * @param type (nullable)
     * @param ignoreAccess - whether to include non-public members
     */
    public Stream<Field> streamFields(
            final @Nullable Class<?> type,
            final boolean ignoreAccess) {

        if(type==null) {
            return Stream.empty();
        }
        if(ignoreAccess) {
            return _NullSafe.stream(type.getDeclaredFields());
        }
        return _NullSafe.stream(type.getFields());
    }

    /**
     * Stream all fields of given {@code type}, up the super class hierarchy.
     * @param type (nullable)
     * @param ignoreAccess - whether to include non-public members
     */
    public Stream<Field> streamAllFields(
            final @Nullable Class<?> type,
            final boolean ignoreAccess) {

        return streamTypeHierarchy(type, InterfacePolicy.EXCLUDE) // interfaces don't have fields
                .filter(t->!Object.class.equals(t)) // do not process Object class.
                .flatMap(t->streamFields(t, ignoreAccess));
    }

    // -- METHODS
    /**
     * Stream methods of given {@code type}.
     * @param type (nullable)
     * @param ignoreAccess - whether to include non-public members
     * @return non-null
     */
    public Stream<Method> streamMethods(
            final @Nullable Class<?> type,
            final boolean ignoreAccess) {

        if(type==null) {
            return Stream.empty();
        }
        if(ignoreAccess) {
            //FIXME does not include public inherited - thats either intended or not by the caller!
            return _NullSafe.stream(type.getDeclaredMethods());
        }
        return _NullSafe.stream(type.getMethods());
    }

    public Stream<Method> streamInheritedMethods(final Method method) {
        return streamAllMethods(method.getDeclaringClass(), true)
                .filter(candidateMethod->methodsSame(candidateMethod, method));
    }

    /**
     * Stream all methods of given {@code type}, up the super class hierarchy.
     * @param type (nullable)
     * @param ignoreAccess - whether to include non-public members
     * @return non-null
     */
    public Stream<Method> streamAllMethods(
            final @Nullable Class<?> type,
            final boolean ignoreAccess) {

        return streamTypeHierarchy(type, InterfacePolicy.INCLUDE)
                .filter(t->!t.equals(Object.class)) // do not process Object class.
                .flatMap(t->streamMethods(t, ignoreAccess));
    }

    // -- SUPER CLASSES

    public enum TypeHierarchyPolicy {
        EXCLUDE,
        INCLUDE;
        public boolean isIncludeTypeHierarchy() {
            return this == TypeHierarchyPolicy.INCLUDE;
        }
    }

    public enum InterfacePolicy {
        EXCLUDE,
        INCLUDE;
        public boolean isIncludeInterfaces() {
            return this == InterfacePolicy.INCLUDE;
        }
    }

    /**
     * Stream all types of given {@code type}, up the super class hierarchy starting with self
     * @param type (nullable)
     * @param interfacePolicy - whether to include all interfaces implemented by given {@code type} at the end
     * @return non-null
     */
    public Stream<Class<?>> streamTypeHierarchy(
            final @Nullable Class<?> type,
            final @NonNull InterfacePolicy interfacePolicy) {

        return interfacePolicy.isIncludeInterfaces()
                ? Stream.concat(
                        Stream.<Class<?>>iterate(type, Objects::nonNull, Class::getSuperclass),
                        ClassUtils.getAllInterfacesForClassAsSet(type).stream())
                : Stream.iterate(type, Objects::nonNull, Class::getSuperclass);

    }

    // -- ANNOTATIONS

    /**
     * Searches for annotation on provided class or any of its super-classes up the type hierarchy
     * or any implemented interfaces.
     * @param cls
     * @param annotationClass
     * @return the first matching annotation, or {@code null} if not found
     * @throws NullPointerException - if annotationClass is {@code null}
     */
    public <T extends Annotation> T getAnnotation(
            final Class<?> cls,
            final Class<T> annotationClass) {

        if (cls == null) {
            return null;
        }
        final T annotation = cls.getAnnotation(annotationClass);
        if (annotation != null) {
            return annotation;
        }

        // search super-classes
        final Class<?> superclass = cls.getSuperclass();
        if (superclass != null) {
            try {
                final T annotationFromSuperclass = getAnnotation(superclass, annotationClass);
                if (annotationFromSuperclass != null) {
                    return annotationFromSuperclass;
                }
            } catch (final SecurityException e) {
                // fall through
            }
        }

        // search implemented interfaces
        final Class<?>[] interfaces = cls.getInterfaces();
        for (final Class<?> iface : interfaces) {
            final T annotationFromInterface = getAnnotation(iface, annotationClass);
            if (annotationFromInterface != null) {
                return annotationFromInterface;
            }
        }
        return null;
    }

    /**
     * <p>Gets the annotation object with the given annotation type that is present on the given method
     * or optionally on any equivalent method in super classes and interfaces. Returns null if the annotation
     * type was not present.</p>
     *
     * <p>Stops searching for an annotation once the first annotation of the specified type has been
     * found. Additional annotations of the specified type will be silently ignored.</p>
     * @param <A>
     *            the annotation type
     * @param method
     *            the {@link Method} to query
     * @param annotationCls
     *            the {@link Annotation} to check if is present on the method
     * @param searchSupers
     *            determines if a lookup in the entire inheritance hierarchy of the given class is performed
     *            if the annotation was not directly present
     * @param ignoreAccess
     *            determines if underlying method has to be accessible
     * @return the first matching annotation, or {@code null} if not found
     * @throws NullPointerException
     *            if the method or annotation are {@code null}
     */
    public <A extends Annotation> A getAnnotation(
            final @NonNull Method method,
            final @NonNull Class<A> annotationCls,
            final boolean searchSupers,
            final boolean ignoreAccess) {

        if (!ignoreAccess && !isPublicNonSynthetic(method)) {
            return null;
        }

        if(searchSupers) {
            return AnnotationUtils.findAnnotation(method, annotationCls);
        } else {
            return AnnotationUtils.getAnnotation(method, annotationCls);
        }

//
//
//        final Stream<Method> methods;
//
//        if(searchSupers) {
//            methods = streamAllMethods(method.getDeclaringClass(), ignoreAccess);
//        } else {
//            methods = streamMethods(method.getDeclaringClass(), ignoreAccess);
//        }
//
//        return methods
//                .filter(m->same(method, m))
//                .map(m->m.getAnnotation(annotationCls))
//                .filter(_NullSafe::isPresent)
//                .findFirst()
//                .orElse(null);

    }

    /**
     * Whether given {@code cls} is annotated with any {@link Annotation} of given {@code annotationName}.
     * @param cls
     * @param annotationName - fully qualified class name of the {@link Annotation} to match against
     * @return false - if any of the arguments is null
     */
    public boolean containsAnnotation(final @Nullable Class<?> cls, final @Nullable String annotationName) {
        if(cls==null || _Strings.isEmpty(annotationName)) {
            return false;
        }
        for(Annotation annot : cls.getAnnotations()) {
            if(annot.annotationType().getName().equals(annotationName)) {
                return true;
            }
        }
        return false;
    }

    // -- FIND GETTER

    @SneakyThrows
    public Stream<PropertyDescriptor> streamGetters(final @NonNull Class<?> cls) {
        return Stream.of(
                Introspector.getBeanInfo(cls, Object.class)
                    .getPropertyDescriptors())
                .filter(pd->pd.getReadMethod()!=null);
    }

    public Map<String, Method> getGettersByName(final @NonNull Class<?> cls) {
        return streamGetters(cls)
                .collect(Collectors.toMap(PropertyDescriptor::getName, PropertyDescriptor::getReadMethod));
    }

    public Method getGetter(final Class<?> cls, final String propertyName) throws IntrospectionException {
        final BeanInfo beanInfo = Introspector.getBeanInfo(cls);
        for(PropertyDescriptor pd:beanInfo.getPropertyDescriptors()){
            if(!pd.getName().equals(propertyName))
                continue;
            return pd.getReadMethod();
        }
        return null;
    }

    @SneakyThrows
    public Object readFromGetterOn(
            final @NonNull Method getter,
            final @NonNull Object target) {
        return getter.invoke(target);
    }

    // -- FIND SETTER

    public Method getSetter(final Class<?> cls, final String propertyName) throws IntrospectionException {
        final BeanInfo beanInfo = Introspector.getBeanInfo(cls);
        for(PropertyDescriptor pd:beanInfo.getPropertyDescriptors()){
            if(!pd.getName().equals(propertyName))
                continue;
            return pd.getWriteMethod();
        }
        return null;
    }

    @SneakyThrows
    public void writeToSetterOn(
            final @NonNull Method setter,
            final @NonNull Object target,
            final @NonNull Object value) {
        setter.invoke(target, value);
    }

    // -- MODIFIERS

    public Object getFieldOn(
            final @NonNull Field field,
            final @NonNull Object target) throws IllegalArgumentException, IllegalAccessException {

        /*sonar-ignore-on*/
        if(canAccess(field, target)) {
            return field.get(target);
        }
        try {
            field.setAccessible(true);
            return field.get(target);
        } finally {
            field.setAccessible(false);
        }
        /*sonar-ignore-off*/
    }

    public void setFieldOn(
            final @NonNull Field field,
            final @NonNull Object target,
            final Object fieldValue) throws IllegalArgumentException, IllegalAccessException {

        /*sonar-ignore-on*/
        if(canAccess(field, target)) {
            field.set(target, fieldValue);
            return;
        }
        try {
            field.setAccessible(true);
            field.set(target, fieldValue);
        } finally {
            field.setAccessible(false);
        }
        /*sonar-ignore-off*/
    }

    public Try<Object> invokeMethodOn(
            final @NonNull Method method,
            final @NonNull Object target,
            final Object... args) {

        /*sonar-ignore-on*/
        return Try.call(()->{
            if(canAccess(method, target)) {
                return method.invoke(target, args);
            }
            try {
                method.setAccessible(true);
                return method.invoke(target, args);
            } finally {
                method.setAccessible(false);
            }
        });
        /*sonar-ignore-off*/
    }

    public <T> Try<T> invokeConstructor(
            final @NonNull Constructor<T> constructor,
            final Object... args) {

        /*sonar-ignore-on*/
        return Try.call(()->{
            if(canAccess(constructor, null)) {
                return constructor.newInstance(args);
            }
            try {
                constructor.setAccessible(true);
                return constructor.newInstance(args);
            } finally {
                constructor.setAccessible(false);
            }
        });
        /*sonar-ignore-off*/
    }

    // -- COMMON CONSTRUCTOR IDIOMS

    public Can<Constructor<?>> getDeclaredConstructors(final Class<?> cls) {
        return Can.ofArray(cls.getDeclaredConstructors());
    }

    public Can<Constructor<?>> getPublicConstructors(final Class<?> cls) {
        return Can.ofArray(cls.getConstructors());
    }

    // -- PREDICATES

    public boolean methodSignatureMatch(final Class<?>[] parameterTypes, final Class<?>[] matchingParamTypes) {
        final int aSize = _NullSafe.size(parameterTypes);
        final int bSize = _NullSafe.size(matchingParamTypes);
        if(aSize == 0 && bSize == 0) return true;
        if(aSize != bSize) return false;
        for (int c = 0; c < aSize; c++) {
            if(!Objects.equals(parameterTypes[c], matchingParamTypes[c])) return false;
        }
        return true;
    }
    public boolean methodSignatureAssignableTo(final Class<?>[] parameterTypes, final Class<?>[] requiredParamTypes) {
        final int aSize = _NullSafe.size(parameterTypes);
        final int bSize = _NullSafe.size(requiredParamTypes);
        if(aSize == 0 && bSize == 0) return true;
        if(aSize != bSize) return false;
        for (int c = 0; c < aSize; c++) {
            if(!requiredParamTypes[c].isAssignableFrom(parameterTypes[c])) return false;
        }
        return true;
    }
    public boolean methodSignatureWeaklyMatch(final Class<?>[] parameterTypes, final Class<?>[] otherParamTypes) {
        final int aSize = _NullSafe.size(parameterTypes);
        final int bSize = _NullSafe.size(otherParamTypes);
        if(aSize == 0 && bSize == 0) return true;
        if(aSize != bSize) return false;
        return _Arrays.testAllMatch(parameterTypes, otherParamTypes, (p1, p2)->p1.isAssignableFrom(p2))
                || _Arrays.testAllMatch(parameterTypes, otherParamTypes, (p1, p2)->p2.isAssignableFrom(p1));
    }

    @UtilityClass
    public class predicates {

        public Predicate<Executable> isPublic() {
            return ex->Modifier.isPublic(ex.getModifiers());
        }

        public Predicate<Executable> paramCount(final int paramCount) {
            return ex->ex.getParameterCount() == paramCount;
        }

        public Predicate<Executable> paramAssignableFrom(final int paramIndex, final Class<?> paramType) {
            return ex->ex.getParameterTypes()[paramIndex].isAssignableFrom(paramType);
        }

//        public Predicate<Class<?>[]> methodSignatureMatch(final Class<?>[] matchingParamTypes) {
//            return parameterTypes->_Reflect.methodSignatureMatch(parameterTypes, matchingParamTypes);
//        }
//        public Predicate<Class<?>[]> methodSignatureAssignableTo(final Class<?>[] requiredParamTypes) {
//            return parameterTypes->_Reflect.methodSignatureAssignableTo(parameterTypes, requiredParamTypes);
//        }
//        public Predicate<Class<?>[]> methodSignatureWeaklyMatch(final Class<?>[] otherParamTypes) {
//            return parameterTypes->_Reflect.methodSignatureWeaklyMatch(parameterTypes, otherParamTypes);
//        }

        public Predicate<Executable> paramAssignableFromValue(final int paramIndex, final @Nullable Object value) {
            if(value==null) {
                return _Predicates.alwaysTrue();
            }
            return ex->ex.getParameterTypes()[paramIndex].isAssignableFrom(value.getClass());
        }

    }

    public Method guardAgainstSynthetic(final @NonNull Method method) {
        _Assert.assertFalse(method.isSynthetic(), ()->
            String.format("unsupported synthetic method %s", method));
        return method;
    }

    // -- SPECIAL PREDICATES

    public boolean isNonFinalObjectMethod(final @NonNull Method method) {
        for(var m : _Constants.nonFinalObjectMethods) {
            if(methodsSame(m, method)) return true;
        }
        return false;
    }

    /**
     * Whether given {@link Method} overrides {@link Object#toString()}
     * and its declaring {@link Class} is a non Java API class.
     * @see #isJavaApiClass(Class)
     */
    public boolean isOverwrittenToString(final @NonNull Method method) {
        return method.getName().equals("toString")
                && method.getParameterCount() == 0
                && !isJavaApiClass(method.getDeclaringClass());
    }

    public boolean isJavaApiClass(final @NonNull Class<?> cls) {
        var className = cls.getName();
        return className.startsWith("java.")
                || className.startsWith("sun.");
    }

    //XXX no longer needed
//    /**
//     * Lookup regular method for a synthetic one in the method's declaring class type-hierarchy.
//     */
//    public Optional<Method> lookupRegularMethodForSynthetic(final @NonNull Method syntheticMethod) {
//
//        if(!syntheticMethod.isSynthetic()) {
//            return Optional.of(syntheticMethod);
//        }
//
//        return streamTypeHierarchy(syntheticMethod.getDeclaringClass(), InterfacePolicy.INCLUDE)
//        .flatMap(type->_NullSafe.stream(type.getDeclaredMethods()))
//        .filter(methodMatcherOnNameAndSignature(syntheticMethod))
//        .filter(method->!method.isSynthetic())
//        .findFirst();
//    }
//
//    private Predicate<Method> methodMatcherOnNameAndSignature(final @NonNull Method ref) {
//        var refSignature = ref.getParameterTypes();
//        return other->
//            (!ref.getName().equals(other.getName()))
//            ? false
//            : Arrays.equals(refSignature, other.getParameterTypes());
//    }

    public String methodToShortString(final @NonNull Method method) {
        return method.getName() + "(" +
            Stream.of(method.getParameterTypes())
            .map(parameterType->parameterType.getTypeName())
            .collect(Collectors.joining(", "))
        + ")";
    }

    /**
     * Debugging utility.
     */
    public String methodSummary(final @Nullable Method method) {
        return methodSummary(method,
                cls->cls.getSimpleName().substring(0, 3));
    }

    /**
     * Debugging utility.
     */
    public String methodSummary(
            final @Nullable Method method, final Function<Class<?>, String> typeToShortName) {
        return Optional.ofNullable(method)
        .map(Method::getName)
        .map(name->String.format("%s(%s)%s%s%s%s",
                name,
                _NullSafe.stream(method.getParameterTypes())
                    .map(typeToShortName)
                    .collect(Collectors.joining(",")),
                _Reflect.hasGenericParam(method) ? "p" : "",
                _Reflect.hasGenericReturn(method) ? "r" : "",
                method.isSynthetic() ? "s" : "",
                method.isBridge() ? "b" : ""))
        .orElse("-");
    }

    /**
     * Determine if the supplied method is declared within a non-static <em>inner class</em>.
     */
    public boolean isNonStaticInnerMethod(final @NonNull Method method) {
        return ClassUtils.isInnerClass(method.getDeclaringClass());
    }

    /**
     * Whether has any generic bounds on any return type or parameter type(s).
     */
    public boolean hasGenericBounds(final @NonNull Method method) {
        return hasGenericReturn(method)
                || hasGenericParam(method);
    }

    /**
     * Whether has any generic bounds on the method return type.
     */
    public boolean hasGenericReturn(final @NonNull Method method) {
        return method.getGenericReturnType() instanceof TypeVariable<?>;
    }

    /**
     * Whether has any generic bounds any parameter type(s).
     */
    public boolean hasGenericParam(final @NonNull Method method) {
        final Type[] genericParameterTypes = method.getGenericParameterTypes();
        for(int i=0; i<method.getParameterCount(); ++i) {
            if(genericParameterTypes[i] instanceof TypeVariable<?>) {
                return true;
            }
        }
        return false;
    }

    public Class<?> mostSpecificType(final Class<?> a, final Class<?> b) {
        if(a.equals(b)) return b; // an arbitrary pick
        _Assert.assertTrue(
                _Reflect.shareSameTypeHierarchy(a, b),
                ()->String.format("declared types %s and %s don't share the same type hierarchy", a, b));
        return a.isAssignableFrom(b)
                ? b
                : a;
    }

    // -- HELPER - FIELD FOR GETTER

    Optional<Field> fieldForGetter(final Method getterCandidate) {
        return Optional.ofNullable(findFieldForGetter(getterCandidate));
    }

    private Field findFieldForGetter(final Method getterCandidate) {
        if(ReflectionUtils.isObjectMethod(getterCandidate)) {
            return null;
        }
        var fieldNameCandidate = fieldNameForGetter(getterCandidate);
        if(fieldNameCandidate==null) {
            return null;
        }
        var declaringClass = getterCandidate.getDeclaringClass();
        return ReflectionUtils.findField(declaringClass, fieldNameCandidate);
    }

    private static String fieldNameForGetter(final Method getter) {
        if(getter.getParameterCount()>0) {
            return null;
        }
        if(getter.getReturnType()==void.class) {
            return null;
        }
        var methodName = getter.getName();
        String fieldName = null;
        if(methodName.startsWith("is") &&  methodName.length() > 2) {
            fieldName = methodName.substring(2);
        } else if(methodName.startsWith("get") &&  methodName.length() > 3) {
            fieldName = methodName.substring(3);
        } else {
            return null;
        }
        return _Strings.decapitalize(fieldName);
    }

}
