/**
 * 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.EnumSet;
import javax.decorator.Decorator;
import javax.enterprise.inject.spi.InjectionPoint;
import javax.faces.FactoryFinder;
import javax.faces.application.Application;
import javax.faces.application.ApplicationFactory;
import javax.faces.application.ViewHandler;
import javax.faces.lifecycle.LifecycleFactory;
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.el.WeldELContextListener;
import org.jboss.weld.environment.servlet.Listener;
import org.jboss.weld.environment.servlet.deployment.ServletDeployment;
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.jsf.ConversationAwareViewHandler;
import org.jboss.weld.jsf.WeldPhaseListener;
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.util.execution.ExecutionResult;
import org.ow2.util.execution.IExecution;
import org.ow2.util.execution.helper.RunnableHelper;
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;

    private Bootstrap bootstrap;


    private ClassLoader webappClassLoader;

    /**
     * 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) {
        IExecution<Void> execution = new IExecution<Void>() {
            public Void execute() throws Exception {
                ApplicationFactory applicationFactory = (ApplicationFactory)
                        FactoryFinder.getFactory(FactoryFinder.APPLICATION_FACTORY);
                Application application = applicationFactory.getApplication();
                if (application != null) {
                    ViewHandler currentHandler = application.getViewHandler();
                    application.setViewHandler(new ConversationAwareViewHandler(currentHandler));
                }
                LifecycleFactory factory = (LifecycleFactory) FactoryFinder.getFactory(FactoryFinder.LIFECYCLE_FACTORY);
                // Maybe the JSF implementation is not installed
                if (factory != null) {
                    javax.faces.lifecycle.Lifecycle lc = factory.getLifecycle(LifecycleFactory.DEFAULT_LIFECYCLE);
                    lc.addPhaseListener(new WeldPhaseListener());
                }
                return null;
            }
        };
        ExecutionResult<Void> result = new RunnableHelper<Void>().execute(webappClassLoader, execution);
        if (result.hasException()) {
            logger.debug("The current Webapp {0} is not JSF-enabled", context.getName(), result.getException());
        }

    }

    /**
     * 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);
        ServletDeployment deployment = new ServletDeployment(servletContext, bootstrap);

        // Provides our Application ClassLoader backed ResourceLoader
        webappClassLoader = context.getLoader().getClassLoader();
        deployment.getServices().add(ResourceLoader.class, new ClassLoaderResourceLoader(webappClassLoader));

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

        // 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(context);
        wrapInstanceManager(context);

        // Initialization process
        bootstrap.deployBeans();
        bootstrap.validateBeans();
        bootstrap.endInitialization();
    }

    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());
        }
    }

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

    private void wrapInstanceManager(StandardContext context) {
        // Replace the AnnotationProcessor with our own version
        final InstanceManager original = context.getInstanceManager();
        final InstanceManager weldInstanceManager = new WeldInstanceManager2(manager);

        // Place the new processor back into the Context
        context.setInstanceManager(new WeldForwardingInstanceManager(original, weldInstanceManager));
    }

    private void bindBeanManager(StandardContext context) {
        try {
            InitialContext initialContext = new InitialContext();
            initialContext.rebind("java:comp/BeanManager", manager);
        } catch (NamingException e) {
            logger.error("Unable to register BeanManager into JNDI.", e);
            context.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);
        }
    }

}
