/*
 * 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.axis2.transport.http.server;

import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.description.TransportInDescription;
import org.apache.axis2.engine.ListenerManager;
import org.apache.axis2.transport.http.HTTPWorkerFactory;

import org.apache.hc.core5.http.ConnectionReuseStrategy;
import org.apache.hc.core5.http.config.Http1Config;
import org.apache.hc.core5.http.impl.DefaultConnectionReuseStrategy;
import org.apache.hc.core5.http.impl.io.DefaultClassicHttpResponseFactory;
import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.http.protocol.HttpProcessor;
import org.apache.hc.core5.http.protocol.HttpProcessorBuilder;
import org.apache.hc.core5.http.protocol.ResponseConnControl;
import org.apache.hc.core5.http.protocol.ResponseContent;
import org.apache.hc.core5.http.protocol.ResponseDate;
import org.apache.hc.core5.http.protocol.ResponseServer;

import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * Factory used to configure and create the various instances required in http transports.
 * Either configure this class in axis2.xml, or in code via the setters, or subclass it and specialize some factory methods to gain more control.
 */
public class HttpFactory {

    /**
     * Name of axis2.xml port parameter for SimpleHTTPServer configuration
     */
    public static final String PARAMETER_PORT = "port";

    /**
     * Name of axis2.xml hostname parameter for SimpleHTTPServer configuration
     */
    public static final String PARAMETER_HOST_ADDRESS = "hostname";

    /**
     * Name of axis2.xml originServer parameter for SimpleHTTPServer configuration
     */
    public static final String PARAMETER_ORIGIN_SERVER = "originServer";

    /**
     * Name of axis2.xml requestTimeout parameter for SimpleHTTPServer configuration
     */
    public static final String PARAMETER_REQUEST_SOCKET_TIMEOUT = "requestTimeout";

    /**
     * Name of axis2.xml requestTcpNoDelay parameter for SimpleHTTPServer configuration
     */
    public static final String PARAMETER_REQUEST_TCP_NO_DELAY = "requestTcpNoDelay";

    /**
     * Name of axis2.xml requestCoreThreadPoolSize parameter for SimpleHTTPServer configuration
     */
    public static final String PARAMETER_REQUEST_CORE_THREAD_POOL_SIZE =
            "requestCoreThreadPoolSize";

    /**
     * Name of axis2.xml requestMaxThreadPoolSize parameter for SimpleHTTPServer configuration
     */
    public static final String PARAMETER_REQUEST_MAX_THREAD_POOL_SIZE = "requestMaxThreadPoolSize";

    /**
     * Name of axis2.xml threadKeepAliveTime parameter for SimpleHTTPServer configuration
     */
    public static final String PARAMETER_THREAD_KEEP_ALIVE_TIME = "threadKeepAliveTime";

    /**
     * Name of axis2.xml threadKeepAliveTimeUnit parameter for SimpleHTTPServer configuration
     */
    public static final String PARAMETER_THREAD_KEEP_ALIVE_TIME_UNIT = "threadKeepAliveTimeUnit";

    private ConfigurationContext configurationContext;
    private TransportInDescription httpConfiguration;
    private int port;
    private String hostAddress;
    private String originServer;
    private int requestSocketTimeout;
    private boolean requestTcpNoDelay;
    private int requestCoreThreadPoolSize;
    private int requestMaxThreadPoolSize;
    private long threadKeepAliveTime;
    private TimeUnit threadKeepAliveTimeUnit;

    private WorkerFactory requestWorkerFactory = null;

    /**
     * Create and configure a new HttpFactory
     */
    public HttpFactory(ConfigurationContext configurationContext) throws AxisFault {
        this.configurationContext = configurationContext;
        httpConfiguration = configurationContext.getAxisConfiguration().getTransportIn(Constants.TRANSPORT_HTTP);
        port = getIntParam(PARAMETER_PORT, 6060);
        hostAddress = getStringParam(PARAMETER_HOST_ADDRESS, null);
        originServer = getStringParam(PARAMETER_ORIGIN_SERVER, "Simple-Server/1.1");
        requestSocketTimeout = getIntParam(PARAMETER_REQUEST_SOCKET_TIMEOUT, 20000);
        requestTcpNoDelay = getBooleanParam(PARAMETER_REQUEST_TCP_NO_DELAY, true);
        requestCoreThreadPoolSize = getIntParam(PARAMETER_REQUEST_CORE_THREAD_POOL_SIZE, 100);
        requestMaxThreadPoolSize = getIntParam(PARAMETER_REQUEST_MAX_THREAD_POOL_SIZE, 150);
        threadKeepAliveTime = getLongParam(PARAMETER_THREAD_KEEP_ALIVE_TIME, 180L);
        threadKeepAliveTimeUnit =
                getTimeUnitParam(PARAMETER_THREAD_KEEP_ALIVE_TIME_UNIT, TimeUnit.SECONDS);
    }

    /**
     * Create and configure a new HttpFactory
     */
    public HttpFactory(ConfigurationContext configurationContext, int port) throws AxisFault {
        this(configurationContext);
        this.port = port;
    }

    /**
     * Create and configure a new HttpFactory
     */
    public HttpFactory(ConfigurationContext configurationContext, int port,
                       WorkerFactory requestWorkerFactory) throws AxisFault {
        this(configurationContext, port);
        this.requestWorkerFactory = requestWorkerFactory;
    }

    private int getIntParam(String name, int def) {
        String config = getStringParam(name, null);
        if (config != null) {
            return Integer.parseInt(config);
        } else {
            return def;
        }
    }

    private long getLongParam(String name, long def) {
        String config = getStringParam(name, null);
        if (config != null) {
            return Long.parseLong(config);
        } else {
            return def;
        }
    }

    private boolean getBooleanParam(String name, boolean def) throws AxisFault {
        String config = getStringParam(name, null);
        if (config != null) {
            if (config.equals("yes") || config.equals("true")) {
                return true;
            } else if (config.equals("no") || config.equals("false")) {
                return false;
            } else {
                throw new AxisFault("Boolean value must be yes, true, no or false for parameter " +
                        name + ":  " + config);
            }
        }
        return def;
    }

    private TimeUnit getTimeUnitParam(String name, TimeUnit def) throws AxisFault {
        String config = getStringParam(name, null);
        if (config != null) {
            try {
                return (TimeUnit) TimeUnit.class.getField(config).get(null);
            } catch (Exception e) {
                throw AxisFault.makeFault(e);
            }
        }
        return def;
    }

    private String getStringParam(String name, String def) {
        Parameter param = httpConfiguration.getParameter(name);
        if (param != null) {
//            assert param.getParameterType() == Parameter.TEXT_PARAMETER;
            String config = (String) param.getValue();
            if (config != null) {
                return config;
            }
        }
        return def;
    }

    /**
     * Return the configured listener manager or create and configure one with configurationContext
     */
    public ListenerManager getListenerManager() {
        ListenerManager lm = configurationContext.getListenerManager();
        if (lm == null) {
            lm = new ListenerManager();
            lm.init(configurationContext);
        }
        return lm;
    }

    /**
     * Create the executor used to launch the single requestConnectionListener
     */
    public ExecutorService newListenerExecutor(int port) {
        return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue(),
                                      new DefaultThreadFactory(
                                              new ThreadGroup("Listener thread group"),
                                              "HttpListener-" + this.port));
    }

    /**
     * Create the listener for request connections
     */
    public IOProcessor newRequestConnectionListener(
	    String scheme,	    
            int port,
            final HttpConnectionManager manager, 
            final Http1Config http1Config, SocketConfig socketConfig) throws IOException {
        return new DefaultConnectionListener(
		scheme,	
                port, 
                manager, 
                new DefaultConnectionListenerFailureHandler(), 
                http1Config,
                socketConfig);
    }

    /**
     * Create and set the parameters applied to incoming HTTP request connections
     */
    public Http1Config newRequestConnectionHttp1Config() {
        // Create HTTP/1.1 protocol configuration
        Http1Config http1Config = Http1Config.custom()
            .setMaxHeaderCount(500)
            .setMaxLineLength(4000)
            .setMaxEmptyLineCount(1)
            .build();

	return http1Config;
    }

    /**
     * Create and set the parameters applied to socket connections
     */
    public SocketConfig newSocketConfig() {

        SocketConfig socketConfig = SocketConfig.custom()
                 .setSoTimeout(requestSocketTimeout, TimeUnit.MILLISECONDS)
                 .setTcpNoDelay(requestTcpNoDelay)
                 .setSndBufSize(8 * 1024)
                 .setRcvBufSize(8 * 1024)
                 .build();

        return socketConfig;
    }

    /**
     * Create the connection manager used to launch request threads
     */
    public HttpConnectionManager newRequestConnectionManager(ExecutorService requestExecutor,
                                                             WorkerFactory workerFactory,
							     Http1Config h1Config) {
        return new DefaultHttpConnectionManager(configurationContext, requestExecutor,
                                                workerFactory, h1Config);
    }

    /**
     * Create the executor use the manage request processing threads
     */
    public ExecutorService newRequestExecutor(int port) {
        return new ThreadPoolExecutor(requestCoreThreadPoolSize, requestMaxThreadPoolSize,
                                      threadKeepAliveTime, threadKeepAliveTimeUnit,
                                      newRequestBlockingQueue(),
                                      new DefaultThreadFactory(
                                              new ThreadGroup("Connection thread group"),
                                              "HttpConnection-" + port));
    }

    /**
     * Create the queue used to hold incoming requests when requestCoreThreadPoolSize threads are busy.
     * Default is an unbounded queue.
     */
    public BlockingQueue newRequestBlockingQueue() {
        return new LinkedBlockingQueue();
    }

    /**
     * Create the factory for request workers
     */
    public WorkerFactory newRequestWorkerFactory() {
        if (requestWorkerFactory != null) {
            return requestWorkerFactory;
        } else {
            return new HTTPWorkerFactory();
        }
    }

    public HttpProcessor newHttpProcessor() {
        final HttpProcessorBuilder b = HttpProcessorBuilder.create();	
        b.addAll(
            new RequestSessionCookie());
        b.addAll(
            new ResponseDate(),
            new ResponseServer(originServer),
            new ResponseContent(),
            new ResponseConnControl(),
            new ResponseSessionCookie());
        HttpProcessor httpProcessor = b.build();
        return httpProcessor;
    }

    public ConnectionReuseStrategy newConnStrategy() {
        return new DefaultConnectionReuseStrategy();
    }

    public DefaultClassicHttpResponseFactory newResponseFactory() {
        return DefaultClassicHttpResponseFactory.INSTANCE;
    }

    // *****
    // Getters and Setters
    // *****

    /**
     * Getter for configurationContext
     */
    public ConfigurationContext getConfigurationContext() {
        return configurationContext;
    }

    /**
     * Getter for httpConfiguration
     */
    public TransportInDescription getHttpConfiguration() {
        return httpConfiguration;
    }

    /**
     * Getter for port
     * return the port on which to listen for http connections (default = 6060)
     */
    public int getPort() {
        return port;
    }

    /**
     * Setter for port
     */
    public void setPort(int port) {
        this.port = port;
    }

    /**
     * Getter for hostAddress
     *
     * @return the host address (or name) to be use in reply-to endpoint references, or null if not specified (default = null)
     */
    public String getHostAddress() {
        return hostAddress;
    }

    /**
     * Setter for hostAddress
     */
    public void setHostAddress(String hostAddress) {
        this.hostAddress = hostAddress;
    }

    /**
     * Getter for originServer
     *
     * @return the Server header string for outgoing messages (default "Simple-Server/1.1")
     */
    public String getOriginServer() {
        return originServer;
    }

    /**
     * Setter for originServer
     */
    public void setOriginServer(String originServer) {
        this.originServer = originServer;
    }

    /**
     * Getter for requestSocketTimeout
     *
     * @return the maximum time in millis to wait for data on a request socket (default 20000)
     */
    public int getRequestSocketTimeout() {
        return requestSocketTimeout;
    }

    /**
     * Setter for requestSocketTimeout
     */
    public void setRequestSocketTimeout(int requestSocketTimeout) {
        this.requestSocketTimeout = requestSocketTimeout;
    }

    /**
     * Getter for requestTcpNoDelay
     * return false iff Nagle's algorithm should be used to conserve bandwidth by minimizing segments
     * at the cost of latency and performance (default true, i.e. maximize performance at
     * the cost of bandwidth)
     */
    public boolean getRequestTcpNoDelay() {
        return requestTcpNoDelay;
    }

    /**
     * Setter for requestTcpNoDelay
     */
    public void setRequestTcpNoDelay(boolean requestTcpNoDelay) {
        this.requestTcpNoDelay = requestTcpNoDelay;
    }

    /**
     * Getter for RequestCoreThreadPoolSize
     *
     * @return the size of the thread pool use to process requests assuming there is adequate queue space (default 25)
     */
    public int getRequestCoreThreadPoolSize() {
        return requestCoreThreadPoolSize;
    }

    /**
     * Setter for RequestCoreThreadPoolSize
     */
    public void setRequestCoreThreadPoolSize(int requestCoreThreadPoolSize) {
        this.requestCoreThreadPoolSize = requestCoreThreadPoolSize;
    }

    /**
     * Getter for requestMaxThreadPoolSize
     *
     * @return the maximum size of the thread pool used to process requests if the queue fills up (default 150).
     *         Since the default queue is unbounded this parameter is meaningless unless you override newRequestBlockingQueue()
     */
    public int getRequestMaxThreadPoolSize() {
        return requestMaxThreadPoolSize;
    }

    /**
     * Setter for requestMaxThreadPoolSize
     */
    public void setRequestMaxThreadPoolSize(int requestMaxThreadPoolSize) {
        this.requestMaxThreadPoolSize = requestMaxThreadPoolSize;
    }

    /**
     * Getter for threadKeepAliveTime
     *
     * @return how long a request processing thread in excess of the core pool size will be kept alive it if is inactive
     *         (default with threadKeepAliveTimeUnit to 180 seconds)
     */
    public long getThreadKeepAliveTime() {
        return threadKeepAliveTime;
    }

    /**
     * Setter for threadKeepAliveTime
     */
    public void setThreadKeepAliveTime(long threadKeepAliveTime) {
        this.threadKeepAliveTime = threadKeepAliveTime;
    }

    /**
     * Getter for threadKeepAliveTimeUnit
     * return the time unit for threadKeepAliveTime (default SECONDS)
     */
    public TimeUnit getThreadKeepAliveTimeUnit() {
        return threadKeepAliveTimeUnit;
    }

    /**
     * Setter for threadKeepAliveTimeUnit
     */
    public void setThreadKeepAliveTimeUnit(TimeUnit threadKeepAliveTimeUnit) {
        this.threadKeepAliveTimeUnit = threadKeepAliveTimeUnit;
    }

}
