#include eventtarget.js
#include websock.js

Umi.Protocol.SockChat.Protocol = function(pingDuration) {
    if(typeof pingDuration !== 'number')
        throw 'pingDuration must be a number';

    const eventTarget = new MamiEventTarget('mami:proto');

    const parseUserColour = str => {
        // todo
        return str;
    };

    let parseUserPermsSep;
    const parseUserPerms = str => {
        parseUserPermsSep ??= str.includes("\f") ? "\f" : ' ';
        return str.split(parseUserPermsSep);
    };

    const parseMsgFlags = str => {
        return {
            nameBold: str[0] !== '0',
            nameItalics: str[1] !== '0',
            nameUnderline: str[2] !== '0',
            showColon: str[3] !== '0',
            isPM: str[4] !== '0',
            isAction: str[1] !== '0' && str[3] === '0',
        };
    };

    let wasConnected = false;
    let wasKicked = false;
    let isRestarting = false;
    let dumpPackets = false;

    let sock;
    let selfUserId, selfChannelName, selfPseudoChannelName;
    let lastPing, lastPong, pingTimer, pingWatcher;
    let openResolve, openReject;

    const handlers = {};

    const stopPingWatcher = () => {
        if(pingWatcher !== undefined) {
            clearTimeout(pingWatcher);
            pingWatcher = undefined;
        }
    };
    const startPingWatcher = () => {
        if(pingWatcher === undefined)
            pingWatcher = setTimeout(() => {
                stopPingWatcher();

                if(lastPong === undefined)
                    eventTarget.dispatch('ping:long');
            }, 2000);
    };

    const send = (...args) => {
        if(args.length < 1)
            throw 'you must specify at least one argument as an opcode';

        const pack = args.join("\t");

        if(dumpPackets)
            console.log(pack);

        sock?.send(pack);
    };

    const onSendPing = () => {
        if(selfUserId === undefined)
            return;

        eventTarget.dispatch('ping:send');
        startPingWatcher();

        lastPong = undefined;
        lastPing = Date.now();
    };
    const sendAuth = (...args) => {
        if(selfUserId === undefined)
            send('1', ...args);
    };
    const sendMessage = text => {
        if(selfUserId === undefined)
            return;

        if(text.substring(0, 1) !== '/' && selfPseudoChannelName !== undefined)
            text = `/msg ${selfPseudoChannelName} ${text}`;

        send('2', selfUserId, text);
    };

    const startKeepAlive = () => sock?.sendInterval(`0\t${selfUserId}`, pingDuration);
    const stopKeepAlive = () => sock?.clearIntervals();

    const onOpen = ev => {
        if(dumpPackets)
            console.log(ev);

        isRestarting = false;

        if(typeof openResolve === 'function') {
            openResolve();
            openResolve = undefined;
        }
    };

    const onClose = ev => {
        if(dumpPackets)
            console.log(ev);

        selfUserId = undefined;
        selfChannelName = undefined;
        selfPseudoChannelName = undefined;
        stopPingWatcher();
        stopKeepAlive();

        if(wasKicked)
            return;

        let code = ev.detail.code;
        if(isRestarting && code === 1006) {
            code = 1012;
        } else if(code === 1012)
            isRestarting = true;

        if(typeof openReject === 'function') {
            openReject({
                code: code,
                wasConnected: wasConnected,
                isRestarting: isRestarting,
            });
            openReject = undefined;
        }

        eventTarget.dispatch('conn:lost', {
            wasConnected: wasConnected,
            isRestarting: isRestarting,
            code: code,
        });
    };

    const unfuckText = text => text.replace(/ <br\/> /g, "\n");

    const onMessage = ev => {
        const args = ev.detail.data.split("\t");
        let handler = handlers;

        if(dumpPackets)
            console.log(args);

        for(;;) {
            handler = handler[args.shift()];
            if(handler === undefined)
                break;

            if(typeof handler === 'function') {
                handler(...args);
                break;
            }
        }
    };


    // pong handler
    handlers['0'] = () => {
        lastPong = Date.now();
        eventTarget.dispatch('ping:recv', {
            ping: lastPing,
            pong: lastPong,
            diff: lastPong - lastPing,
        });
    };

    // join/auth
    handlers['1'] = (successOrTimeStamp, userIdOrReason, userNameOrExpiry, userColour, userPerms, chanNameOrMsgId, maxLength) => {
        if(successOrTimeStamp === 'y') {
            selfUserId = userIdOrReason;
            selfChannelName = chanNameOrMsgId;

            eventTarget.dispatch('session:start', {
                wasConnected: wasConnected,
                session: { success: true },
                ctx: {
                    maxMsgLength: parseInt(maxLength),
                },
                user: {
                    id: selfUserId,
                    self: true,
                    name: userNameOrExpiry,
                    colour: parseUserColour(userColour),
                    perms: parseUserPerms(userPerms),
                    permsRaw: userPerms,
                },
                channel: {
                    name: selfChannelName,
                },
            });

            startKeepAlive();
            wasConnected = true;
            return;
        }

        if(successOrTimeStamp === 'n') {
            wasKicked = true;

            const failInfo = {
                session: {
                    success: false,
                    reason: userIdOrReason,
                    needsAuth: userIdOrReason === 'authfail',
                },
            };
            if(userNameOrExpiry !== undefined)
                failInfo.baka = {
                    type: 'join',
                    perma: userNameOrExpiry === '-1',
                    until: userNameOrExpiry === '-1' ? undefined : new Date(parseInt(userNameOrExpiry) * 1000),
                };

            eventTarget.dispatch('session:fail', failInfo);
            return;
        }

        eventTarget.dispatch('user:add', {
            msg: {
                id: chanNameOrMsgId,
                time: new Date(parseInt(successOrTimeStamp) * 1000),
                channel: selfChannelName,
                botInfo: {
                    type: 'join',
                    args: [userNameOrExpiry],
                },
            },
            user: {
                id: userIdOrReason,
                self: userIdOrReason === selfUserId,
                name: userNameOrExpiry,
                colour: parseUserColour(userColour),
                perms: parseUserPerms(userPerms),
                permsRaw: userPerms,
            },
        });
    };

    // message add
    handlers['2'] = (timeStamp, userId, msgText, msgId, msgFlags) => {
        let mText = unfuckText(msgText);
        let mChannelName = selfChannelName;

        if(msgFlags[4] !== '0') {
            if(userId === selfUserId) {
                const mTextParts = mText.split(' ');
                mChannelName = `@${mTextParts.shift()}`;
                mText = mTextParts.join(' ');
            } else {
                mChannelName = `@~${userId}`;
            }
        }

        const msgInfo = {
            msg: {
                id: msgId,
                time: new Date(parseInt(timeStamp) * 1000),
                channel: mChannelName,
                sender: {
                    id: userId,
                    self: userId === selfUserId,
                },
                flags: parseMsgFlags(msgFlags),
                flagsRaw: msgFlags,
                isBot: userId === '-1',
                text: mText,
            },
        };

        if(msgInfo.msg.isBot) {
            const botParts = msgText.split("\f");
            msgInfo.msg.botInfo = {
                isError: botParts[0] === '1',
                type: botParts[1],
                args: botParts.slice(2),
            };
        }

        eventTarget.dispatch('msg:add', msgInfo);
    };

    // user leave
    handlers['3'] = (userId, userName, reason, timeStamp, msgId) => {
        eventTarget.dispatch('user:remove', {
            leave: { type: reason },
            msg: {
                id: msgId,
                time: new Date(parseInt(timeStamp) * 1000),
                channel: selfChannelName,
                botInfo: {
                    type: reason,
                    args: [userName],
                },
            },
            user: {
                id: userId,
                self: userId === selfUserId,
                name: userName,
            },
        });
    };

    // channel add/upd/del
    handlers['4'] = {};

    // channel add
    handlers['4']['0'] = (name, hasPass, isTemp) => {
        eventTarget.dispatch('chan:add', {
            channel: {
                name: name,
                hasPassword: hasPass !== '0',
                isTemporary: isTemp !== '0',
            },
        });
    };

    // channel update
    handlers['4']['1'] = (prevName, name, hasPass, isTemp) => {
        eventTarget.dispatch('chan:update', {
            channel: {
                previousName: prevName,
                name: name,
                hasPassword: hasPass !== '0',
                isTemporary: isTemp !== '0',
            },
        });
    };

    // channel remove
    handlers['4']['2'] = name => {
        eventTarget.dispatch('chan:remove', {
            channel: { name: name },
        });
    };

    // user channel move
    handlers['5'] = {};

    // user join channel
    handlers['5']['0'] = (userId, userName, userColour, userPerms, msgId) => {
        eventTarget.dispatch('chan:join', {
            user: {
                id: userId,
                self: userId === selfUserId,
                name: userName,
                colour: parseUserColour(userColour),
                perms: parseUserPerms(userPerms),
                permsRaw: userPerms,
            },
            msg: {
                id: msgId,
                channel: selfChannelName,
                botInfo: {
                    type: 'jchan',
                    args: [userName],
                },
            },
        });
    };

    // user leave channel
    handlers['5']['1'] = (userId, msgId) => {
        eventTarget.dispatch('chan:leave', {
            user: {
                id: userId,
                self: userId === selfUserId,
            },
            msg: {
                id: msgId,
                channel: selfChannelName,
                botInfo: {
                    type: 'lchan',
                    args: [userId],
                },
            },
        });
    };

    // user forced switch channel
    handlers['5']['2'] = name => {
        selfChannelName = name;

        eventTarget.dispatch('chan:focus', {
            channel: { name: selfChannelName },
        });
    };

    // message delete
    handlers['6'] = msgId => {
        eventTarget.dispatch('msg:remove', {
            msg: {
                id: msgId,
                channel: selfChannelName,
            },
        });
    };

    // context populate
    handlers['7'] = {};

    // existing users
    handlers['7']['0'] = (count, ...args) => {
        count = parseInt(count);
        eventTarget.dispatch('user:clear');

        for(let i = 0; i < count; ++i) {
            const offset = 5 * i;

            eventTarget.dispatch('user:add', {
                user: {
                    id: args[offset],
                    self: args[offset] === selfUserId,
                    name: args[offset + 1],
                    colour: parseUserColour(args[offset + 2]),
                    perms: parseUserPerms(args[offset + 3]),
                    permsRaw: args[offset + 3],
                    hidden: args[offset + 4] !== '0',
                },
            });
        }
    };

    // existing message
    handlers['7']['1'] = (timeStamp, userId, userName, userColour, userPerms, msgText, msgId, msgNotify, msgFlags) => {
        const info = {
            msg: {
                id: msgId,
                time: new Date(parseInt(timeStamp) * 1000),
                channel: selfChannelName,
                sender: {
                    id: userId,
                    self: userId === selfUserId,
                    name: userName,
                    colour: parseUserColour(userColour),
                    perms: parseUserColour(userPerms),
                    permsRaw: userPerms,
                },
                isBot: userId === '-1',
                silent: msgNotify === '0',
                flags: parseMsgFlags(msgFlags),
                flagsRaw: msgFlags,
                text: unfuckText(msgText),
            },
        };

        const msgIdFirst = info.msg.id.charCodeAt(0);
        if(msgIdFirst < 48 || msgIdFirst > 57)
            info.msg.id = (Math.round(Number.MIN_SAFE_INTEGER * Math.random())).toString();

        if(info.msg.isBot) {
            const botParts = msgText.split("\f");
            info.msg.botInfo = {
                isError: botParts[0] === '1',
                type: botParts[1],
                args: botParts.slice(2),
            };
        }

        eventTarget.dispatch('msg:add', info);
    };

    // existing channels
    handlers['7']['2'] = (count, ...args) => {
        count = parseInt(count);
        eventTarget.dispatch('chan:clear');

        for(let i = 0; i < count; ++i) {
            const offset = 3 * i;

            eventTarget.dispatch('chan:add', {
                channel: {
                    name: args[offset],
                    hasPassword: args[offset + 1] !== '0',
                    isTemporary: args[offset + 2] !== '0',
                    isCurrent: args[offset] === selfChannelName,
                },
            });
        }

        eventTarget.dispatch('chan:focus', {
            channel: { name: selfChannelName },
        });
    };

    // context clear
    handlers['8'] = mode => {
        if(mode === '0' || mode === '3' || mode === '4')
            eventTarget.dispatch('msg:clear');

        if(mode === '1' || mode === '3' || mode === '4')
            eventTarget.dispatch('user:clear');

        if(mode === '2' || mode === '4')
            eventTarget.dispatch('chan:clear');
    };

    // baka (ban/kick)
    handlers['9'] = (type, expiry) => {
        wasKicked = true;

        const bakaInfo = {
            session: { success: false },
            baka: {
                type: type === '0' ? 'kick' : 'ban',
            },
        };

        if(bakaInfo.baka.type === 'ban') {
            bakaInfo.baka.perma = expiry === '-1';
            bakaInfo.baka.until = expiry === '-1' ? undefined : new Date(parseInt(expiry) * 1000);
        }

        eventTarget.dispatch('session:term', bakaInfo);
    };

    // user update
    handlers['10'] = (userId, userName, userColour, userPerms) => {
        eventTarget.dispatch('user:update', {
            user: {
                id: userId,
                self: userId === selfUserId,
                name: userName,
                colour: parseUserColour(userColour),
                perms: parseUserPerms(userPerms),
                permsRaw: userPerms,
            },
        });
    };


    return {
        sendAuth: sendAuth,
        sendMessage: sendMessage,
        open: url => {
            return new Promise((resolve, reject) => {
                if(typeof url !== 'string')
                    throw 'url must be a string';
                if(url.startsWith('//'))
                    url = location.protocol.replace('http', 'ws') + url;

                openResolve = resolve;
                openReject = reject;

                sock?.close();

                sock = new UmiWebSocket(url);
                sock.watch('open', onOpen);
                sock.watch('close', onClose);
                sock.watch('message', onMessage);
                sock.watch('create_interval', ev => {
                    pingTimer = ev.detail.id;
                });
                sock.watch('call_interval', ev => {
                    if(ev.detail.id === pingTimer)
                        onSendPing();
                });
                sock.watch('create_intervals', ev => {
                    pingTimer = undefined;
                });
            });
        },
        close: () => {
            sock?.close();
            sock = undefined;
        },
        watch: eventTarget.watch,
        unwatch: eventTarget.unwatch,
        setDumpPackets: state => dumpPackets = !!state,
        switchChannel: channelInfo => {
            if(selfUserId === undefined)
                return;

            const name = channelInfo.getName();

            if(channelInfo.isUserChannel()) {
                selfPseudoChannelName = name.substring(1);
            } else {
                selfPseudoChannelName = undefined;
                if(selfChannelName === name)
                    return;

                selfChannelName = name;
                sendMessage(`/join ${name}`);
            }
        },
    };
};
