/*
 * JBoss, Home of Professional Open Source
 * Copyright 2010, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.test.faces.jetty;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.EventListener;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.Servlet;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.RequestEntity;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.jboss.test.faces.ApplicationServer;
import org.jboss.test.faces.FilterHolder;
import org.jboss.test.faces.ServletHolder;
import org.jboss.test.faces.TestException;
import org.jboss.test.faces.staging.HttpConnection;
import org.jboss.test.faces.staging.ServerResourcePath;
import org.jboss.test.faces.staging.StaggingJspApplicationContext;
import org.mortbay.jetty.Handler;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.handler.DefaultHandler;
import org.mortbay.jetty.handler.HandlerList;
import org.mortbay.jetty.servlet.DefaultServlet;
import org.mortbay.jetty.webapp.WebAppContext;
import org.mortbay.resource.Resource;

/**
 * @author Nick Belaevski
 * 
 */
public class JettyServer extends ApplicationServer {

    private final class JettyConnection extends HttpConnection {
        private final URL url;
        private String requestContentType;
        private String requestEncoding="UTF-8";
        private String requestBody;
        private final HttpClient client;
        private HttpMethodBase httpClientMethod;
        private int statusCode;
        private Map<String,String> requestHeaders = new HashMap<String, String>();

        public JettyConnection(URL url) {
            client = new HttpClient();
            // TODO set default values for host, port, protocol.
            this.url = url;
        }

        @Override
        public void start() {
            throw new UnsupportedOperationException("Jetty server does not allow in-process requests");
            
        }

        @Override
        public void setRequestContentType(String contentType) {
            this.requestContentType = contentType;
        }

        @Override
        public void setRequestCharacterEncoding(String charset) throws UnsupportedEncodingException {
            this.requestEncoding = charset;
        }

        @Override
        public void setRequestBody(String body) {
            this.requestBody = body;
            
        }

        @Override
        public boolean isStarted() {
            return null != httpClientMethod;
        }

        @Override
        public boolean isFinished() {
            return null != httpClientMethod && httpClientMethod.isRequestSent();
        }

        @Override
        public int getResponseStatus() {
            return statusCode;
        }

        @Override
        public Map<String, String[]> getResponseHeaders() {
            Header[] headers = httpClientMethod.getResponseHeaders();
            HashMap<String, String[]> map = new HashMap<String, String[]>(headers.length);
            for (Header header : headers) {
                map.put(header.getName(), new String[]{header.getValue()});
            }
            return map;
        }

        @Override
        public String getResponseContentType() {
            Header responseHeader = httpClientMethod.getResponseHeader("Content-Type");
            return null!=responseHeader?responseHeader.getValue():null;
        }

        @Override
        public long getResponseContentLength() {
            return httpClientMethod.getResponseContentLength();
        }

        @Override
        public String getResponseCharacterEncoding() {
            return httpClientMethod.getResponseCharSet();
        }

        @Override
        public byte[] getResponseBody() {
            try {
                return httpClientMethod.getResponseBody();
            } catch (IOException e) {
                throw new TestException(e);
            }
        }

        @Override
        public HttpServletResponse getResponse() {
            throw new UnsupportedOperationException("Jetty server does not allow in-process requests");
        }

        @Override
        public HttpServletRequest getRequest() {
            throw new UnsupportedOperationException("Jetty server does not allow in-process requests");
        }

        @Override
        public String getErrorMessage() {
            return httpClientMethod.getStatusText();
        }


        @Override
        public String getContentAsString() {
            try {
                return httpClientMethod.getResponseBodyAsString();
            } catch (IOException e) {
                throw new TestException(e);
            }
        }

        @Override
        public void finish() {
            if(null != httpClientMethod){
                httpClientMethod.releaseConnection();
            }
            
        }

        @Override
        public void execute() {
            switch (getRequestMethod()) {
                case GET:
                    this.httpClientMethod = new GetMethod(url.toExternalForm());
                    break;
                case POST:
                    PostMethod postMethod = new PostMethod(url.toExternalForm());
                    if(null != requestBody){
                        try {
                            RequestEntity body = new StringRequestEntity(requestBody,requestContentType,requestEncoding);
                            postMethod.setRequestEntity(body);
                        } catch (UnsupportedEncodingException e) {
                            throw new TestException(e);
                        }
                    } 
                    this.httpClientMethod = postMethod;
                    break;

                default:
                    throw new UnsupportedOperationException("Http Method "+getRequestMethod()+" is not supported");
            }
            String queryString = getRequestQueryString();
            try {
                if (null != queryString) {
                    httpClientMethod.setQueryString(queryString);
                }
                for (Map.Entry<String,String> entry : requestHeaders.entrySet()) {
                    httpClientMethod.addRequestHeader(entry.getKey(), entry.getValue());
                }
                // TODO - add parameters to request.
                this.statusCode = client.executeMethod(httpClientMethod);
            } catch (IOException e) {
                throw new TestException(e);
            }
        }

        @Override
        public void addRequestHeaders(Map<String, String> headers) {
            requestHeaders.putAll(headers);
        }

        @Override
        protected String getRequestCharacterEncoding() {
            return requestEncoding;
        }

        
    }

    private static final String WEB_XML = "/WEB-INF/web.xml";

    private static final String DUMMY_WEB_XML = "org/jboss/test/faces/jetty/dummy/web.xml";
    
    private static final int DEFAULT_PORT = 8880;

    private int port;

    private Server server;

    private WebAppContext webAppContext;

    private VirtualDirectoryResource serverRoot = new VirtualDirectoryResource("");

    private HttpSession session;

    public JettyServer() {
        this(DEFAULT_PORT);
    }

    public JettyServer(int port) {
        this.port = port;
        createContext();
    }

    private ClassLoader getClassLoader() {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        if (classLoader == null) {
            classLoader = getClass().getClassLoader();
        }
        
        return classLoader;
    }
    
    private void createContext() {
        webAppContext = new WebAppContext();
        webAppContext.setContextPath("/");
        webAppContext.setBaseResource(serverRoot);
        webAppContext.setClassLoader(getClassLoader());
        
        org.mortbay.jetty.servlet.ServletHolder defaultServletHolder = 
            new org.mortbay.jetty.servlet.ServletHolder(new DefaultServlet());
        //defaultServletHolder.setInitParameter("aliases", Boolean.FALSE.toString());

        webAppContext.addServlet(defaultServletHolder, "/");
        
        webAppContext.addEventListener(new HttpSessionListener() {
            
            public void sessionDestroyed(HttpSessionEvent se) {
                session = null;
            }
            
            public void sessionCreated(HttpSessionEvent se) {
                session = se.getSession();
            }
        });
    }

    private boolean isClassAvailable(String elFactoryClass)  {
        try {
            Class.forName(elFactoryClass);
            return true;
        } catch (ClassNotFoundException e) {
            return false;
        }
    }

    @Override
    public void init() {
        server = new Server(port);

        HandlerList handlers = new HandlerList();

        handlers.setHandlers(new Handler[] { webAppContext, new DefaultHandler() });
        server.setHandler(handlers);
        // Try to register EL Expression factory explicitly, because we does not use JSF.
        if(isClassAvailable(StaggingJspApplicationContext.SUN_EXPRESSION_FACTORY)){
            addInitParameter(StaggingJspApplicationContext.FACES_EXPRESSION_FACTORY, StaggingJspApplicationContext.SUN_EXPRESSION_FACTORY);
        } else if(isClassAvailable(StaggingJspApplicationContext.JBOSS_EXPRESSION_FACTORY)){
            addInitParameter(StaggingJspApplicationContext.FACES_EXPRESSION_FACTORY, StaggingJspApplicationContext.JBOSS_EXPRESSION_FACTORY);
        }
        //JSF initialization listener requires web.xml file, so add dummy web.xml if none was registered
        Resource webXml;
        try {
            webXml = webAppContext.getResource(WEB_XML);
            if (webXml == null || !webXml.exists()) {
                URL dummyWebXml = webAppContext.getClassLoader().getResource(DUMMY_WEB_XML);
                addResource(WEB_XML, dummyWebXml);
            }
        } catch (MalformedURLException e1) {
            //ignore
        }
        
        try {
            server.start();
            server.setStopAtShutdown(true);
        } catch (Exception e) {
            throw new TestException(e.getMessage(), e);
        }
    }

    @Override
    public void destroy() {
        try {
            server.stop();
            server.setStopAtShutdown(false);
            server.destroy();
        } catch (Exception e) {
            throw new TestException(e.getMessage(), e);
        } finally {
            session = null;
            server = null;
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public void addInitParameter(String name, String value) {
        webAppContext.getInitParams().put(name, value);
    }

    @Override
    public void addWebListener(EventListener listener) {
        webAppContext.addEventListener(listener);
    }

    @Override
    public ServletContext getContext() {
        if (!webAppContext.isStarted()) {
            throw new IllegalStateException("Server should be started before getContext() can be called!");
        }
        return webAppContext.getServletContext();
    }

    @Override
    public void addMimeType(String extension, String mimeType) {
        webAppContext.getMimeTypes().addMimeMapping(extension, mimeType);
    }

    @Override
    public void addContent(String path, String content) {
        serverRoot.addResource(path, new StringContentResource(content, path));
    }

    @Override
    public void addResource(String path, String resource) {
        serverRoot.addResource(path, Resource.newClassPathResource(resource));
    }

    @Override
    public void addResource(String path, URL resource) {
        try {
            serverRoot.addResource(path, Resource.newResource(resource));
        } catch (IOException e) {
            throw new TestException(e.getMessage(), e);
        }
    }

    @Override
    public void addFilter(FilterHolder filerHolder) {
        Map<String, String> initParameters = filerHolder.getInitParameters();
        String mapping = filerHolder.getMapping();
        String name = filerHolder.getName();
        Filter filter = filerHolder.getFilter();

        org.mortbay.jetty.servlet.FilterHolder jettyFilterHolder = new org.mortbay.jetty.servlet.FilterHolder(filter);
        jettyFilterHolder.setName(name);
        jettyFilterHolder.setInitParameters(initParameters);

        webAppContext.addFilter(jettyFilterHolder, mapping, Handler.ALL);
    }

    @Override
    public void addServlet(ServletHolder servletHolder) {
        Map<String, String> initParameters = servletHolder.getInitParameters();
        String mapping = servletHolder.getMapping();
        String name = servletHolder.getName();
        Servlet servlet = servletHolder.getServlet();

        org.mortbay.jetty.servlet.ServletHolder jettyServletHolder = new org.mortbay.jetty.servlet.ServletHolder(
            servlet);
        jettyServletHolder.setName(name);
        jettyServletHolder.setInitParameters(initParameters);

        webAppContext.addServlet(jettyServletHolder, mapping);
    }
    
    @Override
    public int getPort() {
        return port;
    }
    
    @Override
    public HttpConnection getConnection(URL url) {
        return new JettyConnection(url);
    }

    @Override
    public HttpSession getSession() {
        return getSession(true);
    }

    @Override
    public HttpSession getSession(boolean create) {
        if (session == null && create) {
            throw new UnsupportedOperationException("Session creation is not supported by JettyServer");
        }
        
        return session;
    }

    @Override
    public boolean isSessionPerThread() {
        return false;
    }

    @Override
    public void setSessionPerThread(boolean sessionPerThread) {
        //do nothing
    }

    @Override
    protected void addDirectory(String directoryPath) {
        serverRoot.createChildDirectory(new ServerResourcePath(directoryPath));
    }
}
