/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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.apache.webbeans.web.tomcat;

import org.apache.AnnotationProcessor;
import org.apache.catalina.Container;
import org.apache.catalina.ContainerEvent;
import org.apache.catalina.ContainerListener;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Service;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.core.ContainerBase;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardEngine;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.core.StandardServer;
import org.apache.naming.ContextAccessController;
import org.apache.webbeans.servlet.WebBeansConfigurationListener;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

import javax.servlet.ServletContext;

/**
 * Context lifecycle listener. Adapted from
 * OpenEJB Tomcat and updated.
 * 
 * @version $Rev$ $Date$
 *
 */
public class ContextLifecycleListener implements PropertyChangeListener, LifecycleListener, ContainerListener
{

    private StandardServer standardServer;

    public ContextLifecycleListener()
    {
        System.out.println("MSX TODO REMOVE ContextLifecycleListener invoked");
    }

    @Override
    public void lifecycleEvent(LifecycleEvent event)
    {
        try
        {
            if (event.getSource() instanceof StandardServer)
            {
                if (event.getType().equals(Lifecycle.START_EVENT))
                {
                    this.standardServer = (StandardServer) event.getSource();
                    start();
                }
            }
            else if (event.getSource() instanceof StandardContext)
            {
                StandardContext context = (StandardContext) event.getSource();
                
                if (event.getType().equals(Lifecycle.START_EVENT))
                {
                    ServletContext scontext = context.getServletContext();
                    URL url = getBeansXml(scontext);
                    if (url != null)
                    {
                        //Registering ELResolver with JSP container
                        System.setProperty("org.apache.webbeans.application.jsp", "true");
                        
                        String[] oldListeners = context.findApplicationListeners();
                        LinkedList<String> listeners = new LinkedList<String>();
                        listeners.addFirst(WebBeansConfigurationListener.class.getName());
                        for(String listener : oldListeners)
                        {
                            listeners.add(listener);
                            context.removeApplicationListener(listener);
                        }
                        
                        for(String listener : listeners)
                        {
                            context.addApplicationListener(listener);
                        }                        
                        
                        context.addApplicationListener(TomcatSecurityListener.class.getName());
                        context.addContainerListener(this);

                        //context.addInstanceListener(TomcatInstanceListener.class.getName());
                    }
                }
            }                        
        }
        catch(Exception e)
        {
            throw new RuntimeException(e);
        }        
    }

    @Override
    public void containerEvent(ContainerEvent event)
    {
        StandardContext context = null;
        
        try
        {
            if(event.getSource() instanceof StandardContext)
            {               
                context = (StandardContext)event.getSource();
                
                if(event.getType().equals("beforeContextInitialized"))
                {
                    ClassLoader loader = context.getLoader().getClassLoader();
                    Object listener = event.getData();
                    
                    if(listener.getClass().getName().equals(WebBeansConfigurationListener.class.getName()))
                    {
                       ContextAccessController.setWritable(context.getNamingContextListener().getName(), context);                       
                       return;
                    }
                    else
                    {
                        URL url = getBeansXml(context.getServletContext());
                        if(url != null)
                        {
                            TomcatUtil.inject(listener, loader);   
                        }
                    }
                    
                }
                else if(event.getType().equals("afterContextInitialized"))
                {
                    ClassLoader loader = context.getLoader().getClassLoader();
                    Object listener = event.getData();
                    
                    if(listener.getClass().getName().equals(WebBeansConfigurationListener.class.getName()))
                    {   
                        AnnotationProcessor processor = context.getAnnotationProcessor();
                        AnnotationProcessor custom = new TomcatAnnotProcessor(context.getLoader().getClassLoader(),processor);
                        context.setAnnotationProcessor(custom);
                        
                        context.getServletContext().setAttribute(AnnotationProcessor.class.getName(), custom);
                        
                        ContextAccessController.setReadOnly(context.getNamingContextListener().getName());
                        
                        URL url = getBeansXml(context.getServletContext());
                        if(url != null)
                        {
                            Object[] listeners = context.getApplicationEventListeners();
                            for(Object instance : listeners)
                            {
                                if(!instance.getClass().getName().equals(WebBeansConfigurationListener.class.getName()))
                                {                                
                                    TomcatUtil.inject(instance, loader);   
                                }
                            }                                                                                    
                        }                        
                    }                                
                }
                else if(event.getType().equals("beforeContextDestroyed"))
                {
                    Object listener = event.getData();
                    if(listener.getClass().getName().equals(WebBeansConfigurationListener.class.getName()))
                    {
                        ContextAccessController.setWritable(context.getNamingContextListener().getName(),context);   
                    }
                }
            }        
            
        }
        catch(Exception e)
        {
            throw new RuntimeException(e);
        }
    }
    
    public void start()
    {
        // hook the hosts so we get notified before contexts are started
        standardServer.addPropertyChangeListener(this);
        standardServer.addLifecycleListener(this);
        for (Service service : standardServer.findServices())
        {
            serviceAdded(service);
        }
    }

    public void stop()
    {
        standardServer.removePropertyChangeListener(this);
    }

    private void serviceAdded(Service service)
    {
        Container container = service.getContainer();
        if (container instanceof StandardEngine)
        {
            StandardEngine engine = (StandardEngine) container;
            engineAdded(engine);
        }
    }

    private void engineAdded(StandardEngine engine)
    {
        addContextListener(engine);
        for (Container child : engine.findChildren())
        {
            if (child instanceof StandardHost)
            {
                StandardHost host = (StandardHost) child;
                hostAdded(host);
            }
        }
    }


    private void hostAdded(StandardHost host)
    {
        addContextListener(host);
        host.addLifecycleListener(this);
        for (Container child : host.findChildren())
        {
            if (child instanceof StandardContext)
            {
                StandardContext context = (StandardContext) child;
                contextAdded(context);
            }
        }
    }


    private void contextAdded(StandardContext context)
    {
        // put this class as the first listener so we can process the
        // application before any classes are loaded
        forceFirstLifecycleListener(context);
    }

    private void forceFirstLifecycleListener(StandardContext context)
    {
        LifecycleListener[] listeners = context.findLifecycleListeners();

        // if we are already first return
        if (listeners.length > 0 && listeners[0] == this)
        {
            return;
        }

        // remove all of the current listeners
        for (LifecycleListener listener : listeners)
        {
            context.removeLifecycleListener(listener);
        }

        // add this class (as first)
        context.addLifecycleListener(this);
        context.addContainerListener(this);

        // add back all listeners
        for (LifecycleListener listener : listeners)
        {
            if (listener != this)
            {
                context.addLifecycleListener(listener);
            }
        }
    }


    @Override
    public void propertyChange(PropertyChangeEvent event)
    {
        if ("service".equals(event.getPropertyName()))
        {
            Object oldValue = event.getOldValue();
            Object newValue = event.getNewValue();
            if (oldValue == null && newValue instanceof Service)
            {
                serviceAdded((Service) newValue);
            }
        }
        if ("children".equals(event.getPropertyName()))
        {
            Object source = event.getSource();
            Object oldValue = event.getOldValue();
            Object newValue = event.getNewValue();
            if (source instanceof StandardEngine)
            {
                if (oldValue == null && newValue instanceof StandardHost)
                {
                    hostAdded((StandardHost) newValue);
                }
            }
            if (source instanceof StandardHost)
            {
                if (oldValue == null && newValue instanceof StandardContext)
                {
                    contextAdded((StandardContext) newValue);
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void addContextListener(ContainerBase containerBase)
    {
        try
        {
            Field field = (Field)AccessController.doPrivileged(new PrivilegedActionForClass(ContainerBase.class, "children"));
            AccessController.doPrivileged(new PrivilegedActionForAccessibleObject(field, true));
            Map<Object,Object> children = (Map<Object,Object>) field.get(containerBase);
            if (children instanceof ContextLifecycleListener.MoniterableHashMap)
            {
                return;
            }
            children = new ContextLifecycleListener.MoniterableHashMap(children, containerBase, "children", this);
            field.set(containerBase, children);
        }
        catch (Exception e)
        {
            throw new RuntimeException(e);
        }

    }

    public static class MoniterableHashMap extends HashMap<Object,Object>
    {
        private static final long serialVersionUID = 1L;

        private final Object source;
        private final String propertyName;
        private final PropertyChangeListener listener;

        public MoniterableHashMap(Map<Object,Object> m, Object source, String propertyName, PropertyChangeListener listener)
        {
            super(m);
            this.source = source;
            this.propertyName = propertyName;
            this.listener = listener;
        }

        @Override
        public Object put(Object key, Object value)
        {
            Object oldValue = super.put(key, value);
            PropertyChangeEvent event = new PropertyChangeEvent(source, propertyName, null, value);
            listener.propertyChange(event);
            return oldValue;
        }

        @Override
        public Object remove(Object key)
        {
            Object value = super.remove(key);
            PropertyChangeEvent event = new PropertyChangeEvent(source, propertyName, value, null);
            listener.propertyChange(event);
            return value;
        }
    }

    private URL getBeansXml(ServletContext scontext) throws MalformedURLException
    {
        URL url = scontext.getResource("/WEB-INF/beans.xml");
        if (url == null)
        {
            url = scontext.getResource("/WEB-INF/classes/META-INF/beans.xml");
        }
        return url;
    }


    protected static class PrivilegedActionForAccessibleObject implements PrivilegedAction<Object>
    {

        AccessibleObject object;

        boolean flag;

        protected PrivilegedActionForAccessibleObject(AccessibleObject object, boolean flag)
        {
            this.object = object;
            this.flag = flag;
        }

        @Override
        public Object run()
        {
            object.setAccessible(flag);
            return null;
        }
    }

    protected static class PrivilegedActionForClass implements PrivilegedAction<Object>
    {
        Class<?> clazz;

        Object parameters;

        protected PrivilegedActionForClass(Class<?> clazz, Object parameters)
        {
            this.clazz = clazz;
            this.parameters = parameters;
        }

        @Override
        public Object run()
        {
            try
            {
                return clazz.getDeclaredField((String)parameters);
            }
            catch (NoSuchFieldException e)
            {
                throw new RuntimeException(e);
            }
        }

    }

    
}
