package com.atlassian.stash.internal.plugin;

import com.atlassian.plugin.module.PrefixDelegatingModuleFactory;
import com.atlassian.plugin.osgi.container.OsgiContainerManager;
import com.atlassian.plugin.osgi.hostcomponents.HostComponentRegistration;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Array;
import java.util.Map;
import java.util.Set;

@Component
public class OsgiSafeProxyProvider {
    // pluginModuleFactory needs to be proxied at the class level (not interface) because AbstractModuleDescriptor does
    // an instanceof check against the module factory, which would fail for a proxy.
    private static final Set<Class<?>> CLASS_PROXY_REQUIRED = Sets.<Class<?>>newHashSet(
            PrefixDelegatingModuleFactory.class);

    private static final Logger log = LoggerFactory.getLogger(OsgiSafeProxyProvider.class);

    private final ApplicationContext applicationContext;
    private final Map<Class<?>, Object> proxies;

    private volatile boolean initialized;

    @Autowired
    public OsgiSafeProxyProvider(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
        proxies = Maps.newConcurrentMap();
    }

    public Object proxy(Object value, Class<?> type) {
        if (!shouldProxy(type)) {
            return value;
        }

        maybeInitialize();

        if (type.isArray()) {
            int length = Array.getLength(value);
            Object arr = Array.newInstance(type.getComponentType(), length);
            for (int i = 0; i < length; ++i) {
                Array.set(arr, i, proxy(Array.get(value, i), type.getComponentType()));
            }
            return arr;
        }

        Object proxy = proxies.get(type);
        return proxy == null ? createNewProxy(value, type) : proxy;
    }

    public Object[] proxy(Object[] values, Class<?>[] parameterTypes) {
        Object[] result = new Object[values.length];
        for (int i = 0; i < result.length; ++i) {
            result[i] = proxy(values[i], parameterTypes[i]);
        }
        return result;
    }

    @VisibleForTesting
    protected void register(Object proxy, Class<?>... implementedInterfaces) {
        for (Class<?> type : implementedInterfaces) {
            proxies.put(type, proxy);
        }
    }

    private Object createNewProxy(final Object value, Class<?> type) {
        ProxyFactory factory = new ProxyFactory(value);
        if (!type.isInterface() || CLASS_PROXY_REQUIRED.contains(value.getClass())) {
            // make sure the target is proxied, even if value implements one or more interfaces
            factory.setProxyTargetClass(true);
        }

        factory.addAdvice(new MethodInterceptor() {
            @Override
            public Object invoke(MethodInvocation invocation) throws Throwable {
                Thread currentThread = Thread.currentThread();
                ClassLoader original = currentThread.getContextClassLoader();
                try {
                    currentThread.setContextClassLoader(value.getClass().getClassLoader());
                    return invocation.proceed();
                } finally {
                    currentThread.setContextClassLoader(original);
                }
            }
        });
        try {
            return factory.getProxy();
        } catch (RuntimeException e) {
            log.debug("Error creating OSGI safe proxy. Returning unproxied bean instead.", e);
            return value;
        }
    }

   private Object getOsgiProxyFor(OsgiContainerManager osgiContainerManager, HostComponentRegistration registration) {
        for (Class<?> mainInterface : registration.getMainInterfaceClasses()) {
           try {
               Object service =  osgiContainerManager.getServiceTracker(mainInterface.getName()).waitForService(1L);
               if (isInstanceOfAll(service, registration.getMainInterfaceClasses())) {
                   return service;
               }
           } catch (InterruptedException e) {
               Thread.currentThread().interrupt();
           }
       }
       return null;
   }

   private boolean isInstanceOfAll(Object value, Class<?>... types) {
       if (value == null) {
           return false;
       }
       for (Class<?> clazz: types) {
           if (!clazz.isAssignableFrom(value.getClass())) {
               return false;
           }
       }
       return true;
   }

   private void maybeInitialize() {
       if (initialized) {
           return;
       }

       try {
           // any components that are exported as OSGI services can define a custom contextClassLoader strategy, so
           // the OSGI proxy should be used
           ConfigurableListableBeanFactory parentContext = (ConfigurableListableBeanFactory)
                   applicationContext.getAutowireCapableBeanFactory();
           OsgiContainerManager osgiManager = parentContext.getBean(OsgiContainerManager.class);

           for (HostComponentRegistration registration : osgiManager.getHostComponentRegistrations()) {
               if (CLASS_PROXY_REQUIRED.contains(registration.getInstance().getClass())) {
                   // the class is marked as needing to be proxied at the class level and the OSGI proxy cannot
                   // be used (as it won't pass instanceof checks)
                   continue;
               }

               Object osgiProxy = getOsgiProxyFor(osgiManager, registration);
               if (osgiProxy != null) {
                   register(osgiProxy, registration.getMainInterfaceClasses());
               }
           }
           initialized = true;
       } catch (NoSuchBeanDefinitionException e) {
           // no osgiManager available yet - app context initializing
       }
    }

    private boolean shouldProxy(Class<?> type) {
        // don't proxy java.* and javax.* classes
        String fqn = type.getName();
        return !(fqn.startsWith("javax.") || fqn.startsWith("java."));
    }
}
