/*
 * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors.
 *
 * 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
 * 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.switchyard.component.test.mixins.cdi;

import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.spi.AnnotatedType;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.InjectionTarget;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import org.jboss.weld.bootstrap.api.Bootstrap;
import org.jboss.weld.bootstrap.spi.Deployment;
import org.jboss.weld.environment.se.Weld;
import org.jboss.weld.environment.se.WeldContainer;
import org.jboss.weld.environment.se.events.ContainerInitialized;
import org.jboss.weld.resources.spi.ResourceLoader;
import org.junit.Assert;
import org.switchyard.ServiceDomain;
import org.switchyard.SwitchYardException;
import org.switchyard.common.type.Classes;
import org.switchyard.component.test.mixins.naming.NamingMixIn;
import org.switchyard.deploy.ServiceDomainManager;
import org.switchyard.deploy.internal.AbstractDeployment;
import org.switchyard.test.MixInDependencies;
import org.switchyard.test.SimpleTestDeployment;
import org.switchyard.test.TestMixIn;
import org.switchyard.test.mixins.AbstractTestMixIn;

/**
 * CDI Test Mix-In for deploying the Weld CDI Standalone Edition container.
 *
 * @author <a href="mailto:tom.fennelly@gmail.com">tom.fennelly@gmail.com</a>
 */
@MixInDependencies(required={NamingMixIn.class})
public class CDIMixIn extends AbstractTestMixIn {
    private static final String BINDING_CONTEXT = "java:comp";
    private static final String BEAN_MANAGER_NAME = "BeanManager";
    
    private static final Pattern[] RESOURCE_FILTER_PATTERNS = new Pattern[] {
        Pattern.compile(".*jbpm-.*-services.*beans\\.xml"),
        Pattern.compile(".*jbpm-runtime-manager.*beans\\.xml"),
        Pattern.compile(".*jbpm-human-task-.*beans\\.xml")
    };
    
    private Weld _weld;
    private WeldContainer _weldContainer;
    private AbstractDeployment _simpleCdiDeployment;
    private InjectionTarget<Object> _testInjectionTarget;
    private CreationalContext<Object> _testCreationalContext;

    @Override
    public void initialize() {
        super.initialize();

        // Deploy the weld container...
        _weld = new Weld() {
            @Override
            protected Deployment createDeployment(final ResourceLoader resourceLoader, Bootstrap bootstrap) {
                ResourceLoader filterLoader = new ResourceLoader() {
                    public void cleanup() {
                        resourceLoader.cleanup();
                    }
                    public Class<?> classForName(String name) {
                        return resourceLoader.classForName(name);
                    }
                    public URL getResource(String name) {
                        return filter(resourceLoader.getResource(name));
                    }
                    public Collection<URL> getResources(String name) {
                        Collection<URL> urls = new ArrayList<URL>();
                        for (URL url : resourceLoader.getResources(name)) {
                            url = filter(url);
                            if (url != null) {
                                urls.add(url);
                            }
                        }
                        return urls;
                    }
                    private URL filter(URL url) {
                        if (url != null) {
                            for (Pattern pattern : RESOURCE_FILTER_PATTERNS) {
                                if (pattern.matcher(url.toString()).matches()) {
                                    return null;
                                }
                            }
                        }
                        return url;
                    }
                };
                Deployment deployment = super.createDeployment(filterLoader, bootstrap);
                if (getTestKit() != null) {
                    List<TestMixIn> mixins = getTestKit().getMixIns();
                    for (TestMixIn mixin : mixins) {
                        if (mixin instanceof CDIMixInParticipant) {
                           try {
                               ((CDIMixInParticipant) mixin).participate(deployment);
                            } catch (Exception e) {
                                throw new SwitchYardException("Can not initialize Weld due CDIMixIn initialization error", e);
                            }
                        }
                    }
                }
                return deployment;
            }
        };
        _weldContainer = _weld.initialize();
        _weldContainer.event().select(ContainerInitialized.class).fire(new ContainerInitialized());

        // And bind the BeanManager instance into java:comp...
        try {
            Context ctx = (Context) new InitialContext().lookup(BINDING_CONTEXT);
            ctx.rebind(BEAN_MANAGER_NAME, getBeanManager());
        } catch (NamingException e) {
            e.printStackTrace();
            Assert.fail("Failed to bind BeanManager into '" + BINDING_CONTEXT + "'.");
        }

        injectCDIBeans();
    }

    @Override
    public void before(AbstractDeployment deployment) {
        if (deployment instanceof SimpleTestDeployment) {
            // Not a user defined configuration based test... deploy the Services and Transformers
            // found by the CDI discovery process...
            @SuppressWarnings("unchecked")
            Class<? extends AbstractDeployment> simpleCdiDeploymentType = (Class<? extends AbstractDeployment>) Classes.forName("org.switchyard.component.bean.internal.SimpleCDIDeployment", getClass());
            if (simpleCdiDeploymentType == null) {
                if (getTestKit() != null && getTestKit().getTestInstance().getClass().getPackage() == CDIMixIn.class.getPackage()) {
                    return;  // not fatal for unit tests of CDIMixIn since they can't depend on the bean component as this would create a cyclic Maven dependency.
                } else {
                    Assert.fail("Failed to locate the SimpleCDIDeployment class on the classpath.  Module must include the SwitchYard Bean Component as one of its depedencies.");
                }
            }
            try {
                _simpleCdiDeployment = simpleCdiDeploymentType.newInstance();
                _simpleCdiDeployment.setParentDeployment(deployment);
                ServiceDomain domain = new ServiceDomainManager().createDomain();
                _simpleCdiDeployment.init(domain, getTestKit().getActivators());
                _simpleCdiDeployment.start();
            } catch (Exception e) {
                e.printStackTrace();
                Assert.fail("Failed to manually deploy Bean Service.  Exception: " + e.getMessage());
            }
        }
    }

    /**
     * Get the CDI {@link BeanManager}.
     * @return The CDI {@link BeanManager}.
     */
    public BeanManager getBeanManager() {
        return _weldContainer.getBeanManager();
    }

    /**
     * Get the {@link javax.inject.Named @Named} CDI {@link Bean}.
     * @param name The name of the bean.
     * @return The {@link javax.inject.Named @Named} CDI {@link Bean}.
     */
    public Bean<?> getBean(String name) {
        BeanManager beanManager = getBeanManager();
        Set<Bean<?>> beans = beanManager.getBeans(name);

        if (beans != null && !beans.isEmpty()) {
            return beans.iterator().next();
        } else {
            throw new IllegalStateException("Unable to find CDI bean @Named '" + name + "'.  \n\t - Check for a typo in the name. \n\t - Make sure the Class is annotated with the @Name annotation.");
        }
    }

    /**
     * Get the CDI {@link Bean} associated with CDI the specified type.
     * @param type The bean type.
     * @return The the CDI {@link Bean} associated with CDI the specified type.
     */
    public Bean getBean(Class<?> type) {
        BeanManager beanManager = getBeanManager();
        Set<Bean<?>> beans = beanManager.getBeans(type);

        if (beans != null && !beans.isEmpty()) {
            return beans.iterator().next();
        } else {
            throw new IllegalStateException("Unable to find CDI bean of type '" + type.getName() + "'.");
        }
    }

    /**
     * Get the object instance associated with a {@link javax.inject.Named @Named} CDI {@link Bean}.
     * @param name The name of the bean.
     * @return The object instance associated with a {@link javax.inject.Named @Named} CDI {@link Bean}.
     */
    public Object getObject(String name) {
        return createBeanInstance(getBean(name));
    }

    /**
     * Get the object instance associated with a {@link javax.inject.Named @Named} CDI {@link Bean}.
     * @param name The name of the bean.
     * @param type The bean type.
     * @return The object instance associated with a {@link javax.inject.Named @Named} CDI {@link Bean}.
     *
     * @param <T> Type.
     */
    public <T> T getObject(String name, Class<T> type) {
        return type.cast(getObject(name));
    }

    /**
     * Get the object instance associated with CDI {@link Bean} of the specified type.
     * @param type The bean type.
     * @return The object instance associated with CDI {@link Bean} of the specified type.
     *
     * @param <T> Type.
     */
    public <T> T getObject(Class<T> type) {
        return type.cast(createBeanInstance(getBean(type)));
    }

    @Override
    public void after(AbstractDeployment deployment) {
        if (_simpleCdiDeployment != null) {
            _simpleCdiDeployment.stop();
            _simpleCdiDeployment.destroy();
        }
    }

    @Override
    public synchronized void uninitialize() {
        disposeCDIBeans();

        if (_weld != null) {
            _weld.shutdown();
            _weld = null;
        } else {
            Thread.dumpStack();
        }

        // Clean up JNDI tree
        super.uninitialize();
    }

    private Object createBeanInstance(Bean<?> bean) {
        BeanManager beanManager = getBeanManager();
        CreationalContext<?> creationalContext = beanManager.createCreationalContext(null);

        return beanManager.getReference(bean, bean.getBeanClass(), creationalContext);
    }

    @SuppressWarnings("unchecked")
    private void injectCDIBeans() {
        BeanManager beanManager = getBeanManager();
        Object testInstance = getTestKit() != null ? getTestKit().getTestInstance() : null;
        if (beanManager != null && testInstance != null) {
            // delegate dependency injection and lifecycle callbacks to the CDI container
            AnnotatedType<Object> type = beanManager.createAnnotatedType((Class<Object>) testInstance.getClass());
            _testInjectionTarget = beanManager.createInjectionTarget(type);
            _testCreationalContext = beanManager.createCreationalContext(null);
            _testInjectionTarget.inject(getTestKit().getTestInstance(), _testCreationalContext);
            _testInjectionTarget.postConstruct(getTestKit().getTestInstance());
        }
    }

    private void disposeCDIBeans() {
        if (_testInjectionTarget != null) {
            Object testInstance = getTestKit().getTestInstance();
            _testInjectionTarget.preDestroy(testInstance);
            _testInjectionTarget.dispose(testInstance);
            _testCreationalContext.release();
        }
    }
}
