/*******************************************************************************
 * Copyright (c) 2008, 2010 VMware Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   VMware Inc. - initial contribution
 *******************************************************************************/

package org.eclipse.virgo.snaps.core.internal.webapp.container;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;

import org.eclipse.virgo.snaps.core.internal.SnapException;
import org.eclipse.virgo.snaps.core.internal.webapp.ImmutableServletConfig;
import org.eclipse.virgo.snaps.core.internal.webapp.SnapServletContext;
import org.eclipse.virgo.snaps.core.internal.webapp.config.ServletDefinition;
import org.eclipse.virgo.snaps.core.internal.webapp.config.ServletMappingDefinition;
import org.eclipse.virgo.snaps.core.internal.webapp.config.WebXml;
import org.eclipse.virgo.snaps.core.internal.webapp.container.ManagerUtils.ClassLoaderCallback;
import org.eclipse.virgo.snaps.core.internal.webapp.url.Mapping;
import org.eclipse.virgo.snaps.core.internal.webapp.url.UrlPatternMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * TODO Document ServletManager
 * <p />
 * 
 * <strong>Concurrent Semantics</strong><br />
 * 
 * TODO Document concurrent semantics of ServletManager
 * 
 */
final class ServletManager {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private final SnapServletContext snapServletContext;

    private final ClassLoader classLoader;

    private final UrlPatternMatcher patternMatcher = new UrlPatternMatcher();

    private final Map<String, ServletHolder> servlets = new ConcurrentHashMap<String, ServletHolder>();

    ServletManager(WebXml webXml, SnapServletContext snapServletContext, ClassLoader classLoader) {
        this.snapServletContext = snapServletContext;
        this.classLoader = classLoader;

        reifyWebXml(webXml);
    }

    void init() throws ServletException {
        try {
            ManagerUtils.doWithThreadContextClassLoader(this.classLoader, new ClassLoaderCallback<Void>() {

                public Void doWithClassLoader() throws ServletException {
                    for (Map.Entry<String, ServletHolder> entry : servlets.entrySet()) {
                        ServletHolder holder = entry.getValue();
                        ServletConfig config = new ImmutableServletConfig(holder.getDefinition(), snapServletContext);
                        holder.getInstance().init(config);
                        // TODO Log which servlet failed, and re-throw
                    }
                    return null;
                }
            });
        } catch (IOException e) {
            logger.error("Unexpected IOException from servlet init", e);
            throw new ServletException("Unexpected IOException from servlet init", e);
        }
    }

    void destroy() {
        for (Map.Entry<String, ServletHolder> entry : this.servlets.entrySet()) {
            ServletHolder holder = entry.getValue();
            holder.getInstance().destroy();
        }
        this.servlets.clear();
    }

    Match findMatch(String servletPath) {
        Mapping mapping = this.patternMatcher.match(servletPath);
        if (mapping != null) {
            ServletHolder servletHolder = this.servlets.get(mapping.getName());
            return new Match(servletHolder.getInstance(), mapping);
        } else {
            return null;
        }
    }

    private void reifyWebXml(WebXml webXml) {
        processServlets(webXml);
        processServletMappingDefinitions(webXml);
    }

    private void processServlets(WebXml webXml) throws SnapException {
        for (ServletDefinition servletDefinition : webXml.getServletDefinitions()) {
            try {
                Class<?> servletClass = ManagerUtils.loadComponentClass(servletDefinition.getServletClassName(), this.classLoader);
                Servlet servlet = (Servlet) servletClass.newInstance();
                this.servlets.put(servletDefinition.getServletName(), new ServletHolder(servletDefinition, servlet));
            } catch (ClassNotFoundException e) {
                logger.error(String.format("The servlet class '%s' could not be loaded by '%s'", servletDefinition.getServletClassName(),
                    this.classLoader), e);
                throw new SnapException("The servlet class '" + servletDefinition.getServletClassName() + "' could not be loaded by "
                    + this.classLoader, e);
            } catch (InstantiationException e) {
                logger.error(String.format("The servlet class '%s' could not be instantiated", servletDefinition.getServletClassName()), e);
                throw new SnapException("The servlet class '" + servletDefinition.getServletClassName() + "' could not be instantiated", e);
            } catch (IllegalAccessException e) {
                logger.error(String.format("The servlet class '%s' could not be instantiated due to access restrictions",
                    servletDefinition.getServletClassName()), e);
                throw new SnapException("The servlet class '" + servletDefinition.getServletClassName()
                    + "' could not be instantiated due to access restrictions", e);
            }
        }
    }

    private void processServletMappingDefinitions(WebXml webXml) {
        for (ServletMappingDefinition mappingDefinition : webXml.getServletMappingDefinitions()) {
            this.patternMatcher.addMapping(mappingDefinition.getServletName(), ManagerUtils.expandMapping(mappingDefinition.getUrlPattern(),
                this.snapServletContext));
            // TODO Validate, probably in WebXml, the referenced servlets exist, etc.
        }
    }

    static class Match {

        private final Servlet servlet;

        private final Mapping mapping;

        public Match(Servlet servlet, Mapping mapping) {
            this.servlet = servlet;
            this.mapping = mapping;
        }

        /**
         * @return
         */
        public Servlet getServlet() {
            return servlet;
        }

        /**
         * @return
         */
        public Mapping getMapping() {
            return mapping;
        }
    }
}
