// 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 streamer.debug;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

import org.apache.log4j.Logger;

public class MockServer implements Runnable {
    private static final Logger s_logger = Logger.getLogger(MockServer.class);

    private boolean shutdown = false;
    private ServerSocket serverSocket;
    private final Packet[] packets;
    private Throwable exception;
    private boolean shutdowned;

    /**
     * Set to true to enable debugging messages.
     */
    protected boolean verbose = System.getProperty("rdpclient.MockServer.debug", "false").equals("true");

    public MockServer(Packet packets[]) {
        this.packets = packets;
    }

    public void start() throws IOException {
        serverSocket = new ServerSocket(0);

        shutdown = false;
        exception = null;
        shutdowned = false;

        Thread thread = new Thread(this);
        thread.setDaemon(true);
        thread.start();
    }

    @Override
    public void run() {

        try (Socket socket = serverSocket.accept();) {

            if (verbose)
                System.out.println("[" + this + "] INFO: Client connected: " + socket.getRemoteSocketAddress() + ".");

            InputStream is = socket.getInputStream();
            OutputStream os = socket.getOutputStream();

            try {
                for (int i = 0; i < packets.length && !shutdown; i++) {

                    Packet packet = packets[i];
                    switch (packet.type) {
                    case CLIENT: {
                        // Read client data and compare it with mock data
                        // (unless "ignore" option is set)
                        byte actualData[] = new byte[packet.data.length];
                        int actualDataLength = is.read(actualData);

                        if (verbose)
                            System.out.println("[" + this + "] INFO: Data is read: {" + Arrays.toString(Arrays.copyOf(actualData, actualDataLength)) + "}.");

                        if (!packet.ignore) {
                            // Compare actual data with expected data
                            if (actualDataLength != packet.data.length) {
                                throw new AssertionError("Actual length of client request for packet #" + (i + 1) + " (\"" + packet.id + "\")"
                                        + " does not match length of expected client request. Actual length: " + actualDataLength + ", expected length: " + packet.data.length
                                        + ".");
                            }

                            for (int j = 0; j < packet.data.length; j++) {

                                if (packet.data[j] != actualData[j]) {
                                    throw new AssertionError("Actual byte #" + (j + 1) + " of client request for packet #" + (i + 1) + " (\"" + packet.id + "\")"
                                            + " does not match corresponding byte of expected client request. Actual byte: " + actualData[j] + ", expected byte: " + packet.data[j]
                                                    + ".");
                                }
                            }
                        }
                        break;
                    }
                    case SERVER: {
                        // Send mock data to client
                        os.write(packet.data);

                        if (verbose)
                            System.out.println("[" + this + "] INFO: Data is written: {" + Arrays.toString(packet.data) + "}.");

                        break;
                    }
                    case UPGRADE_TO_SSL: {
                        // Attach SSL context to socket

                        final SSLSocketFactory sslSocketFactory = (SSLSocketFactory)SSLSocketFactory.getDefault();
                        SSLSocket sslSocket = (SSLSocket)sslSocketFactory.createSocket(socket, null, serverSocket.getLocalPort(), true);
                        sslSocket.setEnabledCipherSuites(sslSocket.getSupportedCipherSuites());
                        sslSocket.setUseClientMode(false);
                        sslSocket.startHandshake();
                        is = sslSocket.getInputStream();
                        os = sslSocket.getOutputStream();

                        break;
                    }
                    default:
                        throw new RuntimeException("Unknown packet type: " + packet.type);
                    }

                }
            } finally {
                try {
                    is.close();
                } catch (Throwable e) {
                    s_logger.info("[ignored]"
                            + "in stream close failed: " + e.getLocalizedMessage());
                }
                try {
                    os.close();
                } catch (Throwable e) {
                    s_logger.info("[ignored]"
                            + "out stream close failed: " + e.getLocalizedMessage());
                }
                try {
                    serverSocket.close();
                } catch (Throwable e) {
                    s_logger.info("[ignored]"
                            + "server socket close failed: " + e.getLocalizedMessage());
                }
            }
        } catch (Throwable e) {
            System.err.println("Error in mock server: " + e.getMessage());
            e.printStackTrace(System.err);
            exception = e;
        }
        shutdowned = true;
        if (verbose)
            System.out.println("[" + this + "] INFO: Mock server shutdowned.");

    }

    public void shutdown() {
        shutdown = true;
    }

    public InetSocketAddress getAddress() {
        return (InetSocketAddress)serverSocket.getLocalSocketAddress();
    }

    public Throwable getException() {
        return exception;
    }

    public static class Packet {
        public static enum PacketType {
            SERVER, CLIENT, UPGRADE_TO_SSL;
        }

        public String id = "";

        public Packet() {
        }

        public Packet(String id) {
            this.id = id;
        }

        public PacketType type;

        public boolean ignore = false;

        public byte data[];
    }

    public boolean isShutdowned() {
        return shutdowned;
    }

    public void waitUntilShutdowned(long timeToWaitMilliseconds) throws InterruptedException {
        long deadline = System.currentTimeMillis() + timeToWaitMilliseconds;
        while (!shutdowned && System.currentTimeMillis() < deadline) {
            Thread.sleep(10);
        }
    }
}
