/**
 * 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.openejb.server.httpd;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.StringTokenizer;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import org.apache.openejb.AppContext;
import org.apache.openejb.assembler.classic.WebAppBuilder;
import org.apache.openejb.cdi.Proxys;
import org.apache.openejb.core.ParentClassLoaderFinder;
import org.apache.openejb.core.WebContext;
import org.apache.openejb.loader.IO;
import org.apache.openejb.loader.SystemInstance;
import org.apache.openejb.util.AppFinder;
import org.apache.openejb.web.LightweightWebAppBuilder;


/**
 * @version $Revision$ $Date$
 */
public class HttpListenerRegistry implements HttpListener {
    private final Map<String, HttpListener> registry = new LinkedHashMap<>();
    private final Map<String, Collection<HttpListener>> filterRegistry = new LinkedHashMap<>();
    private final ThreadLocal<FilterListener> currentFilterListener = new ThreadLocal<>();
    private final ThreadLocal<HttpRequest> request = new ThreadLocal<>();
    private final ClassLoader defaultClassLoader;
    private final File[] resourceBases;
    private final Map<String, String> defaultContextTypes = new HashMap<>();
    private final String welcomeFile = SystemInstance.get().getProperty("openejb.http.welcome", "index.html");
    private final Map<String, byte[]> cache = new HashMap<>();
    private final boolean cacheResources = "true".equals(SystemInstance.get().getProperty("openejb.http.resource.cache", "false"));

    public HttpListenerRegistry() {
        HttpServletRequest mock = null;
        final SystemInstance systemInstance = SystemInstance.get();
        if ("true".equalsIgnoreCase(systemInstance.getProperty("openejb.http.mock-request", "false"))) {
            HttpRequestImpl mockRequest = null;
            try {
                mockRequest = new HttpRequestImpl(new URI("http://mock/"));
                mockRequest.parseURI(new StringTokenizer("mock\n"));  // will do http://mock/mock, we don't really care
                mock = mockRequest;
            } catch (final Exception e) {
                // no-op
            }
        }
        if (systemInstance.getComponent(HttpServletRequest.class) == null) {
            systemInstance.setComponent(HttpServletRequest.class, Proxys.threadLocalProxy(HttpServletRequest.class, request, mock));
        }
        if (systemInstance.getComponent(HttpSession.class) == null) {
            final javax.servlet.http.HttpSession delegate = mock != null ? mock.getSession() : null;
            systemInstance.setComponent(javax.servlet.http.HttpSession.class, Proxys.threadLocalRequestSessionProxy(request, new ServletSessionAdapter(delegate) {
                @Override
                public void invalidate() {
                    final Object web = AppFinder.findAppContextOrWeb(Thread.currentThread().getContextClassLoader(), AppFinder.AppOrWebContextTransformer.INSTANCE);
                    if (WebContext.class.isInstance(web)) {
                        doInvokeSpecificListeners(WebContext.class.cast(web).getContextRoot());
                    } else if (AppContext.class.isInstance(web)) {
                        doInvokeSpecificListeners(AppContext.class.cast(web).getId());
                    }
                    super.invalidate();
                }

                private void doInvokeSpecificListeners(final String web) {
                    final WebAppBuilder wab = SystemInstance.get().getComponent(WebAppBuilder.class);
                    if (LightweightWebAppBuilder.class.isInstance(wab)) {
                        final Collection<HttpSessionListener> listeners = LightweightWebAppBuilderListenerExtractor.findByTypeForContext(web, HttpSessionListener.class);
                        final HttpSessionEvent event = new HttpSessionEvent(this);
                        for (final HttpSessionListener o : listeners) {
                            try {
                                o.sessionDestroyed(event);
                            } catch (final Throwable th) {
                                // ignore, may be undeployed
                            }
                        }
                    }
                }
            }));
        }
        if (systemInstance.getComponent(ServletContext.class) == null) { // a poor impl but at least we set something
            systemInstance.setComponent(ServletContext.class, new EmbeddedServletContext());
        }

        defaultClassLoader = ParentClassLoaderFinder.Helper.get();

        String resourceFolderPaths = SystemInstance.get().getProperty("openejb.embedded.http.resources");
        Collection<File> resources = new LinkedList<>();
        if (resourceFolderPaths != null) {
            for (final String path : resourceFolderPaths.split(" , ")) {
                if (!path.isEmpty()) {
                    resources.add(new File(path));
                }
            }
        }
        resourceBases = resources.toArray(new File[resources.size()]);

        defaultContextTypes.put("html", "text/html");
        defaultContextTypes.put("html", "text/html");
        defaultContextTypes.put("css", "text/css");
        defaultContextTypes.put("txt", "text/plain");
        defaultContextTypes.put("xml", "application/xml");
        defaultContextTypes.put("xsl", "application/xml");
        defaultContextTypes.put("js", "application/javascript");
        defaultContextTypes.put("gif", "image/gif");
        defaultContextTypes.put("jpeg", "image/jpeg");
        defaultContextTypes.put("jpg", "image/jpeg");
        defaultContextTypes.put("png", "image/png");
        defaultContextTypes.put("tiff", "image/tiff");
    }

    @Override
    public void onMessage(final HttpRequest request, final HttpResponse response) throws Exception {
        final String path;
        if (!HttpRequestImpl.class.isInstance(request)) {
            path = request.getRequestURI();
        } else {
            path = getRequestHandledPath(request);
        }

        final FilterListener currentFL = currentFilterListener.get();

        // first look filters
        Map<String, Collection<HttpListener>> filters;
        synchronized (filterRegistry) {
            filters = new HashMap<>(filterRegistry);
        }

        final HttpRequest registered = this.request.get();
        final boolean reset = registered == null;
        try {
            if (reset) {
                this.request.set(request);
            }
            boolean lastWasCurrent = false;
            for (Map.Entry<String, Collection<HttpListener>> entry : filters.entrySet()) {
                String pattern = entry.getKey();
                for (HttpListener listener : entry.getValue()) {
                    if ((lastWasCurrent || currentFL == null) && path.matches(pattern)) {
                        listener.onMessage(request, response);
                        return;
                    }
                    lastWasCurrent = listener == currentFL;
                }
            }


            // then others
            Map<String, HttpListener> listeners;
            synchronized (registry) {
                listeners = new HashMap<>(registry);
            }

            boolean found = false;
            for (final Map.Entry<String, HttpListener> entry : listeners.entrySet()) {
                final String pattern = entry.getKey();
                if (path.matches(pattern) || path.equals(pattern)) {
                    if (pattern.contains("/.*\\.") && HttpRequestImpl.class.isInstance(request)) { // TODO: enhance it, basically servlet *.xxx
                        HttpRequestImpl.class.cast(request).noPathInfo();
                    }
                    entry.getValue().onMessage(request, response);
                    found = true;
                    break;
                }
            }
            if (!found) {
                final String servletPath = request.getServletPath();
                if (servletPath != null) {
                    URL url = SystemInstance.get().getComponent(ServletContext.class).getResource(servletPath);
                    if (url != null) {
                        serveResource(servletPath, response, url);
                    } else {
                        final String pathWithoutSlash = "/".equals(path) || "".equals(servletPath) || "/".equals(servletPath) ? welcomeFile :
                                (servletPath.startsWith("/") ? servletPath.substring(1) : servletPath);
                        url = defaultClassLoader.getResource("META-INF/resources/" + pathWithoutSlash);
                        if (url != null) {
                            serveResource(servletPath, response, url);
                        } else if (resourceBases.length > 0) {
                            for (final File f : resourceBases) {
                                final File file = new File(f, pathWithoutSlash);
                                if (file.isFile()) {
                                    url = file.toURI().toURL();
                                    serveResource(servletPath, response, url);
                                    break;
                                }
                            }
                        }
                    }
                    if (url != null) {
                        final int dot = servletPath.lastIndexOf('.');
                        if (dot > 0 && dot < servletPath.length() - 1) {
                            final String ext = servletPath.substring(dot + 1);
                            final String ct = defaultContextTypes.get(ext);
                            if (ct != null) {
                                response.setContentType(ct);
                            } else {
                                final String uct = SystemInstance.get().getProperty("openejb.embedded.http.content-type." + ext);
                                if (uct != null) {
                                    response.setContentType(uct);
                                }
                            }
                        }
                    }
                } // TODO else 404
            }
        } finally {
            if (currentFL == null) {
                currentFilterListener.set(null);
            }
            if (reset) {
                this.request.set(null);
            }
        }
    }

    private void serveResource(final String key, final HttpResponse response, final URL url) throws IOException {
        if (cacheResources) {
            byte[] value = cache.get(key);
            if (value == null) {
                final InputStream from = url.openStream();
                try {
                    ByteArrayOutputStream to = new ByteArrayOutputStream();
                    IO.copy(from, to);
                    value = to.toByteArray();
                    cache.put(key, value);
                } finally {
                    IO.close(from);
                }
            }
            response.getOutputStream().write(value);
        } else {
            final InputStream from = url.openStream();
            try {
                IO.copy(from, response.getOutputStream());
            } finally {
                IO.close(from);
            }
        }
    }

    private String getRequestHandledPath(final HttpRequest request) {
        final String servletPath = request.getServletPath();
        return request.getContextPath() + (!servletPath.startsWith("/") ? "/" : "") + servletPath;
    }

    public void addHttpListener(HttpListener listener, String regex) {
        synchronized (registry) {
            registry.put(regex, listener);
        }
    }

    public HttpListener removeHttpListener(String regex) {
        HttpListener listener;
        synchronized (registry) {
            listener = registry.remove(regex);
        }
        return listener;
    }

    public void addHttpFilter(HttpListener listener, String regex) {
        synchronized (filterRegistry) {
            if (!filterRegistry.containsKey(regex)) {
                filterRegistry.put(regex, new ArrayList<HttpListener>());
            }
            filterRegistry.get(regex).add(listener);
        }
    }

    public Collection<HttpListener> removeHttpFilter(String regex) {
        synchronized (filterRegistry) {
            return filterRegistry.remove(regex);
        }
    }

    public void setOrigin(final FilterListener origin) {
        if (origin == null) {
            currentFilterListener.remove();
        } else {
            currentFilterListener.set(origin);
        }
    }
}
