package ru.yandex.qe.jetty.server;

import java.util.Arrays;
import java.util.Set;

import javax.annotation.PostConstruct;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebServlet;

import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlet.ServletMapping;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.XmlWebApplicationContext;

import ru.yandex.qe.spring.AnnotationScanner;

/**
 * Scans classpath for @WebServlet and @WebFilter annotations and adds servlets and filters.
 * This is a replacement for J2EE application functionality. According to J2EE spec,
 * WEB-INF/classes and all jars in WEB-INF/lib must be scanned; in non-J2EE environment we
 * do not have these directories. Instead, we are using package name to scan inside
 * (like Spring component-scan) to limit the classpath scope.
 * It creates a WebApplicationContext and bounds it to a ServerContext
 * using {@link ContextLoaderListener}.
 * This class does not process  ServletContainerInitializer services
 * (and hence any WebApplicationInitializer descendants on the classpath)
 * @author lvovich
 */
public class AnnotationServletContextHandler extends ServletContextHandler implements ApplicationContextAware {

    private final static Logger LOG = LoggerFactory.getLogger(AnnotationServletContextHandler.class);

    private ApplicationContext applicationContext;

    private boolean useTestPlainErrorHandler = true;

    public AnnotationServletContextHandler() {
        _scontext.setExtendedListenerTypes(true);
    }

    public AnnotationServletContextHandler(final int options) {
        super(options);
        _scontext.setExtendedListenerTypes(true);
    }

    public void setApplicationContext(final ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    private String basePackage;

    public void setBasePackage(final String basePackage) {
        this.basePackage = basePackage;
    }

    @PostConstruct
    public void init() {
        addAnnotatedServletsAndFilters();
    }

    @Override
    protected void startContext() throws Exception {
        final XmlWebApplicationContext webContext = new XmlWebApplicationContext();
        // do not leave null: it means default (like "/WEB-INF/applicationContext.xml")
        // set explicitly to empty array
        webContext.setConfigLocations(new String[0]);
        webContext.setParent(applicationContext);
        getServletContext().addListener(new ContextLoaderListener(webContext));
        super.startContext();
    }

    private void addAnnotatedServletsAndFilters() {
        final AnnotationScanner scanner = new AnnotationScanner(WebServlet.class, WebFilter.class);
        final Set<Class<?>> annotatedClasses = scanner.findAnnotatedClasses(basePackage);
        final ServletHandler servletHandler = getServletHandler();
        for (final Class<?> clazz : annotatedClasses) {
            if (clazz.getAnnotation(WebServlet.class) != null) {
                final AnnotationConfiguredServlet configuration = AnnotationConfiguredServlet.construct(clazz);
                if (configuration == null) {
                    // misconfigured WebServlet
                    LOG.warn("Misconfigured servlet: {}; see messages above", clazz.getName());
                    continue;
                }
                final ServletHolder holder = configuration.getServletHolder();
                    // do not override explicitly added servlets with the same name
                if (servletHandler.getServlet(holder.getName()) == null) {
                    servletHandler.addServlet(holder);
                    final ServletMapping mapping = configuration.getServletMapping();
                    servletHandler.addServletMapping(mapping);
                    LOG.info("Servlet {} added to context, mapped to {}", clazz.getName(), Arrays.toString(mapping.getPathSpecs()));
                } else {
                    LOG.warn("Servlet {} NOT added to context: name {} already registered", clazz.getName(), holder.getName());
                }
            } else if (clazz.getAnnotation(WebFilter.class) != null) {
                final AnnotationConfiguredFilter configuration = AnnotationConfiguredFilter.construct(clazz);
                if (configuration == null) {
                    continue;
                }
                final FilterHolder holder = configuration.getFilterHolder();
                if (servletHandler.getFilter(holder.getName()) == null) {
                    servletHandler.addFilter(holder);
                    servletHandler.addFilterMapping(configuration.getFilterMapping());
                }
            } else {
                throw new IllegalStateException("Unexpected class: " + clazz);
            }
        }
        setErrorHandler(useTestPlainErrorHandler ? new TextPlainErrorHandler() : new ErrorHandler());
    }

    public void setUseTestPlainErrorHandler(final boolean useTestPlainErrorHandler) {
        this.useTestPlainErrorHandler = useTestPlainErrorHandler;
    }
}
