/*
 * Copyright (c) 2004-2017, by Alex Leigh.
 * All rights reserved.
 *
 * THIS IS UNPUBLISHED PROPRIETARY SOURCE CODE
 * The copyright notice above does not evidence any
 * actual or intended publication of such source code.
 */

package leigh.mecha.http.server;

import jakarta.servlet.Servlet;
import leigh.BuildVersion;
import leigh.mecha.log.MechaLogger;
import leigh.mecha.log.MechaLoggerFactory;
import leigh.mecha.util.UniversalJob;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.websocket.server.JettyWebSocketServlet;
import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory;
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;

import java.util.HashMap;
import java.util.Map;

/**
 * <code>WebServer</code> implements provides a simple API to embed
 * a Jetty HTTP engine into a running VM. Used in conjunction with
 * <code>HandlerPack</code>, end-developers can instantiate HTTP servers
 * and provide processing code without having to use a servlet engine
 * along with it's associated complexities and costs.
 *
 * @author Alex Leigh
 */

public class WebServer {
    private final static MechaLogger logger = MechaLoggerFactory.getLogger(WebServer.class);
    private final WebPipeline handlerPack = new WebPipeline();
    private final int port;
    private final int maxThreads;
    private Server server = null;
    private boolean running = false;
    private String banner = "HttpServer " + BuildVersion.MASSIVE_VERSION;
    private final HashMap<String, WebSocketConnectionFactory> socketHandlers = new HashMap<>();

    /**
     * Create a new webserver, which will listen on the provided port number. The
     * port is not reserved during construction, so no error will present if the port
     * is already in use.
     *
     * @param plaintextListenPort The port to listen on.
     */
    public WebServer(final int plaintextListenPort, final int maxThreads) {
        this.port = plaintextListenPort;
        this.maxThreads = maxThreads;
    }

    /**
     * Add a {@link WebSocketConnection} to this webserver. For the change to take effect, the call must be made before
     * the server is started.
     */
    public void addSocketHandler(WebSocketConnectionFactory handler, String path) {
        socketHandlers.put(path, handler);
    }

    public String getBanner() {
        return banner;
    }

    public void setBanner(String banner) {
        this.banner = banner;
    }

    /**
     * Return the <code>HandlerPack</code> associated with this
     * http server instance.
     *
     * @return
     */
    public WebPipeline getWebPipeline() {
        return handlerPack;
    }

    /**
     * Attempt to start the instance.
     */
    public void start() {
        if (running)
            return;

        QueuedThreadPool pool = new QueuedThreadPool(this.maxThreads, 32);

        new Thread(() -> {
            while (true) {
                logger.info("HTTP Server Status: [port: {}] [busy: {}] [idle: {}] [max: {}]",
                        port,
                        pool.getBusyThreads(),
                        pool.getIdleThreads(),
                        pool.getMaxThreads());
                try {
                    Thread.sleep(10 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        server = new Server(pool);

        // This results in an error from jetty in the log, but works regardless.
        //    server.addBean(new ErrorHandler(banner));
        server.addConnector(makeConnector());

        ContextHandlerCollection handlers = new ContextHandlerCollection();

        for (Map.Entry<String, WebSocketConnectionFactory> entry : socketHandlers.entrySet()) {
            logger.info("Adding WebSocket handler. [path: {}] [handler: {}]", entry.getKey(), entry.getValue());
            ServletContextHandler servletContextHandler = new ServletContextHandler();
            JettyWebSocketServletContainerInitializer.configure(servletContextHandler, null);

            Servlet websocketServlet = new JettyWebSocketServlet() {
                @Override
                protected void configure(JettyWebSocketServletFactory factory) {
                    //factory.addMapping("/", (req, res) -> entry.getValue());
                    factory.setCreator(entry.getValue());
                }
            };
            servletContextHandler.addServlet(new ServletHolder(websocketServlet), entry.getKey());
            handlers.addHandler(servletContextHandler);
        }

        handlers.addHandler(handlerPack);
        server.setHandler(handlers);

        logger.info("Starting web server. [port: {}] [maxThreads: {}]", new Object[]{port,
                maxThreads});
        try {
            server.start();
            final Connector[] c = server.getConnectors();
            for (final Connector a : c) {
                logger.info("Connector2: {}", a);
            }
        } catch (Exception e) {
            logger.warn("Failure to start Jetty", e);
            System.exit(UniversalJob.RET_ERROR);
        }
    }

    private Connector makeConnector() {
        logger.info("Setting up HTTP listen port: [port: {}]", port);
        final HttpConfiguration http_config = new HttpConfiguration();
        http_config.setSendServerVersion(false);
        http_config.setSendXPoweredBy(false);

        final ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(http_config));
        http.setPort(port);
        http.setIdleTimeout(30000);

        return http;
    }

    @Override
    public String toString() {
        return "WebServer{" +
                "handlerPack=" + handlerPack +
                ", port=" + port +
                ", maxThreads=" + maxThreads +
                ", server=" + server +
                ", running=" + running +
                '}';
    }

    /**
     * Stop the running instance. This will block until Jetty claims
     * it has successfully shutdown; after such time, the port should
     * be released and be available for use.
     *
     * @throws Exception
     */
    public void stop() throws Exception {
        logger.info("Stopping server...");
        if (!running)
            return;

        server.stop();

        while (server.isStopping()) {
            try {
                Thread.sleep(250);
            } catch (InterruptedException e) {
                logger.warn("Timer failed: {}", e);
            }
        }

        running = false;

        logger.info("Server stopped.");

        System.exit(0);
    }

}
