import { MODULE_ID } from '../constants.js';
import { localize } from '../utils.js';
import { PresetBrowser } from './browser/browserApp.js';
import { PresetStorage } from './collection.js';
import { PresetContainerV2 } from './containerAppV2.js';
import { callAsyncHook } from './utils.js';

/**
 * Constructs and opens a menu for browsing through Mass Edit presets
 * @param {object} menu    Application's menu structure
 * @param {object} options
 * @returns
 */
export async function openCategoryBrowser(
    menu,
    {
        retainState = false,
        name = 'Category Browser',
        alignment = 'left',
        searchBar = false,
        globalSearch = false,
        globalQuery = '',
        editEnabled = false,
        disableDelete = true,
        categoryDownload = false,
        defaultActiveCategory = '',
        width,
        height,
    } = {},
) {
    // // If category browser is already open close it
    const id = 'mass-edit-category-browser-' + (name?.slugify() ?? foundry.utils.randomID());
    const app = foundry.applications.instances.get(id);
    if (app) {
        app.close(true);
        return;
    }

    new CategoryBrowserApplication(menu, {
        name,
        retainState,
        alignment,
        searchBar,
        globalSearch,
        globalQuery,
        editEnabled,
        disableDelete,
        categoryDownload,
        defaultActiveCategory,
        width,
        height,
        id,
    }).render(true);
}

/**
 * Representation of a menu button
 */
class Category {
    menu = null; // CategoryList this category is part of
    submenu = null; // CategoryList to be displayed when this category is active

    constructor({ title, fa, img, query, disableQuery, disableDownload, tooltip, menu }) {
        this.title = title; // Hover text
        this.fa = fa; // Font Awesome icon
        this.img = img; // Image icon
        this.query = query; // Search query to be ran when active
        this.disableQuery = disableQuery; // Prevent query from being run when category is click (children will still inherit this query)
        this.disableDownload = disableDownload; // Enable right-click download of presets
        this.tooltip = tooltip; // Tooltip text
        this.menu = menu; // CategoryList this category is part of
        this.id = foundry.utils.randomID(); // Unique identifier
    }
}

/**
 * Representation of a single menu (column) within the app
 */
class CategoryList {
    parentCategory = null; // Category
    categories = [];

    constructor(parentCategory) {
        this.parentCategory = parentCategory;
        this.id = foundry.utils.randomID();
        this.active = Boolean(parentCategory?.active);
    }

    set active(val) {
        this._active = val;
    }

    get active() {
        return this._active || this._topMenu;
    }
}

class CategoryBrowserApplication extends PresetContainerV2 {
    static DEFAULT_OPTIONS = {
        tag: 'form',
        classes: ['mass-edit-window-fill'],
        form: {
            handler: undefined,
            submitOnChange: true,
            closeOnSubmit: false,
        },
        window: {
            contentClasses: ['standard-form', 'mass-edit-category-browser'],
            resizable: true,
            minimizable: true,
        },
        position: {
            width: 450,
            height: 450,
        },
        actions: {
            categorySelect: CategoryBrowserApplication._onCategorySelect,
            generateMacro: CategoryBrowserApplication._onGenerateMacro,
            toggleSetting: CategoryBrowserApplication._onToggleSetting,
            globalSearchToggle: CategoryBrowserApplication._onGlobalSearchToggle,
        },
    };

    /** @override */
    static PARTS = {
        main: { template: `modules/${MODULE_ID}/templates/preset/categoryBrowser.hbs` },
    };

    static oldMenuStates = {};

    _menus = [];
    _categories = new Map();

    constructor(menu, options = {}) {
        if (options.retainState && CategoryBrowserApplication.oldMenuStates[options.id]) {
            const { globalSearch } = CategoryBrowserApplication.oldMenuStates[options.id];
            options.globalSearch = globalSearch;
        }

        super({}, options);

        // If the state of the window was set to be retained we retrieve it now
        // and run the necessary queries to get the results
        if (options.retainState && CategoryBrowserApplication.oldMenuStates[options.id]) {
            const { menus, categories, lastSearch } = CategoryBrowserApplication.oldMenuStates[options.id];
            this._menus = menus;
            this._categories = categories;
            this._lastSearch = lastSearch;
        } else {
            // Otherwise we process the fed in JSON menu structure
            this._processMenu(menu)._topMenu = true;
            this._setDefaultActiveCategory();
        }

        this._globalSearch = this.options.globalSearch;
    }

    /** @override */
    _initializeApplicationOptions(options) {
        options = super._initializeApplicationOptions(options);
        options.uniqueId = 'mass-edit-category-browser:' + options.name;
        return options;
    }

    get title() {
        return this.options.name ?? 'Category Browser';
    }

    /** @override */
    async _prepareContext(options) {
        const context = await super._prepareContext(options);

        return Object.assign(context, {
            menus: this._menus.filter((menu) => menu.active),
            presets: this._presetResults,
            alignment: this.options.alignment,
            searchBar: this.options.searchBar,
            globalSearch: this._globalSearch,
            lastSearch: this._lastSearch,
            editEnabled: this.options.editEnabled,
        });
    }

    /** @override */
    _attachPartListeners(partId, element, options) {
        super._attachPartListeners(partId, element, options);
        switch (partId) {
            case 'main':
                element.querySelector('.header-search input').addEventListener('input', this._onSearchInput.bind(this));
                element.querySelectorAll('.category').forEach((element) => {
                    element.addEventListener('contextmenu', this._onRightClickCategory.bind(this));
                });
                break;
        }
    }

    async _onSearchInput(event) {
        clearTimeout(this._searchTimer);
        this._searchTimer = setTimeout(() => this._onSearch(event.target.value), 250);
    }

    async _onSearch(search) {
        this._lastSearch = search.length >= 3 ? search : null;
        if (this._lastSearch || !search) this._runQueryTree();
    }

    static _onGlobalSearchToggle(event) {
        this._globalSearch = !this._globalSearch;

        const element = event.target.closest('.globalSearchToggle');

        if (!this._globalSearch) element.classList.remove('active');
        else element.classList.add('active');

        this._runQueryTree();
    }

    /**
     * Process JSON menu structure into Category and CategoryList instances usable by the application
     * @param {object} submenu                An array of JSON objects representing a `Category`
     * @param {Category|null} parentCategory  A `Category` instance that is the parent of the provided submenu
     * @returns
     */
    _processMenu(submenu, parentCategory = null) {
        const categoryList = new CategoryList(parentCategory);
        this._menus.push(categoryList);

        submenu.forEach((category) => {
            const { title, fa, img, submenu, query, disableQuery, disableDownload, tooltip } = category;
            const cat = new Category({
                title,
                fa,
                img,
                menu: categoryList,
                query,
                disableQuery,
                disableDownload,
                tooltip,
            });
            if (submenu?.length) cat.submenu = this._processMenu(submenu, cat);
            categoryList.categories.push(cat);
            this._categories.set(cat.id, cat);
        });

        return categoryList;
    }

    /**
     * Sets the default active category using the 'defaultActiveCategory' field if provided
     */
    _setDefaultActiveCategory() {
        if (!this.options.defaultActiveCategory?.trim()) return;

        const chain = this.options.defaultActiveCategory
            .split(',')
            .map((c) => c.trim().toLocaleLowerCase())
            .filter(Boolean);
        let currentMenu = this._menus[0];
        for (const title of chain) {
            const category = currentMenu.categories.find((c) => c.title.toLocaleLowerCase() === title);
            if (!category) return;
            category.active = true;
            currentMenu.active = true;
            currentMenu = category.submenu;
            if (!currentMenu) return;
            currentMenu.active = true;
        }
    }

    /**
     * Handle category click event
     * @param {*} event
     */
    static async _onCategorySelect(event, element) {
        const category = this._categories.get(element.dataset.id);

        this._menus.forEach((menu) => (menu.active = false));
        if (category.active) this._setCategoryInactive(category);
        else this._setCategoryActive(category);

        this._presetResults = null;
        this.render(true);
    }

    async _onRightClickCategory(event) {
        const category = this._categories.get(event.target.closest('.category').dataset.id);
        if (!category) return;
        if (this.options.editEnabled) {
            new EditCategory({ category, browser: this }).render(true);
        } else if (this.options.categoryDownload && !category.disableDownload) {
            const { query, title } = EditCategory.determineCompoundQuery(category, this);
            callAsyncHook('MassEdit.categoryDownload', query, title);
        }
    }

    /**
     * Set provided category as active, turning on/off relevant menus and categories
     * @param {Category} category
     */
    _setCategoryActive(category) {
        category.menu.active = true;
        category.menu.categories.forEach((cat) => (cat.active = false));
        category.active = true;

        let parentCategory = category.menu.parentCategory;
        while (parentCategory) {
            parentCategory.menu.active = true;
            parentCategory = parentCategory.menu.parentCategory;
        }

        let submenu = category.submenu;
        while (submenu) {
            submenu.active = true;
            submenu = submenu.categories.find((category) => category.active)?.submenu;
        }
    }

    /**
     * Set provided category as inactive, turning off relevant menus and categories
     * @param {Category} category
     */
    _setCategoryInactive(category) {
        category.active = false;
        category.menu.active = true;

        let parentCategory = category.menu.parentCategory;
        while (parentCategory) {
            parentCategory.menu.active = true;
            parentCategory = parentCategory.menu.parentCategory;
        }
    }

    /**
     * Run queries for active categories and renders the results
     * @param {String} search
     * @param {Boolean} global
     * @returns
     */
    async _runQueryTree() {
        const runTime = new Date().getTime();
        this._queryRunTime = runTime;
        this._presetResults = null;

        await this._renderContent(true);

        const queries = [];

        if (this._lastSearch && this._globalSearch) {
            queries.push(this._lastSearch);
        } else {
            let lastCategory;
            for (const menu of this._menus) {
                if (!menu.active) continue;
                const category = menu.categories.find((category) => category.active);
                if (category?.query?.trim()) {
                    queries.push(category.query);
                    lastCategory = category;
                }
            }
            if (lastCategory?.disableQuery) queries.pop();

            if (this._lastSearch && queries.length) queries.push(this._lastSearch);
        }

        let results;
        if (queries.length) {
            // Insert a global query that applies to all searches
            if (this.options.globalQuery) {
                queries.unshift(this.options.globalQuery);
            }

            const compoundQuery = queries.map((q) => `( ${q} )`).join(' AND ');
            results = await PresetStorage.retrieve({
                query: compoundQuery,
                virtualDirectory: PresetBrowser.CONFIG.virtualDirectory,
                externalCompendiums: PresetBrowser.CONFIG.externalCompendiums,
            });
        }

        if (this._queryRunTime !== runTime) return;
        this._presetResults = results;

        if (PresetBrowser.CONFIG.sortMode === 'alphabetical') {
            this._presetResults?.sort((p1, p2) => p1.name.localeCompare(p2.name));
        }

        return this._renderContent();
    }

    /**
     * Render query results
     * @param {Boolean} loading if true a rotating spinner will be rendered instead of query results
     * @returns
     */
    async _renderContent(loading = false) {
        if (loading) {
            $(this.form)
                .find('.item-list')
                .html(
                    `<div style="width: 100%; height: 100%; text-align: center; font-size: xxx-large;">
            <i class="fa-duotone fa-solid fa-spinner fa-spin-pulse" style="position: relative; top: 30%;"></i>
           </div>`,
                );
        } else {
            return super._renderContent({ presets: this._presetResults });
        }
    }

    /** @override */
    async _onRender(options = {}) {
        this._runQueryTree();
    }

    async close(options = {}) {
        if (this.options.retainState) {
            CategoryBrowserApplication.oldMenuStates[this.id] = {
                menus: this._menus,
                categories: this._categories,
                lastSearch: this._lastSearch,
                globalSearch: this._globalSearch,
            };
        }

        return super.close(options);
    }

    static async _onToggleSetting(event, target) {
        const setting = target.dataset.setting;
        if (!setting) return;

        let value;
        if (setting === 'sortMode')
            value = PresetBrowser.CONFIG[setting] === 'alphabetical' ? 'manual' : 'alphabetical';
        else value = !PresetBrowser.CONFIG[setting];

        await PresetBrowser.setSetting(setting, value);

        const active = setting === 'sortMode' ? value === 'alphabetical' : value;
        $(target).css('color', active ? 'darkorange' : '');
        if (['virtualDirectory', 'externalCompendiums', 'sortMode'].includes(setting)) return this._runQueryTree();
    }

    _getHeaderButtons() {
        const buttons = [];

        if (game.user.isGM) {
            buttons.unshift({
                icon: 'fas fa-archive',
                action: 'openIndexer',
                tooltip: 'Open Directory Indexer',
            });

            buttons.unshift({
                icon: 'fas fa-file-search',
                tooltip: localize('presets.controls.virtual-directory'),
                action: 'toggleSetting',
                setting: 'virtualDirectory',
                active: () => PresetBrowser.CONFIG.virtualDirectory,
            });

            buttons.unshift({
                icon: 'fa-solid fa-books',
                tooltip: localize('presets.controls.external-compendiums'),
                action: 'toggleSetting',
                setting: 'externalCompendiums',
                active: () => PresetBrowser.CONFIG.externalCompendiums,
            });

            buttons.unshift({
                icon: 'fa-solid fa-arrow-down-big-small',
                tooltip: localize('presets.controls.scale-to-grid'),
                action: 'toggleSetting',
                setting: 'autoScale',
                active: () => PresetBrowser.CONFIG.autoScale,
            });

            buttons.unshift({
                icon: 'fa-solid fa-arrows-cross',
                tooltip: localize('presets.controls.layer-switch'),
                action: 'toggleSetting',
                setting: 'switchLayer',
                active: () => PresetBrowser.CONFIG.switchLayer,
            });

            buttons.unshift({
                icon: 'fa-solid fa-arrow-down-a-z',
                tooltip: localize('SIDEBAR.SortModeAlpha', false),
                action: 'toggleSetting',
                setting: 'sortMode',
                active: () => PresetBrowser.CONFIG.sortMode === 'alphabetical',
            });

            if (this.options.editEnabled) {
                buttons.unshift({
                    icon: 'fa-solid fa-dice-d20',
                    action: 'generateMacro',
                });
            }
        }

        return buttons;
    }

    /** @override */
    async _renderFrame(options) {
        const frame = await super._renderFrame(options);
        if (!this.hasFrame) return frame;

        const headerButtons = this._getHeaderButtons();
        let html = '';
        headerButtons.forEach((button) => {
            html += `
        <button ${button.active?.() ? 'style="color: darkorange"' : ''} type="button"
        class="header-control icon ${button.icon}" data-action="${button.action}" data-setting="${button.setting}"
                data-tooltip="${button.tooltip}" aria-label="${button.tooltip}"></button>
      `;
        });
        this.window.close.insertAdjacentHTML('beforebegin', html);

        return frame;
    }

    static _onGenerateMacro() {
        const options = this.options;

        let macro = `
const options = {
  name: "${options.name}",
  alignment: "${options.alignment}",
  retainState: ${options.retainState},
  searchBar: ${options.searchBar},
  globalSearch: ${this._globalSearch},
  globalQuery: "${options.globalQuery}",
  categoryDownload: ${options.categoryDownload},
  defaultActiveCategory: "${options.defaultActiveCategory}",
  editEnabled: ${options.editEnabled},
  disableDelete: ${options.disableDelete}
};

const menu = ${JSON.stringify(this._menuToJson(this._menus[0]), null, 2)};

MassEdit.openCategoryBrowser(menu, options);`;

        new Dialog({
            title: `Open Category Browser Macro`,
            content: `<textarea style="width:100%; height: 300px;">${macro}</textarea>`,
            buttons: {
                close: {
                    label: 'Close',
                },
            },
        }).render(true);
    }

    _menuToJson(menu) {
        return menu.categories.map((c) => this._categoryToJson(c));
    }

    _categoryToJson(category) {
        const json = { title: category.title };
        if (category.fa) json.fa = category.fa;
        if (category.img) json.img = category.img;
        if (category.query) json.query = category.query;
        if (category.disableQuery) json.disableQuery = category.disableQuery;
        if (category.disableDownload) json.disableDownload = category.disableDownload;
        if (category.tooltip) json.tooltip = category.tooltip;
        if (category.submenu) json.submenu = this._menuToJson(category.submenu);
        return json;
    }
}

class EditCategory extends FormApplication {
    constructor({ category, browser } = {}) {
        super({}, {});
        this.category = category;
        this.browser = browser;
    }

    static get defaultOptions() {
        return foundry.utils.mergeObject(super.defaultOptions, {
            classes: ['sheet', 'mass-edit-dark-window'],
            template: `modules/${MODULE_ID}/templates/preset/categoryEdit.html`,
            width: 450,
            height: 'auto',
            resizable: false,
        });
    }

    get title() {
        return 'Edit Category';
    }

    async getData(options) {
        return { category: this.category, categoryDownload: this.browser.options.categoryDownload };
    }

    /**
     * @param {JQuery} html
     */
    activateListeners(html) {
        super.activateListeners(html);

        html.on('click', '.moveUp', () => {
            const menu = this.category.menu;
            const index = menu.categories.indexOf(this.category);
            if (index > 0) {
                menu.categories.splice(index, 1);
                menu.categories.splice(index - 1, 0, this.category);
                this.browser.render(true);
            }
        });
        html.on('click', '.moveDown', () => {
            const menu = this.category.menu;
            const index = menu.categories.indexOf(this.category);
            if (index < menu.categories.length - 1) {
                menu.categories.splice(index, 1);
                menu.categories.splice(index + 1, 0, this.category);
                this.browser.render(true);
            }
        });
        html.on('click', '.calculateAssetSize', async () => {
            const { query } = EditCategory.determineCompoundQuery(this.category, this.browser);
            const result = {};
            await callAsyncHook('MassEdit.calculateCategoryAssetSize', query, result);

            if (result.bytes) {
                const mb = (result.bytes / (1024 * 1024)).toFixed(2);
                html.find('input[name="tooltip"]').val(`${mb} MB`);
            }
        });
    }

    static determineCompoundQuery(category, browser) {
        let tC = category;
        const queries = [];
        const titles = [];
        while (tC) {
            if (tC.query) queries.unshift(tC.query);
            titles.unshift(tC.title);
            tC = tC.menu.parentCategory;
        }
        if (browser.options.globalQuery) queries.unshift(browser.options.globalQuery);
        return { query: queries.map((q) => `( ${q} )`).join(' AND '), title: titles.join(' > ') };
    }

    /**
     * @param {Event} event
     * @param {Object} formData
     */
    async _updateObject(event, formData) {
        const action = event.submitter.value;

        formData.fa = formData.fa.replace('<i class="', '').replace('"></i>', '');

        if (action === 'save') {
            for (const [key, value] of Object.entries(formData)) {
                this.category[key] = value;
            }
            this.browser.render(true);
        } else if (action === 'saveNew') {
            const category = new Category({ menu: this.category.menu });
            for (const [key, value] of Object.entries(formData)) {
                category[key] = value;
            }

            category.menu.categories.push(category);
            this.browser._categories.set(category.id, category);
            this.browser.render(true);
        } else if (action === 'delete') {
            this._deleteCategory(this.category, this.browser);
            if (this.browser._menus.length === 0) this.browser.close(true);
            else this.browser.render(true);
        } else if (action === 'addSumbenu') {
            const categoryList = new CategoryList(this.category);
            this.browser._menus.push(categoryList);

            const cat = new Category({
                title: 'New Category',
                fa: 'fa-solid fa-circle-question',
                img: '',
                menu: categoryList,
                query: '',
            });
            categoryList.categories.push(cat);
            this.browser._categories.set(cat.id, cat);

            this.category.submenu = categoryList;

            this.browser.render(true);
        } else if (action === 'deleteSubmenu') {
            this.category.submenu.categories.forEach((c) => {
                this._deleteCategory(c, this.browser);
            });
            this.browser.render(true);
        }
    }

    _deleteCategory(category, browser) {
        category.menu.categories = category.menu.categories.filter((c) => c.id !== category.id);
        browser._categories.delete(category.id);

        if (this.category.menu.categories.length === 0) {
            browser._menus = browser._menus.filter((m) => m.id !== category.menu.id);
            category.menu.parentCategory.menu = null;
        }

        category.submenu?.categories.forEach((c) => {
            this._deleteCategory(c, browser);
        });
    }
}
