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


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

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

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) {
        console.log('outgoing msg', msg);
        msg.token = account && account.token && account.token;
        ws.send(cbor.encode(msg));
    }

    function sendAccountLogin(name, password) {
        send({ method: 'account_login', params: { name, password } });
    }

    function sendAccountCreate(name, password) {
        send({ method: 'account_create', params: { name, password } });
    }

    function sendAccountDemo() {
        send({ method: 'account_demo', params: {} });
    }

    function sendAccountCryps() {
        send({ method: 'account_cryps', params: {} });
    }

    function sendAccountInstances() {
        send({ method: 'account_instances', params: {} });
    }

    function sendAccountZone() {
        send({ method: 'account_zone', params: {} });
    }

    function sendCrypSpawn(name) {
        send({ method: 'cryp_spawn', params: { name } });
    }

    function sendCrypLearn(id, skill) {
        send({ method: 'cryp_learn', params: { id, skill } });
    }

    function sendCrypForget(id, skill) {
        send({ method: 'cryp_forget', params: { id, skill } });
    }

    function sendGameState(id) {
        send({ method: 'game_state', params: { id } });
    }

    function sendGameJoin(gameId, crypIds) {
        send({ method: 'game_join', params: { game_id: gameId, cryp_ids: crypIds } });
    }

    function sendSpecForget(id, spec) {
        send({ method: 'cryp_unspec', params: { id, spec } });
    }

    function sendPlayerMmCrypsSet(crypIds) {
        send({ method: 'player_mm_cryps_set', params: { cryp_ids: crypIds } });
    }

    function sendInstanceState(instanceId) {
        send({ method: 'instance_state', params: { instance_id: instanceId } });
    }

    function sendVboxAccept(instanceId, group, index) {
        send({ method: 'player_vbox_accept', params: { instance_id: instanceId, group, index } });
    }

    function sendVboxApply(instanceId, crypId, index) {
        send({ method: 'player_vbox_apply', params: { instance_id: instanceId, cryp_id: crypId, index } });
        events.setActiveVar(null);
    }

    function sendVboxUnequip(instanceId, crypId, target) {
        send({ method: 'player_vbox_unequip', params: { instance_id: instanceId, cryp_id: crypId, target } });
        events.clearInfo();
    }

    function sendVboxDiscard(instanceId) {
        send({ method: 'player_vbox_discard', params: { instance_id: instanceId } });
    }

    function sendVboxCombine(instanceId, indices) {
        send({ method: 'player_vbox_combine', params: { instance_id: instanceId, indices } });
        events.clearCombiner();
    }

    function sendVboxReclaim(instanceId, index) {
        send({ method: 'player_vbox_reclaim', params: { instance_id: instanceId, index } });
    }

    function sendGameSkill(gameId, crypId, targetCrypId, skill) {
        send({
            method: 'game_skill',
            params: {
                game_id: gameId, cryp_id: crypId, target_cryp_id: targetCrypId, skill,
            },
        });
        events.setActiveSkill(null);
    }

    function sendGameTarget(gameId, crypId, skillId) {
        send({ method: 'game_target', params: { game_id: gameId, cryp_id: crypId, skill_id: skillId } });
        events.setActiveSkill(null);
    }

    function sendZoneCreate() {
        send({ method: 'zone_create', params: {} });
    }

    function sendZoneJoin(zoneId, nodeId, crypIds) {
        send({ method: 'zone_join', params: { zone_id: zoneId, node_id: nodeId, cryp_ids: crypIds } });
    }

    function sendZoneClose(zoneId) {
        send({ method: 'zone_close', params: { zone_id: zoneId } });
    }

    function sendInstanceJoin(instanceId, cryps) {
        send({ method: 'instance_join', params: { instance_id: instanceId, cryp_ids: cryps } });
    }

    function sendInstanceNew(cryps, name, players) {
        send({ method: 'instance_new', params: { cryp_ids: cryps, name, players } });
    }

    function sendInstanceReady(instanceId) {
        send({ method: 'instance_ready', params: { instance_id: instanceId } });
    }

    function sendInstanceScores(instanceId) {
        send({ method: 'instance_scores', params: { instance_id: instanceId } });
    }


    // -------------
    //   Incoming
    // -------------
    function accountLogin(res) {
        const [struct, login] = res;

        account = login;
        localStorage.setItem('account', JSON.stringify(login));
        events.setAccount(login);
        sendAccountCryps();
        sendAccountInstances();
    }

    function accountInstanceList(res) {
        const [struct, playerList] = res;
        events.setInstanceList(playerList);
    }

    function accountCryps(response) {
        const [structName, cryps] = response;
        events.setCrypList(cryps);
    }

    let gameStateTimeout;
    function gameState(response) {
        const [structName, game] = response;
        clearTimeout(gameStateTimeout);
        gameStateTimeout = setTimeout(() => sendGameState(game.id), 1000);
        events.setGame(game);
    }

    function clearGameStateTimeout() {
        clearTimeout(gameStateTimeout);
    }

    function crypSpawn(response) {
        const [structName, cryp] = response;
    }

    function zoneState(response) {
        const [structName, zone] = response;
        events.setZone(zone);
    }

    let instanceStateTimeout;
    function instanceState(response) {
        const [structName, i] = response;
        clearTimeout(instanceStateTimeout);
        instanceStateTimeout = setTimeout(() => sendInstanceState(i.id), 1000);
        events.setInstance(i);
    }

    function clearInstanceStateTimeout() {
        clearTimeout(instanceStateTimeout);
    }


    function instanceScores(response) {
        const [structName, scores] = response;
        events.setScores(scores);
    }

    // -------------
    //   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 = {
        cryp_spawn: crypSpawn,
        cryp_forget: () => true,
        cryp_learn: () => true,
        game_state: gameState,
        account_login: accountLogin,
        account_create: accountLogin,
        account_cryps: accountCryps,
        account_instances: accountInstanceList,
        instance_scores: instanceScores,
        zone_create: res => console.log(res),
        zone_state: zoneState,
        zone_close: res => console.log(res),
        instance_state: instanceState,
    };

    function logout() {
        localStorage.removeItem('account');
        account = null;
        event.setAccount(null);
    }

    function errHandler(error) {
        switch (error) {
            case 'invalid token': return logout();
            case 'no active zone': return sendZoneCreate();
            case 'no cryps selected': return events.errorPrompt('select_cryps');
            case 'node requirements not met': return events.errorPrompt('complete_nodes');
            case 'cryp 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);
        const { method, params } = res;

        console.log(res);

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

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

        // Connection opened
        ws.addEventListener('open', () => {
            toast.info({
                message: 'connected',
                position: 'bottomCenter',
            });

            if (account) {
                events.setAccount(account);
                sendAccountInstances();
                sendAccountCryps();
            }

            return true;
        });

        // Listen for messages
        ws.addEventListener('message', onMessage);

        ws.addEventListener('error', (event) => {
            console.error('WebSocket error', event);
            // account = null;
            // return setTimeout(connect, 5000);
        });

        ws.addEventListener('close', (event) => {
            console.error('WebSocket closed', event);
            toast.warning({
                message: 'disconnected',
                position: 'bottomCenter',
            });
            return setTimeout(connect, 5000);
        });

        return ws;
    }

    return {
        clearGameStateTimeout,
        clearInstanceStateTimeout,
        sendAccountLogin,
        sendAccountCreate,
        sendAccountDemo,
        sendAccountCryps,
        sendAccountInstances,
        sendAccountZone,
        sendGameState,
        sendGameJoin,
        sendGameSkill,
        sendGameTarget,
        sendCrypSpawn,
        sendCrypLearn,
        sendCrypForget,
        sendSpecForget,
        sendZoneCreate,
        sendZoneJoin,
        sendZoneClose,
        sendInstanceJoin,
        sendInstanceReady,
        sendInstanceNew,
        sendInstanceScores,
        sendPlayerMmCrypsSet,
        sendInstanceState,
        sendVboxAccept,
        sendVboxApply,
        sendVboxReclaim,
        sendVboxCombine,
        sendVboxDiscard,
        sendVboxUnequip,
        connect,
    };
}

module.exports = createSocket;
