/*
 * 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.di.impl;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import org.apache.maven.api.annotations.Nullable;
import org.apache.maven.api.di.Inject;
import org.apache.maven.api.di.Named;
import org.apache.maven.api.di.Priority;
import org.apache.maven.api.di.Qualifier;
import org.apache.maven.di.Key;

import static java.util.stream.Collectors.toList;

public final class ReflectionUtils {
    private static final String IDENT = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*";

    private static final Pattern PACKAGE = Pattern.compile("(?:" + IDENT + "\\.)*");
    private static final Pattern PACKAGE_AND_PARENT = Pattern.compile(PACKAGE.pattern() + "(?:" + IDENT + "\\$\\d*)?");
    private static final Pattern ARRAY_SIGNATURE = Pattern.compile("\\[L(.*?);");

    public static @Nullable Object getOuterClassInstance(Object innerClassInstance) {
        if (innerClassInstance == null) {
            return null;
        }
        Class<?> cls = innerClassInstance.getClass();
        Class<?> enclosingClass = cls.getEnclosingClass();
        if (enclosingClass == null) {
            return null;
        }
        for (Field field : cls.getDeclaredFields()) {
            if (!field.isSynthetic() || !field.getName().startsWith("this$") || field.getType() != enclosingClass) {
                continue;
            }
            field.setAccessible(true);
            try {
                return field.get(innerClassInstance);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
        return null;
    }

    public static @Nullable Object qualifierOf(AnnotatedElement annotatedElement) {
        Object qualifier = null;
        for (Annotation annotation : annotatedElement.getDeclaredAnnotations()) {
            if (annotation.annotationType().isAnnotationPresent(Qualifier.class)) {
                if (qualifier != null) {
                    throw new DIException("More than one qualifier annotation on " + annotatedElement);
                }
                if (annotation instanceof Named named) {
                    qualifier = named.value();
                } else {
                    Class<? extends Annotation> annotationType = annotation.annotationType();
                    qualifier = annotationType.getDeclaredMethods().length == 0 ? annotationType : annotation;
                }
            }
        }
        return qualifier;
    }

    public static @Nullable Annotation scopeOf(AnnotatedElement annotatedElement) {
        Annotation scope = null;
        for (Annotation annotation : annotatedElement.getDeclaredAnnotations()) {
            if (annotation.annotationType().isAnnotationPresent(org.apache.maven.api.di.Scope.class)) {
                if (scope != null) {
                    throw new DIException("More than one scope annotation on " + annotatedElement);
                }
                scope = annotation;
            }
        }
        return scope;
    }

    public static <T> Key<T> keyOf(@Nullable Type container, Type type, AnnotatedElement annotatedElement) {
        return Key.ofType(
                container != null ? Types.bind(type, Types.getAllTypeBindings(container)) : type,
                qualifierOf(annotatedElement));
    }

    public static <T extends AnnotatedElement & Member> List<T> getAnnotatedElements(
            Class<?> cls,
            Class<? extends Annotation> annotationType,
            Function<Class<?>, T[]> extractor,
            boolean allowStatic) {
        List<T> result = new ArrayList<>();
        while (cls != null) {
            for (T element : extractor.apply(cls)) {
                if (element.isAnnotationPresent(annotationType)) {
                    if (!allowStatic && Modifier.isStatic(element.getModifiers())) {
                        throw new DIException(
                                "@" + annotationType.getSimpleName() + " annotation is not allowed on " + element);
                    }
                    result.add(element);
                }
            }
            cls = cls.getSuperclass();
        }
        return result;
    }

    public static <T> @Nullable Binding<T> generateImplicitBinding(Key<T> key) {
        Binding<T> binding = generateConstructorBinding(key);
        if (binding != null) {
            Annotation scope = scopeOf(key.getRawType());
            if (scope != null) {
                binding = binding.scope(scope);
            }
            binding = binding.initializeWith(generateInjectingInitializer(key));
        }
        return binding;
    }

    @SuppressWarnings("unchecked")
    public static <T> @Nullable Binding<T> generateConstructorBinding(Key<T> key) {
        Class<?> cls = key.getRawType();

        List<Constructor<?>> constructors = Arrays.asList(cls.getDeclaredConstructors());
        List<Constructor<?>> injectConstructors = constructors.stream()
                .filter(c -> c.isAnnotationPresent(Inject.class))
                .toList();

        List<Method> factoryMethods = Arrays.stream(cls.getDeclaredMethods())
                .filter(method -> method.getReturnType() == cls && Modifier.isStatic(method.getModifiers()))
                .toList();
        List<Method> injectFactoryMethods = factoryMethods.stream()
                .filter(method -> method.isAnnotationPresent(Inject.class))
                .toList();

        if (!injectConstructors.isEmpty()) {
            if (injectConstructors.size() > 1) {
                throw failedImplicitBinding(key, "more than one inject constructor");
            }
            if (!injectFactoryMethods.isEmpty()) {
                throw failedImplicitBinding(key, "both inject constructor and inject factory method are present");
            }
            return bindingFromConstructor(
                    key, (Constructor<T>) injectConstructors.iterator().next());
        }

        if (!injectFactoryMethods.isEmpty()) {
            if (injectFactoryMethods.size() > 1) {
                throw failedImplicitBinding(key, "more than one inject factory method");
            }
            return bindingFromMethod(injectFactoryMethods.iterator().next());
        }

        if (constructors.isEmpty()) {
            throw failedImplicitBinding(key, "inject annotation on interface");
        }
        if (constructors.size() > 1) {
            throw failedImplicitBinding(key, "inject annotation on class with multiple constructors");
        }
        Constructor<T> declaredConstructor =
                (Constructor<T>) constructors.iterator().next();

        Class<?> enclosingClass = cls.getEnclosingClass();
        if (enclosingClass != null
                && !Modifier.isStatic(cls.getModifiers())
                && declaredConstructor.getParameterCount() != 1) {
            throw failedImplicitBinding(
                    key,
                    "inject annotation on local class that closes over outside variables and/or has no default constructor");
        }
        return bindingFromConstructor(key, declaredConstructor);
    }

    private static DIException failedImplicitBinding(Key<?> requestedKey, String message) {
        return new DIException(
                "Failed to generate implicit binding for " + requestedKey.getDisplayString() + ", " + message);
    }

    public static <T> BindingInitializer<T> generateInjectingInitializer(Key<T> container) {
        Class<T> rawType = container.getRawType();
        List<BindingInitializer<T>> initializers = Stream.concat(
                        getAnnotatedElements(rawType, Inject.class, Class::getDeclaredFields, false).stream()
                                .map(field -> fieldInjector(container, field)),
                        getAnnotatedElements(rawType, Inject.class, Class::getDeclaredMethods, true).stream()
                                .filter(method -> !Modifier.isStatic(
                                        method.getModifiers())) // we allow them and just filter out to allow
                                // static factory methods
                                .map(method -> methodInjector(container, method)))
                .collect(toList());
        return BindingInitializer.combine(initializers);
    }

    public static <T> BindingInitializer<T> fieldInjector(Key<T> container, Field field) {
        field.setAccessible(true);
        Key<Object> key = keyOf(container.getType(), field.getGenericType(), field);
        boolean optional = field.isAnnotationPresent(Nullable.class);
        Dependency<Object> dep = new Dependency<>(key, optional);
        return new BindingInitializer<T>(Collections.singleton(dep)) {
            @Override
            public Consumer<T> compile(Function<Dependency<?>, Supplier<?>> compiler) {
                Supplier<?> binding = compiler.apply(dep);
                return (T instance) -> {
                    Object arg = binding.get();
                    try {
                        field.set(instance, arg);
                    } catch (IllegalAccessException e) {
                        throw new DIException("Not allowed to set injectable field " + field, e);
                    }
                };
            }
        };
    }

    public static <T> BindingInitializer<T> methodInjector(Key<T> container, Method method) {
        method.setAccessible(true);
        Dependency<?>[] dependencies = toDependencies(container.getType(), method);
        return new BindingInitializer<T>(new HashSet<>(Arrays.asList(dependencies))) {
            @Override
            public Consumer<T> compile(Function<Dependency<?>, Supplier<?>> compiler) {
                return instance -> {
                    Object[] args = getDependencies().stream()
                            .map(compiler)
                            .map(Supplier::get)
                            .toArray();
                    try {
                        method.invoke(instance, args);
                    } catch (IllegalAccessException e) {
                        throw new DIException("Not allowed to call injectable method " + method, e);
                    } catch (InvocationTargetException e) {
                        throw new DIException("Failed to call injectable method " + method, e.getCause());
                    }
                };
            }
        };
    }

    public static Dependency<?>[] toDependencies(@Nullable Type container, Executable executable) {
        Dependency<?>[] keys = toArgDependencies(container, executable);
        if (executable instanceof Constructor || Modifier.isStatic(executable.getModifiers())) {
            return keys;
        } else {
            Dependency<?>[] nkeys = new Dependency[keys.length + 1];
            nkeys[0] = new Dependency<>(Key.ofType(container), false);
            System.arraycopy(keys, 0, nkeys, 1, keys.length);
            return nkeys;
        }
    }

    private static Dependency<?>[] toArgDependencies(@Nullable Type container, Executable executable) {
        Parameter[] parameters = executable.getParameters();
        Dependency<?>[] dependencies = new Dependency<?>[parameters.length];
        if (parameters.length == 0) {
            return dependencies;
        }

        Type[] genericParameterTypes = executable.getGenericParameterTypes();
        for (int i = 0; i < dependencies.length; i++) {
            Type type = genericParameterTypes[i];
            Parameter parameter = parameters[i];
            boolean optional = parameter.isAnnotationPresent(Nullable.class);
            dependencies[i] = new Dependency<>(keyOf(container, type, parameter), optional);
        }
        return dependencies;
    }

    @SuppressWarnings("unchecked")
    public static <T> Binding<T> bindingFromMethod(Method method) {
        method.setAccessible(true);
        Binding<T> binding = Binding.to(
                Key.ofType(method.getGenericReturnType(), ReflectionUtils.qualifierOf(method)),
                args -> {
                    try {
                        Object instance;
                        Object[] params;
                        if (Modifier.isStatic(method.getModifiers())) {
                            instance = null;
                            params = args;
                        } else {
                            instance = args[0];
                            params = Arrays.copyOfRange(args, 1, args.length);
                        }
                        T result = (T) method.invoke(instance, params);
                        if (result == null) {
                            throw new NullPointerException(
                                    "@Provides method must return non-null result, method " + method);
                        }
                        return result;
                    } catch (IllegalAccessException e) {
                        throw new DIException("Not allowed to call method " + method, e);
                    } catch (InvocationTargetException e) {
                        throw new DIException("Failed to call method " + method, e.getCause());
                    }
                },
                toDependencies(method.getDeclaringClass(), method));

        Priority priority = method.getAnnotation(Priority.class);
        if (priority != null) {
            binding = binding.prioritize(priority.value());
        }

        return binding;
    }

    public static <T> Binding<T> bindingFromConstructor(Key<T> key, Constructor<T> constructor) {
        constructor.setAccessible(true);

        Dependency<?>[] dependencies = toDependencies(key.getType(), constructor);

        Binding<T> binding = Binding.to(
                key,
                args -> {
                    try {
                        return constructor.newInstance(args);
                    } catch (InstantiationException e) {
                        throw new DIException(
                                "Cannot instantiate object from the constructor " + constructor
                                        + " to provide requested key " + key,
                                e);
                    } catch (IllegalAccessException e) {
                        throw new DIException(
                                "Not allowed to call constructor " + constructor + " to provide requested key " + key,
                                e);
                    } catch (InvocationTargetException e) {
                        throw new DIException(
                                "Failed to call constructor " + constructor + " to provide requested key " + key,
                                e.getCause());
                    }
                },
                dependencies);

        Priority priority = constructor.getDeclaringClass().getAnnotation(Priority.class);
        if (priority != null) {
            binding = binding.prioritize(priority.value());
        }

        return binding.withKey(key);
    }

    public static void getDisplayString(StringBuilder sb, Object object) {
        if (object instanceof Class<?> clazz && clazz.isAnnotation()) {
            //noinspection unchecked
            getDisplayString(sb, (Class<? extends Annotation>) object, null);
        } else if (object instanceof Annotation annotation) {
            getDisplayString(sb, annotation.annotationType(), annotation);
        } else {
            sb.append(object.toString());
        }
    }

    public static String getDisplayName(Type type) {
        Class<?> raw = Types.getRawType(type);
        String typeName;
        if (raw.isAnonymousClass()) {
            Type superclass = raw.getGenericSuperclass();
            typeName = "? extends " + superclass.getTypeName();
        } else {
            typeName = type.getTypeName();
        }

        return PACKAGE_AND_PARENT
                .matcher(ARRAY_SIGNATURE.matcher(typeName).replaceAll("$1[]"))
                .replaceAll("");
    }

    private static void getDisplayString(
            StringBuilder sb, Class<? extends Annotation> annotationType, @Nullable Annotation annotation) {
        if (annotation == null) {
            sb.append("@").append(ReflectionUtils.getDisplayName(annotationType));
        } else {
            String typeName = annotationType.getName();
            String str = annotation.toString();
            if (str.startsWith("@" + typeName)) {
                sb.append("@").append(getDisplayName(annotationType)).append(str.substring(typeName.length() + 1));
            } else {
                sb.append(str);
            }
        }
    }
}
