import { TokenDataAdapter } from '../../data/adapters.js';
import { copyToClipboard } from '../../../applications/formUtils.js';
import { countFolderItems, trackProgress } from '../../../applications/progressDialog.js';
import { importPresetFromJSONDialog } from '../../dialogs.js';
import { SortingHelpersFixed } from '../../fixedSort.js';
import { DragHoverOverlay, localFormat, localize, spawnSceneAsPreset } from '../../utils.js';
import { META_INDEX_ID, PresetAPI, PresetPackFolder, PresetStorage } from '../collection.js';
import { LinkerAPI } from '../../linker/linker.js';
import { DOC_ICONS, Preset } from '../preset.js';
import { placeableToData } from '../utils.js';
import { MODULE_ID, SUPPORTED_PLACEABLES, UI_DOCS } from '../../constants.js';
import PresetBrowserSettings from './settingsApp.js';
import { PresetConfig } from '../editApp.js';
import { PresetContainerV2 } from '../containerAppV2.js';
import { uploadFiles } from '../../auxilaryFeatures/utils.js';
import { collapseFolders, getPresetPackTrees, searchNode } from './tree.js';
import { buildQueryMatcher } from '../search.js';

const SEARCH_MIN_CHAR = 2;

export function openPresetBrowser(documentName) {
    new PresetBrowser(null, null, documentName).render(true);
}

export class PresetBrowser extends PresetContainerV2 {
    static objectHover = false;
    static lastSearch;
    static CONFIG;
    static _matches = 0;

    static async setSetting(setting, value) {
        return await game.settings.set(MODULE_ID, 'presetBrowser', { ...PresetBrowser.CONFIG, [setting]: value });
    }

    static async setSettings() {
        return await game.settings.set(MODULE_ID, 'presetBrowser', { ...PresetBrowser.CONFIG });
    }

    /**
     * Re-render currently active PresetBrowser instance.
     * Called after mutations are performed to preset compendiums
     */
    static async renderActiveBrowser() {
        foundry.applications.instances.get(PresetBrowser.DEFAULT_OPTIONS.id)?.render(true);
    }

    get lastSearch() {
        return this._lastSearch;
    }

    set lastSearch(val) {
        this._lastSearch = val;
        PresetBrowser.lastSearch = val;
    }

    constructor(configApp, callback, documentName, options = {}) {
        super({}, { ...options, sortable: true, duplicatable: true });
        this.callback = callback;

        if (!configApp && UI_DOCS.includes(documentName)) {
            this.documentName = PresetBrowser.CONFIG.documentLock || documentName;
        } else {
            this.configApp = configApp;
            this.documentName = documentName || this.configApp.documentName;
        }

        this.lastSearch = PresetBrowser.CONFIG.persistentSearch ? PresetBrowser.lastSearch : '';
        PresetBrowser._type = this.documentName;
    }

    static DEFAULT_OPTIONS = {
        id: 'mass-edit-presets',
        tag: 'form',
        classes: ['mass-edit-window-fill'],
        form: {
            handler: undefined,
            submitOnChange: true,
            closeOnSubmit: false,
        },
        window: {
            contentClasses: ['standard-form'],
            resizable: true,
            minimizable: true,
            icon: 'fa-solid fa-books',
        },
        position: {
            width: 377,
            height: 900,
        },
        actions: {
            documentChange: PresetBrowser._onDocumentChange,
            toggleSetting: PresetBrowser._onToggleSetting,
            toggleLock: PresetBrowser._onToggleLock,
            toggleSavedSearches: PresetBrowser._onToggleSavedSearches,
            createFolder: PresetBrowser._onCreateFolder,
            createPreset: PresetBrowser._onCreatePreset,
            openSettingConfig: PresetBrowser._onOpenSettingConfig,
            workingPackChange: PresetBrowser._onWorkingPackChange,
            exportPresets: PresetBrowser._onExportPresets,
            importPresets: PresetBrowser._onImportPresets,
            toggleCompendiumLock: PresetBrowser._onToggleCompendiumLock,
            createBag: PresetBrowser._onCreateBag,
            presetCreate: PresetBrowser._onPresetCreate,
            presetUpdate: PresetBrowser._onPresetUpdate,
            applyPreset: PresetBrowser._onApplyPreset,
        },
    };

    /** @override */
    static PARTS = {
        overlay: { template: `modules/${MODULE_ID}/templates/drag-hover-overlay.hbs` },
        main: { template: `modules/${MODULE_ID}/templates/preset/browser.hbs`, scrollable: ['.item-list'] },
    };

    get title() {
        let title = localize('presets.preset-browser');
        if (!UI_DOCS.includes(this.documentName)) title += ` [${this.documentName}]`;
        return title;
    }

    static async buildTree(type, { externalCompendiums = true, virtualDirectory = true } = {}) {
        const { workingTree, externalTrees } = await getPresetPackTrees({
            type,
            virtualDirectory,
            externalCompendiums,
        });
        searchNode(workingTree, null, false, type, false);
        externalTrees.forEach((tree) => searchNode(tree, null, false, type, false));
        return { workingTree, externalTrees };
    }

    async _refreshTree() {
        this.tree = await PresetBrowser.buildTree(this.documentName, {
            externalCompendiums: PresetBrowser.CONFIG.externalCompendiums,
            virtualDirectory: PresetBrowser.CONFIG.virtualDirectory,
        });
    }

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

        await this._refreshTree();

        context.browser = true;
        context.workingTree = this.tree.workingTree;
        context.externalTrees = this.tree.externalTrees;
        context.sortable = true;

        if (PresetBrowser.CONFIG.persistentSearch && this.lastSearch) {
            this._onSearch(this.lastSearch, { render: false });
            context.lastSearch = this.lastSearch;
        } else context.lastSearch = '';

        context.createEnabled = Boolean(this.configApp);
        context.isPlaceable = SUPPORTED_PLACEABLES.includes(this.documentName) || this.documentName === 'ALL';
        context.allowDocumentSwap = UI_DOCS.includes(this.documentName) && !this.configApp;
        context.docLockActive = PresetBrowser.CONFIG.documentLock === this.documentName;
        context.layerSwitchActive = PresetBrowser.CONFIG.switchLayer;
        context.autoScale = PresetBrowser.CONFIG.autoScale;
        context.externalCompendiums = PresetBrowser.CONFIG.externalCompendiums;
        context.virtualDirectory = PresetBrowser.CONFIG.virtualDirectory;
        context.displayDragDropMessage =
            context.allowDocumentSwap &&
            !(
                context.workingTree.folder.presets.length ||
                context.workingTree.children.length ||
                context.externalTrees?.length
            );

        context.docs = [];
        context.docsDropdown = PresetBrowser.CONFIG.dropdownDocuments.length ? [] : null;
        UI_DOCS.forEach((name) => {
            const doc = { name, icon: DOC_ICONS[name], tooltip: name === 'ALL' ? 'Placeables' : name };
            if (PresetBrowser.CONFIG.dropdownDocuments.includes(name)) context.docsDropdown.push(doc);
            else context.docs.push(doc);
        });

        context.documents = UI_DOCS;
        context.currentDocument = this.documentName;

        context.callback = Boolean(this.callback);

        return context;
    }

    /** @override */
    async _preparePartContext(partId, context, options) {
        await super._preparePartContext(partId, context, options);

        switch (partId) {
            case 'overlay':
                context.dragHoverOverlay = localize('presets.drag-over-message');
                break;
        }

        return context;
    }

    /** @override */
    _attachPartListeners(partId, element, options) {
        super._attachPartListeners(partId, element, options);
        switch (partId) {
            case 'overlay':
                DragHoverOverlay.attachListeners(element, {
                    condition: () => {
                        PresetBrowser.objectHover = canvas.activeLayer?.preview?.children.some(
                            (c) => c._original?.mouseInteractionManager?.isDragging,
                        );
                        return PresetBrowser.objectHover;
                    },
                    hoverOutCallback: () => (PresetBrowser.objectHover = false),
                });
                break;
            case 'main':
                this._searchInput = $(element).find('.header-search input');
                this._searchInput.on('input', this._onSearchInput.bind(this));
                break;
        }
    }

    /**
     * Process drag and drop of an Actor or Folder of actors
     * @param {*} event
     * @returns
     */
    _foundryDrop(event) {
        const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event.originalEvent);
        if (!foundry.utils.isEmpty(data)) {
            if (data.type === 'Folder') {
                const folder = fromUuidSync(data.uuid);
                if (folder.type !== 'Actor') return false;
                if (this._importTracker?.active) return false;

                trackProgress({
                    title: 'Converting Actors',
                    total: countFolderItems(folder),
                }).then(async (tracker) => {
                    this._importTracker = tracker;
                    await this._importActorFolder(folder, null, { keepId: true });
                    this._importTracker?.stop();
                });
                return true;
            } else if (data.type === 'Actor') {
                PresetAPI.createPresetFromActorUuid(data.uuid, { keepId: true }).then((preset) => {
                    if (preset) PresetStorage.createDocuments(preset);
                });
                return true;
            }

            return false;
        }
        return false;
    }

    /**
     * Process files being dropped into the container.
     * @param {DropEvent} event
     */
    async _fileDrop(event) {
        const files = event.dataTransfer.files;
        if (!files?.length) return {};

        let multiPreset = true;
        if (files.length > 1) {
            multiPreset = await foundry.applications.api.DialogV2.confirm({
                window: { title: 'Should a preset be created for each file?' },
                content: '',
                yes: { label: 'Multiple Presets', default: false, icon: 'fa-solid fa-layer-group' },
                no: { label: 'Single Preset', default: true, icon: 'fa-solid fa-rectangle' },
            });
        }

        const presets = await uploadFiles(files, 'presets', !multiPreset, event.shiftKey);
        await PresetStorage.createDocuments(presets);

        return { type: 'preset', uuids: presets.map((p) => p.uuid), sortable: true };
    }

    async _importActorFolder(folder, parentFolder = null, options = {}) {
        let nFolder = new Folder.implementation(
            {
                _id: options.keepId ? folder.id : null,
                name: folder.name,
                type: 'JournalEntry',
                sorting: folder.sorting,
                folder: parentFolder,
                color: folder.color ?? '#000000',
                flags: { [MODULE_ID]: { types: ['ALL', 'Token'] } },
            },
            { pack: PresetStorage.workingPack },
        );

        nFolder = await Folder.create(nFolder, {
            pack: nFolder.pack,
            keepId: options.keepId,
        });

        for (const child of folder.children) {
            if (!this._importTracker?.active) break;
            await this._importActorFolder(child.folder, nFolder.id, options);
        }

        const presets = [];

        for (const actor of folder.contents) {
            if (!this._importTracker?.active) break;
            presets.push(
                await PresetAPI.createPresetFromActorUuid(actor.uuid, {
                    folder: nFolder.id,
                    keepId: options.keepId,
                }),
            );
            this._importTracker.incrementCount();
        }

        await PresetStorage.createDocuments(presets);
    }

    async _onExportFolder(uuid) {
        let { pack, keepId } = await getCompendiumDialog({ exportTo: true, keepIdSelect: true, presetCompOnly: true });

        if (pack && !this._importTracker?.active) {
            const folder = fromUuidSync(uuid);
            if (folder) {
                this._importTracker = await trackProgress({
                    title: 'Exporting Folder',
                    total: countFolderItems(fromUuidSync(uuid)),
                });
                await this._onCopyFolder(uuid, null, pack, true, keepId);
                this._importTracker?.stop();
            }
        }
    }

    async _onCopyFolder(uuid, parentId = null, pack, render = true, keepId = true) {
        if (!pack) pack = PresetStorage.workingPack;

        const folder = fromUuidSync(uuid);

        if (folder) {
            let types = folder.flags[MODULE_ID]?.types ?? ['ALL'];

            const data = {
                _id: keepId ? folder.id : null,
                name: folder.name,
                color: folder.color,
                sorting: folder.sorting,
                folder: parentId,
                flags: { [MODULE_ID]: { types } },
                type: 'JournalEntry',
            };
            const nFolder = await Folder.create(data, { pack, keepId });

            const presets = await PresetStorage.batchLoad(folder.presets);

            const toCreate = [];

            for (const preset of presets) {
                if (!this._importTracker?.active) break;
                const p = preset.clone();
                p.folder = nFolder.id;
                if (!keepId) p.id = foundry.utils.randomID();
                toCreate.push(p);
                this._importTracker?.incrementCount();
            }

            await PresetStorage.createDocuments(toCreate, pack);

            for (const child of folder.children) {
                if (!this._importTracker?.active) break;
                await this._onCopyFolder(child.folder.uuid, nFolder.id, pack, false, keepId);
            }
        }
    }

    async _onExportSelectedPresetsToComp() {
        let { pack, keepId } = await getCompendiumDialog({ exportTo: true, keepIdSelect: true, presetCompOnly: true });
        if (pack) this._onCopySelectedPresets({ pack, keepId });
    }

    async _onCopyPresetToClipboard() {
        const { selected } = await this._getSelectedPresets();
        if (selected.length) copyToClipboard(selected[0]);
    }

    static async _onCreateFolder() {
        const types = [];
        if (SUPPORTED_PLACEABLES.includes(this.documentName)) {
            types.push('ALL', this.documentName);
        } else {
            types.push(this.documentName);
        }

        const folder = new Folder.implementation(
            {
                name: Folder.defaultName(),
                type: 'JournalEntry',
                sorting: 'm',
                flags: { [MODULE_ID]: { types } },
            },
            { pack: PresetStorage.workingPack },
        );

        new PresetFolderConfig({ document: folder }).render(true);
    }

    async _onFolderEdit(header) {
        const uuid = header.closest('.folder').dataset.uuid;
        const pFolder = fromUuidSync(uuid);

        let folder;
        if (pFolder instanceof PresetPackFolder) {
            // This is a virtual pack folder
            folder = new Folder.implementation(
                {
                    _id: pFolder.id,
                    name: pFolder.name,
                    type: 'JournalEntry',
                    color: pFolder.color,
                    sorting: pFolder.sorting,
                },
                { pack: pFolder.pack },
            );
        } else {
            folder = fromUuidSync(uuid);
        }

        const options = { ...$(header).offset(), folder: pFolder, document: folder };
        options.top += $(header).height();
        new PresetFolderConfig(options).render(true);
    }

    async _onFolderDelete(uuid, { deleteAll = false } = {}) {
        const folder = fromUuidSync(uuid);
        if (folder) {
            let confirm;

            if (deleteAll) {
                // Construct warning count of what is about to be removed
                const count = { Folder: 0 };
                const traverseFolder = function (folder) {
                    count.Folder += 1;
                    folder.presets?.forEach((p) => {
                        count[p.documentName] = (count[p.documentName] ?? 0) + 1;
                    });
                    folder.children?.forEach((ch) => traverseFolder(ch.folder));
                };
                traverseFolder(folder);

                let countWarning = '';
                if (Object.keys(count).length) {
                    countWarning += '<table>';
                    Object.keys(count).forEach((name) => {
                        countWarning += `<tr><td>${name}s</td><td>${count[name]}</td></tr>`;
                    });
                    countWarning += '</table>';
                }
                // end of constructing warning

                confirm = await Dialog.confirm({
                    title: `${localize('FOLDER.Delete', false)}: ${folder.name}`,
                    content: `<div style="color:red;"><h4>${localize('AreYouSure', false)}</h4><p>${localize(
                        'FOLDER.DeleteWarning',
                        false,
                    )}</p>${countWarning}</div>`,
                });
            } else {
                confirm = await Dialog.confirm({
                    title: `${localize('FOLDER.Remove', false)}: ${folder.name}`,
                    content: `<h4>${localize('AreYouSure', false)}</h4><p>${localize('FOLDER.RemoveWarning', false)}</p>`,
                });
            }

            if (confirm) {
                await folder.delete({ deleteSubfolders: deleteAll, deleteContents: deleteAll });
                this.render(true);
            }
        }
    }

    // Throttle input and perform preset search
    _onSearchInput(event) {
        clearTimeout(this._searchTimer);
        this._searchTimer = setTimeout(() => this._onSearch(event.target.value, { event }), 250);
    }

    async _onSearch(query, { event, render = true } = {}) {
        let previousSearch = this.lastSearch || '';
        this.lastSearch = query;

        if (previousSearch.length >= SEARCH_MIN_CHAR && query.length < SEARCH_MIN_CHAR) {
            if (event) $(event.target).removeClass('active');
            collapseFolders(this.tree.workingTree);
            this.tree.externalTrees.forEach((tree) => collapseFolders(tree));
            searchNode(this.tree.workingTree, null, false, this.documentName, false);
            this.tree.externalTrees.forEach((tree) => searchNode(tree, null, false, this.documentName, false));

            this._savedSearches?.updateQuery(this.lastSearch);
            if (render) this._renderContent();
            return;
        }

        if (query.length < SEARCH_MIN_CHAR) return;

        const matcher = buildQueryMatcher(query);
        if (!matcher) return;

        if (event) $(event.target).addClass('active');

        PresetBrowser._matches = 0;
        searchNode(this.tree.workingTree, matcher, false, this.documentName, true);
        this.tree.externalTrees.forEach((f) => searchNode(f, matcher, false, this.documentName, true));

        this._savedSearches?.updateQuery(this.lastSearch);
        if (render) this._renderContent();
    }

    async _renderContent() {
        await super._renderContent({
            callback: Boolean(this.callback),
            presets: this.tree.workingTree.folder.presets,
            nodes: this.tree.workingTree.children,
            createEnabled: Boolean(this.configApp),
            externalTrees: this.tree.externalTrees.length ? this.tree.externalTrees : null,
            browser: true,
        });
    }

    async _onFolderSort(sourceUuid, targetUuid, { inside = true, folderUuid = null } = {}) {
        let source = fromUuidSync(sourceUuid);
        let target = fromUuidSync(targetUuid);

        let folders;
        if (folderUuid) folders = fromUuidSync(folderUuid).children.map((ch) => ch.folder);
        else folders = this.tree.workingTree.children.map((ch) => ch.folder);

        const siblings = [];
        for (const folder of folders) {
            if (folder.uuid !== sourceUuid) siblings.push(folder);
        }

        const result = SortingHelpersFixed.performIntegerSort(source, {
            target,
            siblings,
            sortBefore: true,
        });

        if (result.length) {
            const updates = [];
            result.forEach((ctrl) => {
                const update = ctrl.update;
                update._id = ctrl.target.id;
                update.folder = fromUuidSync(folderUuid)?.id ?? null;
                updates.push(update);

                ctrl.target.sort = update.sort;
            });
            await Folder.updateDocuments(updates, {
                pack: PresetStorage.workingPack,
            });
        }
    }

    async _onItemSort(sourceUuids, targetUuid, { before = true, folderUuid = null } = {}) {
        const sourceUuidsSet = new Set(sourceUuids);
        const sources = await this._retrievePresets(Array.from(sourceUuidsSet));

        let target = targetUuid ? await this._retrieveSinglePreset(targetUuid) : null;

        // Determine siblings based on folder
        let presets;
        if (folderUuid) presets = fromUuidSync(folderUuid).presets;
        else presets = this.tree.workingTree.folder.presets;

        const siblings = [];
        for (const preset of presets) {
            if (!sourceUuidsSet.has(preset.uuid)) siblings.push(preset);
        }

        const result = SortingHelpersFixed.performIntegerSortMulti(sources, {
            target,
            siblings,
            sortBefore: before,
        });

        if (result.length) {
            const updates = [];
            const folderId = fromUuidSync(folderUuid)?.id ?? null;
            result.forEach((ctrl) => {
                const update = ctrl.update;
                update._id = ctrl.target.id;
                update.folder = folderId;
                updates.push(update);
                ctrl.target.sort = update.sort;
            });

            await PresetStorage.updatePresets(updates);
        }
    }

    /**
     * Called by SavedSearches app to request the display of a saved search.
     * @param {object} search
     */
    async loadSavedSearch(search) {
        this.lastSearch = search.query;

        let saveSettings = false;
        ['switchLayer', 'autoScale', 'externalCompendiums', 'virtualDirectory'].forEach((setting) => {
            if (setting in search && PresetBrowser.CONFIG[setting] !== search[setting]) {
                PresetBrowser.CONFIG[setting] = setting;
                saveSettings = true;
            }
        });

        let fullRender = false;
        if ('documentName' in search) {
            if (this.documentName !== search.documentName) {
                this.documentName = search.documentName;
                PresetBrowser._type = this.documentName;
                if (PresetBrowser.CONFIG.switchLayer)
                    canvas
                        .getLayerByEmbeddedName(this.documentName === 'Actor' ? 'Token' : this.documentName)
                        ?.activate();
                fullRender = true;
            }
        }

        if (saveSettings) await PresetBrowser.setSettings();

        if (fullRender || saveSettings) {
            this.lastSearch = search.query;
            this.render(true);
        } else {
            this._searchInput.val(search.query).trigger('input');
        }
    }

    static _onToggleLock(event, target) {
        const lockControl = $(target);

        let newLock = this.documentName;
        if (newLock !== PresetBrowser.CONFIG.documentLock) lockControl.addClass('active');
        else {
            lockControl.removeClass('active');
            newLock = '';
        }

        PresetBrowser.setSetting('documentLock', newLock);
    }

    static async _onToggleSetting(event, element) {
        const setting = element.dataset.setting;
        await PresetBrowser.setSetting(setting, !PresetBrowser.CONFIG[setting]);
        this.render(true);
    }

    /**
     * Change currently selected document category
     * @param {PointerEvent} event
     * @param {HTMLElement} target
     */
    static _onDocumentChange(event, target) {
        if (target.dataset.name != this.documentName) {
            this.documentName = target.dataset.name;
            PresetBrowser._type = this.documentName;

            if (PresetBrowser.CONFIG.switchLayer)
                canvas.getLayerByEmbeddedName(this.documentName === 'Actor' ? 'Token' : this.documentName)?.activate();

            this.render(true);
        }
    }

    static async _onApplyPreset(event) {
        if (this.callback) {
            const uuid = $(event.target).closest('.item').data('uuid');
            this.callback(await this._retrieveSinglePreset(uuid, true));
        }
    }

    static async _onToggleSavedSearches(event) {
        if (this._savedSearches) {
            this._savedSearches.close(true);
            this._savedSearches = null;
        } else {
            import('./savedSearches.js').then((module) => {
                this._savedSearches = new module.default(this);
                this._savedSearches.render(true);
            });
        }
    }

    async close(options = {}) {
        PresetBrowser.objectHover = false;
        this._savedSearches?.close();

        return super.close(options);
    }

    static async _onPresetUpdate(event) {
        const preset = await this._retrieveSinglePreset(event.target.closest('.item').dataset.uuid, true);
        if (!preset) return;

        const selectedFields = this.configApp.getSelectedFields();
        if (!selectedFields || foundry.utils.isEmpty(selectedFields)) {
            ui.notifications.warn(localize('presets.warn-no-fields'));
            return;
        }

        const randomize = foundry.utils.deepClone(this.configApp.randomizeFields || {});
        const addSubtract = foundry.utils.deepClone(this.configApp.addSubtractFields || {});

        // Detection modes may have been selected out of order
        // Fix that here
        if (this.documentName === 'Token') {
            TokenDataAdapter.correctDetectionModeOrder(selectedFields, randomize);
        }

        preset.update({ data: selectedFields, randomize, addSubtract });

        ui.notifications.info(`Preset "${preset.name}" updated`);
    }

    static async _onPresetCreate(event) {
        const selectedFields = this.configApp.getSelectedFields();
        if (!selectedFields || foundry.utils.isEmpty(selectedFields)) {
            ui.notifications.warn(localize('presets.warn-no-fields'));
            return;
        }

        const preset = new Preset({
            name: localize('presets.default-name'),
            documentName: this.documentName,
            data: selectedFields,
            addSubtract: this.configApp.addSubtractFields,
            randomize: this.configApp.randomizeFields,
        });

        await PresetStorage.createDocuments(preset);

        this._editPresets([preset], { isCreate: true }, event);
    }

    static async _onCreateBag() {
        const presetBag = new Preset({
            name: 'New Bag',
            documentName: 'Bag',
            img: `icons/containers/bags/pack-engraved-leather-tan.webp`,
            data: [
                {
                    uuids: [],
                    searches: {
                        inclusive: [],
                        exclusive: [],
                    },
                    virtualDirectory: true,
                },
            ],
        });
        await PresetStorage.createDocuments(presetBag);
    }

    /**
     * Handle creation of a new preset from the selected placeables
     */
    static _onCreatePreset() {
        // Create Preset from Selected
        const controlled = canvas.activeLayer.controlled;
        if (controlled.length && SUPPORTED_PLACEABLES.includes(controlled[0].document.documentName)) {
            this.dropPlaceable(controlled);
        }
    }

    /**
     * Create a preset from placeables dragged and dropped on the form
     * @param {Array[Placeable]} placeables
     * @param {Event} event
     */
    async dropPlaceable(placeables, event) {
        const presets = await PresetAPI.createPreset(placeables);

        // Switch to just created preset's category before rendering if not set to 'ALL'
        const documentName = placeables[0].document.documentName;
        if (this.documentName !== 'ALL' && this.documentName !== documentName) this.documentName = documentName;

        const options = { isCreate: true };
        options.left = this.position.left + this.position.width + 20;
        options.top = this.position.top;

        const linked = LinkerAPI.getLinkedDocuments(placeables.map((p) => p.document));
        if (linked.size) {
            const response = await new Promise((resolve) => {
                Dialog.confirm({
                    title: 'Attach Linked',
                    content: `<p>Linked placeables have been detected [<b>${linked.size}</b>].</p><p>Should they be included as <b>Attached</b>?</p>`,
                    yes: () => resolve(true),
                    no: () => resolve(false),
                    defaultYes: false,
                });
            });
            if (response) {
                options.attached = Array.from(linked).map((l) => {
                    return { documentName: l.documentName, data: placeableToData(l) };
                });
            }
        }

        this._editPresets(presets, options, event);
    }

    async actorToPreset(actor) {
        const presets = await PresetAPI.createPreset(placeables);
    }

    _getActiveEffectFields() {
        return {
            changes: foundry.utils.deepClone(this.configApp.object.changes ?? []),
        };
    }

    _getHeaderControls() {
        const controls = super._getHeaderControls();

        if (game.packs.get(PresetStorage.workingPack)?.locked) {
            controls.push({
                label: 'Un-Lock Working Compendium',
                icon: 'fas fa-lock fa-fw',
                action: 'toggleCompendiumLock',
            });
        }

        controls.push({
            label: 'Compendium',
            icon: 'fas fa-atlas',
            action: 'workingPackChange',
        });

        controls.push({
            label: 'Directory Indexer',
            icon: 'fas fa-archive',
            action: 'openIndexer',
        });

        controls.push({
            label: 'Import Presets',
            icon: 'fas fa-file-import',
            action: 'importPresets',
        });

        controls.push({
            label: 'Export Presets',
            icon: 'fas fa-file-export',
            action: 'exportPresets',
        });

        controls.push({
            label: 'Browser Settings',
            icon: 'fas fa-gear',
            action: 'openSettingConfig',
        });

        return controls;
    }

    static async _onWorkingPackChange() {
        let { pack } = await getCompendiumDialog();
        if (pack && pack !== PresetStorage.workingPack) {
            const { metadataDocument } = await PresetStorage._initCompendium(pack);
            if (metadataDocument) {
                await game.settings.set(MODULE_ID, 'workingPack', pack);
                this.render(true);
            } else {
                ui.notifications.wan(`Unable to establish ${pack} as Mass Edit Preset compendium.`);
            }
        }
    }

    /**
     * Render PresetBrowser setting configuration form
     */
    static _onOpenSettingConfig() {
        new PresetBrowserSettings(this).render(true);
    }

    static async _onToggleCompendiumLock() {
        const pack = game.packs.get(PresetStorage.workingPack);
        if (pack) {
            await pack.configure({ locked: false });
            this.render(true);
        }
    }

    /**
     * Export all working pack presets as a JSON file
     */
    static _onExportPresets() {
        PresetStorage.exportPresets({ workingCompendium: true });
    }

    static async _onImportPresets() {
        const json = await importPresetFromJSONDialog();
        if (!json) return;

        let importCount = 0;

        if (foundry.utils.getType(json) === 'Array') {
            const presets = [];

            for (const p of json) {
                if (!('documentName' in p)) continue;
                if (!('data' in p) || foundry.utils.isEmpty(p.data)) continue;
                p.uuid = null;
                p.folder = null;

                const preset = new Preset(p);
                preset._pages = p.pages;

                presets.push(preset);

                importCount++;
            }

            await PresetStorage.createDocuments(presets);
        }

        ui.notifications.info(`Mass Edit: ${localFormat('presets.imported', { count: importCount })}`);
    }

    /**
     * @param {Event} event
     * @param {Object} formData
     */
    async _updateObject(event, formData) {
        if (this.callback) {
            this.callback(await this._retrieveSinglePreset(event.submitter.data.id));
        }
    }
}

class PresetFolderConfig extends foundry.applications.sheets.FolderConfig {
    constructor(options = {}) {
        options.classes = ['folder-edit'];
        super(options);
    }

    /** @override */
    static PARTS = {
        body: { template: `modules/${MODULE_ID}/templates/preset/presetFolderEdit.hbs` },
        footer: { template: 'templates/generic/form-footer.hbs' },
    };

    /* -------------------------------------------- */

    /** @override */
    get id() {
        return this.document.id ? super.id : 'folder-create';
    }

    /* -------------------------------------------- */

    /** @override */
    get title() {
        if (this.document.id) return `${localize('FOLDER.Update', false)}: ${this.document.name}`;
        return localize('SIDEBAR.ACTIONS.CREATE.Folder', false);
    }

    /** @override */
    _attachPartListeners(partId, htmlElement, options) {
        super._attachPartListeners(partId, htmlElement, options);
        if (partId === 'body') $(htmlElement).find('.document-select').on('click', this._onDocumentChange.bind(this));
    }

    _onDocumentChange(event) {
        $(event.target).closest('.document-select').toggleClass('active');
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    async _prepareContext(options) {
        const context = await super._prepareContext(options);
        const folder = context.document;
        context.namePlaceholder = folder.constructor.defaultName({ pack: folder.pack });
        const submitText = localize(folder._id ? 'FOLDER.Update' : 'SIDEBAR.ACTIONS.CREATE.Folder', false);
        context.buttons = [{ type: 'submit', icon: 'fa-solid fa-floppy-disk', label: submitText }];

        let folderDocs = folder.flags[MODULE_ID]?.types ?? ['ALL'];

        // This is a non-placeable folder type, so we will not display controls to change types
        let docs;
        if (
            this.options?.folder instanceof PresetPackFolder ||
            (folderDocs.length === 1 && (folderDocs[0] === 'Bag' || !UI_DOCS.includes(folderDocs[0])))
        ) {
            this.displayTypes = false;
        } else {
            this.displayTypes = true;
            docs = [];
            UI_DOCS.forEach((type) => {
                docs.push({
                    name: type,
                    icon: DOC_ICONS[type],
                    active: folderDocs.includes(type),
                });
            });
        }
        context.docs = docs;
        context.virtualPackFolder = this.options.folder instanceof PresetPackFolder;
        context.group = this.options.folder?.group;

        return context;
    }

    /* -------------------------------------------- */

    /** @override */
    async _processSubmitData(event, form, submitData, options) {
        if (this.displayTypes) {
            let visibleTypes = [];
            $(form)
                .find('.document-select.active')
                .each(function () {
                    visibleTypes.push(this.dataset.name);
                });
            if (!visibleTypes.length) visibleTypes.push('ALL');

            submitData[`flags.${MODULE_ID}.types`] = visibleTypes;
        }

        let document = this.document;
        if (this.options.folder instanceof PresetPackFolder) {
            // This is a virtual folder used to store Compendium contents,
            // update using the provided interface
            let update = {};
            submitData.group = $(form).find('[name="group"]').val();

            ['name', 'color', 'group'].forEach((k) => {
                if (!submitData[k]?.trim()) update['-=' + k] = null;
                else update[k] = submitData[k].trim();
            });

            await this.options.folder.update(update);
        } else {
            // This is a real folder, update/create it
            if (!submitData.name?.trim()) submitData.name = Folder.implementation.defaultName();
            if (document.id) await document.update(submitData);
            else {
                document.updateSource(submitData);
                document = await Folder.create(document, { pack: document.pack });
            }
        }

        this.options.resolve?.(document);
    }
}

async function getCompendiumDialog({
    excludePack,
    exportTo = false,
    keepIdSelect = false,
    presetCompOnly = false,
} = {}) {
    let config;
    if (exportTo) {
        config = {
            title: localize('presets.select-compendium'),
            message: '',
            buttonLabel: localize('FOLDER.Export', false),
        };
    } else {
        config = {
            title: localize('presets.select-compendium'),
            message: localize('presets.working-directory-message'),
            buttonLabel: localize('common.swap'),
        };
    }

    let options = '';
    for (const p of game.packs) {
        if (!p.locked && p.documentName === 'JournalEntry') {
            if (p.collection === excludePack) continue;
            if (presetCompOnly && !p.index.get(META_INDEX_ID)) continue;
            const workingPack = p.collection === PresetStorage.workingPack;
            options += `<option value="${p.collection}" ${workingPack ? 'selected="selected"' : ''}>${p.title}</option>`;
        }
    }

    let html = `
  <p style="color: orangered;">${config.message}</p>
  <div class="form-group">
    <label>${localize('PACKAGE.TagCompendium', false)}</label>
    <div class="form-fields">
      <select style="width: 100%; margin-bottom: 10px;" name="pack">${options}</select>
    </div>
  </div>`;

    if (keepIdSelect) {
        html += `
<div class="form-group">
    <label>${localize('presets.keep-ids')}</label>
    <input type="checkbox" name="keepId" checked>
    <p class="hint">${localize('presets.keep-ids-hint')}</p>
</div>`;
    }

    const content = document.createElement('div');
    content.innerHTML = html;

    let result = {};
    await foundry.applications.api.DialogV2.wait({
        window: { title: config.title, icon: 'fas fa-atlas' },
        content,
        position: { width: 400 },
        buttons: [
            {
                action: 'ok',
                label: config.buttonLabel,
                icon: '',
                callback: (event, button) => {
                    const fd = new foundry.applications.ux.FormDataExtended(button.form);
                    result = {
                        pack: fd.object.pack,
                        keepId: fd.object.keepId,
                    };
                },
            },
            {
                action: 'cancel',
                label: 'Cancel',
            },
        ],
    });

    return result;
}

export function registerPresetBrowserHooks() {
    // Intercept and prevent certain placeable drag and drop if they are hovering over the PresetBrowser form
    // passing on the placeable to it to perform preset creation.
    const dragDropHandler = function (wrapped, ...args) {
        if (PresetBrowser.objectHover || PresetConfig.objectHover) {
            this.mouseInteractionManager.cancel(...args);
            let app;

            if (PresetBrowser.objectHover) app = foundry.applications.instances.get(PresetBrowser.DEFAULT_OPTIONS.id);
            else if (PresetConfig.objectHover) app = foundry.applications.instances.get('mass-edit-preset-edit');

            if (app) {
                const placeables = canvas.activeLayer.controlled.length ? [...canvas.activeLayer.controlled] : [this];
                app.dropPlaceable(placeables, ...args);
            }

            // Pass in a fake event that hopefully is enough to allow other modules to function
            this._onDragLeftCancel(...args);
        } else {
            return wrapped(...args);
        }
    };

    SUPPORTED_PLACEABLES.forEach((name) => {
        libWrapper.register(
            MODULE_ID,
            `foundry.canvas.placeables.${name}.prototype._onDragLeftDrop`,
            dragDropHandler,
            'MIXED',
        );
    });

    // Scene Control to open preset browser
    Hooks.on('renderSceneControls', (sceneControls, html, data, options) => {
        if (!game.user.isGM) return;
        if (!game.settings.get(MODULE_ID, 'presetSceneControl')) return;

        if ($(html).find('.mass-edit-scene-control-macro').length) return;

        const displayPresetBrowser = function () {
            let documentName = canvas.activeLayer?.constructor.documentName;
            if (!SUPPORTED_PLACEABLES.includes(documentName)) documentName = 'ALL';
            const presetForm = foundry.applications.instances.get('mass-edit-presets');
            if (presetForm) {
                presetForm.close();
                return;
            }
            new PresetBrowser(null, null, documentName).render(true);
        };

        foundry.applications.handlebars
            .renderTemplate(`modules/${MODULE_ID}/templates/preset/sceneControlMacro.hbs`, {
                macros: MassEdit.registers._sceneControlMacros.length
                    ? [
                          { uuid: 'browser', label: 'Preset Browser', icon: 'fa-solid fa-book-atlas' },
                          ...MassEdit.registers._sceneControlMacros,
                      ]
                    : [],
            })
            .then((controls) => {
                const jControls = $(controls);
                $(html).find('#scene-controls-layers').append(jControls);

                const macros = jControls.find('.macros');

                jControls.on('click', '.browser', (event) => {
                    if (MassEdit.registers._sceneControlMacros.length) macros.toggleClass('active');
                    else displayPresetBrowser();
                });

                macros.on('click', '.macro', (event) => {
                    const uuid = event.target.dataset.uuid;
                    if (uuid === 'browser') {
                        displayPresetBrowser();
                    } else fromUuid(uuid)?.then((macro) => macro?.execute());
                });
            });
    });

    // Change default behavior of JournalEntry click and context menu within the CompendiumDirectory
    libWrapper.register(
        MODULE_ID,
        'foundry.applications.sidebar.tabs.CompendiumDirectory.prototype._getEntryContextOptions',
        function (wrapped, ...args) {
            const options = wrapped(...args);
            options.push({
                name: 'Open Journal Compendium',
                icon: '<i class="fas fa-book-open"></i>',
                condition: (li) => {
                    const pack = game.packs.get(li.dataset.pack);
                    return pack.metadata.type === 'JournalEntry' && pack.index.get(META_INDEX_ID);
                },
                callback: (li) => {
                    const pack = game.packs.get(li.dataset.pack);
                    pack.render(true);
                },
            });
            return options;
        },
        'WRAPPER',
    );

    libWrapper.register(
        MODULE_ID,
        'foundry.applications.sidebar.apps.Compendium.prototype._getEntryContextOptions',
        function (wrapped, ...args) {
            const options = wrapped(...args);

            if (this.collection.documentName !== 'Scene') return options;

            options.push({
                name: 'Spawn as Preset',
                icon: '<i class="fa-solid fa-books"></i>',
                callback: async (li) => {
                    spawnSceneAsPreset(await this.collection.getDocument($(li).data('entryId')));
                },
            });
            return options;
        },
        'WRAPPER',
    );

    libWrapper.register(
        MODULE_ID,
        'foundry.applications.sidebar.tabs.SceneDirectory.prototype._getEntryContextOptions',
        function (wrapped, ...args) {
            const options = wrapped(...args);
            options.push({
                name: 'Spawn as Preset',
                icon: '<i class="fa-solid fa-books"></i>',
                condition: (li) => game.user.isGM && canvas.ready && $(li).data('entryId') !== canvas.scene?.id,
                callback: (li) => {
                    spawnSceneAsPreset(game.scenes.get($(li).data('entryId')));
                },
            });
            return options;
        },
        'WRAPPER',
    );

    libWrapper.register(
        MODULE_ID,
        'foundry.applications.sidebar.tabs.CompendiumDirectory.prototype._onClickEntry',
        async function (wrapped, ...args) {
            const target = args[1];
            const packId = target.closest('[data-pack]').dataset.pack;
            const pack = game.packs.get(packId);
            if (pack.metadata.type === 'JournalEntry' && pack.index.get(META_INDEX_ID)) {
                openPresetBrowser('ALL');
                return;
            }
            return wrapped(...args);
        },
        'MIXED',
    );

    libWrapper.register(
        MODULE_ID,
        'foundry.documents.collections.CompendiumCollection.prototype.maxFolderDepth',
        function (wrapped) {
            const depth = wrapped();
            return this.index.get(META_INDEX_ID) ? 100 : depth;
        },
        'WRAPPER',
    );
}
