package com.example.fitbot.util.server;

import android.support.annotation.Nullable;
import android.util.Log;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class WebSocket {

    private ServerSocket serverSocket;
    private WebSocketConnectionHandler connectionHandler;
    private final Set<Socket> clients = Collections.synchronizedSet(new HashSet<>());
    protected IWebSocketHandler eventHandler = new IWebSocketHandler() {}; // NO-OP event handler.

    /**
     * Constructor for creating a new WebSocket server.
     */
    private WebSocket() {}

    /**
     * Function for creating a new WebSocket server given the provided port.
     * @return A WebSocket connection, or null if something went wrong.
     */
    public static @Nullable WebSocket createServer() {
        try {
            WebSocket webSocket = new WebSocket();
            webSocket.serverSocket = new ServerSocket();
            webSocket.serverSocket.bind(webSocket.serverSocket.getLocalSocketAddress());
            Log.i("WebSocket -- Creating new WebSocket server", "Server created: " + webSocket.serverSocket.getLocalSocketAddress() + ", " + webSocket.serverSocket.getLocalPort());
            return webSocket;
        } catch (IOException error)
        {
            String cause = error.getMessage() == null ? "Unknown reason" : error.getMessage();
            Log.e("WebSocket -- Creating new WebSocket server", cause);
            return null;
        }
    }

    /**
     * Method for listening for incoming connections.
     */
    public void startListening() {
        this.connectionHandler = new WebSocketConnectionHandler(this);
        this.connectionHandler.listen();
    }

    /**
     * Method for stopping the WebSocket server.
     */
    public void stop() {
        try {
            this.serverSocket.close();
            this.connectionHandler.stop();
        } catch (IOException error) {
            String cause = error.getMessage() == null ? "Unknown reason" : error.getMessage();
            Log.e("WebSocket -- Closing server connection", cause);
        }
    }

    /**
     * Method for setting the event handler for this WebSocket server.
     * @param handler The handler to use. This handler will parse all events
     *                that occur in this WebSocket connection. The events are the followed:
     *                - onMessageReceived(Socket, String)
     *                - onConnected(Socket)
     *                - onDisconnected(Socket)
     *                - onError(Socket, String)
     */
    public void setEventHandler(IWebSocketHandler handler) {
        this.eventHandler = handler;
    }

    /**
     * Method for getting the ServerSocket connection
     * @return The ServerSocket connection.
     */
    public ServerSocket getSocket() {
        return this.serverSocket;
    }

    /**
     * Method for checking whether this WebSocket connection is connected.
     * @return The connection status of the WebSocket.
     */
    public boolean isConnected() {
        return !this.serverSocket.isClosed();
    }

    /**
     * Class representing a message received from a WebSocket connection.
     */
    public static class Message {

        // Enumerable representing message type (opcode).
        public enum Opcode {

            CONTINUING((byte) 0x0),
            TEXT((byte) 0x1),
            BINARY((byte) 0x2),
            RES0((byte) 0x3), RES1((byte) 0x4), RES2((byte) 0x5), RES3((byte) 0x6), RES4((byte) 0x7),
            CLOSE_CONNECTION((byte) 0x8),
            PING((byte) 0x9),
            PONG((byte) 0xA),
            RES5((byte) 0xB), RES6((byte) 0xC), RES7((byte) 0xD), RES8((byte) 0xE), RES9((byte) 0xF);
            
            byte opcode;
            Opcode(final byte opcode) {
                this.opcode = opcode;
            }

            /**
             * Method for decoding the opcode of a message.
             * @param opcode The opcode to decode.
             * @return The message type.
             */
            public static Opcode decode(byte opcode) {
                return Opcode.values()[opcode & 0xF];
            }
            // Returns the opcode of this message type.
            public byte getOpcode() { return this.opcode; }
        }

        public String message;
        public WebSocketConnection connection;

        /**
         * Constructor for a WebSocket message.
         * @param message The message that was sent
         * @param connection The connection where the message came from.
         */
        public Message(WebSocketConnection connection, String message) {
            this.message = message;
            this.connection = connection;
        }
    }

    /**
     * Interface for a message reply.
     * This can be used for when a message has been received from a client
     * to reply back to the client.
     */
    public interface MessageReply {
        void reply(String message);
    }
}
