window.Umi = { UI: {}, Protocol: { SockChat: { Protocol: {} } } };

#include animate.js
#include channel.js
#include channels.js
#include common.js
#include compat.js
#include conman.js
#include context.js
#include emotes.js
#include message.js
#include messages.js
#include mszauth.js
#include parsing.js
#include sockchat_old.js
#include txtrigs.js
#include user.js
#include users.js
#include utility.js
#include weeb.js
#include audio/autoplay.js
#include audio/context.js
#include controls/views.js
#include eeprom/eeprom.js
#include settings/backup.js
#include settings/settings.js
#include sound/context.js
#include sound/osukeys.js
#include sound/umisound.js
#include ui/baka.jsx
#include ui/chat-layout.js
#include ui/hooks.js
#include ui/emotes.js
#include ui/input-menus.js
#include ui/loading-overlay.jsx
#include ui/markup.js
#include ui/menus.js
#include ui/messages.jsx
#include ui/view.js
#include ui/ping.jsx
#include ui/settings.jsx
#include ui/toggles.js
#include ui/uploads.js

(async () => {
    const views = new MamiViewsControl({ body: document.body });
    const loadingOverlay = new Umi.UI.LoadingOverlay('spinner', 'Loading...');
    await views.push(loadingOverlay);

    loadingOverlay.setMessage('Loading environment...');
    try {
        window.futami = await FutamiCommon.load();
    } catch(ex) {
        console.error(ex);
        loadingOverlay.setIcon('cross');
        loadingOverlay.setHeader('Failed!');
        loadingOverlay.setMessage('Failed to load common settings!');
        return;
    }

    loadingOverlay.setMessage('Fetching credentials...');
    try {
        const auth = await MamiMisuzuAuth.update();
        if(!auth.ok)
            throw 'Authentication failed.';
    } catch(ex) {
        console.error(ex);
        location.assign(futami.get('login'));
        return;
    }

    setInterval(() => {
        MamiMisuzuAuth.update()
            .then(auth => {
                if(!auth.ok)
                    location.assign(futami.get('login'));
            })
    }, 600000);


    loadingOverlay.setMessage('Loading settings...');

    const settings = new MamiSettings('umi-');

    settings.define('style', 'string', 'dark');
    settings.define('compactView', 'boolean', false);
    settings.define('autoScroll', 'boolean', true);
    settings.define('closeTabConfirm', 'boolean', false);
    settings.define('showChannelList', 'boolean', false);
    settings.define('fancyInfo', 'boolean', true);
    settings.define('autoCloseUserContext', 'boolean', true);
    settings.define('enableParser', 'boolean', true);
    settings.define('enableEmoticons', 'boolean', true);
    settings.define('autoParseUrls', 'boolean', true);
    settings.define('preventOverflow', 'boolean', false);
    settings.define('expandTextBox', 'boolean', false);
    settings.define('eepromAutoInsert', 'boolean', true);
    settings.define('autoEmbedV1', 'boolean', false);
    settings.define('soundEnable', 'boolean', true, false, true);
    settings.define('soundPack', 'string', 'ajax-chat');
    settings.define('soundVolume', 'number', 80);
    settings.define('soundEnableJoin', 'boolean', true);
    settings.define('soundEnableLeave', 'boolean', true);
    settings.define('soundEnableError', 'boolean', true);
    settings.define('soundEnableServer', 'boolean', true);
    settings.define('soundEnableIncoming', 'boolean', true);
    settings.define('onlySoundOnMention', 'boolean', false);
    settings.define('soundEnableOutgoing', 'boolean', true);
    settings.define('soundEnablePrivate', 'boolean', true);
    settings.define('soundEnableForceLeave', 'boolean', true);
    settings.define('minecraft', ['no', 'yes', 'old'], 'no');
    settings.define('windowsLiveMessenger', 'boolean', false);
    settings.define('seinfeld', 'boolean', false);
    settings.define('flashTitle', 'boolean', true);
    settings.define('showServerMsgInTitle', 'boolean', true);
    settings.define('playJokeSounds', 'boolean', true);
    settings.define('weeaboo', 'boolean', false);
    settings.define('motivationalImages', 'boolean', false);
    settings.define('motivationalVideos', 'boolean', false);
    settings.define('osuKeys', 'boolean', false);
    settings.define('osuKeysV2', ['no', 'yes', 'rng'], 'no');
    settings.define('explosionRadius', 'number', 20);
    settings.define('dumpPackets', 'boolean', FUTAMI_DEBUG);
    settings.define('dumpEvents', 'boolean', FUTAMI_DEBUG);
    settings.define('neverUseWorker', 'boolean', false, false, true);
    settings.define('forceUseWorker', 'boolean', false, false, true);
    settings.define('marqueeAllNames', 'boolean', false);
    settings.define('tmpDisableOldThemeSys', 'boolean', false, false, true);

    const noNotifSupport = !('Notification' in window);
    settings.define('enableNotifications', 'boolean', false, noNotifSupport, true);
    settings.define('notificationShowMessage', 'boolean', false, noNotifSupport);
    settings.define('notificationTriggers', 'string', '', noNotifSupport);


    loadingOverlay.setMessage('Loading sounds...');
    const soundCtx = new MamiSoundContext;

    try {
        const sounds = await futami.getJson('sounds2');
        if(Array.isArray(sounds.library))
            soundCtx.library.register(sounds.library, true);
        if(Array.isArray(sounds.packs))
            soundCtx.packs.register(sounds.packs, true);
    } catch(ex) {
        console.error(ex);
    }

    if(!await MamiDetectAutoPlay()) {
        settings.set('soundEnable', false);
        settings.virtualise('soundEnable');
    }

    settings.watch('soundEnable', ev => {
        if(ev.detail.value) {
            if(!soundCtx.ready)
                soundCtx.reset();

            settings.touch('soundVolume');
            settings.touch('soundPack');

            soundCtx.library.play(soundCtx.pack.getEventSound('server'));
        }

        soundCtx.muted = !ev.detail.value;
    });

    settings.watch('soundPack', ev => {
        const packs = soundCtx.packs;
        if(!packs.has(ev.detail.value)) {
            settings.delete(ev.detail.name);
            return;
        }

        soundCtx.pack = packs.get(ev.detail.value);
        if(!ev.detail.initial) soundCtx.library.play(soundCtx.pack.getEventSound('server'));
    });

    settings.watch('soundVolume', ev => {
        soundCtx.volume = ev.detail.value / 100;
    })


    loadingOverlay.setMessage('Loading emoticons...');
    try {
        const emotes = await futami.getJson('emotes');
        MamiEmotes.loadLegacy(emotes);
    } catch(ex) {
        console.error(ex);
    }


    const onHashChange = () => {
        if(location.hash === '#reset') {
            settings.clear(true);
            location.assign('/');
        }
    };

    window.addEventListener('hashchange', onHashChange);
    onHashChange();

    window.addEventListener('keydown', ev => {
        if(ev.altKey && ev.shiftKey && (ev.key === 'R' || ev.key === 'r'))
            location.hash = 'reset';
    });


    let eeprom;

    loadingOverlay.setMessage('Loading EEPROM...');
    try {
        await MamiEEPROM.init();
        eeprom = new EEPROM('1', futami.get('eeprom2'), MamiMisuzuAuth.getLine);
    } catch(ex) {
        console.error(ex);
        eeprom = undefined;
    }


    loadingOverlay.setMessage('Preparing UI...');

    const textTriggers = new MamiTextTriggers;
    const conMan = new MamiConnectionManager(futami.get('servers'));

    // define this as late as possible
    const ctx = new MamiContext({
        settings: settings,
        views: views,
        sound: soundCtx,
        textTriggers: textTriggers,
        eeprom: eeprom,
        conMan: conMan,
    });
    Object.defineProperty(window, 'mami', { enumerable: true, value: ctx });

    // should be dynamic when possible
    const layout = new Umi.UI.ChatLayout;
    await views.unshift(layout);

    Umi.UI.View.AccentReload();
    Umi.UI.Hooks.AddHooks();

    settings.watch('style', ev => { if(!ev.detail.initial) Umi.UI.View.AccentReload(); });
    settings.watch('compactView', ev => { if(!ev.detail.initial) Umi.UI.View.AccentReload(); });
    settings.watch('preventOverflow', ev => document.body.classList.toggle('prevent-overflow', ev.detail.value));
    settings.watch('tmpDisableOldThemeSys', ev => { if(!ev.detail.initial) Umi.UI.View.AccentReload(); });

    settings.watch('minecraft', ev => {
        if(ev.detail.initial && ev.detail.value === 'no')
            return;

        soundCtx.library.play((() => {
            if(ev.detail.initial)
                return 'minecraft:nether:enter';
            if(ev.detail.value === 'yes')
                return 'minecraft:door:open';
            if(ev.detail.value === 'old')
                return 'minecraft:door:open-old';
            return soundCtx.pack.getEventSound('join');
        })());
    });

    settings.watch('enableNotifications', ev => {
        if(!ev.detail.value || !('Notification' in window)
            || (Notification.permission === 'granted' && Notification.permission !== 'denied'))
            return;

        Notification.requestPermission()
            .then(perm => {
                if(perm !== 'granted')
                    settings.set('enableNotifications', false);
            });
    });

    settings.watch('playJokeSounds', ev => {
        if(!ev.detail.value) return;

        if(!textTriggers.hasTriggers())
            futami.getJson('texttriggers').then(trigInfos => textTriggers.addTriggers(trigInfos));
    });

    settings.watch('weeaboo', ev => {
        if(ev.detail.value) Weeaboo.init();
    });

    settings.watch('osuKeysV2', ev => {
        // migrate old value
        if(ev.detail.initial) {
            if(settings.has('osuKeys')) {
                settings.set('osuKeysV2', settings.get('osuKeys') ? 'yes' : 'no');
                settings.delete('osuKeys');
                return;
            }
        }

        OsuKeys.setEnable(ev.detail.value !== 'no');
        OsuKeys.setRandomRate(ev.detail.value === 'rng');
    });


    loadingOverlay.setMessage('Building menus...');

    MamiCompat('Umi.Parser.SockChatBBcode.EmbedStub', { value: () => {} }); // intentionally a no-op
    MamiCompat('Umi.UI.View.SetText', { value: text => console.log(`Umi.UI.View.SetText(text: ${text})`) });
    MamiCompat('Umi.UI.Menus.Add', { value: (baseId, title, initiallyHidden) => console.log(`Umi.UI.Menus.Add(baseId: ${baseId}, title: ${title}, initiallyHidden: ${initiallyHidden})`) });
    MamiCompat('Umi.UI.Menus.Get', { value: (baseId, icon) => console.log(`Umi.UI.Menus.Get(baseId: ${baseId}, icon: ${icon})`) });

    Umi.UI.Menus.Add('users', 'Users');
    Umi.UI.Menus.Add('channels', 'Channels', !settings.get('showChannelList'));
    Umi.UI.Menus.Add('settings', 'Settings');

    let sidebarAnimation = null;

    Umi.UI.Settings.Init();
    Umi.UI.Toggles.Add('menu-toggle', {
        'click': function() {
            const sidebar = $c('sidebar')[0],
                toggle = Umi.UI.Toggles.Get('menu-toggle'),
                toggleOpened = 'sidebar__selector-mode--menu-toggle-opened',
                toggleClosed = 'sidebar__selector-mode--menu-toggle-closed',
                isClosed = toggle.classList.contains(toggleClosed);

            if(sidebarAnimation !== null) {
                sidebarAnimation.cancel();
                sidebarAnimation = null;
            }

            if(isClosed) {
                toggle.classList.add(toggleOpened);
                toggle.classList.remove(toggleClosed);
            } else {
                toggle.classList.add(toggleClosed);
                toggle.classList.remove(toggleOpened);
            }

            let update;
            if(isClosed)
                update = function(t) {
                    sidebar.style.width = (40 + (220 * t)).toString() + 'px';
                };
            else
                update = function(t) {
                    sidebar.style.width = (260 - (220 * t)).toString() + 'px';
                };

            sidebarAnimation = MamiAnimate({
                duration: 500,
                easing: 'outExpo',
                update: update,
            });
        }
    }, 'Toggle Sidebar');

    Umi.UI.Toggles.Get('menu-toggle').classList.add('sidebar__selector-mode--menu-toggle-opened');

    Umi.UI.Toggles.Add('scroll', {
        'click': function() {
            settings.toggle('autoScroll');
        }
    }, 'Autoscroll');
    settings.watch('autoScroll', ev => {
        Umi.UI.Toggles.Get('scroll').classList.toggle('sidebar__selector-mode--scroll-off', !ev.detail.value);
    });

    if(window.innerWidth < 768)
        Umi.UI.Toggles.Get('menu-toggle').click();

    Umi.UI.Toggles.Add('audio', {
        'click': function() {
            settings.toggle('soundEnable');
        }
    }, 'Sounds');
    settings.watch('soundEnable', ev => {
        Umi.UI.Toggles.Get('audio').classList.toggle('sidebar__selector-mode--audio-off', !ev.detail.value);
    });

    Umi.UI.Toggles.Add('unembed', {
        'click': function() {
            const buttons = $qa('[data-embed="1"]');
            for(const button of buttons)
                button.click();
        }
    }, 'Unembed any embedded media');

    Umi.UI.Toggles.Add('clear', {
        'click': function() {
            if(confirm('ARE YOU SURE ABOUT THAT???')) {
                const limit = settings.get('explosionRadius');
                const explode = $e({
                    tag: 'img',
                    attrs: {
                        src: '//static.flash.moe/images/explode.gif',
                        alt: '',
                        style: {
                            position: 'absolute',
                            zIndex: 9001,
                            bottom: 0,
                            right: 0,
                            pointerEvents: 'none',
                        },
                        onLoad: function() {
                            setTimeout(function(){
                                $r(explode);
                            }, 1700);

                            soundCtx.library.play('misc:explode');
                        },
                    },
                });

                document.body.appendChild(explode);

                let backLog = Umi.Messages.All();
                backLog = backLog.slice(Math.max(backLog.length - limit, 0));

                Umi.Messages.Clear();

                for(const blMsg of backLog)
                    Umi.Messages.Add(blMsg);
            }
        }
    }, 'Clear Logs');

    const pingIndicator = new MamiPingIndicator;
    const pingToggle = Umi.UI.Toggles.Add('ping', {
        click: () => alert(pingToggle.title),
    }, 'Ready~');
    pingToggle.appendChild(pingIndicator.getElement());


    if(eeprom !== undefined) {
        Umi.UI.Menus.Add('uploads', 'Upload History', !FUTAMI_DEBUG);

        const doUpload = async file => {
            const uploadEntry = Umi.UI.Uploads.create(file.name);
            const uploadTask = eeprom.create(file);

            uploadTask.onProgress(prog => uploadEntry.setProgress(prog.progress));
            uploadEntry.addOption('Cancel', () => uploadTask.abort());

            try {
                const fileInfo = await uploadTask.start();

                uploadEntry.hideOptions();
                uploadEntry.clearOptions();
                uploadEntry.removeProgress();

                uploadEntry.addOption('Open', fileInfo.url);
                uploadEntry.addOption('Insert', () => Umi.UI.Markup.InsertRaw(insertText, ''));
                uploadEntry.addOption('Delete', () => {
                    eeprom.delete(fileInfo)
                        .then(() => uploadEntry.remove())
                        .catch(ex => {
                            console.error(ex);
                            alert(ex);
                        });
                });

                let insertText;

                if(fileInfo.isImage()) {
                    insertText = `[img]${fileInfo.url}[/img]`;
                    uploadEntry.setThumbnail(fileInfo.thumb);
                } else if(fileInfo.isAudio()) {
                    insertText = `[audio]${fileInfo.url}[/audio]`;
                    uploadEntry.setThumbnail(fileInfo.thumb);
                } else if(fileInfo.isVideo()) {
                    insertText = `[video]${fileInfo.url}[/video]`;
                    uploadEntry.setThumbnail(fileInfo.thumb);
                } else
                    insertText = location.protocol + fileInfo.url;

                if(settings.get('eepromAutoInsert'))
                    Umi.UI.Markup.InsertRaw(insertText, '');
            } catch(ex) {
                if(!ex.aborted) {
                    console.error(ex);
                    alert(ex);
                }

                uploadEntry.remove();
            }
        };

        const uploadForm = $e({
            tag: 'input',
            attrs: {
                type: 'file',
                multiple: true,
                style: { display: 'none' },
                onchange: ev => {
                    for(const file of ev.target.files)
                        doUpload(file);
                },
            },
        });
        document.body.appendChild(uploadForm);

        Umi.UI.InputMenus.AddButton('upload', 'Upload', () => uploadForm.click());

        $i('umi-msg-text').onpaste = ev => {
            if(ev.clipboardData && ev.clipboardData.files.length > 0)
                for(const file of ev.clipboardData.files)
                    doUpload(file);
        };

        document.body.ondragenter = ev => {
            ev.preventDefault();
            ev.stopPropagation();
        };
        document.body.ondragover = ev => {
            ev.preventDefault();
            ev.stopPropagation();
        };
        document.body.ondragleave = ev => {
            ev.preventDefault();
            ev.stopPropagation();
        };
        document.body.ondrop = ev => {
            ev.preventDefault();
            ev.stopPropagation();

            if(ev.dataTransfer && ev.dataTransfer.files.length > 0)
                for(const file of ev.dataTransfer.files) {
                    if(file.name.slice(-5) === '.mami'
                        && confirm('This file appears to be a settings export. Do you want to import it? This will overwrite your existing settings!')) {
                        (new MamiSettingsBackup(settings)).importFile(file);
                        return;
                    }

                    doUpload(file);
                }
        };
    }

    Umi.UI.InputMenus.Add('markup', 'BB Code');
    Umi.UI.InputMenus.Add('emotes', 'Emoticons');

    let isUnloading = false;

    window.addEventListener('beforeunload', function(ev) {
        if(settings.get('closeTabConfirm')) {
            ev.preventDefault();
            return ev.returnValue = 'Are you sure you want to close the tab?';
        }

        isUnloading = true;
    });


    // really not sure about all the watchers for the protocol just kinda being Listed here but we'll see i guess
    loadingOverlay.setMessage('Connecting...');

    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 wsCloseReasons = {
        '_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 does not 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 chat server is restarting.',
        '_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 sessFailReasons = {
        'authfail': 'Authentication failed.',
        'sockfail': 'Too many active connections.',
        'userfail': 'Name in use.',
        'joinfail': 'You are banned.',
    };

    const sockChat = new Umi.Protocol.SockChat.Protocol(futami.get('ping') * 1000);
    MamiCompat('Umi.Server', { get: () => sockChat, configurable: true });

    let dumpEvents = false;
    settings.watch('dumpEvents', ev => dumpEvents = ev.detail.value);
    settings.watch('dumpPackets', ev => sockChat.setDumpPackets(ev.detail.value));

    Umi.UI.Hooks.SetCallbacks(sockChat.sendMessage, sockChat.switchChannel);

    MamiCompat('Umi.Server.SendMessage', { value: text => sockChat.sendMessage(text), configurable: true });
    MamiCompat('Umi.Protocol.SockChat.Protocol.Instance.SendMessage', { value: text => sockChat.sendMessage(text), configurable: true });
    MamiCompat('Umi.Protocol.SockLegacy.Protocol.Instance.SendMessage', { value: text => sockChat.sendMessage(text), configurable: true });

    sockChat.watch('conn:lost', ev => {
        if(dumpEvents) console.log('conn:lost', ev);

        if(conMan.isActive() || isUnloading)
            return;

        const errorCode = ev.detail.code;
        const isRestarting = ev.detail.isRestarting;

        pingToggle.title = '∞ms';
        pingIndicator.setStrength(-1);

        const conManAttempt = ev => {
            if(isRestarting || ev.detail.attempt > 1) {
                let message = ev.detail.attempt > 2 ? `Attempt ${ev.detail.attempt}...` : 'Connecting to server...';
                getLoadingOverlay('spinner', 'Connecting...', message);
            }
        };
        const conManFail = ev => {
            if(isRestarting || ev.detail.attempt > 1) {
                let header = isRestarting ? 'Restarting...' : 'Disconnected';
                let message = wsCloseReasons[`_${errorCode}`] ?? `Something caused an unexpected connection loss. (${errorCode})`;

                if(ev.detail.delay > 0)
                    message += ` Retrying in ${ev.detail.delay / 1000} seconds...`;

                // this is absolutely disgusting but i really don't care right now sorry
                message += ' <a href="javascript:void(0)" onclick="mami.conMan.force()">Retry now</a>';

                getLoadingOverlay('unlink', header, message);
            }
        };

        conMan.watch('attempt', conManAttempt);
        conMan.watch('fail', conManFail);

        conMan.start(async url => {
            await sockChat.open(url);
        }).then(() => {
            conMan.unwatch('attempt', conManAttempt);
            conMan.unwatch('fail', conManFail);

            pingToggle.title = 'Ready~';
            pingIndicator.setStrength(3);

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

    sockChat.watch('ping:send', ev => {
        if(dumpEvents) console.log('ping:send', ev);
    });
    sockChat.watch('ping:long', ev => {
        if(dumpEvents) console.log('ping:long', ev);

        pingToggle.title = '+2000ms';
        pingIndicator.setStrength(0);
    });
    sockChat.watch('ping:recv', ev => {
        if(dumpEvents) console.log('ping:recv', ev);

        let strength = 3;
        if(ev.detail.diff >= 400) --strength;
        if(ev.detail.diff >= 200) --strength;

        pingToggle.title = `${ev.detail.diff.toLocaleString()}ms`;
        pingIndicator.setStrength(strength);
    });

    sockChat.watch('session:start', ev => {
        if(dumpEvents) console.log('session:start', ev);

        const userInfo = new Umi.User(ev.detail.user.id, ev.detail.user.name, ev.detail.user.colour, ev.detail.user.permsRaw);
        Umi.User.setCurrentUser(userInfo);
        Umi.Users.Add(userInfo);

        Umi.UI.Markup.Reset();
        Umi.UI.Emoticons.Init();
        Umi.Parsing.Init();

        if(views.count() > 1)
            views.pop(ctx => MamiAnimate({
                async: true,
                duration: 120,
                easing: 'inOutSine',
                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;
                },
            }));
    });
    sockChat.watch('session:fail', ev => {
        if(dumpEvents) console.log('session:fail', ev);

        if(ev.detail.baka !== undefined) {
            new MamiForceDisconnectNotice(ev.detail.baka).pushOn(views);
            return;
        }

        getLoadingOverlay(
            'cross', 'Failed!',
            sessFailReasons[ev.detail.session.reason] ?? `Unknown reason: ${ev.detail.session.reason}`
        );

        if(ev.detail.session.needsAuth)
            setTimeout(() => location.assign(futami.get('login')), 1000);
    });
    sockChat.watch('session:term', ev => {
        if(dumpEvents) console.log('session:term', ev);

        new MamiForceDisconnectNotice(ev.detail.baka).pushOn(views);
    });

    sockChat.watch('user:add', ev => {
        if(dumpEvents) console.log('user:add', ev);

        if(ev.detail.user.self)
            return;

        const userInfo = new Umi.User(ev.detail.user.id, ev.detail.user.name, ev.detail.user.colour, ev.detail.user.permsRaw);
        Umi.Users.Add(userInfo);

        if(ev.detail.msg !== undefined)
            Umi.Messages.Add(new Umi.Message(
                ev.detail.msg.id, ev.detail.msg.time, undefined, '', ev.detail.msg.channel, false,
                {
                    isError: false,
                    type: ev.detail.msg.botInfo.type,
                    args: ev.detail.msg.botInfo.args,
                    target: userInfo,
                }
            ));
    });
    sockChat.watch('user:remove', ev => {
        if(dumpEvents) console.log('user:remove', ev);

        const userInfo = Umi.Users.Get(ev.detail.user.id);
        if(userInfo === null)
            return;

        if(ev.detail.msg !== undefined)
            Umi.Messages.Add(new Umi.Message(
                ev.detail.msg.id,
                ev.detail.msg.time,
                undefined,
                '',
                ev.detail.msg.channel,
                false,
                {
                    isError: false,
                    type: ev.detail.msg.botInfo.type,
                    args: ev.detail.msg.botInfo.args,
                    target: userInfo,
                },
            ));

        Umi.Users.Remove(userInfo);
    });
    sockChat.watch('user:update', ev => {
        if(dumpEvents) console.log('user:update', ev);

        const userInfo = Umi.Users.Get(ev.detail.user.id);
        userInfo.setName(ev.detail.user.name);
        userInfo.setColour(ev.detail.user.colour);
        userInfo.setPermissions(ev.detail.user.permsRaw);
        Umi.Users.Update(userInfo.getId(), userInfo);
    });
    sockChat.watch('user:clear', ev => {
        if(dumpEvents) console.log('user:clear', ev);

        const self = Umi.User.currentUser;
        Umi.Users.Clear();
        if(self !== undefined)
            Umi.Users.Add(self);
    });

    sockChat.watch('chan:add', ev => {
        if(dumpEvents) console.log('chan:add', ev);

        Umi.Channels.Add(new Umi.Channel(
            ev.detail.channel.name,
            ev.detail.channel.hasPassword,
            ev.detail.channel.isTemporary,
        ));
    });
    sockChat.watch('chan:remove', ev => {
        if(dumpEvents) console.log('chan:remove', ev);

        Umi.Channels.Remove(Umi.Channels.Get(ev.detail.channel.name));
    });
    sockChat.watch('chan:update', ev => {
        if(dumpEvents) console.log('chan:update', ev);

        const chanInfo = Umi.Channels.Get(ev.detail.channel.previousName);
        chanInfo.setName(ev.detail.channel.name);
        chanInfo.setHasPassword(ev.detail.channel.hasPassword);
        chanInfo.setTemporary(ev.detail.channel.isTemporary);
        Umi.Channels.Update(ev.detail.channel.previousName, chanInfo);
    });
    sockChat.watch('chan:clear', ev => {
        if(dumpEvents) console.log('chan:clear', ev);

        Umi.Channels.Clear();
    });
    sockChat.watch('chan:focus', ev => {
        if(dumpEvents) console.log('chan:focus', ev);

        Umi.Channels.Switch(Umi.Channels.Get(ev.detail.channel.name));
    });
    sockChat.watch('chan:join', ev => {
        if(dumpEvents) console.log('chan:join', ev);

        const userInfo = new Umi.User(ev.detail.user.id, ev.detail.user.name, ev.detail.user.colour, ev.detail.user.permsRaw);
        Umi.Users.Add(userInfo);

        if(ev.detail.msg !== undefined)
            Umi.Messages.Add(new Umi.Message(
                ev.detail.msg.id, null, undefined, '', ev.detail.msg.channel, false,
                {
                    isError: false,
                    type: leave.msg.botInfo.type,
                    args: [ userInfo.getName() ],
                    target: userInfo,
                },
            ));
    });
    sockChat.watch('chan:leave', ev => {
        if(dumpEvents) console.log('chan:leave', ev);

        if(ev.detail.user.self)
            return;

        const userInfo = Umi.Users.Get(ev.detail.user.id);
        if(userInfo === null)
            return;

        if(ev.detail.msg !== undefined)
            Umi.Messages.Add(new Umi.Message(
                ev.detail.msg.id, null, undefined, '', ev.detail.msg.channel, false,
                {
                    isError: false,
                    type: ev.detail.msg.botInfo.type,
                    args: [ userInfo.getName() ],
                    target: userInfo,
                },
            ));

        Umi.Users.Remove(userInfo);
    });

    sockChat.watch('msg:add', ev => {
        if(dumpEvents) console.log('msg:add', ev);

        const senderInfo = ev.detail.msg.sender;
        const userInfo = senderInfo.name === undefined
            ? Umi.Users.Get(senderInfo.id)
            : new Umi.User(senderInfo.id, senderInfo.name, senderInfo.colour, senderInfo.permsRaw);

        // hack
        let channelName = ev.detail.msg.channel;
        if(channelName !== undefined && channelName.startsWith('@~')) {
            const chanUserInfo = Umi.Users.Get(channelName.substring(2));
            if(chanUserInfo !== null)
                channelName = `@${chanUserInfo.getName()}`;
        }

        // also hack
        if(ev.detail.msg.flags.isPM) {
            if(Umi.Channels.Get(channelName) === null)
                Umi.Channels.Add(new Umi.Channel(channelName, false, true, true));

            // this should be raised for other channels too, but that is not possible yet
            Umi.UI.Menus.Attention('channels');
        }

        Umi.Messages.Add(new Umi.Message(
            ev.detail.msg.id,
            ev.detail.msg.time,
            userInfo,
            ev.detail.msg.text,
            channelName,
            false,
            ev.detail.msg.botInfo,
            ev.detail.msg.flags.isAction,
            ev.detail.msg.silent,
        ));
    });
    sockChat.watch('msg:remove', ev => {
        if(dumpEvents) console.log('msg:remove', ev);

        Umi.Messages.Remove(Umi.Messages.Get(ev.detail.msg.id));
    });
    sockChat.watch('msg:clear', ev => {
        if(dumpEvents) console.log('msg:clear', ev);

        Umi.UI.Messages.RemoveAll();
    });

    const conManAttempt = ev => {
        let message = ev.detail.attempt > 2 ? `Attempt ${ev.detail.attempt}...` : 'Connecting to server...';
        getLoadingOverlay('spinner', 'Connecting...', message);
    };
    const conManFail = ev => {
        getLoadingOverlay('cross', 'Failed to connect', `Retrying in ${ev.detail.delay / 1000} seconds...`);
    };

    conMan.watch('attempt', conManAttempt);
    conMan.watch('fail', conManFail);

    await conMan.start(async url => {
        await sockChat.open(url);
    });

    conMan.unwatch('attempt', conManAttempt);
    conMan.unwatch('fail', conManFail);

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

    const authInfo = MamiMisuzuAuth.getInfo();
    sockChat.sendAuth(authInfo.method, authInfo.token);

    if(window.dispatchEvent)
        window.dispatchEvent(new Event('umi:connect'));
})();
