#include channel.js
#include channels.js
#include common.js
#include message.js
#include messages.js
#include mszauth.js
#include user.js
#include users.js
#include parsing.js
#include servers.js
#include settings.js
#include txtrigs.js
#include websock.js
#include ui/emotes.js
#include ui/markup.js
#include ui/menus.js
#include ui/messages.jsx
#include ui/view.js
#include ui/loading-overlay.jsx
#include sound/umisound.js

if(!Umi) window.Umi = {};
if(!Umi.Protocol) Umi.Protocol = {};
if(!Umi.Protocol.SockChat) Umi.Protocol.SockChat = {};
if(!Umi.Protocol.SockChat.Protocol) Umi.Protocol.SockChat.Protocol = {};

Umi.Protocol.SockChat.Protocol = function(views) {
    const pub = {};
    Umi.Protocol.SockChat.Protocol.Instance = pub;

    const chatBot = new Umi.User('-1', 'Server');

    let noReconnect = false,
        connectAttempts = 0,
        wasKicked = false,
        isRestarting = false;

    let userId = null,
        channelName = null,
        pmUserName = null;

    let sock = null;

    const send = function(opcode, data) {
        if(!sock) return;
        let msg = opcode;
        if(data) msg += "\t" + data.join("\t");
        if(Umi.Settings.get('dumpPackets'))
            console.log(msg);
        sock.send(msg);
    };
    const sendPing = function() {
        if(userId === null)
            return;
        lastPing = Date.now();
        send('0', [userId]);
    };
    const sendAuth = function(args) {
        if(userId !== null)
            return;
        send('1', args);
    };
    const sendMessage = function(text) {
        if(userId === null)
            return;

        if(text.substring(0, 1) !== '/' && pmUserName !== null)
            text = '/msg ' + pmUserName + ' ' + text;

        send('2', [userId, text]);
    };

    const switchChannel = function(name) {
        if(channelName === name)
            return;

        channelName = name;
        sendMessage('/join ' + name);
    };

    const startKeepAlive = function() {
        if(!sock) return;
        sock.sendInterval("0\t" + userId, futami.get('ping') * 1000);
    };
    const stopKeepAlive = function() {
        if(!sock) return;
        sock.clearIntervals();
    };

    const getLoadingOverlay = async (icon, header, message) => {
        const currentView = views.current();

        if('setIcon' in currentView) {
            currentView.setIcon(icon);
            currentView.setHeader(header);
            currentView.setMessage(message);
            return currentView;
        }

        const loading = new Umi.UI.LoadingOverlay(icon, header, message);
        await views.push(loading);

        return loading;
    };

    const playBannedSfx = function() {
        if(!mami.hasSound())
            return;

        const urls = {
            mp3: '//static.flash.moe/sounds/touhou-death.mp3',
            ogg: '//static.flash.moe/sounds/touhou-death.ogg'
        };

        mami.getSound().load('banSFX', urls, function(success, buffer) {
            if(!success) {
                console.log('Failed to load kick/ban SFX: ' + buffer);
                return;
            }

            buffer.createSource().play();
        });
    };
    const playBannedBgm = function(preload) {
        if(!mami.hasSound())
            return;

        const urls = {
            opus: '//static.flash.moe/sounds/players-score.opus',
            caf: '//static.flash.moe/sounds/players-score.caf'
        };

        mami.getSound().load('banBGM', urls, function(success, buffer) {
            if(!success) {
                console.log('Failed to load kick/ban SFX: ' + buffer);
                return;
            }

            if(!preload) {
                const source = buffer.createSource();
                source.setLoopStart(10.512);
                source.setLoopEnd(38.074);
                source.setLoop();
                source.play();
            }
        });
    };

    const onOpen = function(ev) {
        if(Umi.Settings.get('dumpPackets'))
            console.log(ev);

        wasKicked = false;
        isRestarting = false;

        getLoadingOverlay('spinner', 'Loading...', 'Authenticating...');

        const authInfo = MamiMisuzuAuth.getInfo();
        sendAuth([authInfo.method, authInfo.token]);
    };

    const closeReasons = {
        '_1000': 'The connection has been ended.',
        '_1001': 'Something went wrong on the server side.',
        '_1002': 'Your client sent broken data to the server.',
        '_1003': 'Your client sent data to the server that it doesn\'t understand.',
        '_1005': 'No additional information was provided.',
        '_1006': 'You lost connection unexpectedly!',
        '_1007': 'Your client sent broken data to the server.',
        '_1008': 'Your client did something the server did not agree with.',
        '_1009': 'Your client sent too much data to the server at once.',
        '_1011': 'Something went wrong on the server side.',
        '_1012': 'The server is restarting, reconnecting soon...',
        '_1013': 'You cannot connect to the server right now, try again later.',
        '_1015': 'Your client and the server could not establish a secure connection.',
    };

    const onClose = function(ev) {
        if(Umi.Settings.get('dumpPackets'))
            console.log(ev);

        userId = null;
        channelName = null;
        pmUserName = null;
        stopKeepAlive();

        if(wasKicked)
            return;

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

        const msg = closeReasons['_' + code.toString()]
            || ('Something caused an unexpected connection loss, the error code was: ' + ev.code.toString() + '.');

        getLoadingOverlay('unlink', 'Disconnected!', msg);

        //Umi.Messages.Clear();
        Umi.Users.Clear();

        connectAttempts = 0;

        setTimeout(function() {
            beginConnecting();
        }, 5000);
    };

    const parseBotMessage = function(parts) {
        let text = '';

        switch(parts[1]) {
            case 'silence':
                text = 'You have been silenced!';
                break;

            case 'unsil':
                text = 'You are no longer silenced!';
                break;

            case 'silok':
                text = '{0} is now silenced.'.replace('{0}', parts[2]);
                break;

            case 'usilok':
                text = '{0} is no longer silenced.'.replace('{0}', parts[2]);
                break;

            case 'flood':
                text = '{0} got kicked for flood protection.'.replace('{0}', parts[2]);
                break;

            case 'flwarn':
                text = 'You are about to hit the flood limit! If you continue you will be kicked.';
                break;

            case 'unban':
                text = '{0} is no longer banned.'.replace('{0}', parts[2]);
                break;

            case 'banlist':
                const blentries = parts[2].split(', ');
                for(const i in blentries)
                    blentries[i] = blentries[i].slice(92, -4);

                text = 'Banned: {0}'.replace('{0}', blentries.join(', '));
                break;

            case 'who':
                const wentries = parts[2].split(', ');
                for(const i in wentries) {
                    const isSelf = wentries[i].includes(' style="font-weight: bold;"');
                    wentries[i] = wentries[i].slice(isSelf ? 102 : 75, -4);
                    if(isSelf) wentries[i] += ' (You)';
                }

                text = 'Online: {0}'.replace('{0}', wentries.join(', '));
                break;

            case 'whochan':
                const wcentries = (parts[3] || '').split(', ');
                for(const i in wcentries) {
                    const isSelf = wcentries[i].includes(' style="font-weight: bold;"');
                    wcentries[i] = wcentries[i].slice(isSelf ? 102 : 75, -4);
                    if(isSelf) wcentries[i] += ' (You)';
                }

                text = 'Online in {0}: {1}'.replace('{0}', parts[2]).replace('{1}', wcentries.join(', '));
                break;

            case 'silerr':
                text = 'This user has already been silenced!';
                break;

            case 'usilerr':
                text = "This user isn't silenced!";
                break;

            case 'silperr':
                text = "You aren't allowed to silence this user!";
                break;

            case 'usilperr':
                text = "You aren't allowed to remove the silence on this user!";
                break;

            case 'silself':
                text = 'Why would you even try to silence yourself?';
                break;

            case 'delerr':
                text = "You aren't allowed to delete this message!";
                break;

            case 'notban':
                text = "{0} isn't banned!".replace('{0}', parts[2]);
                break;

            case 'whoerr':
                text = '{0} does not exist!'.replace('{0}', parts[2]);
                break;

            case 'join':
                text = '{0} joined.'.replace('{0}', parts[2]);
                break;

            case 'jchan':
                text = '{0} joined the channel.'.replace('{0}', parts[2]);
                break;

            case 'leave':
                text = '{0} left.'.replace('{0}', parts[2]);
                break;

            case 'timeout':
                text = '{0} exploded.'.replace('{0}', parts[2]);
                break;

            case 'lchan':
                text = '{0} left the channel.'.replace('{0}', parts[2]);
                break;

            case 'kick':
                text = '{0} got kicked.'.replace('{0}', parts[2]);
                break;

            case 'nick':
                text = '{0} changed their name to {1}.'.replace('{0}', parts[2]).replace('{1}', parts[3]);
                break;

            case 'crchan':
                text = '{0} has been created.'.replace('{0}', parts[2]);
                break;

            case 'delchan':
                text = '{0} has been deleted.'.replace('{0}', parts[2]);
                break;

            case 'cpwdchan':
                text = 'Changed the channel password!';
                break;

            case 'cprivchan':
                text = 'Change access level of the channel!';
                break;

            case 'ipaddr':
                text = 'IP of {0}: {1}'.replace('{0}', parts[2]).replace('{1}', parts[3]);
                break;

            case 'cmdna':
                text = "You aren't allowed to use '{0}'.".replace('{0}', parts[2]);
                break;

            case 'nocmd':
                text = "The command '{0}' does not exist.".replace('{0}', parts[2]);
                break;

            case 'cmderr':
                text = "You didn't format the command correctly!";
                break;

            case 'usernf':
                text = "{0} isn't logged in to the chat right now!".replace('{0}', parts[2]);
                break;

            case 'kickna':
                text = "You aren't allowed to kick {0}!".replace('{0}', parts[2]);
                break;

            case 'samechan':
                text = 'You are already in {0}!'.replace('{0}', parts[2]);
                break;

            case 'ipchan':
            case 'nochan':
                text = "{0} doesn't exist!".replace('{0}', parts[2]);
                break;

            case 'nopwchan':
                text = "{0} has a password! Use '/join {0} [password]'.".replace('{0}', parts[2]);
                break;

            case 'ipwchan':
                text = "Wrong password! Couldn't join {0}.".replace('{0}', parts[2]);
                break;

            case 'inchan':
                text = "Channel names can't start with @ or *!";
                break;

            case 'nischan':
                text = '{0} already exists!'.replace('{0}', parts[2]);
                break;

            case 'ndchan':
                text = "You aren't allowed to delete {0}!".replace('{0}', parts[2]);
                break;

            case 'namchan':
                text = "You aren't allowed to edit {0}!".replace('{0}', parts[2]);
                break;

            case 'nameinuse':
                text = '{0} is currently taken!'.replace('{0}', parts[2]);
                break;

            case 'rankerr':
                text = "You can't set the access level of a channel higher than your own!";
                break;

            case 'reconnect':
                text = 'Connection lost! Attempting to reconnect...';
                break;

            case 'generr':
                text = 'Something happened.';
                break;

            case 'say':
            default:
                text = parts[2];
        }

        return text;
    };

    const unfuckText = function(text) {
        const elem = document.createElement('div');
        elem.innerHTML = text.replace(/ <br\/> /g, "\n");
        text = elem.innerText;
        return text;
    };

    const onMessage = function(ev) {
        const data = ev.data.split("\t");

        if(Umi.Settings.get('dumpPackets'))
            console.log(data);

        switch(data[0]) {
            case '0': // ping
                // nothing to do
                break;

            case '1': // join
                if(userId === null) {
                    if(data[1] == 'y') {
                        userId = data[2];
                        channelName = data[6];

                        views.pop(ctx => MamiAnimate({
                            async: true,
                            duration: 120,
                            easing: 'easeInOutSine',
                            start: () => {
                                ctx.toElem.style.zIndex = '100';
                                ctx.fromElem.style.pointerEvents = 'none';
                                ctx.fromElem.style.zIndex = '200';
                            },
                            update: t => {
                                ctx.fromElem.style.transform = `scale(${1 + (.25 * t)})`;
                                ctx.fromElem.style.opacity = 1 - (1 * t).toString();
                            },
                            end: () => {
                                ctx.toElem.style.zIndex = null;
                            },
                        }));

                        startKeepAlive();

                        if(Umi.Settings.get('playSoundOnConnect'))
                            Umi.Sound.Play('join');
                    } else {
                        switch (data[2]) {
                            case 'joinfail':
                                wasKicked = true;

                                const jfuntil = new Date(parseInt(data[3]) * 1000);
                                let banmsg = 'You were banned until {0}!'.replace('{0}', jfuntil.toLocaleString());

                                if(data[3] === '-1')
                                    banmsg = 'You have been banned till the end of time, please try again in a different dimension.';

                                getLoadingOverlay('hammer', 'Banned!', banmsg);
                                playBannedBgm();
                                break;

                            case 'authfail':
                                let message = 'Authentication failed!';
                                const afredir = futami.get('login');

                                if(afredir) {
                                    message = 'Authentication failed, redirecting to login page...';
                                    setTimeout(function() {
                                        location.assign(afredir);
                                    }, 2000);
                                }

                                getLoadingOverlay('cross', 'Failed!', message);
                                break;

                            case 'sockfail':
                                getLoadingOverlay('cross', 'Failed!', 'Too many active connections.');
                                break;

                            default:
                                getLoadingOverlay('cross', 'Failed!', 'Connection failed!');
                                break;
                        }
                        break;
                    }
                }

                const juser = new Umi.User(data[2], data[3], data[4], data[5]);
                
                if(userId === juser.getId())
                    Umi.User.setCurrentUser(juser);

                Umi.Users.Add(juser);

                if(Umi.User.isCurrentUser(juser)) {
                    Umi.UI.Markup.Reset();
                    Umi.UI.Emoticons.Init();
                    Umi.Parsing.Init();
                    break;
                }

                Umi.Messages.Add(new Umi.Message(
                    data[6],
                    data[1],
                    chatBot,
                    '{0} joined.'.replace('{0}', juser.getName()),
                    channelName,
                    false,
                    {
                        type: 'join',
                        isError: false,
                        args: [juser.getName()],
                        target: juser,
                    }
                ));
                Umi.Sound.Play('join');
                break;

            case '2': // message
                let text = data[3],
                    sound = 'incoming';
                const muser = Umi.Users.Get(data[2]) || chatBot,
                    textParts = text.split("\f");
                    isPM = data[5][4] !== '0',
                    isAction = data[5][1] !== '0' && data[5][3] === '0',
                    isBot = data[2] === '-1',
                    pmChannel = '@' + muser.getName(),
                    botInfo = {};

                text = unfuckText(text);

                if(isBot) {
                    sound = 'server';
                    botInfo.isError = textParts[0] !== '0';
                    botInfo.type = textParts[1];
                    botInfo.args = textParts.slice(2);
                    text = parseBotMessage(textParts);

                    if(botInfo.isError)
                        sound = 'error';
                    else if(textParts[1] === 'unban')
                        sound = 'unban';
                }

                if(isPM) {
                    if(muser.getId() === userId) {
                        const tmpMsg = text.split(' ');
                        pmChannel = '@' + tmpMsg.shift();
                        text = tmpMsg.join(' ');
                    }

                    if(muser.getName() !== pmUserName)
                        sound = 'private';

                    if(Umi.Channels.Get(pmChannel) === null)
                        Umi.Channels.Add(new Umi.Channel(pmChannel, false, true, true));

                    Umi.UI.Menus.Attention('channels');
                }

                if(muser.getId() === userId)
                    sound = 'outgoing';

                if(Umi.Settings.get('playJokeSounds'))
                    try {
                        const trigger = mami.getTextTriggers().getTrigger(text);
                        if(trigger.isSoundType()) {
                            sound = '';
                            mami.playLibrarySound(
                                trigger.getRandomSoundName(),
                                null,
                                trigger.getVolume(),
                                trigger.getRate()
                            );
                        }
                    } catch(ex) {}

                Umi.Messages.Add(new Umi.Message(
                    data[4],
                    data[1],
                    muser,
                    text,
                    isPM ? pmChannel : (data[6] || channelName),
                    false,
                    botInfo,
                    isAction
                ));

                if(!Umi.Settings.get('onlySoundOnMention') && sound !== '')
                    Umi.Sound.Play(sound);
                break;

            case '3': // leave
                const luser = Umi.Users.Get(data[1]);
                let ltext = '',
                    lsound = null;

                switch(data[3]) {
                    case 'flood':
                        ltext = '{0} got kicked for flood protection.';
                        lsound = 'flood';
                        break;

                    case 'timeout':
                        ltext = '{0} exploded.';
                        lsound = 'timeout';
                        break;

                    case 'kick':
                        ltext = '{0} got bludgeoned to death.';
                        lsound = 'kick';
                        break;

                    case 'leave':
                    default:
                        ltext = '{0} left.';
                        lsound = 'leave';
                        break;
                }

                Umi.Messages.Add(new Umi.Message(
                    data[5],
                    data[4],
                    chatBot,
                    ltext.replace('{0}', luser.getName()),
                    channelName,
                    false,
                    {
                        type: data[3],
                        isError: false,
                        args: [luser.getName()],
                        target: luser,
                    }
                ));
                Umi.Users.Remove(luser);

                if(lsound !== null)
                    Umi.Sound.Play(lsound);
                break;

            case '4': // channel
                switch(data[1]) {
                    case '0':
                        Umi.Channels.Add(new Umi.Channel(data[2], data[3] !== '0', data[4] !== '0'));
                        break;

                    case '1':
                        const uchannel = Umi.Channels.Get(data[2]);
                        uchannel.setName(data[3]);
                        uchannel.setHasPassword(data[4] !== '0');
                        uchannel.setTemporary(data[5] !== '0');
                        Umi.Channels.Update(data[2], uchannel);
                        break;

                    case '2':
                        Umi.Channels.Remove(Umi.Channels.Get(data[2]));
                        break;
                }
                break;

            case '5': // user move
                switch(data[1]) {
                    case '0':
                        const umuser = new Umi.User(data[2], data[3], data[4], data[5]),
                            text = '{0} joined the channel.';

                        Umi.Users.Add(umuser);
                        Umi.Messages.Add(new Umi.Message(
                            data[6],
                            null,
                            chatBot,
                            text.replace('{0}', umuser.getName()),
                            channelName,
                            false,
                            {
                                type: 'jchan',
                                isError: false,
                                args: [
                                    umuser.getName()
                                ],
                                target: umuser,
                            }
                        ));
                        Umi.Sound.Play('join');
                        break;

                    case '1':
                        if(data[2] === userId)
                            return;

                        const mouser = Umi.Users.Get(+data[2]),
                            motext = '{0} has left the channel.';

                        Umi.Messages.Add(new Umi.Message(
                            data[3],
                            null,
                            chatBot,
                            motext.replace('{0}', mouser.getName()),
                            channelName,
                            false,
                            {
                                type: 'lchan',
                                isError: false,
                                args: [
                                    mouser.getName()
                                ],
                                target: mouser,
                            }
                        ));
                        Umi.Sound.Play('leave');
                        Umi.Users.Remove(mouser);
                        break;

                    case '2':
                        Umi.Channels.Switch(Umi.Channels.Get(data[2]));
                        break;
                }
                break;

            case '6': // message delete
                Umi.Messages.Remove(Umi.Messages.Get(data[1]));
                break;

            case '7': // context populate
                switch(data[1]) {
                    case '0': // users
                        const cpuamount = parseInt(data[2]);
                        let cpustart = 3;

                        for(let i = 0; i < cpuamount; i++) {                                        
                            const user = new Umi.User(data[cpustart], data[cpustart + 1], data[cpustart + 2], data[cpustart + 3]);
                            Umi.Users.Add(user);
                            cpustart += 5;
                        }
                        break;

                    case '1': // message
                        let cmid = +data[8],
                            cmtext = data[7],
                            cmflags = data[10];
                        const cmuser = new Umi.User(data[3], data[4], data[5], data[6]),
                            cmtextParts = cmtext.split("\f"),
                            cmbotInfo = {};

                        cmtext = unfuckText(cmtext);

                        if(isNaN(cmid))
                            cmid = -Math.ceil(Math.random() * 10000000000);

                        if(cmtextParts[1]) {
                            cmbotInfo.isError = cmtextParts[0] !== '0';
                            cmbotInfo.type = cmtextParts[1];
                            cmbotInfo.args = cmtextParts.slice(2);
                            cmtext = parseBotMessage(cmtextParts);
                        }

                        Umi.Messages.Add(new Umi.Message(
                            cmid,
                            data[2],
                            cmuser,
                            cmtext,
                            channelName,
                            false,
                            cmbotInfo,
                            cmflags[1] !== '0' && cmflags[3] === '0',
                        ));
                        break;

                    case '2': // channels
                        const ecpamount = +data[2];
                        let ecpstart = 3;

                        for(let i = 0; i < ecpamount; i++) {
                            const channel = new Umi.Channel(
                                data[ecpstart],
                                data[ecpstart + 1] !== '0',
                                data[ecpstart + 2] !== '0'
                            );
                            Umi.Channels.Add(channel);

                            if(channel.getName() === channelName)
                                Umi.Channels.Switch(channel);

                            ecpstart = ecpstart + 3;
                        }
                        break;
                }
                break;

            case '8': // context clear
                const cckeep = Umi.Users.Get(userId);

                switch(data[1]) {
                    case '0':
                        Umi.UI.Messages.RemoveAll();
                        break;

                    case '1':
                        Umi.Users.Clear();
                        Umi.Users.Add(cckeep);
                        break;

                    case '2':
                        Umi.Channels.Clear();
                        break;

                    case '3':
                        Umi.Messages.Clear();
                        Umi.Users.Clear();
                        Umi.Users.Add(cckeep);
                        break;

                    case '4':
                        Umi.UI.Messages.RemoveAll();
                        Umi.Users.Clear();
                        Umi.Users.Add(cckeep);
                        Umi.Channels.Clear();
                        break;
                }
                break;

            case '9': // baka
                noReconnect = true;
                wasKicked = true;

                const isBan = data[1] !== '0';

                let message = 'You were kicked, refresh to log back in!';
                if(isBan) {
                    if(data[2] === '-1') {
                        message = 'You have been banned till the end of time, please try again in a different dimension.';
                    } else {
                        const until = new Date(parseInt(data[2]) * 1000);
                        message = 'You were banned until {0}!'.replace('{0}', until.toLocaleString());
                    }
                }


                let icon, header;
                if(isBan) {
                    icon = 'hammer';
                    header = 'Banned!';
                } else {
                    icon = 'bomb';
                    header = 'Kicked!';
                }

                const currentView = views.currentElement();

                MamiAnimate({
                    duration: 550,
                    easing: 'easeOutExpo',
                    start: function() {
                        playBannedBgm(true);
                        playBannedSfx();
                    },
                    update: function(t) {
                        const scale = 'scale(' + (1 - .5 * t).toString() + ', ' + (1 - 1 * t).toString() + ')';
                        currentView.style.transform = scale;
                    },
                    end: function() {
                        getLoadingOverlay(icon, header, message).then(() => {
                            playBannedBgm();

                            // there's currently no way to reconnect after a kick/ban so just dispose of the ui entirely
                            if(views.count() > 1)
                                views.shift();
                        });
                    },
                });
                break;

            case '10': // user update
                const spuser = Umi.Users.Get(+data[1]);
                spuser.setName(data[2]);
                spuser.setColour(data[3]);
                spuser.setPermissions(data[4]);
                Umi.Users.Update(spuser.getId(), spuser);
                break;
        }
    };

    const beginConnecting = function() {
        if(noReconnect)
            return;

        ++connectAttempts;

        let str = 'Connecting to server...';
        if(connectAttempts > 1)
           str += ' (Attempt ' + connectAttempts.toString() + ')';

        getLoadingOverlay('spinner', 'Loading...', str);

        UmiServers.getServer(function(server) {
            if(Umi.Settings.get('dumpPackets'))
                console.log('Connecting to ' + server);

            sock = new UmiWebSocket(server, function(ev) {
                switch(ev.act) {
                    case 'ws:open':
                        onOpen(ev);
                        break;
                    case 'ws:close':
                        onClose(ev);
                        break;
                    case 'ws:message':
                        onMessage(ev);
                        break;
                    default:
                        console.log(ev.data);
                        break;
                }
            });
        });
    };

    Umi.Channels.OnSwitch.push(function(old, channel) {
        if(channel.isUserChannel()) {
            pmUserName = channel.getName().substring(1);
        } else {
            pmUserName = null;
            switchChannel(channel.getName());
        }
    });

    pub.sendMessage = sendMessage;

    pub.open = function() {
        noReconnect = false;
        beginConnecting();
    };

    pub.close = function() {
        noReconnect = true;
        sock.close();
    };

    return pub;
};
