const toast = require('izitoast');
const cbor = require('borc');

const SOCKET_URL = process.env.NODE_ENV === 'production' ? 'wss://mnml.gg/api/ws' : 'ws://localhost/api/ws';

function errorToast(err) {
    console.error(err);
    return toast.error({
        title: 'BEEP BOOP',
        message: err,
        position: 'topRight',
    });
}

function createSocket(events) {
    let ws;

    // // handle account auth within the socket itself
    // // https://www.christian-schneider.net/CrossSiteWebSocketHijacking.html
    // let account;
    // try {
    //     account = JSON.parse(localStorage.getItem('account'));
    // } catch (e) {
    //     localStorage.removeItem('account');
    // }

    // -------------
    //   Outgoing
    // -------------
    function send(msg) {
        if (msg.method !== 'Ping') console.log('outgoing msg', msg);
        ws.send(cbor.encode(msg));
    }

    let ping;
    function sendPing() {
        ping = Date.now();
        send(['Ping', {}]);
    }

    function sendAccountConstructs() {
        send(['AccountConstructs', {}]);
    }

    function sendAccountInstances() {
        send(['AccountInstances', {}]);
    }

    function sendGameState(id) {
        send(['GameState', { id }]);
    }

    function sendGameReady(id) {
        send(['GameReady', { id }]);
    }

    function sendInstanceState(instanceId) {
        send(['InstanceState', { instance_id: instanceId }]);
    }

    function sendInstanceList() {
        send(['InstanceList', {}]); }

    function sendVboxAccept(instanceId, group, index) {
        send(['VboxAccept', { instance_id: instanceId, group, index }]);
        events.clearInstance();
    }

    function sendVboxApply(instanceId, constructId, index) {
        send(['VboxApply', { instance_id: instanceId, construct_id: constructId, index }]);
        events.clearInstance();
    }

    function sendVboxUnequip(instanceId, constructId, target) {
        send(['VboxUnequip', { instance_id: instanceId, construct_id: constructId, target }]);
        events.clearInstance();
    }

    function sendVboxDiscard(instanceId) {
        send(['VboxDiscard', { instance_id: instanceId }]);
        events.clearInstance();
    }

    function sendVboxCombine(instanceId, indices) {
        send(['VboxCombine', { instance_id: instanceId, indices }]);
        events.clearCombiner();
    }

    function sendVboxReclaim(instanceId, index) {
        send(['VboxReclaim', { instance_id: instanceId, index }]);
        events.clearInstance();
    }

    function sendItemInfo() {
        send(['ItemInfo', {}]);
    }

    function sendGameSkill(gameId, constructId, targetConstructId, skill) {
        send(['GameSkill',
            { game_id: gameId, construct_id: constructId, target_construct_id: targetConstructId, skill, },
        ]);
        events.setActiveSkill(null);
    }

    function sendGameTarget(gameId, constructId, skillId) {
        send(['GameTarget', { game_id: gameId, construct_id: constructId, skill_id: skillId }]);
        events.setActiveSkill(null);
    }

    function sendInstanceJoin(instanceId, constructs) {
        send(['InstanceJoin', { instance_id: instanceId, construct_ids: constructs }]);
    }

    function sendInstanceNew(constructs, name, pve) {
        send(['InstanceLobby', { construct_ids: constructs, name, pve }]);
    }

    function sendInstanceReady(instanceId) {
        send(['InstanceReady', { instance_id: instanceId }]);
    }

    function sendMtxApply(constructId, mtx) {
        send(['MtxConstructApply', { construct_id: constructId, mtx }]);
        // events.clearMtxActive();
    }

    function sendMtxBuy(mtx) {
        send(['MtxBuy', { mtx }]);
    }

    // -------------
    //   Incoming
    // -------------
    function onAccount(login) {
        events.setAccount(login);
        sendAccountConstructs();
        sendAccountInstances();
    }

    function onAccountShop(shop) {
        events.setShop(shop);
    }

    function onAccountInstances(list) {
        events.setAccountInstances(list);
        setTimeout(sendAccountInstances, 5000);
    }

    function onAccountConstructs(constructs) {
        events.setConstructList(constructs);
    }

    function onGameState(game) {
        events.setGame(game);
    }

    let gameStateTimeout;
    function startGameStateTimeout(id) {
        clearTimeout(gameStateTimeout);
        gameStateTimeout = setTimeout(() => sendGameState(id), 1000);
        return true;
    }

    function clearGameStateTimeout() {
        clearTimeout(gameStateTimeout);
    }

    let instanceStateTimeout;
    function startInstanceStateTimeout(id) {
        clearTimeout(instanceStateTimeout);
        instanceStateTimeout = setTimeout(() => sendInstanceState(id), 1000);
        return true;
    }

    function onInstanceState(instance) {
        events.setInstance(instance);
        return true;
    }

    function onOpenInstances(list) {
        events.setInstanceList(list);
        return true;
    }

    function clearInstanceStateTimeout() {
        clearTimeout(instanceStateTimeout);
    }

    function onItemInfo(info) {
        events.setItemInfo(info);
    }

    let pongTimeout;
    function onPong() {
        events.setPing(Date.now() - ping);
        pongTimeout = setTimeout(sendPing, 1000);
    }

    // -------------
    //   Setup
    // -------------

    // when the server sends a reply it will have one of these message types
    // this object wraps the reply types to a function
    const handlers = {
        AccountState: onAccount,
        AccountConstructs: onAccountConstructs,
        AccountInstances: onAccountInstances,
        AccountShop: onAccountShop,
        GameState: onGameState,
        InstanceState: onInstanceState,
        ItemInfo: onItemInfo,
        OpenInstances: onOpenInstances,
        Pong: onPong,
    };

    function logout() {
        window.location.reload(true);
    }

    function errHandler(error) {
        switch (error) {
            case 'invalid token': return logout();
            case 'no constructs selected': return events.errorPrompt('select_constructs');
            case 'node requirements not met': return events.errorPrompt('complete_nodes');
            case 'construct at max skills (4)': return events.errorPrompt('max_skills');

            default: return errorToast(error);

        }
    }

    // decodes the cbor and
    // calls the handlers defined above based on message type
    function onMessage(event) {
        // decode binary msg from server
        const blob = new Uint8Array(event.data);
        const res = cbor.decode(blob);
        if (res.err) return errHandler(res.err);

        const [msgType, params] = res;
        if (msgType !== 'Pong') console.log(res);

        // check for error and split into response type and data
        if (!handlers[msgType]) return errorToast(`${msgType} handler missing`);
        return handlers[msgType](params);
    }

    // Connection opened
    function onOpen() {
        toast.info({
            message: 'connected',
            position: 'topRight',
        });

        sendPing();
        sendItemInfo();

        return true;
    }

    function onError(event) {
        console.error('WebSocket error', event);
    }

    function onClose(event) {
        console.error('WebSocket closed', event);
        toast.warning({
            message: 'disconnected',
            position: 'topRight',
        });
        return setTimeout(connect, 5000);
    }

    function connect() {
        if (ws) {
            clearGameStateTimeout();
            clearInstanceStateTimeout();
            clearTimeout(pongTimeout);
            ws.removeEventListener('open', onOpen);
            ws.removeEventListener('message', onMessage);
            ws.removeEventListener('error', onError);
            ws.removeEventListener('close', onClose);
            ws = null;
        }

        ws = new WebSocket(SOCKET_URL);
        ws.binaryType = 'arraybuffer';

        // Listen for messages
        ws.addEventListener('open', onOpen);
        ws.addEventListener('message', onMessage);
        ws.addEventListener('error', onError);
        ws.addEventListener('close', onClose);
        return ws;
    }

    return {
        clearGameStateTimeout,
        clearInstanceStateTimeout,
        sendAccountConstructs,
        sendAccountInstances,
        sendGameState,
        sendGameReady,
        sendGameSkill,
        sendGameTarget,
        sendInstanceJoin,
        sendInstanceList,
        sendInstanceReady,
        sendInstanceNew,
        sendInstanceState,
        sendVboxAccept,
        sendVboxApply,
        sendVboxReclaim,
        sendVboxCombine,
        sendVboxDiscard,
        sendVboxUnequip,
        sendItemInfo,
        sendMtxApply,
        sendMtxBuy,
        startInstanceStateTimeout,
        startGameStateTimeout,
        connect,
    };
}

module.exports = createSocket;
