/**
 * Copyright (C) 2006 Google Inc.
 * <p>
 * Licensed 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
 * </p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.struts2.inject;

import org.apache.struts2.inject.util.ReferenceCache;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
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.ReflectPermission;
import java.security.AccessControlException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 * Default {@link Container} implementation.
 *
 * @author crazybob@google.com (Bob Lee)
 * @see ContainerBuilder
 */
class ContainerImpl implements Container {

    final Map<Key<?>, InternalFactory<?>> factories;
    final Map<Class<?>, Set<String>> factoryNamesByType;

    ContainerImpl(Map<Key<?>, InternalFactory<?>> factories) {
        this.factories = factories;
        final Map<Class<?>, Set<String>> map = new HashMap<>();
        for (Key<?> key : factories.keySet()) {
            Set<String> names = map.computeIfAbsent(key.getType(), k -> new HashSet<>());
            names.add(key.getName());
        }

        for (Entry<Class<?>, Set<String>> entry : map.entrySet()) {
            entry.setValue(Collections.unmodifiableSet(entry.getValue()));
        }

        this.factoryNamesByType = Collections.unmodifiableMap(map);
    }

    @SuppressWarnings("unchecked")
    <T> InternalFactory<? extends T> getFactory(Key<T> key) {
        return (InternalFactory<T>) factories.get(key);
    }

    /**
     * Field and method injectors.
     */
    final Map<Class<?>, List<Injector>> injectors =
            new ReferenceCache<>() {
                @Override
                protected List<Injector> create(Class<?> key) {
                    List<Injector> injectors = new ArrayList<>();
                    addInjectors(key, injectors);
                    return injectors;
                }
            };

    /**
     * Recursively adds injectors for fields and methods from the given class to the given list. Injects parent classes
     * before sub classes.
     */
    void addInjectors(Class<?> clazz, List<Injector> injectors) {
        if (clazz == Object.class) {
            return;
        }

        // Add injectors for superclass first.
        addInjectors(clazz.getSuperclass(), injectors);

        // TODO (crazybob): Filter out overridden members.
        addInjectorsForFields(clazz.getDeclaredFields(), false, injectors);
        addInjectorsForMethods(clazz.getDeclaredMethods(), false, injectors);
    }

    void injectStatics(List<Class<?>> staticInjections) {
        final List<Injector> injectors = new ArrayList<>();

        for (Class<?> clazz : staticInjections) {
            addInjectorsForFields(clazz.getDeclaredFields(), true, injectors);
            addInjectorsForMethods(clazz.getDeclaredMethods(), true, injectors);
        }

        callInContext((ContextualCallable<Void>) context -> {
            for (Injector injector : injectors) {
                injector.inject(context, null);
            }
            return null;
        });
    }

    void addInjectorsForMethods(Method[] methods, boolean statics, List<Injector> injectors) {
        addInjectorsForMembers(List.of(methods), statics, injectors, MethodInjector::new);
    }

    void addInjectorsForFields(Field[] fields, boolean statics, List<Injector> injectors) {
        addInjectorsForMembers(Arrays.asList(fields), statics, injectors, FieldInjector::new);
    }

    <M extends Member & AnnotatedElement> void addInjectorsForMembers(
        List<M> members, boolean statics, List<Injector> injectors, InjectorFactory<M> injectorFactory) {
        for (M member : members) {
            if (isStatic(member) == statics) {
                Inject inject = member.getAnnotation(Inject.class);
                if (inject != null) {
                    try {
                        injectors.add(injectorFactory.create(this, member, inject.value()));
                    } catch (MissingDependencyException e) {
                        if (inject.required()) {
                            throw new DependencyException(e);
                        }
                    }
                }
            }
        }
    }

    interface InjectorFactory<M extends Member & AnnotatedElement> {

        Injector create(ContainerImpl container, M member, String name)
            throws MissingDependencyException;
    }

    /**
     * Determines if a given {@link Member} is static or not.
     *
     * @param member checked for the static modifier.
     * @return true if member is static, false otherwise.
     */
    private static boolean isStatic(Member member) {
        return Modifier.isStatic(member.getModifiers());
    }

    /**
     * Determines if a given {@link Member} is considered to be public for reflection usage or not.
     *
     * @param member checked to see if it is public for reflection usage.
     * @return true if member is public for reflection usage, false otherwise.
     */
    private static boolean isPublicForReflection(Member member) {
        return Modifier.isPublic(member.getModifiers()) &&
            Modifier.isPublic(member.getDeclaringClass().getModifiers());
    }

    static class FieldInjector implements Injector {

        final Field field;
        final InternalFactory<?> factory;
        final ExternalContext<?> externalContext;

        public FieldInjector(ContainerImpl container, Field field, String name)
            throws MissingDependencyException {
            this.field = field;
            if (!isPublicForReflection(field) && !field.isAccessible()) {
                SecurityManager sm = System.getSecurityManager();
                try {
                    if (sm != null) {
                        sm.checkPermission(new ReflectPermission("suppressAccessChecks"));
                    }
                    field.setAccessible(true);
                } catch (AccessControlException e) {
                    throw new DependencyException("Security manager in use, could not access field: "
                        + field.getDeclaringClass().getName() + "(" + field.getName() + ")", e);
                }
            }

            final Key<?> key = Key.newInstance(field.getType(), name);
            factory = container.getFactory(key);
            if (factory == null) {
                throw new MissingDependencyException("No mapping found for dependency " + key + " in " + field + ".");
            }

            this.externalContext = ExternalContext.newInstance(field, key, container);
        }

        @Override
        public void inject(InternalContext context, Object o) {
            final ExternalContext<?> previous = context.getExternalContext();
            context.setExternalContext(externalContext);
            try {
                field.set(o, factory.create(context));
            } catch (IllegalAccessException e) {
                throw new AssertionError(e);
            } finally {
                context.setExternalContext(previous);
            }
        }
    }

    /**
     * Gets parameter injectors.
     *
     * @param member         to which the parameters belong
     * @param annotations    on the parameters
     * @param parameterTypes parameter types
     * @return injections
     */
    <M extends AccessibleObject & Member> ParameterInjector<?>[] getParametersInjectors(
        M member,
        Annotation[][] annotations,
        Class<?>[] parameterTypes,
        String defaultName
    ) throws MissingDependencyException {
        final List<ParameterInjector<?>> parameterInjectors = new ArrayList<>();

        final Iterator<Annotation[]> annotationsIterator = Arrays.asList(annotations).iterator();
        for (Class<?> parameterType : parameterTypes) {
            Inject annotation = findInject(annotationsIterator.next());
            String name = annotation == null ? defaultName : annotation.value();
            Key<?> key = Key.newInstance(parameterType, name);
            parameterInjectors.add(createParameterInjector(key, member));
        }

        return toArray(parameterInjectors);
    }

    /**
     * Gets parameter injectors with nulls for optional dependencies.
     *
     * @param member         to which the parameters belong
     * @param annotations    on the parameters
     * @param parameterTypes parameter types
     * @return injections
     */
    <M extends AccessibleObject & Member> List<ParameterInjector<?>> getParametersInjectorsWithNulls(
        M member,
        Annotation[][] annotations,
        Class<?>[] parameterTypes,
        String defaultName
    ) throws MissingDependencyException {
        final List<ParameterInjector<?>> parameterInjectors = new ArrayList<>();

        final Iterator<Annotation[]> annotationsIterator = Arrays.asList(annotations).iterator();
        for (Class<?> parameterType : parameterTypes) {
            Inject annotation = findInject(annotationsIterator.next());
            String name = annotation == null ? defaultName : annotation.value();
            Key<?> key = Key.newInstance(parameterType, name);
            try {
                parameterInjectors.add(createParameterInjector(key, member));
            } catch (MissingDependencyException e) {
                if (annotation != null && annotation.required()) {
                    throw e;
                } else {
                    parameterInjectors.add(createNullParameterInjector(key, member));
                }
            }
        }

        return parameterInjectors;
    }

    <T> ParameterInjector<T> createParameterInjector(Key<T> key, Member member) throws MissingDependencyException {
        final InternalFactory<? extends T> factory = getFactory(key);
        if (factory == null) {
            throw new MissingDependencyException("No mapping found for dependency " + key + " in " + member + ".");
        }

        final ExternalContext<T> externalContext = ExternalContext.newInstance(member, key, this);
        return new ParameterInjector<>(externalContext, factory);
    }

    <T> ParameterInjector<T> createNullParameterInjector(Key<T> key, Member member) {
        final InternalFactory<? extends T> factory = new InternalFactory<>() {
            @Override
            public T create(InternalContext context) {
                return null;
            }

            @Override
            public Class<? extends T> type() {
                return key.getType();
            }
        };

        final ExternalContext<T> externalContext = ExternalContext.newInstance(member, key, this);
        return new ParameterInjector<>(externalContext, factory);
    }

    private ParameterInjector<?>[] toArray(List<ParameterInjector<?>> parameterInjections) {
        return parameterInjections.toArray(new ParameterInjector[0]);
    }

    /**
     * Finds the {@link Inject} annotation in an array of annotations.
     */
    Inject findInject(Annotation[] annotations) {
        for (Annotation annotation : annotations) {
            if (annotation.annotationType() == Inject.class) {
                return (Inject) annotation;
            }
        }
        return null;
    }

    static class MethodInjector implements Injector {

        final Method method;
        final ParameterInjector<?>[] parameterInjectors;

        public MethodInjector(ContainerImpl container, Method method, String name) throws MissingDependencyException {
            this.method = method;
            if (!isPublicForReflection(method) && !method.isAccessible()) {
                final SecurityManager sm = System.getSecurityManager();
                try {
                    if (sm != null) {
                        sm.checkPermission(new ReflectPermission("suppressAccessChecks"));
                    }
                    method.setAccessible(true);
                } catch (AccessControlException e) {
                    throw new DependencyException("Security manager in use, could not access method: "
                        + name + "(" + method.getName() + ")", e);
                }
            }

            final Class<?>[] parameterTypes = method.getParameterTypes();
            if (parameterTypes.length == 0) {
                throw new DependencyException(method + " has no parameters to inject.");
            }
            parameterInjectors = container.getParametersInjectors(
                method, method.getParameterAnnotations(), parameterTypes, name);
        }

        @Override
        public void inject(InternalContext context, Object o) {
            try {
                method.invoke(o, getParameters(method, context, parameterInjectors));
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    Map<Class<?>, ConstructorInjector<?>> constructors = new ReferenceCache<>() {
        @Override
        protected ConstructorInjector<?> create(Class<?> implementation) {
            return new ConstructorInjector<>(ContainerImpl.this, implementation);
        }
    };

    static class ConstructorInjector<T> {

        final Class<T> implementation;
        final List<Injector> injectors;
        final Constructor<T> constructor;
        final ParameterInjector<?>[] parameterInjectors;

        ConstructorInjector(ContainerImpl container, Class<T> implementation) {
            this.implementation = implementation;

            constructor = findConstructorIn(implementation);
            if (!isPublicForReflection(constructor) && !constructor.isAccessible()) {
                final SecurityManager sm = System.getSecurityManager();
                try {
                    if (sm != null) {
                        sm.checkPermission(new ReflectPermission("suppressAccessChecks"));
                    }
                    constructor.setAccessible(true);
                } catch (AccessControlException e) {
                    throw new DependencyException("Security manager in use, could not access constructor: "
                        + implementation.getName() + "(" + constructor.getName() + ")", e);
                }
            }

            MissingDependencyException exception = null;
            Inject inject = null;
            List<ParameterInjector<?>> parameters = null;

            try {
                inject = constructor.getAnnotation(Inject.class);
                parameters = constructParameterInjector(inject, container, constructor);
            } catch (MissingDependencyException e) {
                exception = e;
            }
            parameterInjectors = parameters != null ? container.toArray(parameters) : null;

            if (exception != null) {
                if (inject != null && inject.required()) {
                    throw new DependencyException(exception);
                }
            }
            injectors = container.injectors.get(implementation);
        }

        List<ParameterInjector<?>> constructParameterInjector(
            Inject inject, ContainerImpl container, Constructor<T> constructor) throws MissingDependencyException {
            return constructor.getParameterTypes().length == 0
                ? null // default constructor.
                : container.getParametersInjectorsWithNulls(
                constructor,
                constructor.getParameterAnnotations(),
                constructor.getParameterTypes(),
                inject.value()
            );
        }

        @SuppressWarnings("unchecked")
        private Constructor<T> findConstructorIn(Class<T> implementation) {
            Constructor<T> found = null;
            final Constructor<T>[] declaredConstructors = (Constructor<T>[]) implementation.getDeclaredConstructors();
            for (Constructor<T> constructor : declaredConstructors) {
                if (constructor.getAnnotation(Inject.class) != null) {
                    if (found != null) {
                        throw new DependencyException("More than one constructor annotated"
                            + " with @Inject found in " + implementation + ".");
                    }
                    found = constructor;
                }
            }
            if (found != null) {
                return found;
            }

            // If no annotated constructor is found, look for a no-arg constructor
            // instead.
            try {
                return implementation.getDeclaredConstructor();
            } catch (NoSuchMethodException e) {
                throw new DependencyException("Could not find a suitable constructor in " + implementation.getName() + ".");
            }
        }

        /**
         * Construct an instance. Returns {@code Object} instead of {@code T} because it may return a proxy.
         */
        Object construct(InternalContext context, Class<? super T> expectedType) {
            final ConstructionContext<T> constructionContext = context.getConstructionContext(this);

            // We have a circular reference between constructors. Return a proxy.
            if (constructionContext.isConstructing()) {
                // TODO (crazybob): if we can't proxy this object, can we proxy the
                // other object?
                return constructionContext.createProxy(expectedType);
            }

            // If we're re-entering this factory while injecting fields or methods,
            // return the same instance. This prevents infinite loops.
            T t = constructionContext.getCurrentReference();
            if (t != null) {
                return t;
            }

            try {
                // First time through...
                constructionContext.startConstruction();
                try {
                    final Object[] parameters = getParameters(constructor, context, parameterInjectors);
                    t = constructor.newInstance(parameters);
                    constructionContext.setProxyDelegates(t);
                } finally {
                    constructionContext.finishConstruction();
                }

                // Store reference. If an injector re-enters this factory, they'll
                // get the same reference.
                constructionContext.setCurrentReference(t);

                // Inject fields and methods.
                for (Injector injector : injectors) {
                    injector.inject(context, t);
                }

                return t;
            } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
                throw new RuntimeException(e);
            } finally {
                constructionContext.removeCurrentReference();
            }
        }
    }

    static class ParameterInjector<T> {

        final ExternalContext<T> externalContext;
        final InternalFactory<? extends T> factory;

        public ParameterInjector(ExternalContext<T> externalContext, InternalFactory<? extends T> factory) {
            this.externalContext = externalContext;
            this.factory = factory;
        }

        T inject(Member member, InternalContext context) {
            final ExternalContext<?> previous = context.getExternalContext();
            context.setExternalContext(externalContext);
            try {
                return factory.create(context);
            } finally {
                context.setExternalContext(previous);
            }
        }
    }

    private static Object[] getParameters(Member member, InternalContext context, ParameterInjector<?>[] parameterInjectors) {
        if (parameterInjectors == null) {
            return null;
        }

        final Object[] parameters = new Object[parameterInjectors.length];
        for (int i = 0; i < parameters.length; i++) {
            parameters[i] = parameterInjectors[i].inject(member, context);
        }
        return parameters;
    }

    void inject(Object o, InternalContext context) {
        final List<Injector> injectors = this.injectors.get(o.getClass());
        for (Injector injector : injectors) {
            injector.inject(context, o);
        }
    }

    <T> T inject(Class<T> implementation, InternalContext context) {
        try {
            final ConstructorInjector<T> constructor = getConstructor(implementation);
            return implementation.cast(constructor.construct(context, implementation));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    <T> T getInstance(Class<T> type, String name, InternalContext context) {
        final ExternalContext<?> previous = context.getExternalContext();
        final Key<T> key = Key.newInstance(type, name);
        context.setExternalContext(ExternalContext.newInstance(null, key, this));
        try {
            final InternalFactory<? extends T> o = getFactory(key);
            if (o != null) {
                return getFactory(key).create(context);
            } else {
                return null;
            }
        } finally {
            context.setExternalContext(previous);
        }
    }

    <T> T getInstance(Class<T> type, InternalContext context) {
        return getInstance(type, DEFAULT_NAME, context);
    }

    @Override
    public void inject(final Object o) {
        callInContext((ContextualCallable<Void>) context -> {
            inject(o, context);
            return null;
        });
    }

    @Override
    public <T> T inject(final Class<T> implementation) {
        return callInContext(context -> inject(implementation, context));
    }

    @Override
    public <T> T getInstance(final Class<T> type, final String name) {
        return callInContext(context -> getInstance(type, name, context));
    }

    @Override
    public <T> T getInstance(final Class<T> type) {
        return callInContext(context -> getInstance(type, context));
    }

    @Override
    public Set<String> getInstanceNames(final Class<?> type) {
        Set<String> names = factoryNamesByType.get(type);
        if (names == null) {
            names = Collections.emptySet();
        }
        return names;
    }

    ThreadLocal<Object[]> localContext = ThreadLocal.withInitial(() -> new Object[1]);

    /**
     * Looks up thread local context. Creates (and removes) a new context if necessary.
     */
    <T> T callInContext(ContextualCallable<T> callable) {
        final Object[] reference = localContext.get();
        if (reference[0] == null) {
            reference[0] = new InternalContext(this);
            try {
                return callable.call((InternalContext) reference[0]);
            } finally {
                // Only remove the context if this call created it.
                reference[0] = null;
                // WW-3768: ThreadLocal was not removed
                localContext.remove();
            }
        } else {
            // Someone else will clean up this context.
            return callable.call((InternalContext) reference[0]);
        }
    }

    interface ContextualCallable<T> {
        T call(InternalContext context);
    }

    /**
     * Gets a constructor function for a given implementation class.
     */
    @SuppressWarnings("unchecked")
    <T> ConstructorInjector<T> getConstructor(Class<T> implementation) {
        return (ConstructorInjector<T>) constructors.get(implementation);
    }

    final ThreadLocal<Object> localScopeStrategy = new ThreadLocal<>();

    @Override
    public void setScopeStrategy(Scope.Strategy scopeStrategy) {
        this.localScopeStrategy.set(scopeStrategy);
    }

    @Override
    public void removeScopeStrategy() {
        this.localScopeStrategy.remove();
    }

    /**
     * Injects a field or method in a given object.
     */
    interface Injector extends Serializable {
        void inject(InternalContext context, Object o);
    }

    /**
     * Clears all internal caches, factory maps, and ThreadLocals to release
     * Class references that would otherwise pin the webapp classloader after undeploy.
     * <p>
     * The {@code injectors} and {@code constructors} ReferenceCache maps hold
     * {@code Class<?>} keys and reflection accessor objects ({@code Method},
     * {@code Constructor}) whose JDK-generated {@code DelegatingClassLoader}
     * instances retain the webapp classloader as their parent.
     *
     * @since 7.2.0
     */
    @Override
    public void destroy() {
        injectors.clear();
        constructors.clear();
        localContext.remove();
        localScopeStrategy.remove();
    }

    static class MissingDependencyException extends Exception {
        MissingDependencyException(String message) {
            super(message);
        }
    }
}
