import * as mc from "@minecraft/server";
import * as ui from "@minecraft/server-ui";
import { mcerror } from "./log";
import { addName } from "./namespace";
import { readDynamicProperty, writeDynamicProperty } from "./properties";
/**
 * @remarks The result of a shown menu.
 */
export var MenuResult;
(function (MenuResult) {
    /**
     * @remarks The menu could not be opened as UI is unavailable.
     */
    MenuResult["UserBusy"] = "UserBusy";
    /**
     * @remarks The player closed the menu with the 'x' button or by pressing escape.
     */
    MenuResult["UserClosed"] = "UserClosed";
    /**
     * @remarks The menu could not be opened as the player is not available.
     */
    MenuResult["UserInvalid"] = "UserInvalid";
    /**
     * @remarks The player interacted with the menu and it closed via valid submission.
     */
    MenuResult["UserResolved"] = "UserResolved";
})(MenuResult || (MenuResult = {}));
/**
 * @remarks The base class for menus. Cannot be used directly.
 */
class Menu {
    /**
     * @remarks Creates a new Menu that can be shown to players.
     * @param title A {@link mc.RawMessage} or string. Provides the title of the menu.
     * @param elements An array of {@link MenuElement}s. Provides the look and behaviors of the menu.
     */
    constructor(title, elements) {
        this.onClosedCallback = (player, ...params) => { };
        this.onCancelledCallback = (player, cancelationReason, ...params) => { };
        this.title = title;
        this.elements = elements;
    }
    /**
     * @remarks Handles what the menu should do when closed successfully.
     * @param callback The callback to be executed when the menu is closed successfully.
     * @returns This menu.
     */
    onClose(callback) {
        this.onClosedCallback = callback;
        return this;
    }
    /**
     * @remarks Handles what the menu should do when cancelled.
     * @param callback The callback to be executed when the menu is cancelled (via the exit button or pressing escape).
     * @returns This menu.
     */
    onCancel(callback) {
        this.onCancelledCallback = callback;
        return this;
    }
    /**
     * @remarks Queues a menu to be shown to a player. If their UI hasn't loaded or they are currently in another menu
     * this method will queue the menu to be shown when possible. If the UI is available the menu will be shown immediately.
     * Also note that any callback set via {@link onCancel} will be called every frame the UI is unavailable.
     * @param player The {@link SafetyCheckedPlayer} the menu should be shown to.
     * @param params An array of optional parameters that will be passed to the elements of the menu.
     * @returns A {@link MenuResult} describing the result of the displayed menu. Note that `UserBusy` will never be returned with this method.
     */
    queueMenuForPlayer(player, ...params) {
        return new Promise(async (resolve) => {
            while (player.isValid()) {
                const result = await this.showMenuToPlayer(player, ...params);
                if (result === MenuResult.UserBusy) {
                    continue;
                }
                resolve(result);
                return;
            }
            resolve(MenuResult.UserInvalid);
        });
    }
}
/**
 * @remarks A class for creating advanced {@link ui.ActionFormData} objects.
 */
export class ButtonMenu extends Menu {
    /**
     * @remarks Creates a new ButtonMenu that can be shown to players.
     * @param title A {@link mc.RawMessage} or string. Provides the title of the menu.
     * @param elements An array of {@link ButtonMenuElement}s. Provides the look and behaviors of the menu.
     * @param body An optional {@link mc.RawMessage} or string. Provides the body text of the menu above the buttons.
     * @example
     * https://github.com/Dotzip-Developments/craftee-items-dx/blob/main/scripts/weather_clock.ts
     * ```typescript
     * new utils.ButtonMenu({translate: 'action.menu.time.title'}, [
     *     {
     *         text: {translate: 'action.menu.time.stop'},
     *         texture: 'textures/ui/weather_clock/time.pause',
     *         shouldHide() {
     *             return GlobalTime.Paused;
     *         },
     *         onClick(player) {
     *             printMessageToPlayers(`action.menu.time.message`, {rawtext: [{text: player.nameTag}, {text: 'has paused'}]});
     *             GlobalTime.Paused = true;
     *         },
     *     },
     *     {
     *         text: {translate: 'action.menu.time.resume'},
     *         texture: 'textures/ui/weather_clock/time.resume',
     *         shouldHide() {
     *             return !GlobalTime.Paused;
     *         },
     *         onClick(player) {
     *             printMessageToPlayers(`action.menu.time.message`, {rawtext: [{text: player.nameTag}, {text: 'has resumed'}]});
     *             GlobalTime.Paused = false;
     *         },
     *     },
     *     {
     *         text: {translate: 'action.menu.time.start'},
     *         texture: 'textures/ui/weather_clock/time.start',
     *         onClick(player) {
     *             printMessageToPlayers(`action.menu.time.message`, {rawtext: [{text: player.nameTag}, {text: 'reset'}]});
     *             GlobalTime.Speed = GlobalTime.defaultSpeed;
     *             GlobalTime.Paused = false;
     *         },
     *     },
     *     {
     *         text: {translate: 'action.menu.time.reverse'},
     *         texture: 'textures/ui/weather_clock/time.reverse',
     *         shouldHide() {
     *             return GlobalTime.Paused;
     *         },
     *         onClick(player) {
     *             printMessageToPlayers(`action.menu.time.message`, {rawtext: [{text: player.nameTag}, {text: 'reversed'}]});
     *             GlobalTime.Speed *= -1;
     *         },
     *     },
     *     {
     *         text: {translate: 'action.menu.time.fast'},
     *         texture: 'textures/ui/weather_clock/time.fast',
     *         shouldHide() {
     *             return GlobalTime.Paused;
     *         },
     *         onClick(player) {
     *             if (GlobalTime.Speed === 0) {
     *                 printMessageToPlayers(`action.menu.time.message`, {rawtext: [{text: player.nameTag}, {text: 'sped up'}]});
     *                 GlobalTime.Speed = GlobalTime.defaultSpeed;
     *                 return;
     *             }
     *             if (Math.abs(GlobalTime.Speed * 2) <= GlobalTime.speedMax) {
     *                 printMessageToPlayers(`action.menu.time.message`, {rawtext: [{text: player.nameTag}, {text: 'sped up'}]});
     *                 GlobalTime.Speed *= 2;
     *             } else {
     *                 player.onScreenDisplay.setActionBar({translate: 'action.menu.time.error_max'});
     *             }
     *         },
     *     },
     *     {
     *         text: {translate: 'action.menu.time.slow'},
     *         texture: 'textures/ui/weather_clock/time.slow',
     *         shouldHide() {
     *             return GlobalTime.Paused;
     *         },
     *         onClick(player) {
     *             if (GlobalTime.Speed !== 0) {
     *                 printMessageToPlayers(`action.menu.time.message`, {rawtext: [{text: player.nameTag}, {text: 'slowed down'}]});
     *                 GlobalTime.Speed /= 2;
     *             } else {
     *                 player.onScreenDisplay.setActionBar({translate: 'action.menu.time.error_min'});
     *             }
     *         },
     *     }
     * ]);
     * ```
     */
    constructor(title, elements, body) {
        super(title, elements);
        this.body = body;
    }
    /**
     * @inheritdoc
     * @example
     * https://github.com/Dotzip-Developments/craftee-items-dx/blob/main/scripts/weather_clock.ts
     * ```typescript
     * function interactWithWeatherClock(event: mc.ScriptEventCommandMessageAfterEvent) {
     *     if (event.id === 'ldz:interact_with_weather_clock' && event.sourceEntity) {
     *         const player = new utils.SafetyCheckedPlayer(event.sourceEntity as mc.Player);
     *         utils.getEntitiesFromViewDirectionSafe(player, {maxDistance: 5}).forEach(hit => {
     *             if (hit.entity.typeId === 'ldz:weather_clock') {
     *                 ROOT_MENU.showMenuToPlayer(player, new utils.SafetyCheckedEntity(hit.entity));
     *             }
     *         });
     *     }
     * }
     * ```
     */
    showMenuToPlayer(player, ...params) {
        return new Promise((resolve) => {
            if (!player.isValid()) {
                resolve(MenuResult.UserInvalid);
                return;
            }
            ;
            const form = new ui.ActionFormData();
            form.title(this.title instanceof Function ? this.title(player, ...params) : this.title);
            if (this.body) {
                const body = this.body instanceof Function ? this.body(player, ...params) : this.body;
                form.body(body);
            }
            const filteredElements = this.elements.filter(element => !(element.shouldHide && element.shouldHide(player, ...params)));
            filteredElements.forEach(element => {
                const text = element.text instanceof Function ? element.text(player, ...params) : element.text;
                const texture = element.texture instanceof Function ? element.texture(player, ...params) : element.texture;
                form.button(text, texture);
            });
            form.show(player.UnsafeEntity).then(result => {
                if (result.selection !== undefined && player.isValid()) {
                    filteredElements[result.selection].onClick(player, ...params);
                    resolve(MenuResult.UserResolved);
                    this.onClosedCallback(player, ...params);
                }
                else {
                    resolve(MenuResult[result.cancelationReason]);
                    this.onCancelledCallback(player, result.cancelationReason, ...params);
                }
            });
        });
    }
}
/**
 * @remarks A class for creating advanced {@link ui.ModalFormData} objects.
 */
export class ModalMenu extends Menu {
    constructor(title, elements, submit) {
        super(title, elements);
        this.submit = submit;
    }
    showMenuToPlayer(player, ...params) {
        return new Promise((resolve) => {
            if (!player.isValid()) {
                resolve(MenuResult.UserInvalid);
                return;
            }
            ;
            const form = new ui.ModalFormData();
            form.title(this.title instanceof Function ? this.title(player, ...params) : this.title);
            const filteredElements = this.elements.filter(element => !(element.shouldHide && element.shouldHide(player, ...params)));
            filteredElements.forEach(element => {
                switch (element.type) {
                    case 'dropdown':
                        {
                            const filteredDropdownOptions = element.options.filter(option => !(option.shouldHide && option.shouldHide(player, ...params)));
                            const options = filteredDropdownOptions.map(option => option.text instanceof Function ? option.text(player, ...params) : option.text);
                            const label = element.text instanceof Function ? element.text(player, ...params) : element.text;
                            const defaultValueIndex = element.defaultValueIndex instanceof Function ? element.defaultValueIndex(player, ...params) : element.defaultValueIndex;
                            form.dropdown(label, options, defaultValueIndex);
                        }
                        break;
                    case 'slider':
                        {
                            const label = element.text instanceof Function ? element.text(player, ...params) : element.text;
                            const minimumValue = element.minimumValue instanceof Function ? element.minimumValue(player, ...params) : element.minimumValue;
                            const maximumValue = element.maximumValue instanceof Function ? element.maximumValue(player, ...params) : element.maximumValue;
                            const valueStep = element.valueStep instanceof Function ? element.valueStep(player, ...params) : element.valueStep;
                            const defaultValue = element.defaultValue instanceof Function ? element.defaultValue(player, ...params) : element.defaultValue;
                            form.slider(label, minimumValue, maximumValue, valueStep, defaultValue);
                        }
                        break;
                    case 'textField':
                        {
                            const label = element.text instanceof Function ? element.text(player, ...params) : element.text;
                            const placeholderText = element.placeholderText instanceof Function ? element.placeholderText(player, ...params) : element.placeholderText;
                            const defaultValue = element.defaultValue instanceof Function ? element.defaultValue(player, ...params) : element.defaultValue;
                            form.textField(label, placeholderText, defaultValue);
                        }
                        break;
                    case "toggle":
                        {
                            const label = element.text instanceof Function ? element.text(player, ...params) : element.text;
                            const defaultValue = element.defaultValue instanceof Function ? element.defaultValue(player, ...params) : element.defaultValue;
                            form.toggle(label, defaultValue);
                        }
                        break;
                    default:
                        mcerror(`Unexpected type${new Error().stack}`);
                        break;
                }
            });
            if (this.submit) {
                form.submitButton(this.submit instanceof Function ? this.submit(player, ...params) : this.submit);
            }
            form.show(player.UnsafeEntity).then(result => {
                if (result.formValues) {
                    result.formValues.forEach((value, index) => {
                        const submittedElement = filteredElements[index];
                        switch (submittedElement.type) {
                            case 'dropdown':
                                const filteredDropdownOptions = submittedElement.options.filter(option => !(option.shouldHide && option.shouldHide(player, ...params)));
                                filteredDropdownOptions[Number(value)].onSelected(player, ...params);
                                break;
                            case 'slider':
                                submittedElement.onSubmit(player, Number(value), ...params);
                                break;
                            case 'textField':
                                submittedElement.onSubmit(player, String(value), ...params);
                                break;
                            case "toggle":
                                submittedElement.onSubmit(player, Boolean(value), ...params);
                                break;
                            default:
                                mcerror(`Unexpected type${new Error().stack}`);
                                break;
                        }
                    });
                    resolve(MenuResult.UserResolved);
                    this.onClosedCallback(player, ...params);
                }
                else {
                    resolve(MenuResult[result.cancelationReason]);
                    this.onCancelledCallback(player, result.cancelationReason, ...params);
                }
            });
        });
    }
}
/**
 * @remarks A class for creating advanced {@link ui.MessageFormData} objects.
 */
export class MessageMenu extends Menu {
    /**
     * @remarks Creates a new MessageMenu that can be shown to players.
     * @param title A {@link mc.RawMessage} or string. Provides the title of the menu.
     * @param elements An object containing a confirm and deny {@link MessageButtonMenuElement}. Note that the {@link MenuElement.shouldHide} property does nothing in this menu, there will always be two buttons.
     * @param body An optional {@link mc.RawMessage} or string. Provides the body text of the message above the buttons.
     */
    constructor(title, elements, body) {
        super(title, []);
        this.confirmButton = elements.confirm;
        this.denyButton = elements.deny;
        this.body = body;
    }
    showMenuToPlayer(player, ...params) {
        return new Promise((resolve) => {
            if (!player.isValid()) {
                resolve(MenuResult.UserInvalid);
                return;
            }
            ;
            const form = new ui.MessageFormData();
            form.title(this.title instanceof Function ? this.title(player, ...params) : this.title);
            if (this.body) {
                const body = this.body instanceof Function ? this.body(player, ...params) : this.body;
                form.body(body);
            }
            const confirmText = this.confirmButton.text instanceof Function ? this.confirmButton.text(player, ...params) : this.confirmButton.text;
            form.button1(confirmText);
            const denyText = this.denyButton.text instanceof Function ? this.denyButton.text(player, ...params) : this.denyButton.text;
            form.button2(denyText);
            form.show(player.UnsafeEntity).then(result => {
                if (result.selection === 0 && player.isValid()) {
                    this.confirmButton.onClick(player, ...params);
                    resolve(MenuResult.UserResolved);
                    this.onClosedCallback(player, ...params);
                    return;
                }
                else if (result.selection === 1 && player.isValid()) {
                    this.denyButton.onClick(player, ...params);
                    resolve(MenuResult.UserResolved);
                    this.onClosedCallback(player, ...params);
                    return;
                }
                resolve(MenuResult[result.cancelationReason]);
                this.onCancelledCallback(player, result.cancelationReason, ...params);
            });
        });
    }
}
function resolveStringBuilder(builder, player, ...params) {
    return builder instanceof Function ? builder(player, ...params) : builder;
}
function resolveTextureBuilder(builder, player, ...params) {
    return builder instanceof Function ? builder(player, ...params) : builder;
}
/**
 * @remarks A class for building advanced menus with support for submenu navigation, automatic layout creation, and bookmarking.
 * Note that automatically generated back buttons use the lang key `<namespace>:menu.button.back`.
 */
export class MenuBuilder {
    /**
     * @remarks Creates a new menu builder that can be shown to players.
     * @param homePage The home page, the root of the menu.
     * @param options A {@link MenuBuilderOptions} providing additional options for the menu.
     */
    constructor(homePage, options) {
        this.playerPromises = {};
        this.layout = { root: homePage };
        this.options = options;
    }
    /**
     * @remarks Shows the menu to a given player.
     * @param player The {@link SafetyCheckedPlayer} this menu should be shown to.
     * @param path An optional path to navigate to a specific submenu.
     */
    async showMenuToPlayer(player, path) {
        const promise = new Promise(resolve => {
            var _a, _b, _c, _d, _e;
            this.playerPromises[player.id] = resolve;
            if (!path) {
                if ((_a = this.options) === null || _a === void 0 ? void 0 : _a.bookmarkPropertyKey) {
                    path = (_b = readDynamicProperty(player, this.options.bookmarkPropertyKey)) !== null && _b !== void 0 ? _b : "root/";
                }
                else {
                    path = "root/";
                }
            }
            this.handleHurt(player);
            const page = this.getPageFromString(path);
            const menu = this.getMenuFromPage(page.page, page.path, player);
            if (!menu) {
                resolve(MenuResult.UserResolved);
                return;
            }
            ((_e = (_c = page.page.onPageOpen) !== null && _c !== void 0 ? _c : (_d = this.options) === null || _d === void 0 ? void 0 : _d.defaultOnPageOpen) !== null && _e !== void 0 ? _e : function () { })(player);
            menu.showMenuToPlayer(player);
        });
        return promise;
    }
    /**
     * @remarks Clones a page from this menu builder, allowing you to use an identical page in multiple menus.
     * @param path The path to the desired page.
     * @returns The page at the given path.
     */
    clonePage(path) {
        const { page } = this.getPageFromString(path);
        return page;
    }
    handleHurt(player) {
        var _a;
        if ((_a = this.options) === null || _a === void 0 ? void 0 : _a.closeOnHurt) {
            const hurtEvent = (event) => {
                if (event.hurtEntity.id === player.id) {
                    mc.world.afterEvents.entityHurt.unsubscribe(hurtEvent);
                    ui.uiManager.closeAllForms(player.UnsafeEntity);
                }
            };
            mc.world.afterEvents.entityHurt.subscribe(hurtEvent);
        }
    }
    getPageFromString(path) {
        const keys = path.split("/").filter(key => key !== "");
        let currentLayout = this.layout;
        let currentPage = this.layout.root;
        let currentPath = "";
        for (const key of keys) {
            if (currentLayout && typeof currentLayout === "object" && key in currentLayout) {
                currentPage = currentLayout[key];
                currentPath += key + "/";
                if (currentPage.type === "page_list") {
                    currentLayout = currentPage.contents;
                }
            }
        }
        return { page: currentPage, path: currentPath };
    }
    getMenuFromPage(page, pagePath, player) {
        switch (page.type) {
            case "page_list":
                return this.getLayoutMenu(page, pagePath);
            case "buttons":
                return this.getButtonMenu(page, pagePath);
            case "modal":
                return this.getModalMenu(page, pagePath);
            case "message":
                return this.getMessageMenu(page, pagePath);
            case "event":
                this.playerPromises[player.id](MenuResult.UserResolved);
                page.contents(player);
                return;
        }
    }
    getLayoutMenu(page, pagePath) {
        var _a, _b, _c, _d, _e;
        const buttons = [];
        for (const entry of Object.entries(page.contents)) {
            if (entry[1].hidden)
                continue;
            buttons.push({
                text: (_a = entry[1].buttonLabel) !== null && _a !== void 0 ? _a : entry[1].title,
                texture: entry[1].icon,
                onClick: (player) => {
                    var _a, _b, _c;
                    const newPage = this.getPageFromString(pagePath + entry[0]);
                    const menu = this.getMenuFromPage(newPage.page, newPage.path, player);
                    if (!menu)
                        return;
                    this.updateStoredPath(newPage, player);
                    menu.showMenuToPlayer(player);
                    ((_c = (_a = newPage.page.onPageOpen) !== null && _a !== void 0 ? _a : (_b = this.options) === null || _b === void 0 ? void 0 : _b.defaultOnPageOpen) !== null && _c !== void 0 ? _c : function () { })(player);
                }
            });
        }
        const path = pagePath.split("/").filter(key => key !== "");
        if (path.length > 1) {
            path.pop();
            const backButton = {
                text: addName("menu.button.back"),
                texture: (_b = page.backIcon) !== null && _b !== void 0 ? _b : (_c = this.options) === null || _c === void 0 ? void 0 : _c.defaultBackIcon,
                onClick: (player) => {
                    var _a, _b, _c;
                    const newPage = this.getPageFromString(path.join("/"));
                    const menu = this.getMenuFromPage(newPage.page, newPage.path, player);
                    if (!menu)
                        return;
                    this.updateStoredPath(newPage, player);
                    menu.showMenuToPlayer(player);
                    ((_c = (_a = newPage.page.onPageOpen) !== null && _a !== void 0 ? _a : (_b = this.options) === null || _b === void 0 ? void 0 : _b.defaultOnPageOpen) !== null && _c !== void 0 ? _c : function () { })(player);
                },
            };
            const backPos = (_d = page.backButtonPosition) !== null && _d !== void 0 ? _d : (_e = this.options) === null || _e === void 0 ? void 0 : _e.defaultBackPosition;
            switch (backPos) {
                case "none": break;
                case "top":
                    buttons.unshift(backButton);
                    break;
                case "bottom":
                    buttons.push(backButton);
                    break;
                case "both":
                    buttons.unshift(backButton);
                default:
                    buttons.push(backButton);
                    break;
            }
        }
        if (!buttons.length) {
            buttons.push({
                text: "Missing Buttons",
                onClick() { },
            });
        }
        const menu = new ButtonMenu(page.title, buttons, page.body);
        menu.onCancel(player => this.playerPromises[player.id](MenuResult.UserClosed));
        return menu;
    }
    getButtonMenu(page, pagePath) {
        var _a, _b, _c, _d;
        const buttons = [...page.contents];
        // Update buttons to resolve when closed
        for (const button of buttons) {
            const onClick = button.onClick;
            button.onClick = (player) => {
                onClick(player);
                this.playerPromises[player.id](MenuResult.UserResolved);
            };
        }
        const path = pagePath.split("/").filter(key => key !== "");
        if (path.length > 1) {
            path.pop();
            const backButton = {
                text: addName("menu.button.back"),
                texture: (_a = page.backIcon) !== null && _a !== void 0 ? _a : (_b = this.options) === null || _b === void 0 ? void 0 : _b.defaultBackIcon,
                onClick: (player) => {
                    var _a, _b, _c;
                    const newPage = this.getPageFromString(path.join("/"));
                    const menu = this.getMenuFromPage(newPage.page, newPage.path, player);
                    if (!menu)
                        return;
                    this.updateStoredPath(newPage, player);
                    menu.showMenuToPlayer(player);
                    ((_c = (_a = newPage.page.onPageOpen) !== null && _a !== void 0 ? _a : (_b = this.options) === null || _b === void 0 ? void 0 : _b.defaultOnPageOpen) !== null && _c !== void 0 ? _c : function () { })(player);
                },
            };
            const backPos = (_c = page.backButtonPosition) !== null && _c !== void 0 ? _c : (_d = this.options) === null || _d === void 0 ? void 0 : _d.defaultBackPosition;
            switch (backPos) {
                case "none": break;
                case "top":
                    buttons.unshift(backButton);
                    break;
                case "bottom":
                    buttons.push(backButton);
                    break;
                case "both":
                    buttons.unshift(backButton);
                default:
                    buttons.push(backButton);
                    break;
            }
        }
        if (!buttons.length) {
            buttons.push({
                text: "Missing Buttons",
                onClick() { },
            });
        }
        const menu = new ButtonMenu(page.title, buttons, page.body);
        menu.onCancel(player => this.playerPromises[player.id](MenuResult.UserClosed));
        return menu;
    }
    getModalMenu(page, pagePath) {
        var _a, _b;
        const menu = new ModalMenu(page.title, page.contents, (_a = page.submitText) !== null && _a !== void 0 ? _a : (_b = this.options) === null || _b === void 0 ? void 0 : _b.defaultSubmitText);
        menu.onCancel(player => {
            var _a;
            (_a = page.onCanceled) === null || _a === void 0 ? void 0 : _a.call(page, player);
            this.playerPromises[player.id](MenuResult.UserClosed);
        })
            .onClose(player => {
            var _a;
            (_a = page.onSubmit) === null || _a === void 0 ? void 0 : _a.call(page, player);
            this.playerPromises[player.id](MenuResult.UserResolved);
        });
        return menu;
    }
    getMessageMenu(page, pagePath) {
        const menu = new MessageMenu(page.title, page.contents, page.body);
        menu.onCancel(player => this.playerPromises[player.id](MenuResult.UserClosed))
            .onClose(player => this.playerPromises[player.id](MenuResult.UserResolved));
        return menu;
    }
    updateStoredPath(page, player) {
        var _a;
        if (((_a = this.options) === null || _a === void 0 ? void 0 : _a.bookmarkPropertyKey) && !page.page.ignoreBookmark && page.page.type !== "modal" && page.page.type !== "message") {
            writeDynamicProperty(player, this.options.bookmarkPropertyKey, page.path);
        }
    }
}
