#include animate.js
#include args.js
#include utility.js

const MamiMessageBoxContainer = function() {
    const container = <div class="msgbox-container"/>;
    let raised = false;
    let currAnim;

    return {
        raise: async parent => {
            if(raised) return;
            raised = true;
            container.style.pointerEvents = null;

            if(!parent.contains(container))
                parent.appendChild(container);

            currAnim?.cancel();

            currAnim = MamiAnimate({
                async: true,
                delayed: true,
                duration: 300,
                easing: 'outExpo',
                start: () => { container.style.opacity = '0'; },
                update: t => { container.style.opacity = t; },
                end: () => { container.style.opacity = null; },
            });
            try {
                await currAnim.start();
            } catch(ex) {}
        },
        dismiss: async parent => {
            if(!raised) return;
            raised = false;
            container.style.pointerEvents = 'none';

            currAnim?.cancel();

            currAnim = MamiAnimate({
                async: true,
                delayed: true,
                duration: 300,
                easing: 'outExpo',
                update: t => { container.style.opacity = 1 - t; },
                end: t => { parent.removeChild(container); },
            });
            try {
                await currAnim.start();
            } catch(ex) {}
        },
        show: async dialog => {
            if(typeof dialog !== 'object' || dialog === null)
                throw 'dialog must be a non-null object';

            const backgroundClick = ev => {
                ev.stopPropagation();
                if(ev.target === container)
                    dialog.clickButtonIndex(0);
            };

            try {
                if(dialog.buttonCount === 1)
                    container.addEventListener('click', backgroundClick);
                return await dialog.show(container);
            } finally {
                container.removeEventListener('click', backgroundClick);
                dialog.dismiss();
            }
        },
    };
};

const MamiMessageBoxDialog = function(info) {
    const defaultButtons = [
        { name: 'ok', text: 'OK', primary: true, reject: false },
        { name: 'yes', text: 'Yes', primary: true, reject: false },
        { name: 'no', text: 'No', reject: true },
        { name: 'cancel', text: 'Cancel', reject: true },
    ];

    const dialog = <form class="msgbox-dialog"/>;
    let showResolve, showReject;

    const doResolve = (...args) => {
        if(typeof showResolve !== 'function')
            return;

        showReject = undefined;
        dialog.style.pointerEvents = 'none';
        showResolve(...args);
    };
    const doReject = (...args) => {
        if(typeof showReject !== 'function')
            return;

        showResolve = undefined;
        dialog.style.pointerEvents = 'none';
        showReject(...args);
    };

    const body = <div class="msgbox-dialog-body"/>;
    dialog.appendChild(body);

    if(info.body !== undefined) {
        if(Array.isArray(info.body))
            for(const line of info.body)
                body.appendChild(<p class="msgbox-dialog-line">{line}</p>);
        else
            body.appendChild(<p class="msgbox-dialog-line">{info.body}</p>);
    }

    const buttons = <div class="msgbox-dialog-buttons"/>;
    const buttonActions = {};
    dialog.appendChild(buttons);

    let primaryButton;
    const createButton = button => {
        if(button.name === undefined) {
            console.error('No name specified for dialog button. Skipping...');
            return;
        }

        const buttonName = button.name.toString();
        if(buttons.querySelector(`[name="${buttonName}"]`) !== null) {
            console.error('A duplicate button name was attempted to be registered. Skipping...');
            return;
        }

        const elem = <button class="msgbox-dialog-button" name={buttonName} data-action={button.reject ? 'reject' : 'resolve'}>{button.text ?? buttonName ?? ''}</button>;
        buttons.appendChild(elem);

        if(button.primary) {
            primaryButton = elem;
            elem.classList.add('msgbox-dialog-button-primary');
        }

        if(typeof button.action === 'function')
            buttonActions[buttonName] = button.action;
    };

    for(const item of defaultButtons)
        if(item.name in info) {
            const button = typeof info[item.name] === 'object' && info[item.name] !== null ? info[item.name] : {};

            button.name = item.name;
            button.reject = item.reject;
            if(button.primary === undefined)
                button.primary = item.primary;
            if(button.text === undefined)
                button.text = item.text;

            createButton(button);
        }

    if(Array.isArray(info.buttons))
        for(const button of info.buttons) {
            if(button.text === undefined)
                button.text = button.name;

            createButton(button);
        }

    if(buttons.childElementCount < 1)
        createButton({ name: 'dismiss', text: 'Dismiss', primary: true });

    if(buttons.childElementCount > 2)
        buttons.classList.add('msgbox-dialog-buttons-many');

    return {
        get buttonCount() {
            return buttons.childElementCount;
        },
        clickButtonIndex: num => {
            buttons.children[num].click();
        },

        show: container => {
            return new Promise((resolve, reject) => {
                showResolve = resolve;
                showReject = reject;

                dialog.addEventListener('submit', ev => {
                    ev.preventDefault();

                    if(ev.submitter instanceof HTMLButtonElement) {
                        const action = ev.submitter.dataset.action === 'resolve' ? doResolve : doReject;

                        let result;
                        if(ev.submitter.name in buttonActions)
                            result = buttonActions[ev.submitter.name]();

                        if(result instanceof Promise)
                            result.then(result => { action(ev.submitter.name, result, true); })
                                .catch(result => { action(ev.submitter.name, result, false); });
                        else
                            action(ev.submitter.name, result);
                    } else doResolve();
                });

                dialog.style.transform = 'scale(0)';
                container.appendChild(dialog);

                if(primaryButton instanceof HTMLButtonElement)
                    primaryButton.focus();

                MamiAnimate({
                    async: true,
                    duration: 500,
                    easing: 'outElasticHalf',
                    update: t => {
                        dialog.style.transform = `scale(${t})`;
                    },
                    end: () => {
                        dialog.style.transform = null;
                    },
                });
            });
        },
        dismiss: async () => {
            dialog.style.pointerEvents = 'none';

            await MamiAnimate({
                async: true,
                duration: 600,
                easing: 'outExpo',
                update: t => { dialog.style.opacity = 1 - t; },
                end: () => { dialog.style.opacity = '0'; },
            });

            $r(dialog);
        },
        cancel: () => {
            doReject();
        },
    };
};

const MamiMessageBoxControl = function(options) {
    options = MamiArguments.verify(options, [
        MamiArguments.check('parent', undefined, value => value instanceof Element, true),
    ]);

    const parent = options.parent;
    const container = new MamiMessageBoxContainer;
    const queue = [];
    let currentDialog;

    let processingQueue = false;
    const processQueue = async () => {
        if(processingQueue)
            return;

        try {
            processingQueue = true;

            let item;
            while((item = queue.shift()) !== undefined) {
                if(!processingQueue)
                    break;

                try {
                    currentDialog = new MamiMessageBoxDialog(item.info);
                    item.resolve(await container.show(currentDialog));
                } catch(ex) {
                    item.reject(ex);
                } finally {
                    currentDialog = undefined;
                }
            }
        } finally {
            processingQueue = false;
        }
    };

    const raise = async () => {
        container.raise(parent);

        if(!processingQueue)
            await processQueue();
    };

    const dismiss = async () => {
        processingQueue = false;
        container.dismiss(parent);
        currentDialog?.cancel();
    };

    return {
        raise: raise,
        dismiss: dismiss,
        show: (info, priority) => {
            return new Promise((resolve, reject) => {
                const item = { info: info, resolve: resolve, reject: reject };

                if(priority) queue.unshift(item);
                else queue.push(item);

                let queueWasRunning = processingQueue;
                raise().finally(() => {
                    if(!queueWasRunning)
                        dismiss();
                });
            });
        },
    };
};
