"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HotWebSocketServer = void 0;
const socket_io_1 = require("socket.io");
const HotWebSocketServerClient_1 = require("./HotWebSocketServerClient");
const HotRoute_1 = require("./HotRoute");
const HotRouteMethod_1 = require("./HotRouteMethod");
/**
 * A web socket server.
 */
class HotWebSocketServer {
    constructor(server) {
        this.connection = server;
        this.logger = this.connection.logger;
        this.io = null;
        this.routes = {};
        this.clients = {};
        this.tags = {};
        this.serverOptions = {
            "cors": {
                "origin": this.connection.cors.origin,
                "allowedHeaders": this.connection.cors.allowedHeaders
            },
            maxHttpBufferSize: 1e8
        };
        this.onServerAuthorize = null;
        this.onSuccessfulConnection = null;
        this.onConnection = null;
        this.onDisconnect = null;
    }
    /**
     * Setup the WebSocket server AFTER the http server is running.
     */
    setup() {
        if (this.connection.httpsListener != null) {
            this.io = new socket_io_1.Server(this.connection.httpsListener, this.serverOptions);
            this.logger.info(`Secure WebSocket server listening...`);
        }
        else {
            this.io = new socket_io_1.Server(this.connection.httpListener, this.serverOptions);
            this.logger.info(`WebSocket server listening...`);
        }
        this.io.use((socket, next) => __awaiter(this, void 0, void 0, function* () {
            try {
                let incomingIP = HotWebSocketServer.getIPFromSocket(socket);
                let authorizationValue = null;
                let hasAuthorization = true;
                let executedFailedFunc = false;
                const socketId = socket.id;
                const newSocket = new HotWebSocketServerClient_1.HotWebSocketServerClient(this, socket);
                if (this.onConnection != null) {
                    const canConnect = yield this.onConnection(socket);
                    if (canConnect === false) {
                        next(new Error("Unauthorized"));
                        return;
                    }
                }
                if (this.onServerAuthorize != null) {
                    try {
                        let request = new HotRouteMethod_1.ServerRequest({
                            req: null,
                            res: null,
                            bearerToken: "",
                            authorizedValue: null,
                            jsonObj: socket.handshake.auth,
                            queryObj: socket.handshake.query,
                            files: null,
                            wsSocket: newSocket
                        });
                        authorizationValue = yield this.onServerAuthorize.call(this, request);
                    }
                    catch (ex) {
                        // This is verbose since it SHOULDN'T be an error, it's just a failed authorization.
                        this.logger.verbose(`Authorization error from ip ${incomingIP}: ${ex.message}`);
                        hasAuthorization = false;
                        if (this.onConnectionError != null)
                            yield this.onConnectionError(socket, ex.message);
                        executedFailedFunc = true;
                    }
                    if (authorizationValue === undefined)
                        hasAuthorization = false;
                }
                if (hasAuthorization === false) {
                    // Ensures we execute the onConnectionError function only once.
                    if (executedFailedFunc === false) {
                        if (this.onConnectionError != null)
                            yield this.onConnectionError(socket, "Unauthorized");
                    }
                    this.logger.verbose(`Unauthorized connection from ${incomingIP}`);
                    next(new Error("Unauthorized"));
                    return;
                }
                this.logger.verbose(`Incoming WebSocket connection from ${incomingIP}, Authorized: true`);
                socket.data.authorizationValue = authorizationValue;
                newSocket.authorizedValue = authorizationValue;
                this.clients[socketId] = newSocket;
                next();
            }
            catch (ex) {
                this.logger.error(`Error while authorizing a websocket connection: ${ex}`);
                next(new Error("Internal Server Error"));
            }
        }));
        this.io.on("connection", (socket) => __awaiter(this, void 0, void 0, function* () {
            try {
                let authorizationValue = socket.data.authorizationValue;
                for (let routeName in this.routes) {
                    let route = this.routes[routeName];
                    if (route.methods.length < 1)
                        throw new Error(`HotRoute ${routeName} does not have any methods! Methods are required to be attached prior to adding a route to a websocket.`);
                    for (let iIdx = 0; iIdx < route.methods.length; iIdx++) {
                        let method = route.methods[iIdx];
                        if (!((method.type === HotRouteMethod_1.HotEventMethod.WEBSOCKET_CLIENT_PUB_EVENT) ||
                            (method.type === HotRouteMethod_1.HotEventMethod.POST_AND_WEBSOCKET_CLIENT_PUB_EVENT))) {
                            continue;
                        }
                        let eventName = `${routeName}/${method.name}`;
                        // Only 1 argument will be passed to the jsonObj in the new ServerRequest.
                        // Devs can always pass an array or whatever object(s) they need.
                        socket.on(eventName, (...args) => __awaiter(this, void 0, void 0, function* () {
                            try {
                                let jsonObj = null;
                                if (args != null) {
                                    if (args.length > 0)
                                        jsonObj = args[0];
                                }
                                let uuid = null;
                                let data = jsonObj;
                                if (jsonObj.uuid != null) {
                                    uuid = jsonObj.uuid;
                                    data = jsonObj.data;
                                }
                                const socketId = socket.id;
                                let wsSocket = this.clients[socketId];
                                let request = new HotRouteMethod_1.ServerRequest({
                                    "bearerToken": "",
                                    "authorizedValue": authorizationValue,
                                    "jsonObj": data,
                                    "wsSocket": wsSocket
                                });
                                socket.data.wsSocket = wsSocket;
                                let result = yield method.onServerExecute.call(route, request);
                                if (this.logger.showWebSocketEvents === true) {
                                    this.logger.verbose((result2) => {
                                        let resultStr = "";
                                        if (this.logger.showResponses === true)
                                            resultStr = `, Response: ${JSON.stringify(result2)}`;
                                        return (`WebSocket Event ${eventName}${resultStr}`);
                                    }, result);
                                }
                                if (result !== undefined) {
                                    let resultObj = result;
                                    if (uuid != null)
                                        resultObj = { uuid: uuid, data: result };
                                    socket.emit(`${routeName}/${method.name}`, resultObj);
                                }
                            }
                            catch (ex) {
                                this.logger.error(`WebSocket Execution Error: ${ex}`);
                                socket.emit(`${routeName}/${method.name}`, { error: ex.message });
                            }
                        }));
                        this.logger.verbose(`Adding WebSocket Event: ${eventName}`);
                    }
                }
                const socketId = socket.id;
                socket.on("disconnect", () => {
                    try {
                        this.disconnect(socketId);
                    }
                    catch (ex) {
                        this.logger.error(`Error while disconnecting a websocket client: ${ex}`);
                    }
                });
                this.logger.verbose(`Client successfully connected ${socketId}`);
                if (this.onSuccessfulConnection != null)
                    yield this.onSuccessfulConnection(this.clients[socketId]);
            }
            catch (ex) {
                this.logger.error(`Error while connecting a websocket client: ${ex}`);
                socket.emit("error", "Internal Server Error");
                socket.disconnect();
            }
        }));
    }
    /**
     * Get the IP address from a socket.
     */
    static getIPFromSocket(socket) {
        return (socket.handshake.address);
    }
    /**
     * Tag a client. This is useful when trying to send messages to groups of clients.
     */
    tagClient(client, tag) {
        if (client == null)
            throw new Error(`No client given when tagging with "${tag}"`);
        if (this.tags[tag] == null)
            this.tags[tag] = {};
        this.tags[tag][client.getId()] = client;
    }
    /**
     * Get a list of clients with the same tag.
     */
    getTaggedClients(tag) {
        return (this.tags[tag]);
    }
    /**
     * Clears the list of clients with the same tag.
     */
    clearTag(tag) {
        if (this.tags[tag] == null)
            throw new Error(`Unable to remove tagged clients. Tag ${tag} does not exist.`);
        this.tags[tag] = {};
    }
    /**
     * Clears the list of tagged clients.
     */
    clearAllTags() {
        this.tags = {};
    }
    /**
     * Send an event to all clients with the same tag.
     */
    sendToTaggedClients(tag, event, data) {
        if (this.tags[tag] == null)
            throw new Error(`Unable to send to tagged clients. Tag ${tag} does not exist.`);
        for (let key in this.tags[tag]) {
            let client = this.tags[tag][key];
            client.send(event, data);
        }
    }
    /**
     * Send an event to all clients.
     */
    sendToAll(event, data) {
        for (let key in this.clients) {
            let client = this.clients[key];
            client.send(event, data);
        }
    }
    /**
     * Send an event to a client.
     */
    send(clientId, event, data) {
        let client = this.clients[clientId];
        if (client == null)
            throw new Error(`Unable to find client with id ${clientId}`);
        client.send(event, data);
    }
    /**
     * Disconnect all clients with the same tag.
     */
    disconnectTaggedClients(tag, closeConnection = true) {
        if (this.tags[tag] == null)
            throw new Error(`Unable to send to tagged clients. Tag ${tag} does not exist.`);
        this.tags[tag] = {};
        for (let key in this.tags[tag]) {
            let client = this.tags[tag][key];
            if (closeConnection === true)
                client.disconnect(false);
        }
    }
    /**
     * Disconnect all clients.
     */
    disconnectAll(closeConnection = true) {
        this.clients = {};
        this.clearAllTags();
        for (let key in this.clients) {
            let client = this.clients[key];
            if (closeConnection === true)
                client.disconnect(false);
        }
    }
    /**
     * Send a client.
     */
    disconnect(clientId, closeConnection = true) {
        let client = this.clients[clientId];
        if (client == null)
            throw new Error(`Unable to find client with id ${clientId}`);
        delete this.clients[clientId];
        for (let tag in this.tags) {
            let client2 = this.tags[tag][clientId];
            if (client2 != null)
                delete this.tags[tag][clientId];
        }
        if (this.onDisconnect != null)
            this.onDisconnect(client);
        if (closeConnection === true)
            client.disconnect(false);
    }
    /**
     * Add a route for socket.io. These will only be added to new
     * connections. Existing connections will not receive these.
     */
    addRoute(route, routeMethod = null, executeFunction = null) {
        let routeName = "";
        if (typeof (route) === "string") {
            routeName = route;
            if (this.routes[routeName] == null)
                this.routes[routeName] = new HotRoute_1.HotRoute(this.connection, routeName);
            if (typeof (routeMethod) === "string") {
                this.routes[routeName].addMethod(HotRouteMethod_1.HotRouteMethod.create(this.routes[routeName], routeMethod, executeFunction));
                this.logger.verbose(`Added WebSocket route ${routeName} with method ${routeMethod}`);
            }
            else {
                this.routes[routeName].addMethod(routeMethod);
                this.logger.verbose(`Added WebSocket route ${routeName}`);
            }
        }
        else {
            routeName = route.route;
            this.routes[route.route] = route;
            this.logger.verbose(`Added WebSocket route ${routeName}`);
        }
    }
    /**
     * Stop the socket.io server.
     */
    stop() {
        return __awaiter(this, void 0, void 0, function* () {
            this.disconnectAll();
            yield new Promise((resolve, reject) => {
                this.io.close((err) => {
                    if (err != null) {
                        if (err.code != null) {
                            if (err.code !== "ERR_SERVER_NOT_RUNNING")
                                throw err;
                        }
                    }
                    this.logger.verbose(`WebSocket Server has stopped listening.`);
                    resolve();
                });
            });
            this.logger.info(`WebSocket Server has completely shut down.`);
        });
    }
}
exports.HotWebSocketServer = HotWebSocketServer;
//# sourceMappingURL=HotWebSocketServer.js.map