/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2010-2013 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * http://glassfish.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.jersey.spi.spring.container;

import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.ws.rs.core.Context;

import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.ClassUtils;

import com.sun.jersey.api.core.InjectParam;
import com.sun.jersey.api.core.ResourceConfig;
import com.sun.jersey.api.spring.Autowire;
import com.sun.jersey.core.spi.component.ComponentContext;
import com.sun.jersey.core.spi.component.ComponentScope;
import com.sun.jersey.core.spi.component.ioc.IoCComponentProvider;
import com.sun.jersey.core.spi.component.ioc.IoCComponentProviderFactory;
import com.sun.jersey.core.spi.component.ioc.IoCInstantiatedComponentProvider;
import com.sun.jersey.core.spi.component.ioc.IoCManagedComponentProvider;
import com.sun.jersey.spi.inject.Inject;
import com.sun.jersey.spi.inject.SingletonTypeInjectableProvider;

/**
 * The Spring-based {@link IoCComponentProviderFactory}.
 * <p>
 * Resource and provider classes can be registered Spring-based beans using
 * XML-based registration or auto-wire-based registration.
 *
 * @author Paul.Sandoz@Sun.Com
 */
public class SpringComponentProviderFactory implements IoCComponentProviderFactory {

    private static final Logger LOGGER = Logger.getLogger(SpringComponentProviderFactory.class.getName());

    private final ConfigurableApplicationContext springContext;

    public SpringComponentProviderFactory(ResourceConfig rc, ConfigurableApplicationContext springContext) {

        this.springContext = springContext;

        addAppContextInjectableProvider(rc);
        registerSpringBeans(rc);
    }

    private void addAppContextInjectableProvider(final ResourceConfig rc) {
        rc.getSingletons().add(new SingletonTypeInjectableProvider<Context, ApplicationContext>(ApplicationContext.class, springContext) {});
    }

    private void registerSpringBeans(final ResourceConfig rc) {

        String[] names = BeanFactoryUtils.beanNamesIncludingAncestors(springContext);

        for (String name : names) {
            Class<?> type = ClassUtils.getUserClass(springContext.getType(name));
            if (ResourceConfig.isProviderClass(type)) {
                LOGGER.info("Registering Spring bean, " + name +
                        ", of type " + type.getName() +
                        " as a provider class");
                rc.getClasses().add(type);
            } else if (ResourceConfig.isRootResourceClass(type)) {
                LOGGER.info("Registering Spring bean, " + name +
                        ", of type " + type.getName() +
                        " as a root resource class");
                rc.getClasses().add(type);
            }
        }
    }

    @Override
    public IoCComponentProvider getComponentProvider(Class c) {
        return getComponentProvider(null, c);
    }

    @Override
    public IoCComponentProvider getComponentProvider(ComponentContext cc, Class c) {
        final Autowire autowire = (Autowire) c.getAnnotation(Autowire.class);
            if (autowire != null) {
            if (LOGGER.isLoggable(Level.FINEST)) {
                LOGGER.finest("Creating resource class " +
                        c.getSimpleName() +
                        " annotated with @" +
                        Autowire.class.getSimpleName() +
                        " as spring bean.");
            }
            return new SpringInstantiatedComponentProvider(c, autowire);
        }

        final String beanName = getBeanName(cc, c, springContext);
        if (beanName == null) {
            return null;
        }

        final String scope = findBeanDefinition(beanName).getScope();
        return new SpringManagedComponentProvider(getComponentScope(scope), beanName, c);
    }

    /**
     * Fine the bean definition from a given context or from any of the parent
     * contexts.
     *
     * @param beanName the bean name.
     * @return the bean definition.
     * @throws NoSuchBeanDefinitionException if the bean definition could not
     *         be found.
     */
    private BeanDefinition findBeanDefinition(String beanName) {
        ConfigurableApplicationContext current = springContext;
        BeanDefinition beanDef = null;
        do {
            try {
                return current.getBeanFactory().getBeanDefinition(beanName);
            } catch (NoSuchBeanDefinitionException e) {
                final ApplicationContext parent = current.getParent();
                if (parent != null && parent instanceof ConfigurableApplicationContext) {
                    current = (ConfigurableApplicationContext) parent;
                } else {
                    throw e;
                }
            }
        } while (beanDef == null && current != null);
        return beanDef;
    }

    private ComponentScope getComponentScope(String scope) {
        ComponentScope cs = scopeMap.get(scope);
        return (cs != null) ? cs : ComponentScope.Undefined;
    }

    private final Map<String, ComponentScope> scopeMap = createScopeMap();

    private Map<String, ComponentScope> createScopeMap() {
        Map<String, ComponentScope> m = new HashMap<String, ComponentScope>();
        m.put(BeanDefinition.SCOPE_SINGLETON, ComponentScope.Singleton);
        m.put(BeanDefinition.SCOPE_PROTOTYPE, ComponentScope.PerRequest);
        m.put("request", ComponentScope.PerRequest);
        return m;
    }

    private class SpringInstantiatedComponentProvider implements IoCInstantiatedComponentProvider {

        private final Class c;
        private final Autowire a;

        SpringInstantiatedComponentProvider(Class c, Autowire a) {
            this.c = c;
            this.a = a;
        }

        @Override
        public Object getInstance() {
            return springContext.getBeanFactory().createBean(c,
                    a.mode().getSpringCode(), a.dependencyCheck());
        }

        @Override
        public Object getInjectableInstance(Object o) {
            return SpringComponentProviderFactory.getInjectableInstance(o);
        }
    }

    private class SpringManagedComponentProvider implements IoCManagedComponentProvider {

        private final ComponentScope scope;
        private final String beanName;
        private final Class c;

        SpringManagedComponentProvider(ComponentScope scope, String beanName, Class c) {
            this.scope = scope;
            this.beanName = beanName;
            this.c = c;
        }

        @Override
        public ComponentScope getScope() {
            return scope;
        }

        @Override
        public Object getInjectableInstance(Object o) {
            return SpringComponentProviderFactory.getInjectableInstance(o);
        }

        @Override
        public Object getInstance() {
            return springContext.getBean(beanName, c);
        }
    }

    private static Object getInjectableInstance(Object o) {
        if (AopUtils.isAopProxy(o)) {
            final Advised aopResource = (Advised) o;
            try {
                return aopResource.getTargetSource().getTarget();
            } catch (Exception e) {
                LOGGER.log(Level.SEVERE, "Could not get target object from proxy.", e);
                throw new RuntimeException("Could not get target object from proxy.", e);
            }
        } else {
            return o;
        }
    }

    private static String getBeanName(ComponentContext cc, Class<?> c, ApplicationContext springContext) {
        boolean annotatedWithInject = false;
        if (cc != null) {
            final Inject inject = getAnnotation(cc.getAnnotations(), Inject.class);
            if (inject != null) {
                annotatedWithInject = true;
                if (inject.value() != null && !inject.value().equals("")) {
                    return inject.value();
                }

            }

            final InjectParam injectParam = getAnnotation(cc.getAnnotations(), InjectParam.class);
            if (injectParam != null) {
                annotatedWithInject = true;
                if (injectParam.value() != null && !injectParam.value().equals("")) {
                    return injectParam.value();
                }

            }
        }

        final String names[] = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(springContext, c);

        if (names.length == 0) {
            return null;
        } else if (names.length == 1) {
            return names[0];
        } else {
            // Check if types of the beans names are assignable
            // Spring auto-registration for a type A will include the bean
            // names for classes that extend A
            boolean inheritedNames = false;
            String beanName = null;
            for (String name : names) {
                Class<?> beanType = ClassUtils.getUserClass(springContext.getType(name));

                inheritedNames = c.isAssignableFrom(beanType);

                if (c == beanType)
                    beanName = name;
            }

            if (inheritedNames) {
                if (beanName != null)
                    return beanName;
            }

            final StringBuilder sb = new StringBuilder();
            sb.append("There are multiple beans configured in spring for the type ").
                    append(c.getName()).append(".");

            if (annotatedWithInject) {
                sb.append("\nYou should specify the name of the preferred bean with @InjectParam(\"name\") or @Inject(\"name\").");
            } else {
                sb.append("\nAnnotation information was not available, the reason might be because you're not using " +
                        "@InjectParam. You should use @InjectParam and specifiy the bean name via InjectParam(\"name\").");
            }

            sb.append("\nAvailable bean names: ").append(toCSV(names));

            throw new RuntimeException(sb.toString());
        }
    }

    private static <T extends Annotation> T getAnnotation(Annotation[] annotations,
            Class<T> clazz) {
        if (annotations != null) {
            for (Annotation annotation : annotations) {
                if (annotation.annotationType().equals(clazz)) {
                    return clazz.cast(annotation);
                }
            }
        }
        return null;
    }

    private static <T> String toCSV(T[] items) {
        if (items == null) {
            return null;
        }
        return toCSV(Arrays.asList(items));
    }

    private static <I> String toCSV(Collection<I> items) {
        return toCSV(items, ", ", null);
    }

    private static <I> String toCSV(Collection<I> items, String separator, String delimiter) {
        if (items == null) {
            return null;
        }
        if (items.isEmpty()) {
            return "";
        }
        final StringBuilder sb = new StringBuilder();
        for (final Iterator<I> iter = items.iterator(); iter.hasNext();) {
            if (delimiter != null) {
                sb.append(delimiter);
            }
            final I item = iter.next();
            sb.append(item);
            if (delimiter != null) {
                sb.append(delimiter);
            }
            if (iter.hasNext()) {
                sb.append(separator);
            }
        }
        return sb.toString();
    }
}
