package com.atlassian.stash.internal.plugin;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.*;

import com.atlassian.plugin.hostcontainer.HostContainer;

import com.google.common.base.Function;
import com.google.common.collect.Maps;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.*;
import org.springframework.beans.factory.support.CglibSubclassingInstantiationStrategy;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;

/**
 * {@link HostContainer} implementation that injects OSGI-safe proxies instead of raw beans where possible. The plugin
 * system uses {@link HostContainer} to instantiate plugin module descriptors, plugin modules and other plugin
 * artifacts. These artifacts are accessed by plugins and it is therefore essential that OSGI-safe proxies are used
 * instead of raw beans. For components that are {@link com.atlassian.plugin.spring.AvailableToPlugins exported} as
 * OSGI services, the OSGI proxy is used to ensure that configured
 * {@link com.atlassian.plugin.spring.AvailableToPlugins#contextClassLoaderStrategy()} is honoured. All other components
 * are wrapped by a simple proxy that temporarily sets the context classloader to the core classloader.
 * <p>
 * As an example of the problem, consider a {@code SoyFunction} module that injects the {@code I18nService} to render
 * a message. If the unproxied service is used, a {@code ClassNotFoundException} will be thrown when it loads the
 * current locale. The {@code InternalApplicationPropertiesService} caches the locale internally and the wrapper that it
 * uses is not on the classpath of the soy plugin classloader. By injecting the OSGI proxy to the {@code I18nService}
 * instead of the raw bean, the correct classloader is set.
 * <p>
 * This 'injecting proxies instead of raw beans' feature should _only_ be applied to plugin modules and module
 * descriptors as it adds additional proxying overhead. To achieve this, a separate child bean factory is created and
 * configured to inject OSGI proxies instead of raw beans. Note that this bean factory can see all beans from the parent
 * context, but not the other way around.
 * <p>
 * {@code OsgiSafeHostContainer} registers a custom {@link org.springframework.beans.factory.support.InstantiationStrategy}
 * that intercepts bean creation and replaces constructor args with OSGI proxies. This covers all autowired
 * constructors and factory methods.
 */
@Component("hostContainer")
public class OsgiSafeHostContainer implements InternalHostContainer {

    private final ApplicationContext applicationContext;
    private final DefaultListableBeanFactory beanFactory;
    private final OsgiSafeProxyProvider proxyProvider;

    @Autowired
    public OsgiSafeHostContainer(ApplicationContext applicationContext, OsgiSafeProxyProvider proxyProvider) {
        this.applicationContext = applicationContext;
        this.proxyProvider = proxyProvider;

        // create a child bean factory with custom configuration to enforce injection of OSGI-safe proxies
        // instead of raw beans
        beanFactory = new DefaultListableBeanFactory(applicationContext.getAutowireCapableBeanFactory());

        // custom InstantiationStrategy to intercept constructor and factory method injection. Setter injection is not
        // supported by OsgiSafeHostContainer, so no need to worry about that
        beanFactory.setInstantiationStrategy(new ProxyingInstantiationStrategy(proxyProvider));
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T create(final Class<T> moduleClass) throws IllegalArgumentException {
        return (T) beanFactory
                .autowire(moduleClass, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false);
    }

    @Nonnull
    @Override
    public <T> Map<String, T> getComponentsOfType(@Nonnull final Class<T> tClass) {
        // Retrieve the beans from the parent context and wrap them in proxies.
        return Maps.transformValues(applicationContext.getBeansOfType(tClass), new Function<T, T>() {
            @Override
            public T apply(T value) {
                return tClass.cast(proxyProvider.proxy(value, tClass));
            }
        });
    }

    private static class ProxyingInstantiationStrategy extends CglibSubclassingInstantiationStrategy {
        private final OsgiSafeProxyProvider proxyFactory;

        private ProxyingInstantiationStrategy(OsgiSafeProxyProvider proxyFactory) {
            this.proxyFactory = proxyFactory;
        }

        @Override
        public Object instantiate(RootBeanDefinition beanDefinition, String beanName, BeanFactory owner,
                                  Constructor<?> ctor, Object[] args) {
            return super.instantiate(beanDefinition, beanName, owner, ctor,
                    proxyFactory.proxy(args, ctor.getParameterTypes()));
        }

        @Override
        public Object instantiate(RootBeanDefinition beanDefinition, String beanName, BeanFactory owner,
                                  Object factoryBean, Method factoryMethod, Object[] args) {
            return super.instantiate(beanDefinition, beanName, owner, factoryBean, factoryMethod,
                    proxyFactory.proxy(args, factoryMethod.getParameterTypes()));
        }
    }
}
