/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * 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 io.undertow.servlet.core;

import io.undertow.servlet.UndertowServletLogger;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextAttributeEvent;
import javax.servlet.ServletContextAttributeListener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestAttributeEvent;
import javax.servlet.ServletRequestAttributeListener;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionIdListener;
import javax.servlet.http.HttpSessionListener;
import java.util.ArrayList;
import java.util.List;

import static io.undertow.servlet.core.ApplicationListeners.ListenerState.DECLARED_LISTENER;
import static io.undertow.servlet.core.ApplicationListeners.ListenerState.PROGRAMATIC_LISTENER;

/**
 * Class that is responsible for invoking application listeners.
 * <p>
 * This class does not perform any context setup, the context must be setup
 * before invoking this class.
 * <p>
 * Note that arrays are used instead of lists for performance reasons.
 *
 * @author Stuart Douglas
 */
public class ApplicationListeners implements Lifecycle {


    private static final ManagedListener[] EMPTY = {};

    private static final Class[] LISTENER_CLASSES = {ServletContextListener.class,
            ServletContextAttributeListener.class,
            ServletRequestListener.class,
            ServletRequestAttributeListener.class,
            javax.servlet.http.HttpSessionListener.class,
            javax.servlet.http.HttpSessionAttributeListener.class,
            HttpSessionIdListener.class};

    private static final ThreadLocal<ListenerState> IN_PROGRAMATIC_SC_LISTENER_INVOCATION = new ThreadLocal<ListenerState>() {
        @Override
        protected ListenerState initialValue() {
            return ListenerState.NO_LISTENER;
        }
    };

    private ServletContext servletContext;
    private final List<ManagedListener> allListeners = new ArrayList<>();
    private ManagedListener[] servletContextListeners;
    private ManagedListener[] servletContextAttributeListeners;
    private ManagedListener[] servletRequestListeners;
    private ManagedListener[] servletRequestAttributeListeners;
    private ManagedListener[] httpSessionListeners;
    private ManagedListener[] httpSessionAttributeListeners;
    private ManagedListener[] httpSessionIdListeners;
    private volatile boolean started = false;

    public ApplicationListeners(final List<ManagedListener> allListeners, final ServletContext servletContext) {
        this.servletContext = servletContext;
        servletContextListeners = EMPTY;
        servletContextAttributeListeners = EMPTY;
        servletRequestListeners = EMPTY;
        servletRequestAttributeListeners = EMPTY;
        httpSessionListeners = EMPTY;
        httpSessionAttributeListeners = EMPTY;
        httpSessionIdListeners = EMPTY;
        for (final ManagedListener listener : allListeners) {
            addListener(listener);
        }
    }

    public void addListener(final ManagedListener listener) {
        if (ServletContextListener.class.isAssignableFrom(listener.getListenerInfo().getListenerClass())) {
            ManagedListener[] old = servletContextListeners;
            servletContextListeners = new ManagedListener[old.length + 1];
            System.arraycopy(old, 0, servletContextListeners, 0, old.length);
            servletContextListeners[old.length] = listener;
        }
        if (ServletContextAttributeListener.class.isAssignableFrom(listener.getListenerInfo().getListenerClass())) {

            ManagedListener[] old = servletContextAttributeListeners;
            servletContextAttributeListeners = new ManagedListener[old.length + 1];
            System.arraycopy(old, 0, servletContextAttributeListeners, 0, old.length);
            servletContextAttributeListeners[old.length] = listener;
        }
        if (ServletRequestListener.class.isAssignableFrom(listener.getListenerInfo().getListenerClass())) {
            ManagedListener[] old = servletRequestListeners;
            servletRequestListeners = new ManagedListener[old.length + 1];
            System.arraycopy(old, 0, servletRequestListeners, 0, old.length);
            servletRequestListeners[old.length] = listener;
        }
        if (ServletRequestAttributeListener.class.isAssignableFrom(listener.getListenerInfo().getListenerClass())) {
            ManagedListener[] old = servletRequestAttributeListeners;
            servletRequestAttributeListeners = new ManagedListener[old.length + 1];
            System.arraycopy(old, 0, servletRequestAttributeListeners, 0, old.length);
            servletRequestAttributeListeners[old.length] = listener;
        }
        if (HttpSessionListener.class.isAssignableFrom(listener.getListenerInfo().getListenerClass())) {
            ManagedListener[] old = httpSessionListeners;
            httpSessionListeners = new ManagedListener[old.length + 1];
            System.arraycopy(old, 0, httpSessionListeners, 0, old.length);
            httpSessionListeners[old.length] = listener;
        }
        if (HttpSessionAttributeListener.class.isAssignableFrom(listener.getListenerInfo().getListenerClass())) {
            ManagedListener[] old = httpSessionAttributeListeners;
            httpSessionAttributeListeners = new ManagedListener[old.length + 1];
            System.arraycopy(old, 0, httpSessionAttributeListeners, 0, old.length);
            httpSessionAttributeListeners[old.length] = listener;
        }
        if (HttpSessionIdListener.class.isAssignableFrom(listener.getListenerInfo().getListenerClass())) {
            ManagedListener[] old = httpSessionIdListeners;
            httpSessionIdListeners = new ManagedListener[old.length + 1];
            System.arraycopy(old, 0, httpSessionIdListeners, 0, old.length);
            httpSessionIdListeners[old.length] = listener;
        }
        this.allListeners.add(listener);
        if(started) {
            try {
                listener.start();
            } catch (ServletException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public void start() throws ServletException {
        started = true;
        for (ManagedListener listener : allListeners) {
            listener.start();
        }
    }

    public void stop() {
        if (started) {
            started = false;
            for (final ManagedListener listener : allListeners) {
                listener.stop();
            }
        }
    }

    @Override
    public boolean isStarted() {
        return started;
    }

    public void contextInitialized() {
        if(!started) {
            return;
        }
        //new listeners can be added here, so we don't use an iterator
        final ServletContextEvent event = new ServletContextEvent(servletContext);
        for (int i = 0; i < servletContextListeners.length; ++i) {
            ManagedListener listener = servletContextListeners[i];
            IN_PROGRAMATIC_SC_LISTENER_INVOCATION.set(listener.isProgramatic() ? PROGRAMATIC_LISTENER : DECLARED_LISTENER);
            try {
                this.<ServletContextListener>get(listener).contextInitialized(event);
            } finally {
                IN_PROGRAMATIC_SC_LISTENER_INVOCATION.remove();
            }
        }
    }

    public void contextDestroyed() {
        if(!started) {
            return;
        }
        final ServletContextEvent event = new ServletContextEvent(servletContext);
        for (int i = servletContextListeners.length - 1; i >= 0; --i) {
            ManagedListener listener = servletContextListeners[i];
            try {
                this.<ServletContextListener>get(listener).contextDestroyed(event);
            } catch (Throwable t) {
                UndertowServletLogger.REQUEST_LOGGER.errorInvokingListener("contextDestroyed", listener.getListenerInfo().getListenerClass(), t);
            }
        }
    }

    public void servletContextAttributeAdded(final String name, final Object value) {
        if(!started) {
            return;
        }
        final ServletContextAttributeEvent sre = new ServletContextAttributeEvent(servletContext, name, value);
        for (int i = 0; i < servletContextAttributeListeners.length; ++i) {
            this.<ServletContextAttributeListener>get(servletContextAttributeListeners[i]).attributeAdded(sre);
        }
    }

    public void servletContextAttributeRemoved(final String name, final Object value) {
        if(!started) {
            return;
        }
        final ServletContextAttributeEvent sre = new ServletContextAttributeEvent(servletContext, name, value);
        for (int i = 0; i < servletContextAttributeListeners.length; ++i) {
            this.<ServletContextAttributeListener>get(servletContextAttributeListeners[i]).attributeRemoved(sre);
        }
    }

    public void servletContextAttributeReplaced(final String name, final Object value) {
        if(!started) {
            return;
        }
        final ServletContextAttributeEvent sre = new ServletContextAttributeEvent(servletContext, name, value);
        for (int i = 0; i < servletContextAttributeListeners.length; ++i) {
            this.<ServletContextAttributeListener>get(servletContextAttributeListeners[i]).attributeReplaced(sre);
        }
    }

    public void requestInitialized(final ServletRequest request) {
        if(!started) {
            return;
        }
        if(servletRequestListeners.length > 0) {
            int i = 0;
            final ServletRequestEvent sre = new ServletRequestEvent(servletContext, request);
            try {
                for (; i < servletRequestListeners.length; ++i) {
                    this.<ServletRequestListener>get(servletRequestListeners[i]).requestInitialized(sre);
                }
            } catch (RuntimeException e) {
                UndertowServletLogger.REQUEST_LOGGER.errorInvokingListener("requestInitialized", servletRequestListeners[i].getListenerInfo().getListenerClass(), e);
                for (; i >= 0; i--) {
                    try {
                        this.<ServletRequestListener>get(servletRequestListeners[i]).requestDestroyed(sre);
                    } catch (Throwable t) {
                        UndertowServletLogger.REQUEST_LOGGER.errorInvokingListener("requestDestroyed", servletRequestListeners[i].getListenerInfo().getListenerClass(), e);
                    }
                }
                throw e;
            }
        }
    }

    public void requestDestroyed(final ServletRequest request) {
        if(!started) {
            return;
        }
        if(servletRequestListeners.length > 0) {
            final ServletRequestEvent sre = new ServletRequestEvent(servletContext, request);
            for (int i = servletRequestListeners.length - 1; i >= 0; --i) {
                ManagedListener listener = servletRequestListeners[i];
                try {
                    this.<ServletRequestListener>get(listener).requestDestroyed(sre);
                } catch (Exception e) {
                    UndertowServletLogger.REQUEST_LOGGER.errorInvokingListener("requestDestroyed", listener.getListenerInfo().getListenerClass(), e);
                }
            }
        }
    }

    public void servletRequestAttributeAdded(final HttpServletRequest request, final String name, final Object value) {
        if(!started) {
            return;
        }
        final ServletRequestAttributeEvent sre = new ServletRequestAttributeEvent(servletContext, request, name, value);
        for (int i = 0; i < servletRequestAttributeListeners.length; ++i) {
            this.<ServletRequestAttributeListener>get(servletRequestAttributeListeners[i]).attributeAdded(sre);
        }
    }

    public void servletRequestAttributeRemoved(final HttpServletRequest request, final String name, final Object value) {
        if(!started) {
            return;
        }
        final ServletRequestAttributeEvent sre = new ServletRequestAttributeEvent(servletContext, request, name, value);
        for (int i = 0; i < servletRequestAttributeListeners.length; ++i) {
            this.<ServletRequestAttributeListener>get(servletRequestAttributeListeners[i]).attributeRemoved(sre);
        }
    }

    public void servletRequestAttributeReplaced(final HttpServletRequest request, final String name, final Object value) {
        if(!started) {
            return;
        }
        final ServletRequestAttributeEvent sre = new ServletRequestAttributeEvent(servletContext, request, name, value);
        for (int i = 0; i < servletRequestAttributeListeners.length; ++i) {
            this.<ServletRequestAttributeListener>get(servletRequestAttributeListeners[i]).attributeReplaced(sre);
        }
    }

    public void sessionCreated(final HttpSession session) {
        if(!started) {
            return;
        }
        final HttpSessionEvent sre = new HttpSessionEvent(session);
        for (int i = 0; i < httpSessionListeners.length; ++i) {
            this.<HttpSessionListener>get(httpSessionListeners[i]).sessionCreated(sre);
        }
    }

    public void sessionDestroyed(final HttpSession session) {
        if(!started) {
            return;
        }
        final HttpSessionEvent sre = new HttpSessionEvent(session);
        for (int i = httpSessionListeners.length - 1; i >= 0; --i) {
            ManagedListener listener = httpSessionListeners[i];
            this.<HttpSessionListener>get(listener).sessionDestroyed(sre);
        }
    }

    public void httpSessionAttributeAdded(final HttpSession session, final String name, final Object value) {
        if(!started) {
            return;
        }
        final HttpSessionBindingEvent sre = new HttpSessionBindingEvent(session, name, value);
        for (int i = 0; i < httpSessionAttributeListeners.length; ++i) {
            this.<HttpSessionAttributeListener>get(httpSessionAttributeListeners[i]).attributeAdded(sre);
        }
    }

    public void httpSessionAttributeRemoved(final HttpSession session, final String name, final Object value) {
        if(!started) {
            return;
        }
        final HttpSessionBindingEvent sre = new HttpSessionBindingEvent(session, name, value);
        for (int i = 0; i < httpSessionAttributeListeners.length; ++i) {
            this.<HttpSessionAttributeListener>get(httpSessionAttributeListeners[i]).attributeRemoved(sre);
        }
    }

    public void httpSessionAttributeReplaced(final HttpSession session, final String name, final Object value) {
        if(!started) {
            return;
        }
        final HttpSessionBindingEvent sre = new HttpSessionBindingEvent(session, name, value);
        for (int i = 0; i < httpSessionAttributeListeners.length; ++i) {
            this.<HttpSessionAttributeListener>get(httpSessionAttributeListeners[i]).attributeReplaced(sre);
        }
    }

    public void httpSessionIdChanged(final HttpSession session, final String oldSessionId) {
        if(!started) {
            return;
        }
        final HttpSessionEvent sre = new HttpSessionEvent(session);
        for (int i = 0; i < httpSessionIdListeners.length; ++i) {
            this.<HttpSessionIdListener>get(httpSessionIdListeners[i]).sessionIdChanged(sre, oldSessionId);
        }
    }

    private <T> T get(final ManagedListener listener) {
        return (T) listener.instance();
    }

    /**
     * returns true if this is in in a
     */
    public static ListenerState listenerState() {
        return IN_PROGRAMATIC_SC_LISTENER_INVOCATION.get();
    }

    /**
     * @param clazz The potential listener class
     * @return true if the provided class is a valid listener class
     */
    public static boolean isListenerClass(final Class<?> clazz) {
        for (Class c : LISTENER_CLASSES) {
            if (c.isAssignableFrom(clazz)) {
                return true;
            }
        }
        return false;
    }

    public enum ListenerState {
        NO_LISTENER,
        DECLARED_LISTENER,
        PROGRAMATIC_LISTENER,
    }

}
