import { MODULE_ID } from '../constants.js';
import { SeededRandom, localize } from '../utils.js';
import { FileIndexer } from './fileIndexer.js';
import { PresetBrowser } from './browser/browserApp.js';
import { DOCUMENT_FIELDS, Preset, VirtualFilePreset } from './preset.js';
import { decodeURIComponentSafely, exportPresets, placeableToData } from './utils.js';
import { buildQueryMatcher } from './search.js';

export const META_INDEX_ID = 'MassEditMetaData';
export const META_INDEX_FIELDS = ['img', 'documentName', 'tags'];

export class PresetAPI {
    static name = 'PresetAPI';

    /**
     * Create a Token preset from the provided Actor
     */
    static async createPresetFromActor(actor, { keepId = false, folder } = {}) {
        if (!actor || actor.documentName !== 'Actor') return;

        const presetData = {
            id: keepId ? actor.id : null,
            name: actor.name,
            documentName: 'Token',
            img: actor.img,
            data: [actor.prototypeToken.toJSON()],
            folder: folder,
        };

        presetData.gridSize = canvas.scene.grid.size;

        const preset = new Preset(presetData);
        return preset;
    }

    static async createPresetFromActorUuid(uuid, options = {}) {
        const actor = await fromUuid(uuid);
        if (!actor.documentName === 'Actor') return;

        return await this.createPresetFromActor(actor, options);
    }

    /**
     * Create Presets from passed in placeables
     * @param {PlaceableObject|Array[PlaceableObject]} placeables Placeable/s to create the presets from.
     * @param {object} [options={}]                               Optional Preset information
     * @param {String} [options.name]                             Preset name
     * @param {String} [options.img]                              Preset thumbnail image
     * @returns {Preset|Array[Preset]}
     */
    static async createPreset(placeables, options = {}) {
        if (!placeables) return;
        if (!(placeables instanceof Array)) placeables = [placeables];

        // Alike placeables will be made into single presets. Lets batch them up together.

        const groups = {};
        for (const placeable of placeables) {
            const documentName = placeable.document.documentName;
            if (!groups.hasOwnProperty(documentName)) groups[documentName] = [];
            groups[documentName].push(placeable);
        }

        const presets = [];
        for (const [documentName, placeables] of Object.entries(groups)) {
            const data = [];
            for (const placeable of placeables) {
                data.push(placeableToData(placeable));
            }

            // Preset data before merging with user provided
            const defPreset = {
                name: localize('presets.default-name'),
                documentName,
                data: data,
            };

            // Assign preset image
            switch (defPreset.documentName) {
                case 'Token':
                case 'Tile':
                case 'Note':
                    defPreset.img = data[0].texture.src;
                    break;
                case 'AmbientSound':
                    defPreset.img = 'icons/svg/sound.svg';
                    break;
                case 'AmbientLight':
                    defPreset.img = 'icons/svg/light.svg';
                    break;
                case 'Drawing':
                    defPreset.img = 'icons/svg/acid.svg';
                    break;
                case 'MeasuredTemplate':
                    defPreset.img = 'icons/svg/circle.svg';
                    break;
            }

            //  Assign preset name
            switch (defPreset.documentName) {
                case 'Token':
                    defPreset.name = data[0].name;
                    break;
                default:
                    const taggerTag = data[0].flags?.tagger?.tags?.[0];
                    if (taggerTag) defPreset.name = taggerTag;
                    break;
            }

            defPreset.gridSize = placeables[0].document.parent.grid.size;

            foundry.utils.mergeObject(defPreset, options, { inplace: true });

            const preset = new Preset(defPreset);
            presets.push(preset);
        }

        await PresetStorage.createDocuments(presets);

        return presets;
    }

    /**
     * Update preset tags using provided uuid to tag mappings.
     * {
     *  "uuid1": ["tag1"],
     *  "uuid2": ["tag1", "tag2"],
     * }
     * @param {object} mappings
     */
    static async updateTags(mappings) {
        const uuids = Object.keys(mappings);
        const presets = await PresetStorage.retrieve({ uuid: uuids, load: true });
        for (const preset of presets) {
            let tags = mappings[preset.uuid];
            if (tags && Array.isArray(tags)) {
                tags = tags.map((t) => t.slugify({ strict: true })).filter(Boolean);
                if (tags.length) preset.update({ tags }, true);
            }
        }

        return Preset.processBatchUpdates();
    }
}

export class PresetFolder {
    constructor({
        id,
        name,
        sorting = 'm',
        color = '#000000',
        sort = 0,
        children = [],
        presets = [],
        folder = null,
        types = [],
    } = {}) {
        this.id = id;
        this.uuid = `ME-Folder.${this.id}`;
        this.name = name;
        this.sorting = sorting;
        this.color = color;
        this.sort = sort;
        this.children = children;
        this.presets = presets;

        this.folder = folder;
        this.types = types;
        this.flags = { [MODULE_ID]: { types } };

        if (!CONFIG['ME-Folder']) CONFIG['ME-Folder'] = { collection: { instance: new Collection() } };
        CONFIG['ME-Folder'].collection.instance.set(this.id, this);
    }

    async update(data) {
        const doc = await fromUuid(this.uuid);
        if (doc) {
            foundry.utils.mergeObject(this, data);
            await doc.update(data);
        }
    }

    get expanded() {
        return Boolean(game.folders._expanded[this.uuid]);
    }
}

export class PresetVirtualFolder extends PresetFolder {
    constructor(options) {
        super(options);
        this.virtual = true;
        this.static = true;
    }

    async update(data) {}
}

export class VirtualFileFolder extends PresetVirtualFolder {
    constructor(options) {
        options.id = SeededRandom.randomID(options.path);
        super(options);
        if (!options.types) this.types = ['ALL'];
        this.bucket = options.bucket;
        this.source = options.source;
        this.name = decodeURIComponentSafely(this.name);
        this.icon = options.icon;
        this.subtext = options.subtext;
        this.path = options.path;
        if (options.source && ['data', 'forgevtt'].includes(options.source)) this.indexable = true;
    }

    async delete() {
        await FileIndexer.deleteFolder(this);
        CONFIG['ME-Folder']?.collection?.instance?.delete(this.id);
    }
}

export class PresetPackFolder extends PresetVirtualFolder {
    constructor(compendium, metaDoc, children = []) {
        const packFolderData = metaDoc.flags[MODULE_ID]?.folder ?? {};
        const id = SeededRandom.randomID(compendium.collection);
        const uuid = `ME-Folder.${id}`;
        super({
            id,
            uuid,
            name: packFolderData.name ?? compendium.title,
            color: packFolderData.color ?? '#000000',
            children,
        });
        this.group = packFolderData.group;
        this.pack = compendium.collection;
        this.editDisabled = compendium.editDisabled;
        this.typeless = true;
    }

    async update(data = {}) {
        const pack = game.packs.get(this.pack);

        if (!pack || pack.locked) return;
        if (data.hasOwnProperty('name') && data.name === pack.title) delete data.name;
        if (foundry.utils.isEmpty(data)) return;

        const { metadataDocument } = await PresetStorage._initCompendium(this.pack);
        await metadataDocument.setFlag(MODULE_ID, 'folder', data);

        foundry.utils.mergeObject(this, data);
    }

    get _meMatch() {
        return this.children.some((ch) => ch.folder._meMatch) || this.presets.some((p) => p._meMatch);
    }

    set _meMatch(val) {}
}

export class PresetStorage {
    static DEFAULT_PACK = 'world.mass-edit-presets-main';

    /**
     * Revert the working compendium to default pack
     */
    static async fallbackToDefaultPack() {
        await game.settings.set(MODULE_ID, 'workingPack', PresetStorage.DEFAULT_PACK);
    }

    /**
     * Construct a collection of presets representing the index of the passed in compendium
     * @param {Collection} pack
     * @returns
     */
    static async _loadIndex(pack, force = false) {
        if (pack._meIndex && !force) return pack._meIndex;
        const metadataDocument = await pack.getDocument(META_INDEX_ID);
        const rawIndex = metadataDocument.getFlag(MODULE_ID, 'index');

        const index = new Collection();
        for (const [id, content] of Object.entries(rawIndex)) {
            if (id !== META_INDEX_ID) {
                const i = pack.index.get(id);
                if (i) {
                    const { folder, name, sort, uuid, _id } = i;
                    index.set(id, new Preset({ ...content, folder, name, sort, uuid, _id }));
                }
            }
        }

        pack._meIndex = index;
        return index;
    }

    /**
     * Fully reload the index of the passed in compendium
     * @param {Collection} pack
     */
    static async reloadIndex(pack) {
        await this._loadIndex(pack, true);
        pack.initializeTree?.();
        foundry.applications.instances.get(PresetBrowser.DEFAULT_OPTIONS.id)?.render(true);
    }

    /**
     * Retrieves a compendium and create a metadata document within it
     * If it's a DEFAULT_PACK and does not exist it will be created
     * @param {string} packId
     * @returns
     */
    static async _initCompendium(packId) {
        // Get/Create compendium
        let compendium = game.packs.get(packId);
        if (!compendium && packId === PresetStorage.DEFAULT_PACK) {
            if (!this._creatingDefaultCompendium)
                this._creatingDefaultCompendium = foundry.documents.collections.CompendiumCollection.createCompendium({
                    label: 'Mass Edit: Presets (MAIN)',
                    type: 'JournalEntry',
                    packageType: 'world',
                });

            compendium = await this._creatingDefaultCompendium;
        }

        // Get/Create metadata document
        let metadataDocument = await compendium?.getDocument(META_INDEX_ID);
        if (compendium && !metadataDocument) {
            if (!compendium._creatingMetadataDocument) {
                compendium._creatingMetadataDocument = compendium.documentClass.createDocuments(
                    [
                        {
                            _id: META_INDEX_ID,
                            name: '!!! METADATA: DO NOT DELETE !!!',
                            flags: { [MODULE_ID]: { index: {} } },
                        },
                    ],
                    {
                        pack: packId,
                        keepId: true,
                    },
                );
            }

            const documents = await compendium._creatingMetadataDocument;
            metadataDocument = documents[0];
        }

        return { compendium, metadataDocument };
    }

    /**
     * Update multiple presets at the same time
     * @param {Array[]} updates
     */
    static async updatePresets(updates, pack = this.workingPack) {
        return game.packs.get(pack).documentClass.updateDocuments(updates, { pack });
    }

    /**
     * Creates a JournalEntry document representing the passed in preset/s
     * @param {Preset|Array[Preset]} presets
     */
    static async createDocuments(presets, pack = this.workingPack) {
        if (!Array.isArray(presets)) presets = [presets];

        const compendium = game.packs.get(pack);

        presets = presets.filter((p) => !compendium.index.get(p.id));
        if (!presets.length) return;

        const data = presets.map((preset) => {
            return {
                _id: preset.id,
                name: preset.name,
                pages: preset.pages ?? [],
                folder: preset.folder,
                flags: { [MODULE_ID]: { preset: preset.toJSON() } },
            };
        });

        const documents = await JournalEntry.createDocuments(data, {
            pack: pack,
            keepId: true,
        });

        for (const preset of presets) {
            const document = documents.find((d) => d.id === preset.id);
            preset.uuid = document.uuid;
            await preset.load({ document });
        }

        return presets;
    }

    /**
     * Delete presets and their underlying documents
     * @param {Preset|Array[Preset]} presets
     */
    static async delete(presets) {
        if (!presets) return;
        if (!(presets instanceof Array)) presets = [presets];

        // Sort by compendium
        const sorted = {};
        const virtual = [];
        for (const preset of presets) {
            if (preset.virtual) {
                virtual.push(preset);
                continue;
            }
            let { collection } = foundry.utils.parseUuid(preset.uuid);
            if (!collection) continue;
            collection = collection.collection;
            if (!sorted[collection]) sorted[collection] = [preset];
            else sorted[collection].push(preset);
        }

        // Delete in batches
        for (const pack of Object.keys(sorted)) {
            await JournalEntry.deleteDocuments(
                sorted[pack].map((p) => p.id),
                { pack },
            );
        }
        for (const vPreset of virtual) {
            vPreset.delete();
        }
    }

    // Initialize hooks to manage update, deletion, and creation of preset JournalEntry's,
    // hiding of managed compendiums
    static _init() {
        Hooks.on(`preUpdateJournalEntry`, this._preUpdate.bind(this));
        Hooks.on(`preCreateJournalEntry`, this._preCreate.bind(this));
        Hooks.on('updateCompendium', this._updateCompendium.bind(this));

        // Hide managed compendiums
        Hooks.on('activateCompendiumDirectory', (directory) => {
            const hide = Boolean(game.settings.get(MODULE_ID, 'hideManagedPacks'));
            game.packs
                .filter((p) => p.index.get(META_INDEX_ID))
                .forEach((pack) => {
                    const el = directory.element.querySelector(`[data-pack="${pack.collection}"]`);
                    if (hide) el?.setAttribute('hidden', true);
                    else el?.removeAttribute('hidden');
                });
        });
    }

    static _updateCompendium(compendium, documents, operation, userId) {
        if (!compendium.index?.get(META_INDEX_ID)) return;

        if (operation.data) this._updateCompendiumCreate(compendium, documents, operation, userId);
        else if (operation.updates) this._updateCompendiumUpdate(compendium, documents, operation, userId);
        else if (operation.ids) this._updateCompendiumDelete(compendium, documents, operation, userId);

        // Re-render the Preset Browser if it's open to reflect the new changes
        foundry.applications.instances.get(PresetBrowser.DEFAULT_OPTIONS.id)?.render(true);
    }

    static _updateCompendiumCreate(compendium, documents, operation, userId) {
        if (documents[0] instanceof Folder) return;

        if (compendium._meIndex) {
            for (const data of operation.data) {
                const index_fields = { ...compendium.index.get(data._id) };
                const presetData = foundry.utils.getProperty(data, `flags.${MODULE_ID}.preset`) ?? {};
                META_INDEX_FIELDS.forEach((f) => {
                    if (f in presetData) index_fields[f] = presetData[f];
                });
                compendium._meIndex.set(data._id, new Preset(index_fields));
            }
        }

        if (game.user.id === userId) {
            const indexUpdate = {};
            for (const data of operation.data) {
                // if (data._id === META_INDEX_ID) {
                //   // If a METADATA file has been created and there are existing documents within the compendium
                //   // we will attempt to reconstruct the index assuming that the documents present are Preset containers
                //   if (compendium.index.size > 1) this._recoverIndex(compendium);
                //   continue;
                // }

                const preset = foundry.utils.getProperty(data, `flags.${MODULE_ID}.preset`) ?? {};
                const index = {};
                META_INDEX_FIELDS.forEach((k) => {
                    if (k in preset) index[k] = preset[k];
                });

                foundry.utils.setProperty(indexUpdate, `flags.${MODULE_ID}.index.${data._id}`, index);
            }

            compendium.getDocument(META_INDEX_ID).then((metaDocument) => {
                metaDocument.update(indexUpdate);
            });
        }
    }

    // Sync document update with index
    static _updateCompendiumUpdate(compendium, documents, operation, userId) {
        if (!compendium.index.get(operation.updates[0]._id)) return; // Folder update

        const indexUpdate = {};
        for (const update of operation.updates) {
            if (update._id === META_INDEX_ID) continue;

            const preset = foundry.utils.getProperty(update, `flags.${MODULE_ID}.preset`);
            if (preset) {
                const indexChanges = {};
                META_INDEX_FIELDS.forEach((k) => {
                    if (k in preset) indexChanges[k] = preset[k];
                });
                if (compendium._meIndex) {
                    const preset = compendium._meIndex.get(update._id);
                    Object.assign(preset, indexChanges);
                    DOCUMENT_FIELDS.forEach((f) => {
                        if (f in update) preset[f] = update[f];
                    });
                }
                if (!foundry.utils.isEmpty(indexChanges))
                    foundry.utils.setProperty(indexUpdate, `flags.${MODULE_ID}.index.${update._id}`, indexChanges);
            }
        }

        if (game.user.id === userId && !foundry.utils.isEmpty(indexUpdate)) {
            compendium.getDocument(META_INDEX_ID).then((metaDocument) => {
                metaDocument.update(indexUpdate);
            });
        }
    }

    // Document deletion within managed collection automatically remove it from the metadata document index
    static _updateCompendiumDelete(compendium, documents, operation, userId) {
        if (documents[0] instanceof Folder) return;
        if (compendium._meIndex) operation.ids.forEach((id) => compendium._meIndex.delete(id));

        if (game.user.id === userId) {
            const update = {};
            operation.ids.forEach((id) => {
                update[`flags.${MODULE_ID}.index.-=${id}`] = null;
            });
            compendium.getDocument(META_INDEX_ID).then((metaDocument) => {
                metaDocument.update(update).then(PresetBrowser.renderActiveBrowser());
            });
        }
    }

    // Re-construct preset index. This is called when a metadata document is created within a compendium
    // with other existing documents. It's possible that the metadata document has been deleted and the compendium
    // is now being attempted to be used as a Preset compendium again.
    static async _recoverIndex(packId) {
        const { compendium, metadataDocument } = await this._initCompendium(packId);
        if (!compendium) return;

        const indexUpdate = {};
        const documents = await compendium.getDocuments();
        for (const document of documents) {
            if (document.id === META_INDEX_ID) continue;

            const preset = document.getFlag(MODULE_ID, 'preset');
            if (preset) {
                const update = {};
                META_INDEX_FIELDS.forEach((k) => {
                    if (k in preset) update[k] = preset[k];
                });

                foundry.utils.setProperty(indexUpdate, `flags.${MODULE_ID}.index.${document.id}`, update);
            }
        }

        if (!foundry.utils.isEmpty(indexUpdate)) {
            await metadataDocument.update(indexUpdate);
            await this._loadIndex(compendium, true);
            compendium.initializeTree?.();
        }
    }

    /**
     * Sync DOCUMENT_FIELDS updates with preset flag and document itself
     * @param {Document} document
     * @param {object} change
     * @param {object} options
     * @param {string} userId
     */
    static _preUpdate(document, change, options, userId) {
        if (document.collection.index?.get(META_INDEX_ID) && document.id !== META_INDEX_ID) {
            const preset = foundry.utils.getProperty(change, `flags.${MODULE_ID}.preset`) ?? {};
            DOCUMENT_FIELDS.forEach((f) => {
                if (f in change) foundry.utils.setProperty(change, `flags.${MODULE_ID}.preset.${f}`, change[f]);
                else if (f in preset) change[f] = preset[f];
            });
        }
    }

    /**
     * If a Document has been created within a managed compendium without the use of MassEdit API default preset data will be inserted here.
     * @param {Document} document
     * @param {object} data
     * @param {object} options
     * @param {object} userId
     */
    static _preCreate(document, data, options, userId) {
        if (
            document.collection.index?.get(META_INDEX_ID) &&
            !foundry.utils.getProperty(data, `flags.${MODULE_ID}.preset`)
        ) {
            foundry.utils.setProperty(
                data,
                `flags.${MODULE_ID}.preset`,
                new Preset({ name: document.name, data: [{}] }).toJSON(),
            );
        }
    }

    // =========================================================================
    // Preset retrieval API

    /**
     * Retrieve preset
     * @param {object} [options={}]
     * @param {String} [options.uuid]                      Preset UUID
     * @param {String} [options.name]                      Preset name
     * @param {Array[String]} [options.types]              Preset types ("Token", "Tile", etc)
     * @param {String} [options.query]                     Search query to be ran. Format: "blue #castle @AmbientLight"
     *                                                     Terms: blue, Tags: castle, Type: AmbientLight
     *                                                     None, or all component of the query can be provided or excluded
     * @param {String|Array[String]|Object} [options.tags] Tags to match a preset against. Can be provided as an array or a comma separated string
     * @param {String} [options.folder]                    Folder name
     * @param {Boolean} [options.random]                   If multiple presets are found a random one will be chosen
     * @returns {Array[Preset]|Array[String]|Array[Object]}
     */
    static async retrieve({
        uuid,
        name,
        types,
        query,
        tags,
        virtualDirectory = true,
        externalCompendiums = true,
        full = undefined, // deprecated
        load = false,
        presets,
    } = {}) {
        if (full !== undefined) load = full;

        if (uuid) {
            const uuids = Array.isArray(uuid) ? uuid : [uuid];
            presets = await this.retrieveFromUUID(uuids, { load });
        } else if (!name && !types && !tags && !query)
            throw Error('UUID, Name, Type, Folder, Tags, and/or Query required to retrieve Presets.');
        else if (query && (types || tags || name))
            throw console.warn(`When 'query' is provided 'types', 'tags', and 'name' arguments are ignored.`);
        else {
            let matcher;

            if (query) {
                matcher = buildQueryMatcher(query);
            } else {
                query = '';
                if (name) query += name;
                if (types) types.forEach((t) => (query += ' @' + t));
                if (tags) {
                    if (typeof tags === 'object') tags = tags.tags;
                    else if (typeof tags === 'string')
                        tags = tags
                            .split(',')
                            .map((t) => t.trim())
                            .filter(Boolean);

                    tags.forEach((t) => (query += ' #' + t));
                }

                if (query) matcher = buildQueryMatcher(query);
            }
            if (!matcher) return [];

            if (presets) presets = presets.filter((p) => matcher(p));
            else presets = await this._search(matcher, { virtualDirectory, externalCompendiums });
        }

        if (load) await this.batchLoad(presets);

        return presets;
    }

    /**
     * Retrieve a single preset that matches the provided criteria
     * See retrieve(...)
     * @returns {Preset}
     */
    static async retrieveSingle(options = {}) {
        const presets = await this.retrieve({ ...options, load: false });

        const preset = options.random ? presets[Math.floor(Math.random() * presets.length)] : presets[0];
        if (preset && options.load) await preset.load();

        return preset;
    }

    /**
     * Search all managed packs
     * @param {function} matcher
     * @returns
     */
    static async _search(matcher, { virtualDirectory, externalCompendiums } = {}) {
        const packs = [];

        if (externalCompendiums) {
            for (const pack of game.packs) {
                if (pack.index.get(META_INDEX_ID)) packs.push(pack);
            }
        } else {
            const workingPack = game.packs.find((pack) => pack.collection === this.workingPack);
            if (workingPack) packs.push(workingPack);
        }

        if (virtualDirectory) {
            const virtualPack = await FileIndexer.collection();
            if (virtualPack) packs.push(virtualPack);
        }

        const results = [];
        for (const pack of packs) {
            if (!pack._meIndex) await this._loadIndex(pack);

            for (const entry of pack._meIndex) {
                if (matcher(entry)) results.push(entry);
            }
        }

        return results;
    }

    /**
     * Returns provided UUIDs as Presets
     * @param {Array[string]|string} uuids
     * @param {object} [options]
     * @param {boolean} [options.load] Should the associated entry documents be immediately loaded?
     * @returns {Array[Entry]}
     */
    static async retrieveFromUUID(uuids, { load = true }) {
        if (!Array.isArray(uuids)) uuids = [uuids];
        const presets = [];

        for (const uuid of uuids) {
            if (uuid.startsWith('virtual@')) {
                presets.push(await FileIndexer.retrieve(uuid));
                continue;
            }

            const { collection, documentId } = foundry.utils.parseUuid(uuid);
            if (!collection) {
                console.warn('Invalid UUID: ', uuid);
                continue;
            }
            const index = collection.index.get(documentId);

            if (index) {
                if (!collection._meIndex) await this._loadIndex(collection);
                presets.push(collection._meIndex.get(documentId));
            }
        }

        if (load) return this.batchLoad(presets);
        return presets;
    }

    /**
     * Batch load preset documents using pack.getDocuments({ _id__in: ids }) query.
     * @param {Array[Preset]} presets to be loaded with their document
     * @returns {Array[Preset]}
     */
    static async batchLoad(presets) {
        // Organize presets according to their packs
        const packToPreset = {};
        for (const preset of presets) {
            if (preset instanceof VirtualFilePreset) continue;

            if (!preset.document && preset.uuid) {
                const { collection, documentId } = foundry.utils.parseUuid(preset.uuid);
                const pack = collection.collection;
                if (!packToPreset[pack]) packToPreset[pack] = {};
                packToPreset[pack][documentId] = preset;
            }
        }

        // Load documents from each pack and assign them to entries
        for (const [pack, idToPresets] of Object.entries(packToPreset)) {
            const documents = await game.packs.get(pack).getDocuments({ _id__in: Object.keys(idToPresets) });
            for (const document of documents) {
                await idToPresets[document.id].load({ document });
            }
        }

        return presets;
    }

    /**
     * Mass export presets
     * @param {object} options
     * @param {boolean} options.workingCompendium
     * @param {boolean} options.externalCompendiums
     * @param {boolean} options.virtualDirectory
     * @param {boolean} options.json
     * @returns
     */
    static async exportPresets({
        workingCompendium = false,
        externalCompendiums = false,
        virtualDirectory = false,
        query = '',
        json = false,
    } = {}) {
        let toExport = [];
        let packs = [];

        if (workingCompendium) {
            const pack = game.packs.get(PresetStorage.workingPack);
            if (pack) packs.push(pack);
        }

        if (externalCompendiums) {
            for (const pack of game.packs) {
                if (!pack.index.get(META_INDEX_ID)) continue;
                if (externalCompendiums && pack.collection !== PresetStorage.workingPack) packs.push(pack);
            }
        }

        for (const pack of packs) {
            if (!pack._meIndex) await PresetStorage._loadIndex(pack);
            toExport = toExport.concat(pack._meIndex.contents);
        }

        // Filter via query
        if (query) toExport = await PresetStorage.retrieve({ query, presets: toExport });

        await PresetStorage.batchLoad(toExport);

        if (virtualDirectory) {
            const virtualPack = await FileIndexer.collection();
            if (virtualPack) {
                if (!virtualPack._meIndex) await PresetStorage._loadIndex(virtualPack);
                let virtualPresets = virtualPack._meIndex.contents;

                // Filter via query
                if (query) virtualPresets = await PresetStorage.retrieve({ query, presets: virtualPresets });

                toExport = toExport.concat(virtualPresets);
            }
        }

        if (toExport.length) return exportPresets(toExport, { load: false, json });
    }
}
