import { MODULE_ID, PIVOTS, SUPPORTED_PLACEABLES } from './constants.js';
import { DataTransformer } from './data/transformer.js';
import { LinkerAPI } from './linker/linker.js';
import { Mouse3D } from './mouse3d.js';
import { getDataBounds, getPivotPoint, getPresetDataBounds } from './presets/utils.js';
import { Scenescape } from './scenescape/scenescape.js';
import { pickerSelectMultiLayerDocuments, updateEmbeddedDocumentsViaGM } from './utils.js';

export class TransformBus {
    static transformers = [];
    static _transformAccumulator = { rotation: 0, scale: 1 };

    static active() {
        return Boolean(this.transformers.length);
    }

    static register(transformer) {
        this.transformers.push(new WeakRef(transformer));
    }

    static unregister(transformer) {
        this.transformers = this.transformers.filter((ref) => ref.deref() != transformer);
    }

    static clear() {
        this.transformers = [];
    }

    static resolve(confirm) {
        this.transformers = this.transformers.filter((ref) => {
            const transformer = ref.deref();
            transformer.callback?.(confirm);
            return transformer;
        });
    }

    // ***********
    // Static functions to be called by external systems to feed transformations to an active crosshair/preview

    static addRotation(rotation) {
        this.transformers = this.transformers.filter((ref) => ref.deref()?.rotate(rotation));
        this._transformAccumulator.rotation += rotation;
    }

    static addScaling(scale) {
        this.transformers = this.transformers.filter((ref) => ref.deref()?.scale(1 + scale));
        this._transformAccumulator.scale *= 1 + scale;
    }

    static addElevation(elevation) {
        this.transformers = this.transformers.filter((ref) => ref.deref()?.elevate(elevation));
    }

    static resetTransformAccumulator() {
        this._transformAccumulator.rotation = 0;
        this._transformAccumulator.scale = 1;
    }

    static getTransformAccumulator() {
        return foundry.utils.deepClone(this._transformAccumulator);
    }

    static mirrorX() {
        this.transformers = this.transformers.filter((ref) => ref.deref()?.mirrorX());
    }

    static mirrorY() {
        this.transformers = this.transformers.filter((ref) => ref.deref()?.mirrorY());
    }

    static position(pos) {
        this.transformers = this.transformers.filter((ref) => ref.deref()?.position(pos));
    }
}

/**
 * Cross-hair and optional preview image/label that can be activated to allow the user to select
 * an area on the screen.
 */
export class MassTransformer {
    static crosshairOverlay;

    static active() {
        return Boolean(this.crosshairOverlay);
    }

    constructor(options = {}) {
        if (options instanceof foundry.canvas.placeables.PlaceableObject) {
            options = { documents: [options] };
        } else if (options instanceof Array) {
            options = { documents: options };
        }

        let {
            docToData = new Map(),
            documents,
            pivotDocument,
            snap = true,
            restrict = null,
            pivot = PIVOTS.CENTER,
            preview = false,
            crosshair = false,
            callback,
            scale = null,
            rotation = null,
            x = null,
            y = null,
        } = options;

        this.callback = callback;
        this._docToData = docToData;
        this._docToDataOriginal = new Map();
        this._restrict = restrict;

        if (documents) this.documents(documents);
        if (pivotDocument) this.pivotDocument(pivotDocument);
        this.pivot(pivot);
        this.snap(snap);
        if (rotation != null) this.rotate(rotation);
        if (scale != null) this.scale(scale);
        if (x != null && y != null) this.position({ x, y });
        if (preview) this.preview();
        if (crosshair) this.crosshair(crosshair instanceof Object ? crosshair : {});
    }

    _copyDocToData() {}

    /**
     * Add documents/placeables to the transformer
     * @param {PlaceableObject | Iterable<PlaceableObject> | Document | Iterable<Document>} documents
     * @returns {MassTransformer}
     */
    documents(documents) {
        if (!(Symbol.iterator in Object(documents))) {
            documents = [documents];
        }

        for (let document of documents) {
            document = document.document ?? document;

            // It is assumed that all documents belong to the same scene
            // let fetch it from one of them
            if (!this._scene) this._scene = document.parent;

            const dataArr = this._docToData.get(document.documentName) ?? [];
            if (!dataArr.find((d) => d._id === document.id)) dataArr.push(this._saveDataOriginal(document));
            this._docToData.set(document.documentName, dataArr);
        }

        return this;
    }

    /**
     * Adds currently selected documents to the transformer
     * @param linked      include placeables linked to the selected
     * @param hardLinked  only hard (SEND) links are considered
     * @returns {MassTransformer}
     */
    selected({ linked = true, hardLinked = true } = {}) {
        const selected = [...canvas.activeLayer.controlled].map((p) => p.document);
        if (!selected.length) return this;
        this.documents(selected);
        if (linked) this.documents(LinkerAPI.getLinkedDocuments(selected, { hardLinked }));
        return this;
    }

    _saveDataOriginal(document) {
        const data = document.toObject();
        const dataArr = this._docToDataOriginal.get(document.documentName) ?? [];
        dataArr.push(foundry.utils.deepClone(data));
        this._docToDataOriginal.set(document.documentName, dataArr);

        return data;
    }

    /**
     * Set a document to be used as a pivot for all transformations.
     * @see pivot
     * @param {PlaceableObject | Document} document
     * @returns {MassTransformer}
     */
    pivotDocument(document) {
        document = document.document ?? document;
        this.documents(document);

        const data = this._docToData.get(document.documentName).find((d) => d._id === document.id);
        this._pivotReferenceDocument = { documentName: document.documentName, data };
        return this;
    }

    /**
     * Set the pivot location.
     * @see PIVOTS
     * @param {string} pivot
     * @returns {MassTransformer}
     */
    pivot(pivot) {
        if (!PIVOTS.hasOwnProperty(pivot)) throw Error('Invalid Pivot');
        this._pivot = Scenescape.active ? PIVOTS.BOTTOM : pivot;
        return this;
    }

    /**
     * Should the data be snapped to the canvas grid
     * @param {boolean} val
     * @returns {MassTransformer}
     */
    snap(val) {
        this._snap = Scenescape.active ? false : val;
        return this;
    }

    /**
     * Rotate data by provided number of degrees
     * @param {number} degrees
     * @returns {MassTransformer}
     */
    rotate(degrees) {
        this.applyTransform({ rotation: degrees }, this.pivotPoint(this._pivot));
        return this;
    }

    /**
     * Raise or lower elevation
     * @param {number} elevation
     * @returns {MassTransformer}
     */
    elevate(elevation) {
        this.applyTransform({ z: elevation }, this.pivotPoint(this._pivot));
        if (MassTransformer._label) {
            MassTransformer._label.text = `[${getPresetDataBounds(this._docToData).elevation.bottom.toFixed(2)}]`;
            MassTransformer._label.anchor.set(1, -2);
        }
        return this;
    }

    /**
     * Scale data by the provided value
     * @param {number} val
     * @returns {MassTransformer}
     */
    scale(val) {
        this.applyTransform({ scale: val }, this.pivotPoint(this._pivot));

        // If we're on a scenescape we want manual changes to the token scale to override the actor defined token size
        if (Scenescape.active) {
            if (this._docToData.get('Token')) {
                this._docToData.get('Token').forEach((d) => {
                    foundry.utils.setProperty(d, `flags.${MODULE_ID}.size`, d.height);
                });
            }
        }
        return this;
    }

    /**
     * Mirrors data on the x-axis around the center pivot
     * @returns {MassTransformer}
     */
    mirrorX() {
        this.applyTransform({ mirrorX: true }, this.pivotPoint(PIVOTS.CENTER));
        return this;
    }

    /**
     * Mirrors data on the y-axis around the center pivot
     * @returns {MassTransformer}
     */
    mirrorY() {
        this.applyTransform({ mirrorY: true }, this.pivotPoint(PIVOTS.CENTER));
        return this;
    }

    /**
     * Position data on the provided coordinates
     * @param {number | Object} x either a numerical x coordinate or an object containing x, y, and/or z coordinates
     * @param {number} y
     * @param {number} z
     * @returns {MassTransformer}
     */
    position(x, y, z) {
        if (x instanceof Object) ({ x, y, z } = x);
        if (x == null && y == null && z == null) throw Error('Invalid position.');
        let pos = { x, y, z };

        // Place the preview pivot point on the provided position
        const b = this._pivotReferenceBounds();
        let pivotPoint = this.pivotPoint(this._pivot);
        let transform = { x: pos.x - pivotPoint.x, y: pos.y - pivotPoint.y, z: 0 };

        if (pos.z != null) {
            transform.z = pos.z - b.elevation.bottom;
        }

        // Snap bounds after transform
        if (
            this._snap &&
            this._layer &&
            !game.keyboard.isModifierActive(foundry.helpers.interaction.KeyboardManager.MODIFIER_KEYS.SHIFT)
        ) {
            const postTransformPos = { x: b.x + transform.x, y: b.y + transform.y };
            const snapped = this._layer.getSnappedPoint(postTransformPos);

            transform.x += snapped.x - postTransformPos.x;
            transform.y += snapped.y - postTransformPos.y;

            // TODO apply this to pivot point too?
        }

        let paraScale;
        if (Scenescape.active && Scenescape.autoScale) {
            const previousParams = Scenescape.getParallaxParameters(pivotPoint);
            const params = Scenescape.getParallaxParameters({ x, y });

            if (previousParams.scale !== params.scale) {
                paraScale = 1 * (params.scale / previousParams.scale);
            }

            // Correct token size by adjusting the scale to match the expected token size at the new parallax parameter
            // TODO: Move into a function?
            const data = this._docToData.get('Token')?.[0];
            if (data) {
                data.height = foundry.utils.getProperty(data, `flags.${MODULE_ID}.height`) ?? data.height;

                const expectedHeight = (Scenescape.getTokenSize(data) / canvas.dimensions.size) * params.scale;
                const actualHeight = paraScale * data.height;
                paraScale *= expectedHeight / actualHeight;
            }

            transform.z = params.elevation - b.elevation.bottom;
            if (this._label) this._label.text = '';
        }

        // - end of transform calculations
        this.applyTransform(transform, pos);
        if (paraScale) this.applyTransform({ scale: paraScale }, pos);

        if (MassTransformer._label) {
            MassTransformer._label.x = pos.x;
            MassTransformer._label.y = pos.y - 38;
        }
        return this;
    }

    applyTransform(transform, origin) {
        origin = origin ?? this.pivotPoint(this._pivot);
        // Apply transformations
        if (this._previews) {
            for (const previewContainer of this._previews) {
                const preview = previewContainer.preview;

                const documentName = previewContainer.documentName;
                DataTransformer.apply(documentName, previewContainer.data, origin, transform, preview);

                // =====
                // Hacks
                // =====
                if (preview) {
                    preview.renderFlags.set({ refresh: true });
                    const doc = preview.document;

                    // Elevation, sort, and z order hacks to make sure previews are always rendered on-top
                    // TODO: improve _meSort, _meElevation
                    if (doc.sort != null) {
                        if (!preview._meSort) preview._meSort = doc.sort;
                        doc.sort = preview._meSort + 10000;
                    }
                    if (!Scenescape.active) {
                        if (!game.Levels3DPreview?._active && doc.elevation != null) {
                            if (!preview._meElevation) preview._meElevation = doc.elevation;
                            doc.elevation = preview._meElevation + 10000;
                        }
                    }
                    // end of sort hacks

                    // For some reason collision bool is refreshed after creation of the preview
                    if (preview._l3dPreview) preview._l3dPreview.collision = false;

                    // Special position update conditions
                    // - Region: We need to simulate doc update via `_onUpdate` call
                    // - AmbientLight and AmbientSound sources need to be re-initialized to have their fields properly rendered
                    switch (documentName) {
                        case 'Region':
                            preview.document._onUpdate({ shapes: null }, { preview: true });
                            break;
                        case 'AmbientLight':
                            preview.initializeLightSource();
                            break;
                        case 'AmbientSound':
                            preview.initializeSoundSource();
                            break;
                        case 'Wall':
                            preview.createDoorMeshes();
                            break;
                    }
                }
                // End of Hacks
            }
        } else {
            DataTransformer.applyToMap(this._docToData, origin, transform);
        }
        return this;
    }

    /**
     * Clear previews
     * @see preview
     */
    destroyPreview(confirm) {
        if (this._previewDocuments) {
            this._previewDocuments.forEach((name) => {
                const layer = canvas.getLayerByEmbeddedName(name);
                if (layer) {
                    if (game.Levels3DPreview?._active) {
                        layer.preview.children.forEach((c) => {
                            c._l3dPreview?.destroy();
                        });
                    }
                    if (name === 'Wall') {
                        // Door meshes for some reason are not cleared via clearPreviewContainer()
                        // lets do it manually ourselves
                        layer.preview.children.forEach((c) => {
                            c.destroyDoorMeshes();
                        });
                    }
                    layer.clearPreviewContainer();
                }
            });
        }

        if (confirm != null) this.callback?.(confirm);

        this._layer = null;
        this._previewDocuments = null;
        this._previews = null;
    }

    /**
     * Create a preview out of documents/data fed in via the constructor or documents(...)
     * @see documents
     * @returns {MassTransformer}
     */
    preview() {
        if (!this._docToData.size)
            throw Error('Cannot enable preview before assigning data/documents to the transformer.');

        this.destroyPreview(null);

        let { previews, previewDocuments } = MassTransformer._genPreviews(this._restrict, this._docToData);
        this._layer =
            previewDocuments.size !== 1 ? canvas.walls : canvas.getLayerByEmbeddedName(previewDocuments.first());
        this._previewDocuments = previewDocuments;
        this._previews = previews;
        return this;
    }

    /**
     * Create a crosshair cursor which continually feed its position via position(...) function
     * @see position
     * @param {Function} callback optional callback function to be called when crosshair is exited out of
     * @returns {MassTransformer}
     */
    crosshair({ callback } = {}) {
        if (callback) this.callback = callback;
        else if (!this.callback)
            this.callback = (confirm) => {
                if (confirm) this.update();
            };

        MassTransformer.destroyCrosshair();
        MassTransformer.createCrosshair({ transformer: this });
        return this;
    }

    static destroyCrosshair() {
        if (this.crosshairOverlay) {
            this.crosshairOverlay.transformer.destroyPreview(false);
            TransformBus.unregister(this.crosshairOverlay.transformer);
            this.crosshairOverlay.parent?.removeChild(this.crosshairOverlay);
            this.crosshairOverlay.destroy(true);
            this.crosshairOverlay.children?.forEach((c) => c.destroy(true));
            this.crosshairOverlay = null;
            this._label = null;
        }
        Mouse3D.deactivate();
    }

    /**
     * Performs document updates using the transformed data
     * @param {object} context
     * @param {Scene} scene
     * @returns {MassTransformer}
     */
    async update(context = {}, scene = this._scene) {
        for (let [documentName, dataArr] of this._docToData.entries()) {
            const originalDataArr = this._docToDataOriginal.get(documentName);
            if (originalDataArr) {
                dataArr = dataArr
                    .map((data) => {
                        const originalData = originalDataArr.find((d) => d._id === data._id);
                        if (originalData) return { _id: data._id, ...foundry.utils.diffObject(originalData, data) };
                        else return data;
                    })
                    .filter((data) => Object.keys(data).length > 1);
            }

            await updateEmbeddedDocumentsViaGM(documentName, dataArr, context, scene);
        }
        return this;
    }

    pivotPoint(pivot) {
        return getPivotPoint(pivot, null, this._pivotReferenceBounds());
    }

    _pivotReferenceBounds() {
        return this._pivotReferenceDocument
            ? getDataBounds(this._pivotReferenceDocument.documentName, this._pivotReferenceDocument.data)
            : getPresetDataBounds(this._docToData);
    }

    static createCrosshair({ transformer } = {}) {
        if (game.Levels3DPreview?._active) {
            Mouse3D.activate({
                transformer,
            });
        } else {
            const crosshairOverlay = new PIXI.Container();
            crosshairOverlay.transformer = transformer;

            let label = new foundry.canvas.containers.PreciseText('', { ...CONFIG.canvasTextStyle, _fontSize: 24 });
            label.anchor.set(0.5, 1);
            this._label = crosshairOverlay.addChild(label);

            crosshairOverlay.on('pointermove', (event) => {
                const client = event.data.client;
                if (
                    client.x !== MassTransformer.crosshairOverlay.lastX ||
                    client.y !== MassTransformer.crosshairOverlay.lastY
                ) {
                    MassTransformer.crosshairOverlay.lastX = client.x;
                    MassTransformer.crosshairOverlay.lastY = client.y;
                    TransformBus.position(canvas.mousePosition);
                }
            });

            crosshairOverlay.hitArea = canvas.dimensions.rect;
            crosshairOverlay.cursor = 'crosshair';
            crosshairOverlay.interactive = true;
            crosshairOverlay.zIndex = 5;
            crosshairOverlay.on('remove', () => crosshairOverlay.off('pick'));
            crosshairOverlay.on('mouseup', (event) => {
                TransformBus.resolve(event.nativeEvent.which !== 2);
                this.destroyCrosshair();
            });

            canvas.stage.addChild(crosshairOverlay);
            this.crosshairOverlay = crosshairOverlay;
            crosshairOverlay.transformer.position(canvas.mousePosition);
        }

        TransformBus.register(transformer);
        TransformBus.position(canvas.mousePosition);
    }

    static resolve(confirm) {
        this.crosshairOverlay?.callback?.(confirm);
        this.destroyCrosshair();
    }

    // Modified Foundry _createPreview
    // Does not throw warning if user lacks document create permissions
    static _createPreview(createData) {
        const documentName = this.constructor.documentName;
        const cls = getDocumentClass(documentName);
        createData._id = foundry.utils.randomID(); // Needed to allow rendering of multiple previews at the same time
        const document = new cls(createData, { parent: canvas.scene });

        const object = new CONFIG[documentName].objectClass(document);
        document._object = object;
        object.eventMode = 'none';
        if (!Scenescape.active) {
            object.document.alpha = 0.4;
            if (object.document.occlusion) object.document.occlusion.alpha = 0.4;
        }
        this.preview.addChild(object);
        object.draw().then(() => {
            // Since we do elevation manipulation to force previews to be rendered on top
            // we don't want the user to see these temporary values
            if (object.tooltip) object.tooltip.renderable = false;
            if (object.controlIcon?.tooltip) object.controlIcon.tooltip.renderable = false;

            object.visible = false;

            // Foundry as well as various modules might have complex `isVisible` and 'visible' conditions
            // lets simplify by overriding this function to make sure the preview is always visible
            MassTransformer._overridePlaceableVisibility(object);

            // 3D Canvas
            if (game.Levels3DPreview?._active) {
                let l3dPreview;
                if (documentName === 'Tile') {
                    game.Levels3DPreview.createTile(object);
                    l3dPreview = game.Levels3DPreview.tiles[object.id];

                    l3dPreview.castShadow = false;
                    l3dPreview.collision = false;
                } else if (documentName === 'Token') {
                    // Tokens get async loaded without a way to await them
                    // We'll need to retrieve the 3D token when the transforms are actually getting applied
                    game.Levels3DPreview.addToken(object);
                    object._l3dPreview = null; // required
                } else if (documentName === 'AmbientLight') {
                    game.Levels3DPreview.addLight(object);
                    l3dPreview = game.Levels3DPreview.lights[object.id];
                }

                if (l3dPreview) {
                    // Disable interactivity with the 3D preview
                    l3dPreview._onClickLeft = () => {};
                    l3dPreview._onClickLeft2 = () => {};
                    l3dPreview._onClickRight = () => {};
                    l3dPreview._onClickRight2 = () => {};

                    object._l3dPreview = l3dPreview;
                }
            }
        });

        return object;
    }

    static _overridePlaceableVisibility(placeable) {
        Object.defineProperty(placeable, 'isVisible', {
            get: function () {
                return true;
            },
            set: function () {},
        });
        Object.defineProperty(placeable, 'visible', {
            get: function () {
                return true;
            },
            set: function () {},
        });
        if (placeable.controlIcon) {
            placeable.controlIcon.alpha = 0.4;
            Object.defineProperty(placeable.controlIcon, 'visible', {
                get: function () {
                    return true;
                },
                set: function () {},
            });
        }
    }

    static _genPreviews(restrict, docToData) {
        const toCreate = [];
        for (const [documentName, dataArr] of docToData.entries()) {
            for (const data of dataArr) {
                toCreate.push({ documentName, data });

                if (documentName === 'Token') {
                    this._genTAPreviews(data, data, toCreate);
                }

                if (documentName === 'Tile' && Scenescape.active) {
                    // TODO: move setting of these fields to a more appropriate spot
                    foundry.utils.setProperty(data, 'restrictions.light', true);
                    foundry.utils.setProperty(data, 'occlusion.alpha', 1);
                }
            }
        }

        const previewDocuments = new Set();
        const previews = [];
        for (const { documentName, data } of toCreate) {
            const previewContainer = { documentName, data };

            if (!restrict?.includes(documentName)) {
                previewContainer.preview = this._createPreview.call(
                    canvas.getLayerByEmbeddedName(documentName),
                    foundry.utils.deepClone(data),
                );
                previewDocuments.add(documentName);
            }

            previews.push(previewContainer);
        }
        return { previews, previewDocuments };
    }

    static _genTAPreviews(data, parent, toCreate) {
        if (!game.modules.get('token-attacher')?.active) return;

        const attached = foundry.utils.getProperty(data, 'flags.token-attacher.prototypeAttached');
        const pos = foundry.utils.getProperty(data, 'flags.token-attacher.pos');
        const grid = foundry.utils.getProperty(data, 'flags.token-attacher.grid');

        if (!(attached && pos && grid)) return;

        const ratio = canvas.grid.size / grid.size;

        for (const [name, dataList] of Object.entries(attached)) {
            for (let data of dataList) {
                if (['Token', 'Tile', 'Drawing'].includes(name)) {
                    data.width *= ratio;
                    data.height *= ratio;
                }

                if (name === 'Wall') {
                    data.c[0] = parent.x + (data.c[0] - pos.xy.x) * ratio;
                    data.c[1] = parent.y + (data.c[1] - pos.xy.y) * ratio;
                    data.c[2] = parent.x + (data.c[2] - pos.xy.x) * ratio;
                    data.c[3] = parent.y + (data.c[3] - pos.xy.y) * ratio;
                } else if (name === 'Region') {
                    data.shapes?.forEach((shape) => {
                        if (shape.type === 'polygon') {
                            for (let i = 0; i < shape.points.length; i += 2) {
                                shape.points[i] = parent.x + (shape.points[i] - pos.xy.x) * ratio;
                                shape.points[i + 1] = parent.y + (shape.points[i + 1] - pos.xy.y) * ratio;
                            }
                        } else {
                            shape.x = parent.x + (shape.x - pos.xy.x) * ratio;
                            shape.y = parent.y + (shape.y - pos.xy.y) * ratio;
                            if (shape.type === 'ellipse') {
                                shape.radiusX *= ratio;
                                shape.radiusY *= ratio;
                            } else if (shape.type === 'rectangle') {
                                shape.width *= ratio;
                                shape.height *= ratio;
                            }
                        }
                    });
                } else {
                    data.x = parent.x + (data.x - pos.xy.x) * ratio;
                    data.y = parent.y + (data.y - pos.xy.y) * ratio;
                }

                toCreate.push({ documentName: name, data });
            }
        }
    }
}

export async function editPreviewPlaceables(
    { placeables, callback = null, mainPlaceable = null, hardLinked = false } = {},
    context = {},
) {
    const controlled = new Set();
    let hoveredDocument = mainPlaceable?.document;

    if (placeables?.length) {
        placeables.forEach((p) => {
            controlled.add(p.document);
            LinkerAPI.getLinkedDocuments(p.document, { hardLinked }).forEach((d) => controlled.add(d));
        });
    } else {
        SUPPORTED_PLACEABLES.forEach((documentName) => {
            canvas.getLayerByEmbeddedName(documentName).controlled.forEach((p) => {
                controlled.add(p.document);
                LinkerAPI.getLinkedDocuments(p.document, { hardLinked }).forEach((d) => controlled.add(d));
            });
            const hover = canvas.getLayerByEmbeddedName(documentName).hover;
            if (hover) {
                if (!hoveredDocument) hoveredDocument = hover.document;
                controlled.add(hover.document);
                LinkerAPI.getLinkedDocuments(hover.document, { hardLinked }).forEach((d) => controlled.add(d));
            }
        });
    }

    if (!controlled.size && !game.Levels3DPreview?._active) {
        const pickerSelected = await pickerSelectMultiLayerDocuments();
        pickerSelected.forEach((d) => controlled.add(d));
    }

    if (!controlled.size) return false;

    const transformer = new MassTransformer({ documents: controlled, pivotDocument: hoveredDocument })
        .snap(true)
        .pivot(PIVOTS.CENTER)
        .preview()
        .crosshair({
            callback: async (confirm) => {
                if (!confirm) return callback?.();
                await transformer.update({
                    ignoreLinks: true,
                    animate: false,
                    scenescape: Scenescape.active,
                    ...context,
                });

                callback?.(confirm);
            },
        });

    return true;
}
