/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2010 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.cdi.weld.internal.tomcat;

import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;

import javax.decorator.Decorator;
import javax.enterprise.inject.spi.InjectionPoint;
import javax.inject.Inject;
import javax.interceptor.Interceptor;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import javax.servlet.jsp.JspApplicationContext;
import javax.servlet.jsp.JspFactory;

import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.InstanceManager;
import org.jboss.weld.bootstrap.WeldBootstrap;
import org.jboss.weld.bootstrap.api.Bootstrap;
import org.jboss.weld.bootstrap.api.Environments;
import org.jboss.weld.ejb.spi.EjbServices;
import org.jboss.weld.el.WeldELContextListener;
import org.jboss.weld.environment.servlet.Listener;
import org.jboss.weld.environment.servlet.deployment.URLScanner;
import org.jboss.weld.environment.tomcat7.WeldForwardingInstanceManager;
import org.jboss.weld.environment.tomcat7.WeldInstanceManager;
import org.jboss.weld.exceptions.DefinitionException;
import org.jboss.weld.injection.spi.EjbInjectionServices;
import org.jboss.weld.injection.spi.JpaInjectionServices;
import org.jboss.weld.injection.spi.ResourceInjectionServices;
import org.jboss.weld.manager.api.WeldManager;
import org.jboss.weld.resources.ClassLoaderResourceLoader;
import org.jboss.weld.resources.spi.ResourceLoader;
import org.jboss.weld.servlet.ConversationPropagationFilter;
import org.ow2.easybeans.api.Factory;
import org.ow2.easybeans.api.injection.ResourceInjector;
import org.ow2.easybeans.persistence.api.EZBPersistenceUnitManager;
import org.ow2.jonas.cdi.weld.internal.bootstrap.JOnASServletDeployment;
import org.ow2.jonas.cdi.weld.internal.easybeans.EZBFactoriesRegistry;
import org.ow2.jonas.cdi.weld.internal.easybeans.WeldResourceInjector;
import org.ow2.jonas.cdi.weld.internal.ejb.EZBEjbDescriptor;
import org.ow2.jonas.cdi.weld.internal.jsf.FacesApplicationCustomizer;
import org.ow2.jonas.cdi.weld.internal.services.JOnASEjbInjectionServices;
import org.ow2.jonas.cdi.weld.internal.services.JOnASEjbServices;
import org.ow2.jonas.cdi.weld.internal.services.JOnASJpaInjectionServices;
import org.ow2.jonas.cdi.weld.internal.services.JOnASResourceServices;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;

/**
 * A {@code WeldLifeCycleListener} is a Tomcat Context Lifecycle listener.
 * It is in charge of Weld container start-up and shut-down process for a web-app.
 *
 * @author Guillaume Sauthier
 */
public class WeldLifeCycleListener implements LifecycleListener {

    /**
     * Logger.
     */
    private static Log logger = LogFactory.getLog(WeldLifeCycleListener.class);

    /**
     * BeanManager for this web application.
     */
    private WeldManager manager;

    /**
     * Weld Bootstrap
     */
    private Bootstrap bootstrap;

    /**
     * The actual webapp class loader
     */
    private ClassLoader webappClassLoader;

    /**
     * The EZBPersistenceUnitManager if this webapp contains EJBs null otherwise
     */
    private EZBPersistenceUnitManager persistenceUnitManager;

    /**
     * The module name used to retrieve EasyBeans factories
     */
    private String moduleName;

    /**
     * The EasyBeans factories registry
     */
    private EZBFactoriesRegistry factoriesRegistry;

    /**
     * @param moduleName             The module name used to retrieve EasyBeans factories
     * @param persistenceUnitManager The EZBPersistenceUnitManager if this webapp contains EJBs null otherwise
     * @param factoriesRegistry      The EasyBeans factories registry
     */
    public WeldLifeCycleListener(String moduleName, EZBPersistenceUnitManager persistenceUnitManager,
            EZBFactoriesRegistry factoriesRegistry) {
        this.moduleName = moduleName;
        this.persistenceUnitManager = persistenceUnitManager;
        this.factoriesRegistry = factoriesRegistry;
    }

    /**
     * React when an event is fired by the Context.
     *
     * @param event tomcat Context's life-cycle event
     */
    public void lifecycleEvent(final LifecycleEvent event) {
        if (event.getLifecycle() instanceof StandardContext) {
            StandardContext context = (StandardContext) event.getLifecycle();
            if (Lifecycle.CONFIGURE_START_EVENT.equals(event.getType())) {
                configureStart(context);
            } else if (Lifecycle.AFTER_START_EVENT.equals(event.getType())) {
                afterStart(context);
            } else if (Lifecycle.AFTER_STOP_EVENT.equals(event.getType())) {
                afterStop(context);
            }
        }
    }


    /**
     * Plug-in a JSF PhaseListener in order to bound conversation's scopes
     *
     * @param context Tomcat Context
     */
    private void afterStart(final StandardContext context) {
        FacesApplicationCustomizer.customizeFacesApplication(webappClassLoader, context.getName());
    }

    /**
     * Stop the Weld container (if any has been started)
     *
     * @param context Tomcat Context
     */
    private void afterStop(final StandardContext context) {

        // Stop the weld container
        if (bootstrap != null) {
            logger.debug("Stop Weld container for ''{0}'' ...", context.getName());
            bootstrap.shutdown();
        }
    }

    /**
     * Start the Weld container
     *
     * @param context Tomcat Context
     */
    private void configureStart(final StandardContext context) {

        logger.debug("Start Weld container for ''{0}'' ...", context.getName());

        bootstrap = new WeldBootstrap();
        ServletContext servletContext = context.getServletContext();
        URLScanner scanner = new URLScanner(context.getLoader().getClassLoader());
        servletContext.setAttribute(URLScanner.class.getName(), scanner);

        // Provides our Application ClassLoader backed ResourceLoader
        webappClassLoader = context.getLoader().getClassLoader();
        JOnASServletDeployment deployment = new JOnASServletDeployment(servletContext, bootstrap);

        ResourceLoader resourceLoader = new ClassLoaderResourceLoader(webappClassLoader);
        deployment.getServices().add(ResourceLoader.class, resourceLoader);
        deployment.getWebAppBeanDeploymentArchive().getServices().add(ResourceLoader.class, resourceLoader);

        // TODO Beurk, this HAS to be improved
        performUglyClassLoaderHack(webappClassLoader);

        InitialContext initialContext;
        try {
            initialContext = new InitialContext();
        } catch (NamingException e) {
            logger.error("Unable to get initial JNDI context.", e);
            context.setConfigured(false);
            return;
        }

        // Initialize EJBDescriptors
        for (Factory<?, ?> factory : factoriesRegistry.getFactories(moduleName)) {
            deployment.getWebAppBeanDeploymentArchive().getEjbs()
                    .add(new EZBEjbDescriptor(deployment.getWebAppBeanDeploymentArchive(), factory));
        }

        // Add custom EJB service
        deployment.getServices().add(EjbServices.class, new JOnASEjbServices());
        deployment.getServices().add(EjbInjectionServices.class, new JOnASEjbInjectionServices(initialContext));

        // Add custom JPA service if there is an EZBPersistenceUnitManager
        if (persistenceUnitManager != null) {
            deployment.getWebAppBeanDeploymentArchive().getServices()
                    .add(JpaInjectionServices.class, new JOnASJpaInjectionServices(persistenceUnitManager));
        }

        // Add custom Resource service
        deployment.getWebAppBeanDeploymentArchive().getServices()
                .add(ResourceInjectionServices.class, new JOnASResourceServices(initialContext));
        // Start Weld
        // TODO Maybe the Servlet Env is not well suited for our case
        // Consider built up our own Env with a minimal required set of services
        bootstrap.startContainer(Environments.SERVLET, deployment);
        bootstrap.startInitialization();

        manager = bootstrap.getManager(deployment.getWebAppBeanDeploymentArchive());
        servletContext.setAttribute(Listener.BEAN_MANAGER_ATTRIBUTE_NAME, manager);

        registerFilters(servletContext);

        configureJsp(servletContext);

        bindBeanManager(initialContext, context);
        wrapInstanceManager(context);
        configureWeldResourceInjector();
        // Initialization process
        bootstrap.deployBeans();
        bootstrap.validateBeans();
        bootstrap.endInitialization();

        addPreserveExceptions(servletContext);
    }

    private void addPreserveExceptions(ServletContext servletContext) {
            Collection<Class<?>> preserve = new HashSet<Class<?>>();
            preserve.add(DefinitionException.class);
            Object o = servletContext.getAttribute("jonas.tomcat.load.on.startup.preserve.exceptions");
            if (o != null && o instanceof Collection && !((Collection)o).isEmpty() &&
                                        ((Collection)o).iterator().next() instanceof Class<?>) {
                Collection collection = (Collection) o;
                collection.addAll(preserve);
            } else {
                servletContext.setAttribute("jonas.tomcat.load.on.startup.preserve.exceptions", preserve);
            }
        }

    /**
     * Configure our {@link WeldResourceInjector} with the {@link WeldManager}
     */
    private void configureWeldResourceInjector() {
        Collection<Factory<?, ?>> factories = factoriesRegistry.getFactories(moduleName);
        if (factories.size() > 0) {
            for (ResourceInjector resourceInjector : factories.iterator().next().getContainer().getConfiguration().getInjectors()) {
                if (resourceInjector instanceof WeldResourceInjector) {
                    WeldResourceInjector injector = (WeldResourceInjector) resourceInjector;
                    injector.setBeanManager(manager);
                }
            }
        }
    }

    /**
     * Configure the Unified EL for JSP
     *
     * @param servletContext
     */
    private void configureJsp(ServletContext servletContext) {
        JspFactory jspFactory = JspFactory.getDefaultFactory();
        if (jspFactory != null) {
            JspApplicationContext jspApplicationContext = jspFactory.getJspApplicationContext(servletContext);
            jspApplicationContext.addELResolver(manager.getELResolver());
            jspApplicationContext.addELContextListener(new WeldELContextListener());
        }
    }

    /**
     * Register a {@link ConversationPropagationFilter}
     *
     * @param servletContext
     */
    private void registerFilters(ServletContext servletContext) {
        FilterRegistration registration = servletContext.addFilter("ConversationPropagationFilter", new ConversationPropagationFilter());
        registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
    }

    /**
     * Wrap Tomcat's {@link InstanceManager} into a {@link WeldInstanceManager}
     *
     * @param context Tomcat's {@link StandardContext}
     */
    private void wrapInstanceManager(StandardContext context) {
        // Replace the AnnotationProcessor with our own version
        final InstanceManager original = context.getInstanceManager();
        final InstanceManager weldInstanceManager = new WeldInstanceManager2(manager);

        final InstanceManager forwardingInstanceManager = new WeldForwardingInstanceManager(original, weldInstanceManager);
        // Place the new processor back into the Context
        context.setInstanceManager(forwardingInstanceManager);
        // Required by the JSF Mojarra 2.0 Service to allow to perform injection into non contextual JSF managed beans
        context.getServletContext().setAttribute("org.apache.tomcat.InstanceManager", forwardingInstanceManager);
    }

    /**
     * Bind the bean manager into JNDI
     * @param initialContext
     * @param servletContext
     */
    private void bindBeanManager(InitialContext initialContext, StandardContext servletContext) {
        try {
            initialContext.rebind("java:comp/BeanManager", manager);
        } catch (NamingException e) {
            logger.error("Unable to register BeanManager into JNDI.", e);
            servletContext.setConfigured(false);
        }
    }

    private void performUglyClassLoaderHack(ClassLoader loader) {

        // TODO Fix me this is a ugly hack to force loading of some packages
        // This is required because the WebApp ClassLoader parent (indirect parent)
        // has a "DynamicImport-Package *". DIP has a something subtle: it seems the
        // wires are not created eagerly.
        // So in order to force the wire creation, we pre-load some classes to unsure
        // that the wire is done.
        try {
            loader.loadClass("org.jboss.interceptor.util.proxy.TargetInstanceProxy");
            loader.loadClass("javassist.util.proxy.ProxyObject");
            loader.loadClass(Decorator.class.getName());
            loader.loadClass(Inject.class.getName());
            loader.loadClass(Interceptor.class.getName());
            loader.loadClass(InjectionPoint.class.getName());
        } catch (ClassNotFoundException e) {
            logger.debug("Cannot load a class from {0}", loader, e);
        }
    }

    private static class WeldInstanceManager2 extends WeldInstanceManager {

        public WeldInstanceManager2(WeldManager manager) {
            super(manager);
        }
    }

}
