window.Umi = { UI: {} };

#include animate.js
#include common.js
#include compat.js
#include conman.js
#include context.js
#include emotes.js
#include events.js
#include messages.js
#include mszauth.js
#include txtrigs.js
#include utility.js
#include weeb.js
#include worker.js
#include audio/autoplay.js
#include controls/msgbox.jsx
#include controls/views.js
#include eeprom/eeprom.js
#include settings/backup.js
#include settings/settings.js
#include sockchat/client.js
#include sockchat/handlers.js
#include sound/context.js
#include sound/osukeys.js
#include ui/chat-layout.js
#include ui/hooks.js
#include ui/input-menus.js
#include ui/loading-overlay.jsx
#include ui/markup.js
#include ui/menus.js
#include ui/ping.jsx
#include ui/settings.jsx
#include ui/toggles.js
#include ui/uploads.js
#include ui/view.js

(async () => {
    const eventTarget = new MamiEventTargetWindow;

    const ctx = new MamiContext(eventTarget);
    Object.defineProperty(window, 'mami', { enumerable: true, value: ctx });

    ctx.views = new MamiViewsControl({ body: document.body });
    ctx.msgbox = new MamiMessageBoxControl({ views: ctx.views });

    const loadingOverlay = new Umi.UI.LoadingOverlay('spinner', 'Loading...');
    await ctx.views.push(loadingOverlay);

    loadingOverlay.setMessage('Loading environment...');
    try {
        window.futami = await FutamiCommon.load();
    } catch(ex) {
        console.error('Failed to load common settings.', 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-', ctx.events.scopeTo('settings'));
    ctx.settings = settings;

    settings.define('style').default('dark').create();
    settings.define('compactView').default(false).create();
    settings.define('autoScroll').default(true).create();
    settings.define('closeTabConfirm').default(false).create();
    settings.define('showChannelList').default(false).create();
    settings.define('fancyInfo').default(true).create();
    settings.define('autoCloseUserContext').default(true).create();
    settings.define('enableParser').default(true).create();
    settings.define('enableEmoticons').default(true).create();
    settings.define('autoParseUrls').default(true).create();
    settings.define('preventOverflow').default(false).create();
    settings.define('expandTextBox').default(false).create();
    settings.define('eepromAutoInsert').default(true).create();
    settings.define('autoEmbedV1').default(false).create();
    settings.define('soundEnable').default(true).critical().create();
    settings.define('soundPack').default('').create();
    settings.define('soundVolume').default(80).min(0).max(100).create();
    settings.define('soundEnableJoin').default(true).create();
    settings.define('soundEnableLeave').default(true).create();
    settings.define('soundEnableError').default(true).create();
    settings.define('soundEnableServer').default(true).create();
    settings.define('soundEnableIncoming').default(true).create();
    settings.define('onlySoundOnMention').default(false).create();
    settings.define('soundEnableOutgoing').default(true).create();
    settings.define('soundEnablePrivate').default(true).create();
    settings.define('soundEnableForceLeave').default(true).create();
    settings.define('minecraft').type(['no', 'yes', 'old']).default('no').create();
    settings.define('windowsLiveMessenger').default(false).create();
    settings.define('seinfeld').default(false).create();
    settings.define('flashTitle').default(true).create();
    settings.define('showServerMsgInTitle').default(true).create();
    settings.define('onlyConnectWhenVisible').default(true).create();
    settings.define('playJokeSounds').default(true).create();
    settings.define('weeaboo').default(false).create();
    settings.define('motivationalImages').default(false).create();
    settings.define('motivationalVideos').default(false).create();
    settings.define('osuKeys').default(false).create();
    settings.define('osuKeysV2').type(['no', 'yes', 'rng']).default('no').create();
    settings.define('explosionRadius').default(20).min(0).create();
    settings.define('dumpPackets').default(FUTAMI_DEBUG).create();
    settings.define('dumpEvents').default(FUTAMI_DEBUG).create();
    settings.define('marqueeAllNames').default(false).create();
    settings.define('tmpDisableOldThemeSys').default(false).critical().create();
    settings.define('dbgAnimDurationMulti').default(1).min(0).max(10).create();

    const noNotifSupport = !('Notification' in window);
    settings.define('enableNotifications').default(false).immutable(noNotifSupport).critical().create();
    settings.define('notificationShowMessage').default(false).immutable(noNotifSupport).create();
    settings.define('notificationTriggers').default('').immutable(noNotifSupport).create();


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

    futami.getJson('sounds2')
        .catch(ex => { console.error('Failed to load sound library and packs.', ex); })
        .then(sounds => {
            if(Array.isArray(sounds.library))
                soundCtx.library.register(sounds.library, true);

            if(Array.isArray(sounds.packs)) {
                soundCtx.packs.register(sounds.packs, true);
                settings.touch('soundPack', true);
            }
        });

    MamiDetectAutoPlay()
        .then(canAutoPlay => {
            if(canAutoPlay) return;

            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', true);

            // do we need to do this?
            if(!ev.detail.initial && !ev.detail.silent && ev.detail.local)
                soundCtx.library.play(soundCtx.pack.getEventSound('server'));
        }

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

    settings.watch('soundPack', ev => {
        const packs = soundCtx.packs;
        let packName = ev.detail.value;

        if(packName === '') {
            const names = packs.names();
            if(names.length < 1)
                return;

            packName = names[0];
        } else if(!packs.has(packName))
            return;

        soundCtx.pack = packs.get(packName);

        if(!ev.detail.initial && !ev.detail.silent && ev.detail.local)
            soundCtx.library.play(soundCtx.pack.getEventSound('server'));
    });

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


    // loading these asynchronously makes them not show up in the backlog
    // revisit when emote reparsing is implemented
    loadingOverlay.setMessage('Loading emoticons...');
    try {
        const emotes = await futami.getJson('emotes');
        MamiEmotes.loadLegacy(emotes);
    } catch(ex) {
        console.error('Failed to load emoticons.', ex);
    } finally {
        // this is currently called in the sock chat handlers
        // does a permissions check which it can't do at this point
        //Umi.UI.Emoticons.Init();
    }


    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';
    });


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

    ctx.textTriggers = new MamiTextTriggers;

    // should be dynamic when possible
    const layout = new Umi.UI.ChatLayout;
    await ctx.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(!ctx.textTriggers.hasTriggers())
            futami.getJson('texttriggers').then(trigInfos => ctx.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];
            const toggle = Umi.UI.Toggles.Get('menu-toggle');
            const isClosed = toggle.classList.contains('sidebar__selector-mode--menu-toggle-closed');

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

            toggle.classList.toggle('sidebar__selector-mode--menu-toggle-opened', isClosed);
            toggle.classList.toggle('sidebar__selector-mode--menu-toggle-closed', !isClosed);

            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() {
            ctx.msgbox.show({ body: 'ARE YOU SURE ABOUT THAT???', yes: true, no: true }).then(() => {
                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);
            }).catch(() => {});
        }
    }, 'Clear Logs');

    const pingIndicator = new MamiPingIndicator;
    const pingToggle = Umi.UI.Toggles.Add('ping', {
        click: () => { ctx.msgbox.show({ body: `Your current ping is ${pingToggle.title}` }); },
    }, 'Ready~');
    pingToggle.appendChild(pingIndicator.getElement());

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

    let doUpload;
    ctx.eeprom = new MamiEEPROM(futami.get('eeprom2'), MamiMisuzuAuth.getLine);
    ctx.eeprom.init()
        .catch(ex => {
            console.log('Failed to initialise EEPROM.', ex);
        })
        .then(() => {
            Umi.UI.Menus.Add('uploads', 'Upload History', !FUTAMI_DEBUG);

            doUpload = async file => {
                const uploadEntry = Umi.UI.Uploads.create(file.name);
                const uploadTask = ctx.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', () => {
                        ctx.eeprom.delete(fileInfo)
                            .then(() => uploadEntry.remove())
                            .catch(ex => {
                                console.error(ex);
                                ctx.msgbox.show({ body: ['An error occurred while trying to delete an uploaded file:', 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);
                        ctx.msgbox.show({ body: ['An error occurred while trying to upload a file:', 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(), 'markup');

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

    // figure out how to display a UI for this someday
    //document.body.addEventListener('dragenter', ev => { console.info('dragenter', ev); });
    //document.body.addEventListener('dragleave', ev => { console.info('dragleave', ev); });
    document.body.addEventListener('dragover', ev => { ev.preventDefault(); });
    document.body.addEventListener('drop', ev => {
        if(ev.dataTransfer === undefined || ev.dataTransfer === null || ev.dataTransfer.files.length < 1)
            return;

        ev.preventDefault();

        for(const file of ev.dataTransfer.files) {
            if(file.name.slice(-5) === '.mami') {
                ctx.msgbox.show({
                    body: [
                        'This file appears to be a settings export.',
                        'Do you want to import it? This will overwrite your existing settings!',
                    ],
                    yes: true,
                    no: true,
                })
                .then(() => {
                    (new MamiSettingsBackup(settings)).importFile(file);
                })
                .catch(() => {
                    if(doUpload !== undefined)
                        doUpload(file);
                });
            } else if(doUpload !== undefined)
                doUpload(file);
        }
    });

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

        ctx.isUnloading = true;
    });


    loadingOverlay.setMessage('Connecting...');

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

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

        if(!optional) {
            const loading = new Umi.UI.LoadingOverlay(icon, header, message);
            await ctx.views.push(loading);
        }
    };

    const protoWorker = new MamiWorker(MAMI_PROTO_JS, ctx.events.scopeTo('worker'));
    ctx.protoWorker = protoWorker;

    const sockChat = new MamiSockChat(protoWorker);
    const conMan = new MamiConnectionManager(sockChat, settings, futami.get('servers'), ctx.events.scopeTo('conn'));
    ctx.conMan = conMan;

    let sockChatRestarting;

    const sockChatReconnect = () => {
        if(conMan.isActive)
            return;

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

        const reconManAttempt = ev => {
            if(sockChatRestarting || ev.detail.delay > 2000)
                setLoadingOverlay('spinner', 'Connecting...', 'Connecting to server...');
        };
        const reconManFail = ev => {
            // this is absolutely disgusting but i really don't care right now sorry
            if(sockChatRestarting || ev.detail.delay > 2000)
                setLoadingOverlay('unlink', sockChatRestarting ? 'Restarting...' : 'Disconnected', `Attempting to reconnect in ${(ev.detail.delay / 1000).toLocaleString()} seconds...<br><a href="javascript:void(0)" onclick="mami.conMan.force()">Retry now</a>`);
        };
        const reconManSuccess = () => {
            conMan.unwatch('success', reconManSuccess);
            conMan.unwatch('attempt', reconManAttempt);
            conMan.unwatch('fail', reconManFail);
        };

        conMan.watch('attempt', reconManAttempt);
        conMan.watch('fail', reconManFail);
        conMan.watch('success', reconManSuccess);

        conMan.start();
    };

    const sockChatHandlers = new MamiSockChatHandlers(ctx, sockChat, setLoadingOverlay, sockChatReconnect, pingIndicator, pingToggle);
    settings.watch('dumpEvents', ev => sockChatHandlers.setDumpEvents(ev.detail.value));
    settings.watch('dumpPackets', ev => sockChat.setDumpPackets(ev.detail.value));
    sockChatHandlers.register();

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

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

    let workerStarting = false;
    const initWorker = async () => {
        if(workerStarting)
            return;
        workerStarting = true;

        if(FUTAMI_DEBUG)
            console.info('[proto] initialising worker...');

        try {
            await protoWorker.connect();
            await sockChat.create();
            conMan.client = sockChat;
            await conMan.start();
        } finally {
            workerStarting = false;
        }
    };

    protoWorker.watch(':timeout', ev => {
        console.warn('worker timeout', ev.detail);
        initWorker();
    });

    window.addEventListener('visibilitychange', () => {
        if(document.visibilityState === 'visible') {
            protoWorker.ping().catch(ex => {
                console.warn('worker died', ex);
                initWorker();
            });
        }
    });

    await initWorker();
})();
