/**
 * 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.cxf.common.injection;


import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import javax.annotation.Resources;


import org.apache.cxf.common.annotation.AbstractAnnotationVisitor;
import org.apache.cxf.common.annotation.AnnotationProcessor;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.ClassHelper;
import org.apache.cxf.common.util.ReflectionUtil;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.resource.ResourceManager;
import org.apache.cxf.resource.ResourceResolver;


/**
 * injects references specified using @Resource annotation
 *
 */
public class ResourceInjector extends AbstractAnnotationVisitor {

    private static final Logger LOG = LogUtils.getL7dLogger(ResourceInjector.class);
    private static final List<Class<? extends Annotation>> ANNOTATIONS = new ArrayList<>();

    static {
        ANNOTATIONS.add(Resource.class);
        ANNOTATIONS.add(Resources.class);
    }


    private final ResourceManager resourceManager;
    private final List<ResourceResolver> resourceResolvers;

    public ResourceInjector(ResourceManager resMgr) {
        this(resMgr, resMgr == null ? null : resMgr.getResourceResolvers());
    }

    public ResourceInjector(ResourceManager resMgr, List<ResourceResolver> resolvers) {
        super(ANNOTATIONS);
        resourceManager = resMgr;
        resourceResolvers = resolvers;
    }

    private static Field getField(Class<?> cls, String name) {
        if (cls == null) {
            return null;
        }
        try {
            Field f = ReflectionUtil.getDeclaredField(cls, name);
            if (f == null) {
                f = getField(cls.getSuperclass(), name);
            }
            return f;
        } catch (Exception ex) {
            return getField(cls.getSuperclass(), name);
        }
    }

    public static boolean processable(Class<?> cls, Object o) {
        if (cls.getName().startsWith("java.")
            || cls.getName().startsWith("javax.")) {
            return false;
        }
        NoJSR250Annotations njsr = cls.getAnnotation(NoJSR250Annotations.class);
        if (njsr != null) {
            for (String s : njsr.unlessNull()) {
                try {
                    Field f = getField(cls, s);
                    ReflectionUtil.setAccessible(f);
                    if (f.get(o) == null) {
                        return true;
                    }
                } catch (Exception ex) {
                    return true;
                }
            }
            return false;
        }
        return true;
    }

    public void inject(Object o) {
        inject(o, o.getClass());
    }

    public void inject(Object o, Class<?> claz) {
        if (processable(claz, o)) {
            AnnotationProcessor processor = new AnnotationProcessor(o);
            processor.accept(this, claz);
        }
    }

    public void construct(Object o) {
        setTarget(o);
        if (processable(targetClass, o)) {
            invokePostConstruct();
        }
    }
    public void construct(Object o, Class<?> cls) {
        setTarget(o, cls);
        if (processable(targetClass, o)) {
            invokePostConstruct();
        }
    }


    public void destroy(Object o) {
        setTarget(o);
        if (processable(targetClass, o)) {
            invokePreDestroy();
        }
    }


    // Implementation of org.apache.cxf.common.annotation.AnnotationVisitor

    @Override
    public final void visitClass(final Class<?> clz, final Annotation annotation) {

        assert annotation instanceof Resource || annotation instanceof Resources : annotation;

        if (annotation instanceof Resource) {
            injectResourceClassLevel((Resource)annotation);
        } else if (annotation instanceof Resources) {
            Resources resources = (Resources)annotation;
            for (Resource resource : resources.value()) {
                injectResourceClassLevel(resource);
            }
        }

    }

    private void injectResourceClassLevel(Resource res) {
        if (res.name() == null || "".equals(res.name())) {
            LOG.log(Level.INFO, "RESOURCE_NAME_NOT_SPECIFIED", target.getClass().getName());
            return;
        }

        Object resource;
        // first find a setter that matches this resource
        Method setter = findSetterForResource(res);
        if (setter != null) {
            Class<?> type = getResourceType(res, setter);
            resource = resolveResource(res.name(), type);
            if (resource == null) {
                LOG.log(Level.INFO, "RESOURCE_RESOLVE_FAILED");
                return;
            }

            invokeSetter(setter, resource);
            return;
        }

        Field field = findFieldForResource(res);
        if (field != null) {
            Class<?> type = getResourceType(res, field);
            resource = resolveResource(res.name(), type);
            if (resource == null) {
                LOG.log(Level.INFO, "RESOURCE_RESOLVE_FAILED");
                return;
            }
            injectField(field, resource);
            return;
        }
        LOG.log(Level.SEVERE, "NO_SETTER_OR_FIELD_FOR_RESOURCE", getTarget().getClass().getName());
    }

    public final void visitField(final Field field, final Annotation annotation) {

        assert annotation instanceof Resource : annotation;

        Resource res = (Resource)annotation;

        String name = getFieldNameForResource(res, field);
        Class<?> type = getResourceType(res, field);

        Object resource = resolveResource(name, type);
        if (resource == null
            && "".equals(res.name())) {
            resource = resolveResource(null, type);
        }
        if (resource != null) {
            injectField(field, resource);
        } else {
            LOG.log(Level.INFO, "RESOURCE_RESOLVE_FAILED", name);
        }
    }

    public final void visitMethod(final Method method, final Annotation annotation) {

        assert annotation instanceof Resource : annotation;

        Resource res = (Resource)annotation;

        String resourceName = getResourceName(res, method);
        Class<?> clz = getResourceType(res, method);

        Object resource = resolveResource(resourceName, clz);
        if (resource == null
            && "".equals(res.name())) {
            resource = resolveResource(null, clz);
        }
        if (resource != null) {
            invokeSetter(method, resource);
        } else {
            LOG.log(Level.FINE, "RESOURCE_RESOLVE_FAILED", new Object[] {resourceName, clz});
        }
    }

    private Field findFieldForResource(Resource res) {
        assert target != null;
        assert res.name() != null;

        for (Field field : target.getClass().getFields()) {
            if (field.getName().equals(res.name())) {
                return field;
            }
        }

        for (Field field : target.getClass().getDeclaredFields()) {
            if (field.getName().equals(res.name())) {
                return field;
            }
        }
        return null;
    }


    private Method findSetterForResource(Resource res) {
        assert target != null;

        String setterName = resourceNameToSetter(res.name());
        Method setterMethod = null;

        for (Method method : getTarget().getClass().getMethods()) {
            if (setterName.equals(method.getName())) {
                setterMethod = method;
                break;
            }
        }

        if (setterMethod != null && setterMethod.getParameterTypes().length != 1) {
            LOG.log(Level.WARNING, "SETTER_INJECTION_WITH_INCORRECT_TYPE", setterMethod);
        }
        return setterMethod;
    }

    private static String resourceNameToSetter(String resName) {
        return "set" + StringUtils.capitalize(resName);
    }

    private void invokeSetter(Method method, Object resource) {
        try {
            ReflectionUtil.setAccessible(method);
            if (method.getDeclaringClass().isAssignableFrom(getTarget().getClass())) {
                method.invoke(getTarget(), resource);
            } else { // deal with the proxy setter method
                Method targetMethod = getTarget().getClass().getMethod(method.getName(),
                                                                       method.getParameterTypes());
                targetMethod.invoke(getTarget(), resource);
            }
        } catch (IllegalAccessException e) {
            LOG.log(Level.SEVERE, "INJECTION_SETTER_NOT_VISIBLE", method);
        } catch (InvocationTargetException | SecurityException e) {
            LogUtils.log(LOG, Level.SEVERE, "INJECTION_SETTER_RAISED_EXCEPTION", e, method);
        } catch (NoSuchMethodException e) {
            LOG.log(Level.SEVERE, "INJECTION_SETTER_METHOD_NOT_FOUND", new Object[] {method.getName()});
        }
    }

    private String getResourceName(Resource res, Method method) {
        assert method != null;
        assert res != null;
        assert method.getName().startsWith("set") : method;

        if (res.name() == null || res.name().isEmpty()) {
            String name = method.getName().substring(3);
            name = Character.toLowerCase(name.charAt(0)) + name.substring(1);
            return method.getDeclaringClass().getCanonicalName() + '/' + name;
        }
        return res.name();
    }

    private void injectField(Field field, Object resource) {
        assert field != null;
        assert resource != null;

        boolean accessible = field.isAccessible();
        try {
            if (field.getType().isAssignableFrom(resource.getClass())) {
                ReflectionUtil.setAccessible(field);
                field.set(ClassHelper.getRealObject(getTarget()), resource);
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            LOG.severe("FAILED_TO_INJECT_FIELD");
        } finally {
            ReflectionUtil.setAccessible(field, accessible);
        }
    }

    public void invokePostConstruct() {
        boolean accessible = false;
        for (Method method : getPostConstructMethods()) {
            PostConstruct pc = method.getAnnotation(PostConstruct.class);
            if (pc != null) {
                try {
                    ReflectionUtil.setAccessible(method);
                    method.invoke(target);
                } catch (IllegalAccessException e) {
                    LOG.log(Level.WARNING, "INJECTION_COMPLETE_NOT_VISIBLE", method);
                } catch (InvocationTargetException e) {
                    LOG.log(Level.WARNING, "INJECTION_COMPLETE_THREW_EXCEPTION", e);
                } finally {
                    ReflectionUtil.setAccessible(method, accessible);
                }
            }
        }
    }

    public void invokePreDestroy() {
        boolean accessible = false;
        for (Method method : getPreDestroyMethods()) {
            PreDestroy pd = method.getAnnotation(PreDestroy.class);
            if (pd != null) {
                try {
                    ReflectionUtil.setAccessible(method);
                    method.invoke(target);
                } catch (IllegalAccessException e) {
                    LOG.log(Level.WARNING, "PRE_DESTROY_NOT_VISIBLE", method);
                } catch (InvocationTargetException e) {
                    LOG.log(Level.WARNING, "PRE_DESTROY_THREW_EXCEPTION", e);
                } finally {
                    ReflectionUtil.setAccessible(method, accessible);
                }
            }
        }
    }


    private Collection<Method> getPostConstructMethods() {
        return getAnnotatedMethods(PostConstruct.class);
    }

    private Collection<Method> getPreDestroyMethods() {
        return getAnnotatedMethods(PreDestroy.class);
    }

    private Collection<Method> getAnnotatedMethods(Class<? extends Annotation> acls) {

        Collection<Method> methods = new LinkedList<>();
        addAnnotatedMethods(acls, getTarget().getClass().getMethods(), methods);
        addAnnotatedMethods(acls, ReflectionUtil.getDeclaredMethods(getTarget().getClass()), methods);
        if (getTargetClass() != getTarget().getClass()) {
            addAnnotatedMethods(acls, getTargetClass().getMethods(), methods);
            addAnnotatedMethods(acls, ReflectionUtil.getDeclaredMethods(getTargetClass()), methods);
        }
        return methods;
    }

    private void addAnnotatedMethods(Class<? extends Annotation> acls, Method[] methods,
        Collection<Method> annotatedMethods) {
        for (Method method : methods) {
            if (method.getAnnotation(acls) != null
                && !annotatedMethods.contains(method)) {
                annotatedMethods.add(method);
            }
        }
    }


    /**
     * making this protected to keep pmd happy
     */
    protected Class<?> getResourceType(Resource res, Field field) {
        assert res != null;
        Class<?> type = res.type();
        if (res.type() == null || Object.class == res.type()) {
            type = field.getType();
        }
        return type;
    }


    private Class<?> getResourceType(Resource res, Method method) {
        return res.type() != null && !Object.class.equals(res.type())
            ? res.type()
            : method.getParameterTypes()[0];
    }


    private String getFieldNameForResource(Resource res, Field field) {
        assert res != null;
        if (res.name() == null || "".equals(res.name())) {
            return field.getDeclaringClass().getCanonicalName() + "/" + field.getName();
        }
        return res.name();
    }

    private Object resolveResource(String resourceName, Class<?> type) {
        if (resourceManager == null) {
            return null;
        }
        return resourceManager.resolveResource(resourceName, type, resourceResolvers);
    }

}
