/*
 * Copyright (C) 2016-2019 phantombot.tv
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

//NOTE: this is straight copied from panel's index.js
// Main socket and functions.
$(function() {


    var helpers = {};

    helpers.DEBUG_STATES = {
        NONE: 0,
        DEBUG: 1,
        INFO: 2,
        FORCE: 3
    };
    // Debug status. 0 = off | 1 = on.
    helpers.DEBUG_STATE = (localStorage.getItem('phantombot_debug_state') !== null ? parseInt(localStorage.getItem('phantombot_debug_state')) : helpers.DEBUG_STATES.NONE);
    // Debug types.
    helpers.LOG_TYPE = helpers.DEBUG_STATES;
    
    /*
     * @function Used to print debug messages in the console.
     *
     * @param {String}  message
     * @param {Number} type
     */
    helpers.log = function(message, type) {
        if (helpers.DEBUG_STATE === helpers.DEBUG_STATES.DEBUG || type === helpers.DEBUG_STATE || type === helpers.LOG_TYPE.FORCE) {
            console.log('%c[PhantomBot Log]', 'color: #6441a5; font-weight: 900;', message);
        }
    };

    /*
     * @function Used to print error messages in the console.
     *
     * @param {String}  message
     * @param {Number} type
     */
    helpers.logError = function(message, type) {
        console.log('%c[PhantomBot Error]', 'color: red; font-weight: 900;', message);
    };




    var webSocket = new ReconnectingWebSocket((getProtocol() === 'https://' || window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host + '/ws/panel', null, { reconnectInterval: 500 }),
        callbacks = [],
        listeners = [],
        familyHandlers = {},
        socket = {};

    /*
     * @function Used to send messages to the socket. This should be private to this script.
     *
     * @param {Object} message
     */
    var sendToSocket = function(message) {
        try {
            let json = JSON.stringify(message);

            webSocket.send(json);

            // Make sure to not show the user's token.
            if (json.indexOf('authenticate') !== -1) {
                helpers.log('sendToSocket:: ' + json.substring(0, json.length - 20) + '.."}', helpers.LOG_TYPE.DEBUG);
            } else {
                helpers.log('sendToSocket:: ' + json, helpers.LOG_TYPE.DEBUG);
            }
        } catch (e) {
            helpers.logError('Failed to send message to socket: ' + e.message, helpers.LOG_TYPE.DEBUG);
        }
    };

    /*
     * @function Generates a callback
     *
     * @param {String}   id
     * @param {Array}    tables
     * @param {Boolean}  isUpdate
     * @param {Function} callback
     * @param {Boolean}  storeKey
     */
    var generateCallBack = function(id, tables, isUpdate, isArray, callback, storeKey) {
        if (callbacks[id] !== undefined) {
            helpers.logError('Callback with id "' + id + '" exists already. Aborting update.', helpers.LOG_TYPE.FORCE);
        } else {
            helpers.log('Created callback with id ' + id, helpers.LOG_TYPE.DEBUG);

            callbacks[id] = {
                await: (tables.length === 0 ? 1 : tables.length),
                isUpdate: isUpdate,
                isArray: isArray,
                func: function(e) {
                    try {
                        callback(e);
                    } catch (ex) {
                        // Line number won't be accurate, function will by anonymous, but we get the stack so it should be fine.
                        helpers.logError('Failed to run callback: ' + ex.stack, helpers.LOG_TYPE.FORCE);
                    }
                },
                storeKey: storeKey,
                queryData: []
            };
        }
    };

    /*
     * @function Adds a listener for the socket.
     *
     * @param {String}   listener_id
     * @param {Function} callback
     */
    socket.addListener = function(listener_id, callback) {
        if (listeners[listener_id] === undefined) {
            helpers.log('Adding listener with id ' + listener_id);
            listeners[listener_id] = function(e) {
                try {
                    callback(e);
                } catch (ex) {
                    // Line number won't be accurate, function will by anonymous, but we get the stack so it should be fine.
                    helpers.logError('Failed to run listener: ' + ex.stack, helpers.LOG_TYPE.FORCE);
                }
            };
        }
    };

    /*
     * @function Removes a listener from the socket.
     *
     * @param {String}   listener_id
     */
    socket.removeListener = function(listener_id) {
        if (listeners[listener_id] !== undefined) {
            delete listeners[listener_id];
        }
    };

    socket.addFamilyHandler = function(familyName, callback) {
        familyHandlers[familyName] = function(e) {
            try {
                callback(e);
            } catch (ex) {
                // Line number won't be accurate, function will by anonymous, but we get the stack so it should be fine.
                helpers.logError('Failed to run family handler: ' + ex.stack, helpers.LOG_TYPE.FORCE);
            }
        };
    }

    socket.removeFamilyHandler = function(familyName) {
        if (familyHandlers[familyName] !== undefined) {
            delete listeners[familyName];
        }
    };

    /*
     * @function Runs a bot commands as the bot in async, thus returning right away.
     *
     * @param {String}   callback_id
     * @param {String}   command
     * @param {Function} callback
     */
    socket.sendCommand = function(callback_id, command, callback) {
        // Genetate a callback.
        generateCallBack(callback_id, [], true, false, callback);

        // Send the command.
        sendToSocket({
            command: String(command),
            query_id: callback_id
        });
    };

    /*
     * @function Sends a raw request to the socket.
     *
     * @param {String}   callback_id
     * @param {Function} callback
     */
    socket.getBotVersion = function(callback_id, callback) {
        // Genetate a callback.
        generateCallBack(callback_id, [], true, false, callback);

        // Send the request.
        sendToSocket({
            version: callback_id
        });
    };

    /*
     * @function Runs a bot commands as the bot.
     *
     * @param {String}   callback_id
     * @param {String}   command
     * @param {Function} callback
     */
    socket.sendCommandSync = function(callback_id, command, callback) {
        // Genetate a callback.
        generateCallBack(callback_id, [], true, false, callback);

        // Send the command.
        sendToSocket({
            command_sync: String(command),
            query_id: callback_id
        });
    };

    /*
     * @function Sends the websocket event.
     *
     * @param {String}   callback_id
     * @param {String}   script
     * @param {String}   argsString
     * @param {Array}    args
     * @param {Function} callback
     */
    socket.wsEvent = function(callback_id, script, argsString, args, callback) {
        // Genetate a callback.
        generateCallBack(callback_id, [], true, false, callback);

        // Send event.
        sendToSocket({
            socket_event: callback_id,
            script: script,
            args: {
                arguments: String(argsString),
                args: args
            }
        });
    };

    /*
     * @function Updates a value in the database of the bot.
     *
     * @param {String}   callback_id
     * @param {String}   table
     * @param {String}   key
     * @param {String}   value
     * @param {Function} callback
     */
    socket.updateDBValue = function(callback_id, table, key, value, callback) {
        // Genetate a callback.
        generateCallBack(callback_id, [], true, false, callback);

        // Update the value.
        sendToSocket({
            dbupdate: callback_id,
            update: {
                table: String(table),
                key: String(key),
                value: String(value)
            }
        });
    };

    /*
     * @function Updates values in the database of the bot.
     *
     * @param {String}   callback_id
     * @param {Object}   dataObj {tables: [], keys: [], values: }
     * @param {Function} callback
     */
    socket.updateDBValues = function(callback_id, dataObj, callback) {
        // Genetate a callback.
        generateCallBack(callback_id, dataObj.tables, true, false, callback);

        // Start sending the updates to the socket.
        for (let i = 0; i < dataObj.tables.length; i++) {
            sendToSocket({
                dbupdate: callback_id,
                update: {
                    table: String(dataObj.tables[i]),
                    key: String(dataObj.keys[i]),
                    value: String(dataObj.values[i])
                }
            });
        }
    };

    /*
     * @function Increases a value in the database.
     *
     * @param {String}   callback_id
     * @param {String}   table
     * @param {String}   key
     * @param {String}   value
     * @param {Function} callback
     */
    socket.incrDBValue = function(callback_id, table, key, value, callback) {
        // Genetate a callback.
        generateCallBack(callback_id, [], true, false, callback);

        // Update the value.
        sendToSocket({
            dbincr: callback_id,
            incr: {
                table: table,
                key: key,
                value: value
            }
        });
    };

    /*
     * @function Decreases a value in the database.
     *
     * @param {String}   callback_id
     * @param {String}   table
     * @param {String}   key
     * @param {String}   value
     * @param {Function} callback
     */
    socket.decrDBValue = function(callback_id, table, key, value, callback) {
        // Genetate a callback.
        generateCallBack(callback_id, [], true, false, callback);

        // Update the value.
        sendToSocket({
            dbdecr: callback_id,
            decr: {
                table: table,
                key: key,
                value: value
            }
        });
    };

    /*
     * @function Gets a value from the database
     *
     * @param {String}   callback_id
     * @param {String}   table
     * @param {String}   key
     * @param {Function} callback
     */
    socket.getDBValue = function(callback_id, table, key, callback) {
        // Genetate a callback.
        generateCallBack(callback_id, [], false, false, callback);

        // Query database.
        sendToSocket({
            dbquery: callback_id,
            query: {
                table: String(table),
                key: String(key)
            }
        });
    };

    /*
     * @function Gets values from the database
     *
     * @param {String}   callback_id
     * @param {Object}   dataObj {tables: [], keys: []}
     * @param {Function} callback
     * @param {Boolean}  storeKey - Store the value with the key name from the DB. Default stores it as the table, thus making it only possible to query the table once.
     */
    socket.getDBValues = function(callback_id, dataObj, storeKey, callback) {
        callback = (callback === undefined ? storeKey : callback);

        // Genetate a callback.
        generateCallBack(callback_id, dataObj.tables, false, false, callback, (typeof storeKey === 'function' ? false : true));

        // Start sending the updates to the socket.
        for (let i = 0; i < dataObj.tables.length; i++) {
            sendToSocket({
                dbquery: callback_id,
                query: {
                    table: String(dataObj.tables[i]),
                    key: String(dataObj.keys[i])
                }
            });
        }
    };

    /*
     * @function Gets values from the database by an order.
     *
     * @param {String}   callback_id
     * @param {String}   table
     * @param {Number}   limit
     * @param {Number}   offset
     * @param {String}   order
     * @param {Function} callback
     */
    socket.getDBTableValuesByOrder = function(callback_id, table, limit, offset, order, isNumber, callback) {
        // Genetate a callback.
        generateCallBack(callback_id, [], false, true, callback);

        // Query database.
        sendToSocket({
            dbvaluesbyorder: callback_id,
            query: {
                table: table,
                limit: String(limit),
                offset: String(offset),
                order: order,
                number: String(isNumber)
            }
        });
    };

    /*
     * @function Gets all keys and values from a database table.
     *
     * @param {String}   callback_id
     * @param {String}   table
     * @param {Function} callback
     */
    socket.getDBTableValues = function(callback_id, table, callback) {
        // Genetate a callback.
        generateCallBack(callback_id, [], false, true, callback);

        // Query database.
        sendToSocket({
            dbkeys: callback_id,
            query: {
                table: String(table)
            }
        });
    };

    /*
     * @function Gets all keys and values from multiple database table.
     *
     * @param {String}       callback_id
     * @param {Array Object} tables [{table: 'a'}, {table: 'b'}]
     * @param {Function}     callback
     */
    socket.getDBTablesValues = function(callback_id, tables, callback) {
        // Genetate a callback.
        generateCallBack(callback_id, [], false, true, callback);

        // Query database.
        sendToSocket({
            dbkeyslist: callback_id,
            query: tables
        });
    };

    /*
     * @function Removes the data from the database.
     *
     * @param {String}   callback_id
     * @param {String}   table
     * @param {String}   key
     * @param {Function} callback
     */
    socket.removeDBValue = function(callback_id, table, key, callback) {
        // Genetate a callback.
        generateCallBack(callback_id, [], false, true, callback);

        // Send the event.
        sendToSocket({
            dbdelkey: callback_id,
            delkey: {
                table: String(table),
                key: String(key)
            }
        });
    };

    /*
     * @function Removes the data from the database.
     *
     * @param {String}   callback_id
     * @param {Object}   dataObj {tables: [], keys: []}
     * @param {Function} callback
     */
    socket.removeDBValues = function(callback_id, dataObj, callback) {
        // Genetate a callback.
        generateCallBack(callback_id, dataObj.tables, false, true, callback);

        for (let i = 0; i < dataObj.tables.length; i++) {
            // Send the event.
            sendToSocket({
                dbdelkey: callback_id,
                delkey: {
                    table: String(dataObj.tables[i]),
                    key: String(dataObj.keys[i])
                }
            });
        }
    };

    // WebSocket events.

    socket.getReadyState = function() {return webSocket.readyState;}

    /*
     * @function Called when the socket opens.
     */
    webSocket.onopen = function() {
        helpers.log('Connection established with the websocket.', helpers.LOG_TYPE.FORCE);
        // Auth with the socket.
        sendToSocket({
            authenticate: getAuth()
        });
    };

    /*
     * @function Socket calls when it closes
     */
    webSocket.onclose = function() {
        helpers.logError('Connection lost with the websocket.', helpers.LOG_TYPE.FORCE);
    };

    /*
     * @function Socket calls when it gets message.
     */
    webSocket.onmessage = function(e) {
        try {
            helpers.log('Message from socket: ' + e.data, helpers.LOG_TYPE.DEBUG);

            if (e.data === 'PING') {
                webSocket.send('PONG');
                return;
            }

            let message = JSON.parse(e.data);

            // Check this message here before doing anything else.
            if (message.authresult !== undefined) {
                if (message.authresult === 'false') {
                    helpers.logError('Failed to auth with the socket.', helpers.LOG_TYPE.FORCE);
                } else {
                    // This is to stop a reconnect loading the main page.
                    if (helpers.isAuth === true) {
                        return;
                    } else {
                        helpers.isAuth = true;
                    }

                    // XXX: revent loading main page
                    //$.loadPage('dashboard', 'dashboard.html');
                }
                return;
            }

            // Make sure this isn't a version request.
            if (message.versionresult !== undefined) {
                // Call the callback.
                callbacks[message.versionresult].func(message);
                // Delete the callback.
                delete callbacks[message.versionresult];
            } else if(message.query_id !== undefined) {

                // console.log("base got websocket with data");
                // console.log(e);

                // Handle callbacks.
                let callback = callbacks[message.query_id],
                    listener = listeners[message.query_id];

                if (callback !== undefined) {
                    // Add our data to the callback array.
                    if (!callback.isUpdate) {
                        if (callback.isArray) {
                            callback.queryData = message.results;
                        } else if (callback.storeKey === true) {
                            callback.queryData[Object.keys(message.results)[1]] = message.results.value;
                        } else {
                            callback.queryData[message.results.table] = message.results.value;
                        }
                    }

                    // If we got all the data, run the callback.
                    if (--callback.await === 0) {
                        // Run the function and send the query data with it.
                        callback.func(callback.queryData);
                        // Log this.
                        helpers.log('Called callback with id: ' + message.query_id, helpers.LOG_TYPE.DEBUG);
                        // Remove the callback from the array.
                        delete callbacks[message.query_id];

                        // Remove any active spinners.
                        if (message.query_id !== 'get_bot_updates' && message.query_id.indexOf('get') !== -1) {
                            // Remove any active spinners.
                            $('.load-ajax').remove();
                        }

                        if (message.query_id.indexOf('module_toggle') !== -1 || message.query_id.indexOf('module_status') !== -1
                            || message.query_id.endsWith('module')) {
                            if (message.results.value == 'false') {
                                $('.load-ajax').remove();
                            }
                        }
                    } else {
                        helpers.log('Awaiting for data (' + callback.await + ' left) before calling callback with id: ' + message.query_id, helpers.LOG_TYPE.DEBUG);
                    }
                } else if (listener !== undefined) {
                    // Call the listener.
                    listener(message.results);
                }
            } else if(message.eventFamily !== undefined) {
                let handler = familyHandlers[message.eventFamily];
                if(handler !== undefined) {
                    handler(e)
                }
            }
        } catch (ex) {
            // Line number won't be accurate, function will by anonymous, but we get the stack so it should be fine.
            helpers.logError('Failed to parse message from socket: ' + ex.stack + '\n\n' + e.data, helpers.LOG_TYPE.FORCE);
        }
    };

    // Make this a global object.
    window.socket = socket;
    // Store all timers in here so we can destroy them.
    window.timers = [];
});