import { MODULE_ID } from '../constants.js';
import { Scenescape } from '../scenescape/scenescape.js';
import { is3DModel, isAudio, loadImageVideoDimensions } from '../utils.js';
import { FileIndexer } from './fileIndexer.js';
import { callAsyncHook, decodeURIComponentSafely, isVideo, placeableToData } from './utils.js';

export const DOCUMENT_FIELDS = ['id', 'name', 'sort', 'folder'];

export const PRESET_FIELDS = [
    'id',
    'uuid',
    'name',
    'data',
    'sort',
    'folder',
    'documentName',
    'addSubtract',
    'randomize',
    'img',
    'gridSize',
    'modifyOnSpawn',
    'preSpawnScript',
    'postSpawnScript',
    'spawnRandom',
    'attached',
    'tags',
    'preserveLinks',
];

export const DOC_ICONS = {
    ALL: '<i class="fas fa-globe"></i>',
    Bag: '<i class="fa-solid fa-sack"></i>',
    Token: '<i class="fas fa-user-circle"></i>',
    MeasuredTemplate: '<i class="fas fa-ruler-combined"></i>',
    Tile: '<i class="fa-solid fa-cubes"></i>',
    Drawing: '<i class="fa-solid fa-pencil-alt"></i>',
    Wall: '<i class="fa-solid fa-block-brick"></i>',
    AmbientLight: '<i class="fa-regular fa-lightbulb"></i>',
    AmbientSound: '<i class="fa-solid fa-music"></i>',
    Note: '<i class="fa-solid fa-bookmark"></i>',
    Region: '<i class="fa-regular fa-game-board"></i>',
    Actor: '<i class="fas fa-user-alt"></i>',
    Scene: '<i class="fas fa-map"></i>',
    FauxScene: '<i class="fas fa-map"></i>',
    DEFAULT: '<i class="fa-solid fa-question"></i>',
};

export class Preset {
    // Tag to HTML string mappings used to render icons on Presets
    static _tagIcons = {};

    static registerTagIcons(tagToIcon) {
        Object.assign(Preset._tagIcons, tagToIcon);
    }

    static isEditable(uuid) {
        if (!uuid) return false;
        if (uuid.startsWith('virtual@')) return true;
        let { collection } = foundry.utils.parseUuid(uuid);
        if (!collection) return false;
        return !collection.locked;
    }

    constructor(data) {
        this.id = data.id ?? data._id ?? foundry.utils.randomID();
        this.name = data.name ?? 'Mass Edit Preset';
        this.documentName = data.documentName;
        this.sort = data.sort ?? 0;
        this.tags = data.tags ?? [];
        this.addSubtract =
            data.addSubtract instanceof Array
                ? Object.fromEntries(data.addSubtract)
                : foundry.utils.deepClone(data.addSubtract ?? {});
        this.randomize =
            data.randomize instanceof Array
                ? Object.fromEntries(data.randomize)
                : foundry.utils.deepClone(data.randomize ?? {});
        this.data = foundry.utils.deepClone(data.data);
        this.img = data.img;
        this.folder = data.folder;
        this.uuid = data.uuid;
        this.gridSize = data.gridSize;
        this.modifyOnSpawn = data.modifyOnSpawn;
        this.preSpawnScript = data.preSpawnScript;
        this.postSpawnScript = data.postSpawnScript;
        this.attached = data.attached;
        this.spawnRandom = data.spawnRandom;
        this.preserveLinks = data.preserveLinks;
    }

    get icons() {
        const icons = [DOC_ICONS[this.documentName] ?? DOC_ICONS.DEFAULT];
        this.tags.forEach((t) => {
            if (Preset._tagIcons[t]) icons.push(Preset._tagIcons[t]);
        });

        return icons;
    }

    get thumbnail() {
        if (!this.img) return CONST.DEFAULT_TOKEN;
        else if (isAudio(this.img)) return 'icons/svg/sound.svg';
        else if (isVideo(this.img)) return 'icons/svg/video.svg';
        return this.img;
    }

    get pages() {
        if (this.document?.pages.size) return this.document.toJSON().pages;
        else if (this._pages) return this._pages;
        return null;
    }

    set data(data) {
        if (data instanceof Array) this._data = data.length ? data : [{}];
        else if (data == null) this._data = [{}];
        else this._data = [data];
    }

    get data() {
        return this._data;
    }

    addPostSpawnHook(hook) {
        if (!this._postSpawnHooks) this._postSpawnHooks = [];
        this._postSpawnHooks.push(hook);
    }

    async callPostSpawnHooks(options) {
        if (this._postSpawnHooks) {
            for (const hook of this._postSpawnHooks) {
                await hook(options);
            }
        }
    }

    /**
     * Loads underlying JournalEntry document from the compendium
     * @returns this
     */
    async load({ document, callHook = false } = {}) {
        if (this._loaded) return this;

        if (!this.document && this.uuid) {
            this.document = document ?? (await fromUuid(this.uuid));
        }

        if (this.document) {
            const preset = this.document.getFlag(MODULE_ID, 'preset') ?? {};
            this.documentName = preset.documentName;
            this.img = preset.img;
            this.data = preset.data;
            this.randomize =
                foundry.utils.getType(preset.randomize) === 'Object'
                    ? preset.randomize
                    : Object.fromEntries(preset.randomize ?? []);
            this.addSubtract =
                foundry.utils.getType(preset.addSubtract) === 'Object'
                    ? preset.addSubtract
                    : Object.fromEntries(preset.addSubtract ?? []);
            this.gridSize = preset.gridSize;
            this.modifyOnSpawn = preset.modifyOnSpawn;
            this.preSpawnScript = preset.preSpawnScript;
            this.postSpawnScript = preset.postSpawnScript;
            this.attached = preset.attached;
            this.spawnRandom = preset.spawnRandom;
            this.preserveLinks = preset.preserveLinks;
            this.tags = preset.tags ?? [];
        }

        if (callHook) await callAsyncHook('MassEdit.loadPreset', this);
        this._loaded = true;
        return this;
    }

    /**
     * Delete the preset along with the underlying document
     * @returns
     */
    async delete() {
        if (this.document) return this.document.delete();
        return (await fromUuid(this.uuid))?.delete();
    }

    async openJournal() {
        if (!this.document) await this.load();
        if (this.document) this.document.sheet.render(true);
    }

    /**
     * Looks for a tag in the format of '#ft' and returns the numerical value
     * @returns
     */
    scenescapeSizeOverride() {
        const regex = new RegExp(/(\d+)ft/);
        let size = this.tags.find((t) => t.match(regex))?.match(regex)[1];
        if (!size && this.documentName === 'Token') {
            const actor = game.actors.get(this.data[0].actorId);
            if (actor) return Scenescape._getActorSize(actor, this.data[0]);
            return 6;
        }
        if (size) return Number(size);
        return null;
    }

    /**
     * Attach placeables
     * @param {Placeable|Array[Placeable]} placeables
     * @returns
     */
    async attach(placeables) {
        if (!placeables) return;
        if (!(placeables instanceof Array)) placeables = [placeables];

        if (!this.attached) this.attached = [];
        for (const placeable of placeables) {
            this.attached.push({ documentName: placeable.document.documentName, data: placeableToData(placeable) });
        }

        await this.update({ attached: this.attached });
    }

    static _updateBatch = {};

    /**
     * Collate document updates to be processed at a later time using `processBatchUpdates`
     * @param {Document} document
     * @param {object} update
     */
    static batchUpdate(document, update) {
        this._updateBatch[document.pack] = foundry.utils.mergeObject(this._updateBatch[document.pack] ?? {}, {
            [document.id]: update,
        });
    }

    /**
     * Process updates collated using `batchUpdate`
     */
    static async processBatchUpdates() {
        const batch = this._updateBatch;
        this._updateBatch = {};

        for (const pack of Object.keys(batch)) {
            const updates = [];

            for (const id of Object.keys(batch[pack])) {
                const update = batch[pack][id];
                update._id = id;
                updates.push(update);
            }

            await JournalEntry.updateDocuments(updates, { pack });
        }
    }

    /**
     * Update preset with the provided data
     * @param {Object} update
     */
    async update(update, batch = false) {
        if (this.document) {
            const flagUpdate = {};
            Object.keys(update).forEach((k) => {
                if (k === 'tags') {
                    this.tags = (update[k] ?? []).filter((t) => typeof t === 'string');
                    flagUpdate.tags = this.tags;
                } else if (k === 'randomize' || k === 'addSubtract') {
                    flagUpdate[k] = Object.entries(update[k]);
                    this[k] = update[k];
                } else if (k === 'data' && !(update.data instanceof Array)) {
                    flagUpdate.data = this.data.map((d) => {
                        return foundry.utils.mergeObject(d, update.data);
                    });
                    this.data = flagUpdate.data;
                } else if (PRESET_FIELDS.includes(k) && update[k] !== this[k]) {
                    flagUpdate[k] = update[k];
                    this[k] = update[k];
                }
            });

            if (!foundry.utils.isEmpty(flagUpdate)) {
                const docUpdate = { flags: { [MODULE_ID]: { preset: flagUpdate } } };
                if (batch) Preset.batchUpdate(this.document, docUpdate);
                else await this.document.update(docUpdate);
            }
        } else {
            console.warn('FAILED UPDATE: Updating preset without document', this.id, this.uuid, this.name);
        }
    }

    toJSON() {
        let json = {};
        PRESET_FIELDS.forEach((field) => {
            json[field] = foundry.utils.deepClone(this[field]);
        });

        json.randomize = Object.entries(json.randomize ?? {});
        json.addSubtract = Object.entries(json.addSubtract ?? {});
        const pages = this.pages;
        if (pages) json.pages = pages;

        return json;
    }

    clone() {
        const clone = new Preset(this.toJSON());
        clone.document = this.document;
        clone._postSpawnHooks = this._postSpawnHooks;
        clone._loaded = this._loaded;
        return clone;
    }
}

export class VirtualFilePreset extends Preset {
    /**
     * Constructs a VirtualFilePreset using the provided src/path of some media file
     * @param {string} src path to file
     * @param {string} forceType force preset into a specific document type (for now only forced to Token)
     * @returns
     */
    static fromSrc(src, forceType) {
        const documentName = forceType ?? (isAudio(src) ? 'AmbientSound' : 'Tile');

        let data;

        if (documentName === 'Tile' || documentName === 'Token')
            data = [{ texture: { src, scaleY: 1, scaleX: 1 }, x: 0, y: 0, rotation: 0 }];
        else data = [{ path: src, radius: 20, x: 0, y: 0 }];

        return new VirtualFilePreset({
            name: decodeURIComponentSafely(src.split('/').pop()),
            uuid: 'virtual@' + src,
            documentName,
            data,
            gridSize: 150,
            img: src,
        });
    }

    get virtual() {
        return true;
    }

    async load({ force = false, callHook = false } = {}) {
        if (!force || this._loaded) return this;
        this._loaded = true;

        // Width already determined, no further processing required
        if (this.data[0].width) return this;

        if (callHook) await callAsyncHook('MassEdit.loadPreset', this);

        // Ambient Sound, no further processing required
        if (this.data[0].path) return this;

        // Load image/video/3D Model metadata to retrieve dimensions
        const src = this.data[0].texture?.src;

        if (is3DModel(src) || foundry.utils.getProperty(this.data[0], 'flags.levels-3d-preview.model3d')) {
            await this._load3DModel(src, this.data[0]);
        } else {
            if (this.documentName === 'Token') {
                this.data[0].width = 1;
                this.data[0].height = 1;
                return this;
            }

            let { width, height } = await loadImageVideoDimensions(src);
            this.data[0].width = width ?? 100;
            this.data[0].height = height ?? 100;
        }

        return this;
    }

    async _load3DModel(src, data) {
        src = foundry.utils.getProperty(data, 'flags.levels-3d-preview.model3d') ?? src;

        if (!game.Levels3DPreview || this.documentName === 'Token') {
            foundry.utils.setProperty(data, 'flags.levels-3d-preview.model3d', src);
            foundry.utils.setProperty(data, 'flags.levels-3d-preview.autoCenter', true);
            foundry.utils.setProperty(data, 'texture.src', 'modules/levels-3d-preview/assets/blank.webp');
            return;
        }

        const object3d = await game.Levels3DPreview.helpers.loadModel(src);
        const modelBB = new game.Levels3DPreview.THREE.Box3().setFromObject(object3d.model);
        const depth = (modelBB.max.y - modelBB.min.y) * canvas.grid.size ?? 0.05;

        foundry.utils.mergeObject(
            data,
            {
                flags: {
                    'levels-3d-preview': {
                        model3d: src,
                        autoGround: true,
                        autoCenter: true,
                        cameraCollision: false,
                        castShadow: true,
                        collision: true,
                        color: '#ffffff',
                        dynaMesh: 'default',
                        sight: true,
                        depth,
                    },
                },
            },
            { overwrite: false },
        );

        data.width = canvas.grid.size * (modelBB.max.x - modelBB.min.x);
        data.height = canvas.grid.size * (modelBB.max.z - modelBB.min.z);
        data.texture.src = `modules/levels-3d-preview/assets/blank.webp`;
    }

    async update(update) {
        if (!update.hasOwnProperty('tags')) return;
        this.tags = update.tags;
        this.#processIndexUpdate();
    }

    /**
     * Delete preset from virtual index
     * @returns
     */
    async delete() {
        await FileIndexer.delete(this);
        this.#processIndexUpdate();
    }

    #processIndexUpdate() {
        clearTimeout(VirtualFilePreset._updateTimeout);
        VirtualFilePreset._updateTimeout = setTimeout(
            () => FileIndexer.saveIndexToCache({ processAutoSave: true, notify: false }),
            3000,
        );
    }

    clone() {
        const data = this.toJSON();
        const clone = new VirtualFilePreset(data);
        clone._loaded = this._loaded;
        clone._thumb = this._thumb;
        clone._postSpawnHooks = this._postSpawnHooks;
        return clone;
    }
}
