"use strict";
/* -------------------------------------------- */
/**
 * Narrator Tools configuration menu
 */
class NarratorMenu extends FormApplication {
    static get defaultOptions() {
        return mergeObject(super.defaultOptions, {
            id: 'narrator-config',
            title: game.i18n.localize('NT.Title'),
            classes: ['sheet'],
            template: 'modules/narrator-tools/templates/config.html',
            width: 800,
        });
    }
    /**
     * Get all game settings related to the form, to display them
     * @param _options
     */
    async getData(_options) {
        return {
            FontSize: game.settings.get('narrator-tools', 'FontSize'),
            WebFont: game.settings.get('narrator-tools', 'WebFont'),
            TextColor: game.settings.get('narrator-tools', 'TextColor'),
            TextShadow: game.settings.get('narrator-tools', 'TextShadow'),
            TextCSS: game.settings.get('narrator-tools', 'TextCSS'),
            Pause: game.settings.get('narrator-tools', 'Pause'),
            DurationMultiplier: game.settings.get('narrator-tools', 'DurationMultiplier'),
            BGColor: game.settings.get('narrator-tools', 'BGColor'),
            BGImage: game.settings.get('narrator-tools', 'BGImage'),
            NarrationStartPaused: game.settings.get('narrator-tools', 'NarrationStartPaused'),
            MessageType: game.settings.get('narrator-tools', 'MessageType'),
            CHAT_MESSAGE_TYPES: {
                0: 'Other',
                1: 'Out of Character',
                2: 'In Character',
            },
            PERMScenery: game.settings.get('narrator-tools', 'PERMScenery'),
            PERMDescribe: game.settings.get('narrator-tools', 'PERMDescribe'),
            PERMNarrate: game.settings.get('narrator-tools', 'PERMNarrate'),
            PERMAs: game.settings.get('narrator-tools', 'PERMAs'),
            USER_ROLES: {
                0: game.i18n.localize('USER.RoleNone'),
                1: game.i18n.localize('USER.RolePlayer'),
                2: game.i18n.localize('USER.RoleTrusted'),
                3: game.i18n.localize('USER.RoleAssistant'),
                4: game.i18n.localize('USER.RoleGamemaster'),
            },
        };
    }
    /**
     * Updates the settings to match the forms
     * @param _event
     * @param formData The form data to be saved
     */
    async _updateObject(_event, formData) {
        for (let [k, v] of Object.entries(formData)) {
            game.settings.set('narrator-tools', k, v);
        }
        setTimeout(() => {
            NarratorTools._updateContentStyle();
            game.socket.emit('module.narrator-tools', { command: 'style' });
        }, 200);
    }
}
/* -------------------------------------------- */
/**
 * Primary object used by the Narrator Tools module
 */
const NarratorTools = {
    _element: $('<div id="narrator" class="narrator"><div class="narrator-bg"></div><div class="narrator-frame"><div class="narrator-frameBG"></div><div class="narrator-box"><div class="narrator-content"></div></div><div class="narrator-buttons" style="opacity:0;"><button class="NT-btn-pause"></button><button class="NT-btn-close"></button></div></div><div class="narrator-sidebarBG"></div>'),
    /**
     * Here is where a custom speaker is stored, if you change the value to anything other than '' the next messages will speak as such
     */
    character: '',
    /**
     * Hooked function wich identifies if a message is a Narrator Tools command
     * @param message
     * @param content   Message to be identified
     * @param chatData
     */
    _chatMessage(message, content, chatData) {
        let commands = {};
        content = content.replace(/\n/g, '<br>');
        if (game.user.role >= game.settings.get('narrator-tools', 'PERMAs')) {
            commands.as = new RegExp('^(?:\\/as$|\\/as ([^]*))', 'i');
        }
        if (game.user.role >= game.settings.get('narrator-tools', 'PERMDescribe')) {
            commands.description = new RegExp('^\\/desc(?:ribe|ription|) ([^]*)', 'i');
            commands.notification = new RegExp('^\\/not(?:e|ify|ication) ([^]*)', 'i');
        }
        if (game.user.role >= game.settings.get('narrator-tools', 'PERMNarrate')) {
            commands.narration = new RegExp('^\\/narrat(?:e|ion) ([^]*)', 'i');
        }
        // Iterate over patterns, finding the first match
        let c, rgx, match;
        for ([c, rgx] of Object.entries(commands)) {
            match = content.match(rgx);
            if (match) {
                if (c == 'as') {
                    if (match[1]) {
                        this.character = match[1];
                        $('#chat-message')[0].placeholder = game.i18n.localize('NT.SpeakingAs') + ' ' + this.character;
                    }
                    else {
                        this.character = '';
                        $('#chat-message')[0].placeholder = '';
                    }
                }
                else {
                    if (c == 'narration' && !game.user.hasPermission('SETTINGS_MODIFY'))
                        ui.notifications.error(game.i18n.localize('NT.CantModifySettings'));
                    else
                        this.createChatMessage(c, match[1]);
                }
                return false;
            }
        }
    },
    /**
     * Control the module behavior in response to a change in the sharedState
     * @param state The new application state
     */
    _controller({ narration, scenery }) {
        /**First, we manage the scenery changes */
        this._updateScenery(scenery);
        if (game.user.role >= game.settings.get('narrator-tools', 'PERMScenery')) {
            const btn = $('.control-tool[data-tool=scenery]');
            if (btn) {
                if (scenery)
                    btn[0].classList.add('active');
                else
                    btn[0].classList.remove('active');
            }
        }
        /**Then, we control the narration's behavior */
        const is_narrator = game.user.hasPermission('SETTINGS_MODIFY') && game.user.role >= game.settings.get('narrator-tools', 'PERMNarrate');
        /**If a narration had ocurred and the display now is still on, turn it off */
        if (!narration.display && this.elements.content[0].style.opacity === '1') {
            this.elements.BG.height(0);
            if (is_narrator) {
                this.elements.buttons[0].style.opacity = '0';
                this.elements.buttons[0].style.visibility = 'hidden';
            }
        }
        /**If the message suddenly disappears, turn off the opacity */
        if (!narration.message) {
            this.elements.content[0].style.opacity = '0';
        }
        if (narration.display) {
            const scroll = () => {
                if (!this.sharedState.narration.paused) {
                    let scroll = (this.elements.content.height() ?? 0) - 290; // 310
                    let duration = this.messageDuration(this.sharedState.narration.message.length);
                    /**If the narration is open */
                    if (scroll > 20) {
                        const remaining = 1 - Number(this.elements.content[0].style.top.slice(0, -2)) / -scroll;
                        const duration_multiplier = game.settings.get('narrator-tools', 'DurationMultiplier');
                        const scroll_duration = (duration - 500 - 4500 * duration_multiplier) * remaining;
                        const fun_scroll = () => {
                            this.elements.content.animate({ top: -scroll }, scroll_duration, 'linear');
                            this._timeouts.narrationScrolls = 0;
                        };
                        if (this.elements.content[0].style.top == '0px') {
                            this._timeouts.narrationScrolls = setTimeout(fun_scroll, 3000 * duration_multiplier);
                        }
                        else {
                            fun_scroll();
                            duration = scroll_duration + 4500 * duration_multiplier;
                        }
                    }
                    if (is_narrator) {
                        if (this._timeouts.narrationCloses) {
                            clearTimeout(this._timeouts.narrationCloses);
                            this._timeouts.narrationCloses = 0;
                        }
                        this._timeouts.narrationCloses = setTimeout(NarratorTools._narrationClose, duration);
                    }
                }
            };
            /** If the display is on and the narration.id is a new one, it means a new narration is taking place */
            if (narration.id !== this._id) {
                this._id = narration.id;
                clearTimeout(this._timeouts.narrationOpens);
                this.elements.content[0].style.opacity = '0';
                this.elements.content.stop();
                this._timeouts.narrationOpens = setTimeout(() => {
                    this.elements.content.html(narration.message);
                    this.elements.content[0].style.opacity = '1';
                    this.elements.content[0].style.top = '0px';
                    const height = Math.min(this.elements.content.height() ?? 0, 310);
                    this.elements.BG.height(height * 3);
                    if (is_narrator) {
                        this.elements.buttons[0].style.opacity = '1';
                        this.elements.buttons[0].style.visibility = 'visible';
                        this.elements.buttons[0].style.top = `calc(50% + ${60 + height / 2}px)`;
                        this._updateStopButton(game.settings.get('narrator-tools', 'NarrationStartPaused'));
                    }
                    this._timeouts.narrationOpens = 0;
                    Hooks.call('narration', narration);
                }, 500);
                Hooks.once('narration', scroll);
            }
            else {
                /** If narration is paused, stop animation and clear timeouts */
                if (narration.paused) {
                    if (this._timeouts.narrationScrolls) {
                        clearTimeout(this._timeouts.narrationScrolls);
                        this._timeouts.narrationScrolls = 0;
                    }
                    this.elements.content.stop();
                    if (this._timeouts.narrationCloses) {
                        clearTimeout(this._timeouts.narrationCloses);
                        this._timeouts.narrationCloses = 0;
                    }
                }
                else {
                    scroll();
                }
            }
        }
    },
    /**Hook function wich creates the scenery button */
    _createSceneryButton(control, html, data) {
        const hasPerm = game.user.role >= game.settings.get('narrator-tools', 'PERMScenery');
        if (hasPerm) {
            const name = 'scenery';
            const title = game.i18n.localize('NT.ButtonTitle');
            const icon = 'fas fa-theater-masks';
            const active = this.sharedState.scenery;
            const btn = $(`<li class="control-tool toggle ${active ? 'active' : ''}" title="${title}" data-tool="${name}"><i class="${icon}"></i></li>`);
            btn.on('click', () => this.scenery());
            html.append(btn);
        }
    },
    /**Gets whats selected on screen */
    _getSelectionText() {
        let html = '';
        const selection = window.getSelection();
        if (selection?.rangeCount && !selection.isCollapsed) {
            const fragments = selection.getRangeAt(0).cloneContents();
            const size = fragments.childNodes.length;
            for (let i = 0; i < size; i++) {
                if (fragments.childNodes[i].nodeType == fragments.TEXT_NODE)
                    html += fragments.childNodes[i].wholeText;
                else
                    html += fragments.childNodes[i].outerHTML;
            }
        }
        return html;
    },
    /**The id of the last narration update */
    _id: 0,
    /**
     * Loads a custom font for the narration style
     * @param font Font to load
     */
    _loadFont(font) {
        $('#narratorWebFont').remove();
        if (font == '')
            return;
        let style = document.createElement('style');
        style.id = 'narratorWebFont';
        style.appendChild(document.createTextNode(`@font-face {font-family: NTCustomFont; src: url('${font}');}`));
        document.head.appendChild(style);
    },
    _menu: undefined,
    /**
     * Behavior for when a narration is closed
     */
    _narrationClose() {
        let state = NarratorTools.sharedState.narration;
        Hooks.call('narration_closes', { id: state.id, message: state.message });
        if (NarratorTools._timeouts.narrationCloses) {
            clearTimeout(NarratorTools._timeouts.narrationCloses);
            NarratorTools._timeouts.narrationCloses = 0;
        }
        setTimeout(() => {
            if (state.id == NarratorTools.sharedState.narration.id) {
                state.display = false;
                state.message = '';
                NarratorTools.sharedState.narration = state;
            }
        }, 250);
    },
    _pause() {
        const canScenery = game.user.role >= game.settings.get('narrator-tools', 'PERMScenery');
        if (canScenery && game.settings.get('narrator-tools', 'Pause')) {
            NarratorTools.scenery(game.paused);
        }
    },
    /**
     * Creates an alias and change message type if this.character option is true
     * @param chatMessage The chat message object
     * @param options
     * @param user
     */
    _preCreateChatMessage(chatMessage, options, user) {
        if (game.user.role >= game.settings.get('narrator-tools', 'PERMAs') && this.character) {
            let chatData = {};
            chatData.type = game.settings.get('narrator-tools', 'MessageType');
            chatData.speaker = { alias: this.character };
            chatMessage.data.update(chatData);
        }
    },
    /**Initialization routine for 'ready' hook */
    _ready() {
        this.elements = {
            /**Main Element */
            narrator: this._element,
            frame: this._element.find('.narrator-frame'),
            frameBG: this._element.find('.narrator-frameBG'),
            sidebarBG: this._element.find('.narrator-sidebarBG'),
            BG: this._element.find('.narrator-bg'),
            box: this._element.find('.narrator-box'),
            content: this._element.find('.narrator-content'),
            buttons: this._element.find('.narrator-buttons'),
            buttonPause: this._element.find('.NT-btn-pause'),
            buttonClose: this._element.find('.NT-btn-close'),
        };
        this._updateBGColor();
        this._updateBGImage();
        this._fitSidebar();
        $('body').append(this._element);
        // @ts-ignore
        this._menu = new ContextMenuNT({
            theme: 'default',
            items: [
                {
                    icon: 'comment',
                    name: 'Describe',
                    action: () => {
                        const selection = NarratorTools._getSelectionText();
                        if (selection)
                            NarratorTools.chatMessage.describe(selection);
                    },
                },
                {
                    icon: 'comment-dots',
                    name: 'Narrate',
                    action: () => {
                        const selection = NarratorTools._getSelectionText();
                        if (selection)
                            NarratorTools.chatMessage.narrate(selection);
                    },
                },
            ],
        });
        $(document.getElementById('chat-log')).on('click', '.message.narrator-chat', NarratorTools._onClickMessage.bind(NarratorTools));
        NarratorTools.elements.buttonPause.on('click', () => {
            const pause = !NarratorTools.sharedState.narration.paused;
            NarratorTools.sharedState.narration = {
                ...NarratorTools.sharedState.narration,
                paused: pause,
            };
            NarratorTools._updateStopButton(pause);
        });
        NarratorTools.elements.buttonClose.html(`<i class="fas fa-times-circle"></i> ${game.i18n.localize('Close')}`);
        NarratorTools.elements.buttonClose.on('click', NarratorTools._narrationClose);
        NarratorTools._loadFont(game.settings.get('narrator-tools', 'WebFont'));
        NarratorTools._updateContentStyle();
        this._controller(game.settings.get('narrator-tools', 'sharedState'));
        NarratorTools._pause();
        document.addEventListener('contextmenu', (ev) => {
            if (ev.target.classList.contains('editor-content') || $(ev.target).parents('div.editor-content').length) {
                const time = this._menu.isOpen() ? 100 : 0;
                this._menu.hide();
                setTimeout(() => {
                    this._menu.show(ev.pageX, ev.pageY);
                }, time);
            }
        });
        document.addEventListener('click', () => NarratorTools._menu.hide());
    },
    /**Initialization routine for 'setup' hook */
    _setup() {
        // Game Settings
        // The shared state of the Narrator Tools application, emitted by the DM across all players
        // Q:   Why use a setting instead of sockets?
        // A:   So there is memory. The screen will only update with the DM present and remain in that state.
        //      For instance, the DM might leave the game with a message on screen.
        //      There should be no concurrency between sockets and this config,
        //      so we eliminated sockets altogether.
        game.settings.register('narrator-tools', 'sharedState', {
            name: 'Shared State',
            scope: 'world',
            config: false,
            default: {
                /**Displays information about whats happening on screen */
                narration: {
                    id: 0,
                    display: false,
                    new: false,
                    message: '',
                    paused: false,
                },
                /**If the background scenery is on or off */
                scenery: false,
            },
            onChange: (newState) => this._controller(newState),
        });
        // Register the application menu
        game.settings.registerMenu('narrator-tools', 'settingsMenu', {
            name: game.i18n.localize('SETTINGS.Configure'),
            label: game.i18n.localize('SCENES.Configure'),
            icon: 'fas fa-adjust',
            type: NarratorMenu,
            restricted: true,
        });
        // Menu options
        game.settings.register('narrator-tools', 'FontSize', {
            name: 'Font Size',
            scope: 'world',
            config: false,
            default: '',
            type: String,
        });
        game.settings.register('narrator-tools', 'WebFont', {
            name: 'Web Font',
            scope: 'world',
            config: false,
            default: '',
            type: String,
            onChange: (value) => NarratorTools._loadFont(value),
        });
        game.settings.register('narrator-tools', 'TextColor', {
            name: 'Text Color',
            scope: 'world',
            config: false,
            default: '',
            type: String,
        });
        game.settings.register('narrator-tools', 'TextShadow', {
            name: 'Text Shadow',
            scope: 'world',
            config: false,
            default: '',
            type: String,
        });
        game.settings.register('narrator-tools', 'TextCSS', {
            name: 'TextCSS',
            scope: 'world',
            config: false,
            default: '',
            type: String,
        });
        game.settings.register('narrator-tools', 'Pause', {
            name: 'Pause',
            scope: 'world',
            config: false,
            default: false,
            type: Boolean,
        });
        game.settings.register('narrator-tools', 'DurationMultiplier', {
            name: 'Duration Multiplier',
            scope: 'world',
            config: false,
            default: 1,
            type: Number,
        });
        game.settings.register('narrator-tools', 'BGColor', {
            name: 'Background Color',
            scope: 'world',
            config: false,
            default: '',
            type: String,
            onChange: (color) => NarratorTools._updateBGColor(color),
        });
        game.settings.register('narrator-tools', 'BGImage', {
            name: 'Background Color',
            scope: 'world',
            config: false,
            default: '',
            type: String,
            onChange: (filePath) => NarratorTools._updateBGImage(filePath),
        });
        game.settings.register('narrator-tools', 'NarrationStartPaused', {
            name: 'Start the Narration Paused',
            scope: 'world',
            config: false,
            default: false,
            type: Boolean,
        });
        game.settings.register('narrator-tools', 'MessageType', {
            name: 'Narration Message Type',
            scope: 'world',
            config: false,
            default: CONST.CHAT_MESSAGE_TYPES.OTHER,
            type: Number,
        });
        game.settings.register('narrator-tools', 'PERMScenery', {
            name: 'Permission Required to set the Scenery',
            scope: 'world',
            config: false,
            default: CONST.USER_ROLES.GAMEMASTER,
            type: Number,
        });
        game.settings.register('narrator-tools', 'PERMDescribe', {
            name: 'Permission Required to /describe and /note',
            scope: 'world',
            config: false,
            default: CONST.USER_ROLES.GAMEMASTER,
            type: Number,
        });
        game.settings.register('narrator-tools', 'PERMNarrate', {
            name: 'Permission Required to /narrate',
            scope: 'world',
            config: false,
            default: CONST.USER_ROLES.GAMEMASTER,
            type: Number,
        });
        game.settings.register('narrator-tools', 'PERMAs', {
            name: 'Permission Required to /as',
            scope: 'world',
            config: false,
            default: CONST.USER_ROLES.GAMEMASTER,
            type: Number,
        });
    },
    /**
     * Behavior when a chat message is clicked
     * @param event The event wich triggered the handler
     */
    _onClickMessage(event) {
        if (event && event.target.classList.contains('narrator-chat')) {
            //@ts-ignore
            const roll = $(event.currentTarget);
            const tip = roll.find('.message-metadata');
            if (!tip.is(':visible'))
                tip.slideDown(200);
            else
                tip.slideUp(200);
        }
    },
    /**
     * Process any received messages from the socket
     * @param data Command and value to be addressed by the corresponding function
     */
    _onMessage(data) {
        const commands = {
            style: function () {
                NarratorTools._updateContentStyle();
            },
        };
        commands[data.command]();
    },
    /**
     * Renders the chat message and sets out the message behavior
     * @param message Message object to be rendered
     * @param html HTML element of the message
     * @param data
     */
    _renderChatMessage(message, html, data) {
        const type = message.getFlag('narrator-tools', 'type');
        if (type) {
            html.find('.message-sender').text('');
            html.find('.message-metadata')[0].style.display = 'none';
            html[0].classList.add('narrator-chat');
            if (type == 'narration') {
                html[0].classList.add('narrator-narrative');
            }
            else if (type == 'description') {
                html[0].classList.add('narrator-description');
            }
            else if (type == 'notification') {
                html[0].classList.add('narrator-notification');
            }
        }
    },
    /**Resize the sidebarBG and frame elements to match the sidebars size */
    _fitSidebar() {
        const sidebarWidth = $('body').find('.app.collapsed').length ? 0 : 305;
        this.elements.sidebarBG.width(sidebarWidth);
        this.elements.frame.width(`calc(100% - ${sidebarWidth}px)`);
    },
    /**Object containing all the timeouts called by their numbers */
    _timeouts: {
        narrationOpens: 0,
        narrationCloses: 0,
        narrationScrolls: 0,
    },
    _updateStopButton(pause) {
        if (pause) {
            NarratorTools.elements.buttonPause.html(`<i class='fas fa-play-circle'></i> ${game.i18n.localize('NT.PlayButton')}`);
        }
        else {
            NarratorTools.elements.buttonPause.html(`<i class='fas fa-pause-circle'></i> ${game.i18n.localize('NT.PauseButton')}`);
        }
    },
    _updateBGColor(color) {
        if (!color)
            color = game.settings.get('narrator-tools', 'BGColor');
        if (!color)
            color = '#000000';
        this.elements.frameBG[0].style.boxShadow = `inset 0 0 2000px 100px ${color}`;
        this.elements.BG[0].style.background = `linear-gradient(transparent 0%, ${color}a8 40%, ${color}a8 60%, transparent 100%)`;
    },
    _updateBGImage(filePath) {
        if (!filePath)
            filePath = game.settings.get('narrator-tools', 'BGImage');
        if (!filePath)
            this.elements.frameBG[0].style.background = '';
        else {
            this.elements.frameBG[0].style.background = `url(${filePath})`;
            this.elements.frameBG[0].style.backgroundSize = '100% 100%';
        }
    },
    /**Update the content element style to match the settings */
    _updateContentStyle() {
        const style = game.settings.get('narrator-tools', 'TextCSS');
        if (style) {
            const opacity = this.elements.content[0].style.opacity;
            //@ts-ignore
            this.elements.content[0].style = style;
            this.elements.content[0].style.opacity = opacity;
            return;
        }
        this.elements.content[0].style.fontFamily = `${game.settings.get('narrator-tools', 'WebFont')}` ? 'NTCustomFont' : '';
        this.elements.content[0].style.fontSize = `${game.settings.get('narrator-tools', 'FontSize')}`;
        this.elements.content[0].style.color = `${game.settings.get('narrator-tools', 'TextColor')}`;
        this.elements.content[0].style.textShadow = `${game.settings.get('narrator-tools', 'TextShadow')}`;
    },
    /**Updates the background opacity to match the scenery */
    _updateScenery(scenery) {
        if (!scenery)
            scenery = this.sharedState.scenery;
        const new_state = scenery ? '1' : '0';
        if (this.elements.frameBG[0].style.opacity === new_state)
            return;
        this.elements.frameBG[0].style.opacity = new_state;
        this.elements.sidebarBG[0].style.opacity = new_state;
    },
    /**Shortcut object for creating chat messages */
    chatMessage: {
        /**
         * Creates a 'description' chat message
         * @param message
         * @param options - Change the chat message configuration
         */
        describe(message, options = {}) {
            return NarratorTools.createChatMessage('description', message, options);
        },
        /**
         * Creates a 'narration' chat message
         * @param message - single message or an array of messages to be consecutively displayed
         * @param options - Change the chat message configuration
         */
        narrate(message, options = {}) {
            if (typeof message == 'string') {
                message = [message];
            }
            let res = NarratorTools.createChatMessage('narration', message[0], options);
            if (res) {
                for (let i = 1; i < message.length; i++) {
                    res = res.then(() => NarratorTools.createChatMessage('narration', message[i], options));
                }
            }
            return res;
        },
        /**
         * Creates a 'notification' chat message
         * @param message
         * @param options - Change the chat message configuration
         */
        notify(message, options = {}) {
            return NarratorTools.createChatMessage('notification', message, options);
        },
    },
    /**
     * Creates a chat message of the specified type
     * @param type     'narrate' for narrations or anything else for descriptions
     * @param message
     * @param options - Change the chat message configuration
     */
    createChatMessage(type, message, options = {}) {
        if (type == 'narration' && !game.user.role >= game.settings.get('narrator-tools', 'PERMNarrate'))
            return;
        else if (!game.user.role >= game.settings.get('narrator-tools', 'PERMDescribe'))
            return;
        message = message.replace(/\\n/g, '<br>');
        let chatData = {
            content: message,
            flags: {
                'narrator-tools': {
                    type: type,
                },
            },
            type: game.settings.get('narrator-tools', 'MessageType'),
            speaker: {
                alias: game.i18n.localize('NT.Narrator'),
                scene: game.user.viewedScene,
            },
            whisper: type == 'notification' ? game.users.entities.filter((u) => u.isGM) : [],
            ...options,
        };
        /**If the message is a narration, start the protocol */
        if (type == 'narration') {
            const narration = new Promise((resolve) => {
                Hooks.once('narration_closes', (narration) => {
                    resolve(narration.message == message);
                });
            });
            if (this._timeouts.narrationOpens) {
                clearTimeout(this._timeouts.narrationOpens);
                this._timeouts.narrationOpens = 0;
            }
            if (this._timeouts.narrationCloses) {
                clearTimeout(this._timeouts.narrationCloses);
                this._timeouts.narrationCloses = 0;
            }
            let state = {
                id: this.sharedState.narration.id + 1,
                display: true,
                message: message,
                paused: game.settings.get('narrator-tools', 'NarrationStartPaused'),
            };
            this.sharedState.narration = state;
            ChatMessage.create(chatData, {});
            return narration;
        }
        ChatMessage.create(chatData, {});
    },
    /**Shortcuts for easy access of the elements of the module */
    elements: {},
    /**
     * Returns the calculated duration a string of length size would have
     * @param length    The lenght of the string
     */
    messageDuration(length) {
        //@ts-ignore
        return (Math.clamped(2000, length * 80, 20000) + 3000) * game.settings.get('narrator-tools', 'DurationMultiplier') + 500;
    },
    /**
     * Set the background scenery and calls all clients
     * @param state True to turn on the scenery, false to turn it off
     */
    scenery(state) {
        if (game.user.role >= game.settings.get('narrator-tools', 'PERMScenery')) {
            if (!game.user.hasPermission('SETTINGS_MODIFY'))
                ui.notifications.error(game.i18n.localize('NT.CantModifySettings'));
            else
                this.sharedState.scenery = state ?? !this.sharedState.scenery;
        }
    },
    /**The shared state of the Narrator Tools application, emitted by the DM across all players */
    sharedState: {
        get narration() {
            return game.settings.get('narrator-tools', 'sharedState').narration;
        },
        set narration(state) {
            const sharedState = { ...game.settings.get('narrator-tools', 'sharedState'), narration: state };
            game.settings.set('narrator-tools', 'sharedState', sharedState);
        },
        get scenery() {
            return game.settings.get('narrator-tools', 'sharedState').scenery;
        },
        set scenery(state) {
            const sharedState = { ...game.settings.get('narrator-tools', 'sharedState'), scenery: state };
            game.settings.set('narrator-tools', 'sharedState', sharedState);
        },
    },
};
/* -------------------------------------------- */
Hooks.on('setup', () => NarratorTools._setup());
Hooks.on('ready', () => NarratorTools._ready());
Hooks.on('chatMessage', NarratorTools._chatMessage.bind(NarratorTools)); // This hook spans the chatmsg
Hooks.on('preCreateChatMessage', NarratorTools._preCreateChatMessage.bind(NarratorTools));
Hooks.on('renderChatMessage', NarratorTools._renderChatMessage.bind(NarratorTools)); // This hook changes the chat message in case its a narration + triggers
Hooks.on('renderSceneControls', NarratorTools._createSceneryButton.bind(NarratorTools));
Hooks.on('collapseSidebar', NarratorTools._fitSidebar.bind(NarratorTools));
Hooks.on('pauseGame', (_pause) => NarratorTools._pause());
