import { NEEDLE_progressive } from "@needle-tools/gltf-progressive";
import { Euler, Material, Matrix4, Mesh, Object3D, Quaternion, Vector3 } from "three";

import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../../../engine/debug/index.js";
import { hasProLicense } from "../../../engine/engine_license.js";
import { serializable } from "../../../engine/engine_serialization.js";
import { getFormattedDate, Progress } from "../../../engine/engine_time_utils.js";
import { DeviceUtilities, getParam } from "../../../engine/engine_utils.js";
import { WebXRButtonFactory } from "../../../engine/webcomponents/WebXRButtons.js";
import { InternalUSDZRegistry } from "../../../engine/xr/usdz.js"
import { InstancingHandler } from "../../../engine-components/RendererInstancing.js";
import { Collider } from "../../Collider.js";
import { Behaviour, GameObject } from "../../Component.js";
import { ContactShadows } from "../../ContactShadows.js";
import { EventList } from "../../EventList.js";
import { GroundProjectedEnv } from "../../GroundProjection.js";
import { Renderer } from "../../Renderer.js"
import { Rigidbody } from "../../RigidBody.js";
import { SpriteRenderer } from "../../SpriteRenderer.js";
import { WebARSessionRoot } from "../../webxr/WebARSessionRoot.js";
import { WebXR } from "../../webxr/WebXR.js";
import { XRState, XRStateFlag } from "../../webxr/XRFlag.js";
import type { IUSDExporterExtension } from "./Extension.js";
import { AnimationExtension } from "./extensions/Animation.js"
import { AudioExtension } from "./extensions/behavior/AudioExtension.js";
import { BehaviorExtension } from "./extensions/behavior/Behaviour.js";
import { PhysicsExtension } from "./extensions/behavior/PhysicsExtension.js"
import { TextExtension } from "./extensions/USDZText.js";
import { USDZUIExtension } from "./extensions/USDZUI.js";
import { USDZExporter as ThreeUSDZExporter } from "./ThreeUSDZExporter.js";
import { disableObjectsAtStart, registerAnimatorsImplictly, registerAudioSourcesImplictly } from "./utils/animationutils.js";
import { ensureQuicklookLinkIsCreated } from "./utils/quicklook.js";

const debug = getParam("debugusdz");
const debugUsdzPruning = getParam("debugusdzpruning");

/**
 * Custom branding for the QuickLook overlay, used by {@link USDZExporter}.
 */
export class CustomBranding {
    /** The call to action button text. If not set, the button will close the QuickLook overlay. */
    @serializable()
    callToAction?: string;
    /** The title of the overlay. */
    @serializable()
    checkoutTitle?: string;
    /** The subtitle of the overlay. */
    @serializable()
    checkoutSubtitle?: string;

    /** if assigned the call to action button in quicklook will open the URL. Otherwise it will just close quicklook. */
    @serializable()
    callToActionURL?: string;
}

type BeforeLODExportArguments = {
    readonly exporter: USDZExporter;
    /** The type of LOD being exported, either a texture or a mesh */
    readonly type: "texture" | "mesh";
    /** The Renderer gameobject */
    readonly object: Object3D;
    /** The mesh being exported, if applicable */
    readonly mesh?: Mesh;
    /** The material being exported, if applicable */
    readonly material?: Material;
    /** By default LOD 0 will be exported (highest quality). 
     * Set to a different LOD level if needed. 
     * Set to -1 to disable LOD export and simply export the current version.
    */
    overrideLevel?: number;
}

/**
 * USDZExporter creates USDZ files and opens them in Apple QuickLook on iOS/iPadOS/visionOS.  
 * Enables "View in AR" functionality for Apple devices directly from web experiences.  
 *
 * **Key features:**  
 * - Auto-exports animations and audio sources  
 * - Interactive behaviors via Needle's "Everywhere Actions" system  
 * - RealityKit physics support (iOS 18+, visionOS 1+)
 * - Custom QuickLook overlay with call-to-action buttons
 * - Progressive texture/mesh LOD handling
 * 
 * [![](https://cloud.needle.tools/-/media/YyUz1lUVlOhEY4fWZ-oMsA.gif)](https://engine.needle.tools/samples/?overlay=samples&tag=usdz)
 *
 * **Automatic setup:**  
 * - Creates QuickLook button on compatible devices
 * - Respects {@link XRFlag} for AR-specific visibility
 * - Handles {@link WebARSessionRoot} scale
 *
 * **Custom extensions:**  
 * Add custom behaviors by implementing {@link IUSDExporterExtension} and adding to {@link extensions} array.  
 * 
 * **Debug:** Use `?debugusdz` URL parameter. Press 'T' to trigger export.  
 *
 * @example Basic USDZ export  
 * ```ts
 * const exporter = myObject.addComponent(USDZExporter);
 * exporter.objectToExport = productModel;
 * exporter.autoExportAnimations = true;
 * exporter.interactive = true; // Enable QuickLook behaviors
 *
 * // Trigger export
 * await exporter.exportAndOpen();
 * ```
 *
 * @example Custom branding  
 * ```ts
 * exporter.customBranding = {
 *   callToAction: "Buy Now",
 *   checkoutTitle: "Product Name",
 *   callToActionURL: "https://shop.example.com"
 * };
 * ```
 *
 * @summary Export 3D objects as USDZ files for Apple QuickLook AR
 * @category XR
 * @group Components
 * @see {@link WebXR} for WebXR-based AR/VR
 * @see {@link WebARSessionRoot} for AR placement and scaling
 * @see {@link XRFlag} for AR-specific object visibility
 * @see {@link CustomBranding} for QuickLook overlay customization  
 * @link https://engine.needle.tools/samples/?overlay=samples&tag=usdz
 */
export class USDZExporter extends Behaviour {

    /** Called before the USDZ file is exported */
    static readonly beforeExport = new EventList<{ readonly exporter: USDZExporter }>();
    /** Called after the USDZ file has been exported */
    static readonly afterExport = new EventList<{ readonly exporter: USDZExporter }>();
    /** Called before LOD level are exported. Can be used to override the LOD export settings */
    static readonly beforeLODExport = new EventList<BeforeLODExportArguments>();

    /**
     * Assign the object to export as USDZ file. If undefined or null, the whole scene will be exported.
     */
    @serializable(Object3D)
    objectToExport: Object3D | null | undefined = undefined;

    /** Collect all Animations/Animators automatically on export and emit them as playing at the start.
     * Animator state chains and loops will automatically be collected and exported in order as well.
     * If this setting is off, Animators need to be registered by components – for example from PlayAnimationOnClick.
    */
    @serializable()
    autoExportAnimations: boolean = true;

    /** Collect all AudioSources automatically on export and emit them as playing at the start.
     * They will loop according to their settings. 
     * If this setting is off, Audio Sources need to be registered by components – for example from PlayAudioOnClick.
    */
    @serializable()
    autoExportAudioSources: boolean = true;

    @serializable()
    exportFileName: string | null | undefined = undefined;

    @serializable(URL)
    customUsdzFile: string | null | undefined = undefined;

    @serializable(CustomBranding)
    customBranding?: CustomBranding;

    // Currently not exposed to integrations - not fully tested. Set from code (e.g. image tracking)
    @serializable()
    anchoringType: "plane" | "image" | "face" | "none" = "plane";

    @serializable()
    maxTextureSize: 256 | 512 | 1024 | 2048 | 4096 | 8192 = 2048;

    // Currently not exposed to integrations - not fully tested. Set from code (e.g. image tracking)
    @serializable()
    planeAnchoringAlignment: "horizontal" | "vertical" | "any" = "horizontal";

    /** Enabling this option will export QuickLook-specific preliminary behaviours along with the USDZ files.
     * These extensions are only supported on QuickLook on iOS/visionOS/MacOS. 
     * Keep this option off for general USDZ usage.
     */
    @serializable()
    interactive: boolean = true;

    /** Enabling this option will export the USDZ file with RealityKit physics components. 
     * Rigidbody and Collider components will be converted to their RealityKit counterparts.
     * Physics are supported on QuickLook in iOS 18+ and VisionOS 1+. 
     * Physics export is automatically turned off when there are no Rigidbody components anywhere on the exported object.
     */
    @serializable()
    physics: boolean = true;

    @serializable()
    allowCreateQuicklookButton: boolean = true;

    @serializable()
    quickLookCompatible: boolean = true;

    /**
     * Extensions to add custom behaviors and interactions to the USDZ file.   
     * You can add your own extensions here by extending {@link IUSDExporterExtension}.
     */
    extensions: IUSDExporterExtension[] = [];

    private link!: HTMLAnchorElement;
    private button?: HTMLButtonElement;

    /** @internal */
    start() {
        if (debug) {
            console.log("USDZExporter", this);
            console.log("Debug USDZ Mode. Press 'T' to export")
            window.addEventListener("keydown", (evt) => {
                switch (evt.key) {
                    case "t":
                        this.exportAndOpen();
                        break;
                }
            });
        }

        // fall back to this object or to the scene if it's empty and doesn't have a mesh
        if (!this.objectToExport)
            this.objectToExport = this.gameObject;
        if (!this.objectToExport?.children?.length && !(this.objectToExport as Mesh)?.isMesh)
            this.objectToExport = this.context.scene;
    }

    /** @internal */
    onEnable() {
        const supportsQuickLook = DeviceUtilities.supportsQuickLookAR();
        const ios = DeviceUtilities.isiOS() || DeviceUtilities.isiPad();
        if (!this.button && (debug || supportsQuickLook || ios)) {
            if (this.allowCreateQuicklookButton)
                this.button = this.createQuicklookButton();

            this.lastCallback = this.quicklookCallback.bind(this);
            this.link = ensureQuicklookLinkIsCreated(this.context, supportsQuickLook);
            this.link.addEventListener('message', this.lastCallback);
        }
        if (debug)
            showBalloonMessage("USDZ Exporter enabled: " + this.name);

        document.getElementById("open-in-ar")?.addEventListener("click", this.onClickedOpenInARElement);
        InternalUSDZRegistry.registerExporter(this);
    }

    /** @internal */
    onDisable() {
        this.button?.remove();
        this.link?.removeEventListener('message', this.lastCallback);
        if (debug)
            showBalloonMessage("USDZ Exporter disabled: " + this.name);

        document.getElementById("open-in-ar")?.removeEventListener("click", this.onClickedOpenInARElement);
        InternalUSDZRegistry.unregisterExporter(this);
    }

    private onClickedOpenInARElement = (evt) => {
        evt.preventDefault();
        this.exportAndOpen();
    }

    /**
     * Creates an USDZ file from the current scene or assigned objectToExport and opens it in QuickLook.
     * Use the various public properties of USDZExporter to customize export behaviour.
     * @deprecated use {@link exportAndOpen} instead
     */
    async exportAsync() {
        return this.exportAndOpen();
    }

    /**
     * Creates an USDZ file from the current scene or assigned objectToExport and opens it in QuickLook.
     * @returns a Promise<Blob> containing the USDZ file
     */
    async exportAndOpen(): Promise<Blob | null> {

        let name = this.exportFileName ?? this.objectToExport?.name ?? this.name;
        name += "-" + getFormattedDate(); // seems iOS caches the file in some cases, this ensures we always have a fresh file

        if (!hasProLicense()) {
            if (name !== "") name += "-";
            name += "MadeWithNeedle";
        }

        if (!this.link) this.link = ensureQuicklookLinkIsCreated(this.context, DeviceUtilities.supportsQuickLookAR());

        // ability to specify a custom USDZ file to be used instead of a dynamic one
        if (this.customUsdzFile) {
            if (debug) console.log("Exporting custom usdz", this.customUsdzFile)
            this.openInQuickLook(this.customUsdzFile, name);
            return null;
        }

        if (!this.objectToExport) {
            console.warn("No object to export", this);
            return null;
        }

        USDZExporter.beforeExport.invoke({ exporter: this });
        const blob = await this.export(this.objectToExport)
            .finally(() => {
                USDZExporter.afterExport.invoke({ exporter: this });
            });

        if (!blob) {
            console.error("USDZ generation failed. Please report a bug", this);
            return null;
        }

        if (debug) console.log("USDZ generation done. Downloading as " + name);

        // TODO Potentially we have to detect QuickLook availability here,
        // and download the file instead. But browsers keep changing how they deal with non-user-initiated downloads...
        // https://webkit.org/blog/8421/viewing-augmented-reality-assets-in-safari-for-ios/#:~:text=inside%20the%20anchor.-,Feature%20Detection,-To%20detect%20support
        /*
        if (!DeviceUtilities.supportsQuickLookAR())
            this.download(blob, name);
        else
        */
        this.openInQuickLook(blob, name);

        return blob;
    }

    /**
     * Creates an USDZ file from the current scene or assigned objectToExport and opens it in QuickLook.
     * @returns a Promise<Blob> containing the USDZ file
     */
    async export(objectToExport: Object3D | undefined): Promise<Blob | null> {
        // make sure we have an object to export
        if (!objectToExport) {
            console.warn("No object to export");
            return null;
        }

        // if we are already exporting, wait for the current export to finish
        const taskForThisObject = this._currentExportTasks.get(objectToExport);
        if (taskForThisObject) {
            return taskForThisObject;
        }

        // start the export
        const task = this.internalExport(objectToExport);
        // store the task
        if (task instanceof Promise) {
            this._currentExportTasks.set(objectToExport, task);
            return task.then((blob) => {
                this._currentExportTasks.delete(objectToExport);
                return blob;
            }).catch((e) => {
                this._currentExportTasks.delete(objectToExport);
                console.error("Error during USDZ export – please report a bug!", e);
                return null;
            });
        }

        return task;
    }

    private readonly _currentExportTasks = new Map<Object3D, Promise<Blob | null>>();
    private _previousTimeScale: number = 1;

    private async internalExport(objectToExport: Object3D): Promise<Blob | null> {

        Progress.start("export-usdz", {
            onProgress: (progress) => {
                this.dispatchEvent(new CustomEvent("export-progress", { detail: { progress } }));
            }
        });
        Progress.report("export-usdz", { message: "Starting export", totalSteps: 40, currentStep: 0 });
        Progress.report("export-usdz", { message: "Load progressive textures", autoStep: 5 });
        Progress.start("export-usdz-textures", "export-usdz");

        // force Sprites to be created
        const sprites = GameObject.getComponentsInChildren(objectToExport, SpriteRenderer);
        for (const sprite of sprites) {
            if (sprite && sprite.enabled) {
                sprite.updateSprite(true); // force create
            }
        }

        // trigger progressive textures to be loaded:
        const renderers = GameObject.getComponentsInChildren(objectToExport, Renderer);
        const progressiveLoading = new Array<Promise<any>>();
        let progressiveTasks = 0;
        // TODO: it would be better to directly integrate this into the exporter and *on export* request the correct LOD level for textures and meshes instead of relying on the renderer etc
        for (const rend of renderers) {
            for (const mesh of rend.sharedMeshes) {
                if (mesh) {
                    let lodLevel = 0;
                    const args: BeforeLODExportArguments = { exporter: this, type: "mesh" as const, object: rend.gameObject, mesh: mesh, };
                    USDZExporter.beforeLODExport.invoke(args);
                    if (args.overrideLevel !== undefined) {
                        if (args.overrideLevel === -1) {
                            if (debug) console.warn("Skipping LOD export for mesh due to overrideLevel -1", rend.gameObject, mesh);
                            continue; // skip LOD assignment
                        }
                        else if (args.overrideLevel >= 0) {
                            lodLevel = args.overrideLevel;
                            if (debug) console.log("Overriding LOD level for mesh export to level " + lodLevel + " " + mesh.name);
                        }
                    }
                    const task = NEEDLE_progressive.assignMeshLOD(mesh, lodLevel);
                    if (task instanceof Promise)
                        progressiveLoading.push(new Promise<void>((resolve, reject) => {
                            task.then(() => {
                                progressiveTasks++;
                                Progress.report("export-usdz-textures", { message: "Loaded progressive mesh", currentStep: progressiveTasks, totalSteps: progressiveLoading.length });
                                resolve();
                            }).catch((err) => reject(err));
                        }));
                }
            }
            for (const mat of rend.sharedMaterials) {
                if (mat) {
                    let lodLevel = 0;
                    const args: BeforeLODExportArguments = { exporter: this, type: "texture" as const, object: rend.gameObject, material: mat };
                    USDZExporter.beforeLODExport.invoke(args);
                    if (args.overrideLevel !== undefined) {
                        if (args.overrideLevel === -1) {
                            if (debug) console.warn("Skipping LOD assignment due to overrideLevel -1", rend.gameObject, mat);
                            continue; // skip LOD assignment
                        }
                        else if (args.overrideLevel >= 0) {
                            lodLevel = args.overrideLevel;
                            if (debug) console.log("Overriding LOD level for texture export to level " + lodLevel + " " + mat.name);
                        }
                    }
                    const task = NEEDLE_progressive.assignTextureLOD(mat, lodLevel);
                    if (task instanceof Promise)
                        progressiveLoading.push(new Promise<void>((resolve, reject) => {
                            task.then(() => {
                                progressiveTasks++;
                                Progress.report("export-usdz-textures", { message: "Loaded progressive texture", currentStep: progressiveTasks, totalSteps: progressiveLoading.length });
                                resolve();
                            }).catch((err) => reject(err));
                        }));
                }
            }
        }
        if (debug) showBalloonMessage("Progressive Loading: " + progressiveLoading.length);
        await Promise.all(progressiveLoading);
        if (debug) showBalloonMessage("Progressive Loading: done");
        Progress.end("export-usdz-textures");

        // apply XRFlags
        const currentXRState = XRState.Global.Mask;
        XRState.Global.Set(XRStateFlag.AR);

        const exporter = new ThreeUSDZExporter();
        // We're creating a new animation extension on each export to avoid issues with multiple exports.
        // TODO we probably want to do that with all the extensions...
        // Ordering of extensions is important
        const animExt = new AnimationExtension(this.quickLookCompatible);
        let physicsExt: PhysicsExtension | undefined = undefined;
        const defaultExtensions: IUSDExporterExtension[] = [];
        if (this.interactive) {
            defaultExtensions.push(new BehaviorExtension());
            defaultExtensions.push(new AudioExtension());

            // If physics are enabled, and there are any Rigidbody components in the scene,
            // add the PhysicsExtension to the default extensions.
            if (globalThis["NEEDLE_USE_RAPIER"]) {
                const rigidbodies = GameObject.getComponentsInChildren(objectToExport, Rigidbody);
                if (rigidbodies.length > 0) {
                    if (this.physics) {
                        physicsExt = new PhysicsExtension();
                        defaultExtensions.push(physicsExt);
                    }
                    else if (isDevEnvironment()) {
                        console.warn("USDZExporter: Physics export is disabled, but there are active Rigidbody components in the scene. They will not be exported.");
                    }
                }
            }
            defaultExtensions.push(new TextExtension());
            defaultExtensions.push(new USDZUIExtension());
        }
        const extensions: any = [animExt, ...defaultExtensions, ...this.extensions];

        const eventArgs = { self: this, exporter: exporter, extensions: extensions, object: objectToExport };
        Progress.report("export-usdz", "Invoking before-export");
        this.dispatchEvent(new CustomEvent("before-export", { detail: eventArgs }));

        // make sure we apply the AR scale
        this.applyWebARSessionRoot();

        // freeze time
        this._previousTimeScale = this.context.time.timeScale;
        this.context.time.timeScale = 0;

        // Implicit registration and actions for Animators and Animation components
        // Currently, Animators properly build PlayAnimation actions, but Animation components don't.

        Progress.report("export-usdz", "auto export animations and audio sources");
        const implicitBehaviors = new Array<Object3D>();
        if (this.autoExportAnimations) {
            implicitBehaviors.push(...registerAnimatorsImplictly(objectToExport, animExt));
        }
        const audioExt = extensions.find(ext => ext.extensionName === "Audio");
        if (audioExt && this.autoExportAudioSources)
            implicitBehaviors.push(...registerAudioSourcesImplictly(objectToExport, audioExt as AudioExtension));

        //@ts-ignore
        exporter.debug = debug;
        exporter.pruneUnusedNodes = !debugUsdzPruning;
        const instancedRenderers = InstancingHandler.instance.objs.map(x => x.batchedMesh);
        exporter.keepObject = (object) => {
            let keep = true;
            // This explicitly removes geometry and material data from disabled renderers.
            // Note that this is different to the object itself being active –
            // here, we have an active object with a disabled renderer.
            const renderer = GameObject.getComponent(object, Renderer);
            if (renderer && !renderer.enabled) keep = false;
            // Check if this is an instancing container.
            // Instances are already included in the export.
            if (keep && instancedRenderers.includes(object as any)) keep = false;
            if (keep && GameObject.getComponentInParent(object, ContactShadows)) keep = false;
            if (keep && GameObject.getComponentInParent(object, GroundProjectedEnv)) keep = false;
            if (debug && !keep) console.log("USDZExporter: Discarding object", object);
            return keep;
        }

        exporter.beforeWritingDocument = () => {
            // Warn if there are any physics components on animated objects or their children
            if (isDevEnvironment() && animExt && physicsExt) {
                const animatedObjects = animExt.animatedRoots;
                for (const object of animatedObjects) {
                    const rigidBodySources = GameObject.getComponentsInChildren(object, Rigidbody).filter(c => c.enabled);
                    const colliderSources = GameObject.getComponents(object, Collider).filter(c => c.enabled && !c.isTrigger);
                    if (rigidBodySources.length > 0 || colliderSources.length > 0) {
                        console.error("An animated object has physics components in its child hierarchy. This can lead to undefined behaviour due to a bug in Apple's QuickLook (FB15925487). Remove the physics components from child objects or verify that you get the expected results.", object);
                    }
                }
            }
        };

        // Collect invisible objects so that we can disable them if 
        // - we're exporting for QuickLook
        // - and interactive behaviors are allowed. 
        // When exporting for regular USD, we're supporting the "visibility" attribute anyways.
        const objectsToDisableAtSceneStart = new Array<Object3D>();
        if (this.objectToExport && this.quickLookCompatible && this.interactive) {
            this.objectToExport.traverse((obj) => {
                if (!obj.visible) {
                    objectsToDisableAtSceneStart.push(obj);
                }
            });
        }

        const behaviorExt = extensions.find(ext => ext.extensionName === "Behaviour") as BehaviorExtension | undefined;
        if (this.interactive && behaviorExt && objectsToDisableAtSceneStart.length > 0) {
            behaviorExt.addBehavior(disableObjectsAtStart(objectsToDisableAtSceneStart));
        }

        let exportInvisible = true;
        // The only case where we want to strip out invisible objects is 
        // when we're exporting for QuickLook and we're NOT adding interactive behaviors,
        // since QuickLook on iOS does not support "visibility" tokens.
        if (this.quickLookCompatible && !this.interactive)
            exportInvisible = false;

        // sanitize anchoring types
        if (this.anchoringType !== "plane" && this.anchoringType !== "none" && this.anchoringType !== "image" && this.anchoringType !== "face")
            this.anchoringType = "plane";
        if (this.planeAnchoringAlignment !== "horizontal" && this.planeAnchoringAlignment !== "vertical" && this.planeAnchoringAlignment !== "any")
            this.planeAnchoringAlignment = "horizontal";

        Progress.report("export-usdz", "Invoking exporter.parse");

        const arraybuffer = await exporter.parse(this.objectToExport, {
            ar: {
                anchoring: {
                    type: this.anchoringType,
                },
                planeAnchoring: {
                    alignment: this.planeAnchoringAlignment,
                },
            },
            extensions: extensions,
            quickLookCompatible: this.quickLookCompatible,
            maxTextureSize: this.maxTextureSize,
            exportInvisible: exportInvisible,
        });

        const blob = new Blob([arraybuffer as unknown as BlobPart], { type: 'model/vnd.usdz+zip' });

        this.revertWebARSessionRoot();

        // unfreeze time
        this.context.time.timeScale = this._previousTimeScale;

        Progress.report("export-usdz", "Invoking after-export");

        this.dispatchEvent(new CustomEvent("after-export", { detail: eventArgs }))

        // cleanup – implicit animation behaviors need to be removed again
        for (const go of implicitBehaviors) {
            GameObject.destroy(go);
        }

        // restore XR flags
        XRState.Global.Set(currentXRState);

        // second file: USDA (without assets)
        //@ts-ignore
        // const usda = exporter.lastUsda;
        // const blob2 = new Blob([usda], { type: 'text/plain' });
        // this.link.download = name + ".usda";
        // this.link.href = URL.createObjectURL(blob2);
        // this.link.click();

        Progress.end("export-usdz");
        return blob;
    }

    /**
     * Opens QuickLook on iOS/iPadOS/visionOS with the given content in AR mode.
     * @param content The URL to the .usdz or .reality file or a blob containing an USDZ file.
     * @param name Download filename
     */
    openInQuickLook(content: Blob | string, name: string) {

        const url = content instanceof Blob ? URL.createObjectURL(content) : content;

        // see https://developer.apple.com/documentation/arkit/adding_an_apple_pay_button_or_a_custom_action_in_ar_quick_look
        const overlay = this.buildQuicklookOverlay();
        if (debug) console.log("QuickLook Overlay", overlay);
        const callToAction = overlay.callToAction ? encodeURIComponent(overlay.callToAction) : "";
        const checkoutTitle = overlay.checkoutTitle ? encodeURIComponent(overlay.checkoutTitle) : "";
        const checkoutSubtitle = overlay.checkoutSubtitle ? encodeURIComponent(overlay.checkoutSubtitle) : "";
        this.link.href = url + `#callToAction=${callToAction}&checkoutTitle=${checkoutTitle}&checkoutSubtitle=${checkoutSubtitle}&callToActionURL=${overlay.callToActionURL}`;

        if (!this.lastCallback) {
            this.lastCallback = this.quicklookCallback.bind(this)
            this.link.addEventListener('message', this.lastCallback);
        }

        // Open QuickLook
        this.link.download = name + ".usdz";
        this.link.click();

        // cleanup 
        // TODO check if we can do that immediately or need to wait until the user returns
        // if (content instanceof Blob) URL.revokeObjectURL(url);
    }

    /**
     * Downloads the given blob as a file.
     */
    download(blob: Blob, name: string) {
        USDZExporter.save(blob, name);
    }

    // Matches GltfExport.save(blob, filename)
    private static save(blob, filename) {
        const link = document.createElement('a');
        link.style.display = 'none';
        document.body.appendChild(link); // Firefox workaround, see #6594
        if (typeof blob === "string")
            link.href = blob;
        else
            link.href = URL.createObjectURL(blob);
        link.download = filename;
        link.click();
        link.remove();
        // console.log(link.href);
        // URL.revokeObjectURL( url ); breaks Firefox...
    }


    private lastCallback?: any;

    private quicklookCallback(event: Event) {
        if ((event as any)?.data == '_apple_ar_quicklook_button_tapped') {
            if (debug) showBalloonWarning("Quicklook closed via call to action button");
            var evt = new CustomEvent("quicklook-button-tapped", { detail: this });
            this.dispatchEvent(evt);
            if (!evt.defaultPrevented) {
                const url = new URLSearchParams(this.link.href);
                if (url) {
                    const callToActionURL = url.get("callToActionURL");
                    if (debug)
                        showBalloonMessage("Quicklook url: " + callToActionURL);
                    if (callToActionURL) {
                        if (!hasProLicense()) {
                            console.warn("Quicklook closed: custom redirects require a Needle Engine Pro license: https://needle.tools/pricing", callToActionURL)
                        }
                        else {
                            globalThis.open(callToActionURL, "_blank");
                        }
                    }
                }
            }
        }
    }

    private buildQuicklookOverlay(): CustomBranding {
        const obj: CustomBranding = {};
        if (this.customBranding) Object.assign(obj, this.customBranding);
        if (!hasProLicense()) {
            console.log("Custom Quicklook banner text requires pro license: https://needle.tools/pricing");
            obj.callToAction = "Close";
            obj.checkoutTitle = "🌵 Made with Needle";
            obj.checkoutSubtitle = "_";
        }
        const needsDefaultValues = obj.callToAction?.length || obj.checkoutTitle?.length || obj.checkoutSubtitle?.length;
        if (needsDefaultValues) {
            if (!obj.callToAction?.length)
                obj.callToAction = "\0";
            if (!obj.checkoutTitle?.length)
                obj.checkoutTitle = "\0";
            if (!obj.checkoutSubtitle?.length)
                obj.checkoutSubtitle = "\0";
        }
        // Use the quicklook-overlay event to customize the overlay
        this.dispatchEvent(new CustomEvent("quicklook-overlay", { detail: obj }));
        return obj;
    }

    private static invertForwardMatrix = new Matrix4().makeRotationY(Math.PI);
    private static invertForwardQuaternion = new Quaternion().setFromEuler(new Euler(0, Math.PI, 0));

    private _rootSessionRootWasAppliedTo: Object3D | null = null;
    private _rootPositionBeforeExport: Vector3 = new Vector3();
    private _rootRotationBeforeExport: Quaternion = new Quaternion();
    private _rootScaleBeforeExport: Vector3 = new Vector3();

    getARScaleAndTarget(): { scale: number, _invertForward: boolean, target: Object3D, sessionRoot: Object3D | null } {
        if (!this.objectToExport) return { scale: 1, _invertForward: false, target: this.gameObject, sessionRoot: null };

        const xr = GameObject.findObjectOfType(WebXR);
        let sessionRoot = GameObject.getComponentInParent(this.objectToExport, WebARSessionRoot);
        if (!sessionRoot) sessionRoot = GameObject.getComponentInChildren(this.objectToExport, WebARSessionRoot);

        let arScale = 1;
        let _invertForward = false;
        const target = this.objectToExport;

        // Note: when USDZ is exported, SessionRoot might not have the correct AR scale, since that is populated upon EnterXR from WebXR.
        if (xr) {
            arScale = xr.arScale;
        }
        else if (sessionRoot) {
            arScale = sessionRoot.arScale;
            // eslint-disable-next-line @typescript-eslint/no-deprecated
            _invertForward = sessionRoot.invertForward;
        }

        const scale = 1 / arScale;
        const result = { scale, _invertForward, target, sessionRoot: sessionRoot?.gameObject ?? null };
        return result;
    }

    private applyWebARSessionRoot() {
        if (!this.objectToExport) return;

        const { scale, _invertForward, target, sessionRoot } = this.getARScaleAndTarget();
        const sessionRootMatrixWorld = sessionRoot?.matrixWorld.clone().invert();

        this._rootSessionRootWasAppliedTo = target;
        this._rootPositionBeforeExport.copy(target.position);
        this._rootRotationBeforeExport.copy(target.quaternion);
        this._rootScaleBeforeExport.copy(target.scale);

        target.scale.multiplyScalar(scale);
        if (_invertForward)
            target.quaternion.multiply(USDZExporter.invertForwardQuaternion);

        // udate childs as well
        target.updateMatrix();
        target.updateMatrixWorld(true);
        if (sessionRoot && sessionRootMatrixWorld)
            target.matrix.premultiply(sessionRootMatrixWorld);
    }

    private revertWebARSessionRoot() {
        if (!this.objectToExport) return;
        if (!this._rootSessionRootWasAppliedTo) return;

        const target = this._rootSessionRootWasAppliedTo;
        target.position.copy(this._rootPositionBeforeExport);
        target.quaternion.copy(this._rootRotationBeforeExport);
        target.scale.copy(this._rootScaleBeforeExport);

        // udate childs as well
        target.updateMatrix();
        target.updateMatrixWorld(true);
        this._rootSessionRootWasAppliedTo = null;
    }

    private createQuicklookButton() {
        const buttoncontainer = WebXRButtonFactory.getOrCreate();
        const button = buttoncontainer.createQuicklookButton();
        if (!button.parentNode) this.context.menu.appendChild(button);
        return button;
    }
}
