package org.apereo.cas.adaptors.x509.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;


/**
 * Provides a simple HTTP Web server that can serve out a single resource for
 * all requests.  SSL/TLS is not supported.
 *
 * @author Marvin S. Addison
 * @since 3.4.6
 *
 */
public class MockWebServer {

    private static final Logger LOGGER = LoggerFactory.getLogger(MockWebServer.class);
    
    /** Request handler. */
    private Worker worker;

    /** Controls the worker thread. */
    private Thread workerThread;


    /**
     * Creates a new server that listens for requests on the given port and
     * serves the given resource for all requests.
     *
     * @param port Server listening port.
     * @param resource Resource to serve.
     * @param contentType MIME content type of resource to serve.
     */
    public MockWebServer(final int port, final Resource resource, final String contentType) {
        try {
            this.worker = new Worker(new ServerSocket(port), resource, contentType);
        } catch (final IOException e) {
            throw new RuntimeException("Cannot create Web server", e);
        }
    }

    /** Starts the Web server so it can accept requests on the listening port. */
    public void start() {
        this.workerThread = new Thread(this.worker, "MockWebServer.Worker");
        this.workerThread.start();
    }

    /** Stops the Web server after processing any pending requests. */
    public void stop() {
        if (!isRunning()) {
            return;
        }
        this.worker.stop();
        try {
            this.workerThread.join();
        } catch (final InterruptedException e) {
            LOGGER.error(e.getMessage(), e);
        }
    }

    /**
     * Determines whether the server is running or not.
     *
     * @return True if server is running, false otherwise.
     */
    public boolean isRunning() {
        return this.workerThread.isAlive();
    }

    /**
     * Worker class handles request processing.
     */
    private static class Worker implements Runnable {

        /** Server always returns HTTP 200 response. */
        private static final String STATUS_LINE = "HTTP/1.1 200 Success\r\n";

        /** Separates HTTP header from body. */
        private static final String SEPARATOR = "\r\n";

        /** Response buffer size. */
        private static final int BUFFER_SIZE = 2048;

        private static final Logger LOGGER = LoggerFactory.getLogger(Worker.class);
        
        /** Run flag. */
        private boolean running;

        /** Server socket. */
        private ServerSocket serverSocket;

        /** Resource to serve. */
        private Resource resource;

        /** MIME content type of resource to serve. */
        private String contentType;


        /**
         * Creates a request-handling worker that listens for requests on the
         * given socket and serves the given resource for all requests.
         *
         * @param sock Server socket.
         * @param resource Single resource to serve.
         * @param contentType MIME content type of resource to serve.
         */
        Worker(final ServerSocket sock, final Resource resource, final String contentType) {
            this.serverSocket = sock;
            this.resource = resource;
            this.contentType = contentType;
            this.running = true;
        }

        @Override
        public void run() {
            while (this.running) {
                try {
                    writeResponse(this.serverSocket.accept());
                    Thread.sleep(500);
                } catch (final SocketException e) {
                    LOGGER.debug("Stopping on socket close.");
                    this.running = false;
                } catch (final Exception e) {
                    LOGGER.error(e.getMessage(), e);
                }
            }
        }

        public void stop() {
            try {
                this.serverSocket.close();
            } catch (final IOException e) {
                LOGGER.trace("Exception when closing the server socket: [{}]", e.getMessage());
            }
        }

        private void writeResponse(final Socket socket) throws IOException {
            LOGGER.debug("Socket response for resource [{}]", resource.getFilename());
            final OutputStream out = socket.getOutputStream();
            out.write(STATUS_LINE.getBytes());
            out.write(header("Content-Length", this.resource.contentLength()));
            out.write(header("Content-Type", this.contentType));
            out.write(SEPARATOR.getBytes());

            final byte[] buffer = new byte[BUFFER_SIZE];
            try(InputStream in = this.resource.getInputStream()) {
                int count = 0;
                while ((count = in.read(buffer)) > -1) {
                    out.write(buffer, 0, count);
                }
            }
            LOGGER.debug("Wrote response for resource [{}] for [{}]",
                    resource.getFilename(),
                    resource.contentLength());

            socket.shutdownOutput();
        }

        private static byte[] header(final String name, final Object value) {
            return String.format("%s: %s\r\n", name, value).getBytes();
        }
    }
}
