import { Object3D } from "three";

import { isDevEnvironment, showBalloonWarning } from "../../engine/debug/index.js";
import { AssetReference } from "../../engine/engine_addressables.js";
import { findObjectOfType } from "../../engine/engine_components.js";
import { serializable } from "../../engine/engine_serialization.js";
import { delayForFrames, DeviceUtilities, getParam } from "../../engine/engine_utils.js";
import { type NeedleXREventArgs, NeedleXRSession } from "../../engine/engine_xr.js";
import { ButtonsFactory } from "../../engine/webcomponents/buttons.js";
import { WebXRButtonFactory } from "../../engine/webcomponents/WebXRButtons.js";
import { PlayerSync } from "../../engine-components-experimental/networking/PlayerSync.js";
import { Behaviour, GameObject } from "../Component.js";
import { USDZExporter } from "../export/usdz/USDZExporter.js";
import { NeedleMenu } from "../NeedleMenu.js";
import { SpatialGrabRaycaster } from "../ui/Raycaster.js";
import { Avatar } from "./Avatar.js";
import { XRControllerModel } from "./controllers/XRControllerModel.js";
import { XRControllerMovement } from "./controllers/XRControllerMovement.js";
import { WebARSessionRoot } from "./WebARSessionRoot.js";
import { XRState, XRStateFlag } from "./XRFlag.js";

const debug = getParam("debugwebxr");
const debugQuicklook = getParam("debugusdz");

/**
 * WebXR component to enable VR, AR and Quicklook on iOS in your scene.  
 * It provides a simple wrapper around the {@link NeedleXRSession} API and adds some additional features like creating buttons or enabling default movement behaviour.
 * @category XR
 * @group Components
 */
export class WebXR extends Behaviour {

    // UI
    /** When enabled a button will be added to the UI to enter VR */
    @serializable()
    createVRButton: boolean = true;
    /** When enabled a button will be added to the UI to enter AR */
    @serializable()
    createARButton: boolean = true;
    /** When enabled a send to quest button will be shown if the device does not support VR */
    @serializable()
    createSendToQuestButton: boolean = true;
    /** When enabled a QRCode will be created to open the website on a mobile device */
    @serializable()
    createQRCode: boolean = true;

    // VR Settings
    /** When enabled default movement behaviour will be added */
    @serializable()
    useDefaultControls: boolean = true;
    /** When enabled controller models will automatically be created and updated when you are using controllers in WebXR */
    @serializable()
    showControllerModels: boolean = true;
    /** When enabled hand models will automatically be created and updated when you are using hands in WebXR */
    @serializable()
    showHandModels: boolean = true;

    // AR Settings
    /** When enabled the scene must be placed in AR */
    @serializable()
    usePlacementReticle: boolean = true;
    /** When assigned this object will be used as the AR placement reticle */
    @serializable(AssetReference)
    customARPlacementReticle?: AssetReference;
    /** When enabled you can position, rotate or scale your AR scene with one or two fingers */
    @serializable()
    usePlacementAdjustment: boolean = true;
    /** Used when `usePlacementReticle` is enabled. This is the scale of the user in the scene in AR. Larger values make the 3D content appear smaller */
    @serializable()
    arScale: number = 1;
    /** Experimental: When enabled an XRAnchor will be created for the AR scene and the position will be updated to the anchor position every few frames */
    @serializable()
    useXRAnchor: boolean = false;
    /**
     * When enabled the scene will be placed automatically when a point in the real world is found
     */
    @serializable()
    autoPlace: boolean = true;
    /** When enabled the AR session root center will be automatically adjusted to place the center of the scene */
    @serializable()
    autoCenter: boolean = false;

    /** When enabled a USDZExporter component will be added to the scene (if none is found) */
    @serializable()
    useQuicklookExport: boolean = false;


    /** Preview feature enabling occlusion (when available: https://github.com/cabanier/three.js/commit/b6ee92bcd8f20718c186120b7f19a3b68a1d4e47)
     * Enables the 'depth-sensing' WebXR feature to provide realtime depth occlusion. Only supported on Oculus Quest right now.
     */
    @serializable()
    useDepthSensing: boolean = false;

    /**
     * When enabled the spatial grab raycaster will be added or enabled in the scene
     * @default true
     */
    @serializable()
    useSpatialGrab: boolean = true;

    /** This avatar representation will be spawned when you enter a webxr session */
    @serializable(AssetReference)
    defaultAvatar?: AssetReference | boolean;


    private _playerSync?: PlayerSync;
    /** these components were created by the WebXR component on session start and will be cleaned up again in session end */
    private readonly _createdComponentsInSession: Behaviour[] = [];

    private _usdzExporter?: USDZExporter;

    static activeWebXRComponent: WebXR | null = null;

    awake() {
        NeedleXRSession.getXRSync(this.context);
    }

    onEnable(): void {
        // check if we're on a secure connection:
        if (window.location.protocol !== "https:") {
            showBalloonWarning("<a href=\"https://developer.mozilla.org/en-US/docs/Web/API/WebXR_Device_API\" target=\"_blank\">WebXR</a> only works on secure connections (https).");
        }

        if (this.useQuicklookExport) {
            const existingUSDZExporter = GameObject.findObjectOfType(USDZExporter);
            if (!existingUSDZExporter) {
                // if no USDZ Exporter is found we add one and assign the scene to be exported
                if (debug) console.log("WebXR: Adding USDZExporter");
                this._usdzExporter = GameObject.addComponent(this.gameObject, USDZExporter);
                this._usdzExporter.objectToExport = this.context.scene;
                this._usdzExporter.autoExportAnimations = true;
                this._usdzExporter.autoExportAudioSources = true;
            }
        }

        this.handleCreatingHTML();
        this.handleOfferSession();

        // If the avatar is undefined (meaning not set and also not null) we will use the fallback default avatar
        if (this.defaultAvatar === true) {
            if (debug) console.warn("WebXR: No default avatar set, using static default avatar")
            this.defaultAvatar = new AssetReference("https://cdn.needle.tools/static/avatars/DefaultAvatar.glb")
        }

        if (this.defaultAvatar) {
            this._playerSync = this.gameObject.getOrAddComponent(PlayerSync);
            this._playerSync.autoSync = false;
        }
        if (this._playerSync && typeof this.defaultAvatar != "boolean") {
            this._playerSync.asset = this.defaultAvatar;
            this._playerSync.onPlayerSpawned?.removeEventListener(this.onAvatarSpawned);
            this._playerSync.onPlayerSpawned?.addEventListener(this.onAvatarSpawned);
        }
    }

    onDisable(): void {
        this._usdzExporter?.destroy();
        this.removeButtons();
    }

    private async handleOfferSession() {
        if (this.createVRButton) {
            const hasVRSupport = await NeedleXRSession.isVRSupported();
            if (hasVRSupport && this.createVRButton) {
                return NeedleXRSession.offerSession("immersive-vr", "default", this.context);
            }
        }
        if (this.createARButton) {
            const hasARSupport = await NeedleXRSession.isARSupported();
            if (hasARSupport && this.createARButton) {
                return NeedleXRSession.offerSession("immersive-ar", "default", this.context);
            }
        }
        return false;
    }

    /** the currently active webxr input session */
    get session(): NeedleXRSession | null {
        return NeedleXRSession.active ?? null;
    }
    /** immersive-vr or immersive-ar */
    get sessionMode(): XRSessionMode | null {
        return NeedleXRSession.activeMode ?? null;;
    }

    /** Call to start an WebVR session */
    async enterVR(init?: XRSessionInit): Promise<NeedleXRSession | null> {
        return NeedleXRSession.start("immersive-vr", init, this.context);
    }
    /** Call to start an WebAR session */
    async enterAR(init?: XRSessionInit): Promise<NeedleXRSession | null> {
        return NeedleXRSession.start("immersive-ar", init, this.context);
    }
    /** Call to end a WebXR (AR or VR) session */
    exitXR() {
        NeedleXRSession.stop();
    }

    private _exitXRMenuButton?: HTMLElement;
    private _previousXRState: number = 0;
    private _spatialGrabRaycaster?: SpatialGrabRaycaster;

    private get isActiveWebXR() {
        return !WebXR.activeWebXRComponent || WebXR.activeWebXRComponent === this;
    }

    onBeforeXR(_mode: XRSessionMode, args: XRSessionInit): void {
        if (!this.isActiveWebXR) {
            console.warn(`WebXR: another WebXR component is already active (${WebXR.activeWebXRComponent?.name}). This is ignored: ${this.name}`);
            return;
        }
        WebXR.activeWebXRComponent = this;

        if (_mode == "immersive-ar" && this.useDepthSensing) {
            args.optionalFeatures = args.optionalFeatures || [];
            args.optionalFeatures.push("depth-sensing");
        }
    }

    async onEnterXR(args: NeedleXREventArgs) {
        if (!this.isActiveWebXR) return;

        if (debug) console.log("WebXR onEnterXR")
        // set XR flags
        this._previousXRState = XRState.Global.Mask;
        const isVR = args.xr.isVR;
        XRState.Global.Set(isVR ? XRStateFlag.VR : XRStateFlag.AR);

        // Handle AR session root
        if (this.usePlacementReticle && args.xr.isAR) {
            let sessionroot = GameObject.findObjectOfType(WebARSessionRoot);
            if (!sessionroot) {
                const implicitSessionRoot = new Object3D();
                for (const ch of this.context.scene.children)
                    implicitSessionRoot.add(ch);
                this.context.scene.add(implicitSessionRoot);
                sessionroot = GameObject.addComponent(implicitSessionRoot, WebARSessionRoot)!;
                this._createdComponentsInSession.push(sessionroot);
            }
            
            sessionroot.customReticle = this.customARPlacementReticle;
            sessionroot.arScale = this.arScale;
            sessionroot.arTouchTransform = this.usePlacementAdjustment;
            sessionroot.autoPlace = this.autoPlace;
            sessionroot.autoCenter = this.autoCenter;
            sessionroot.useXRAnchor = this.useXRAnchor;
        }

        // handle VR controls
        if (this.useDefaultControls) {
            this.setDefaultMovementEnabled(true);
        }
        if (this.showControllerModels || this.showHandModels) {
            this.setDefaultControllerRenderingEnabled(true);
        }

        // ensure we have a spatial grab raycaster for close grabs
        if (this.useSpatialGrab) {
            this._spatialGrabRaycaster = GameObject.findObjectOfType(SpatialGrabRaycaster) ?? undefined;
            if (!this._spatialGrabRaycaster) {
                this._spatialGrabRaycaster = this.gameObject.addComponent(SpatialGrabRaycaster);
            }
        }

        this.createLocalAvatar(args.xr);

        // for mobile screen AR we have the "X" button to exit and don't need to add an extra menu button to close
        // https://linear.app/needle/issue/NE-5716
        if (args.xr.isScreenBasedAR) {

        }
        else {
            this._exitXRMenuButton = this.context.menu.appendChild({
                label: "Quit XR",
                onClick: () => this.exitXR(),
                icon: "exit_to_app",
                priority: 20_000,
            });
        }
    }

    onUpdateXR(_args: NeedleXREventArgs): void {
        if (!this.isActiveWebXR) return;
        if (this._spatialGrabRaycaster) {
            this._spatialGrabRaycaster.enabled = this.useSpatialGrab;
        }
    }

    onLeaveXR(_: NeedleXREventArgs): void {
        this._exitXRMenuButton?.remove();

        if (!this.isActiveWebXR) return;

        // revert XR flags
        XRState.Global.Set(this._previousXRState);

        this._playerSync?.destroyInstance();

        for (const comp of this._createdComponentsInSession) {
            comp.destroy();
        }
        this._createdComponentsInSession.length = 0;

        this.handleOfferSession();

        delayForFrames(1).then(() => WebXR.activeWebXRComponent = null);
    }


    /** Call to enable or disable default controller behaviour */
    setDefaultMovementEnabled(enabled: boolean): XRControllerMovement | null {
        let movement = this.gameObject.getComponent(XRControllerMovement)
        if (!movement && enabled) {
            movement = this.gameObject.addComponent(XRControllerMovement)!;
            this._createdComponentsInSession.push(movement);
        }
        if (movement) movement.enabled = enabled;
        return movement;
    }
    /** Call to enable or disable default controller rendering */
    setDefaultControllerRenderingEnabled(enabled: boolean): XRControllerModel | null {
        let models = this.gameObject.getComponent(XRControllerModel);
        if (!models && enabled) {
            models = this.gameObject.addComponent(XRControllerModel)!;
            this._createdComponentsInSession.push(models);
            models.createControllerModel = this.showControllerModels;
            models.createHandModel == this.showHandModels;
        }
        if (models) models.enabled = enabled;
        return models;
    }



    protected async createLocalAvatar(xr: NeedleXRSession) {
        if (this._playerSync && xr.running && typeof this.defaultAvatar != "boolean") {
            this._playerSync.asset = this.defaultAvatar;
            await this._playerSync.getInstance();
        }
    }

    private onAvatarSpawned = (instance: Object3D) => {
        // spawned webxr avatars must have a avatar component
        if (debug) console.log("WebXR.onAvatarSpawned", instance);
        let avatar = GameObject.getComponentInChildren(instance, Avatar);
        avatar ??= GameObject.addComponent(instance, Avatar)!;
    };




    // HTML UI

    /** @deprecated use `getButtonsFactory()` or access `WebXRButtonFactory.getOrCreate()` directory */
    getButtonsContainer(): WebXRButtonFactory {
        return this.getButtonsFactory();
    }

    /** Calling this function will get the Needle WebXR button factory (it will be created if it doesnt exist yet)  
     * @returns the Needle WebXR button factory */
    getButtonsFactory(): WebXRButtonFactory {
        if (!this._buttonFactory) {
            this._buttonFactory = WebXRButtonFactory.getOrCreate();
        }
        return this._buttonFactory;
    }

    private _buttonFactory?: WebXRButtonFactory;

    private handleCreatingHTML() {
        const xrButtonsPriority = 50;

        if (this.createARButton || this.createVRButton || this.useQuicklookExport) {
            // Quicklook / iOS
            if ((DeviceUtilities.isiOS() && DeviceUtilities.isSafari()) || debugQuicklook) {
                if (this.useQuicklookExport) {
                    const usdzExporter = GameObject.findObjectOfType(USDZExporter);
                    if (!usdzExporter || (usdzExporter && usdzExporter.allowCreateQuicklookButton)) {
                        const button = this.getButtonsFactory().createQuicklookButton();
                        this.addButton(button, xrButtonsPriority);
                    }
                }
            }
            // WebXR
            if (this.createARButton) {
                const arbutton = this.getButtonsFactory().createARButton();
                this.addButton(arbutton, xrButtonsPriority);
            }
            if (this.createVRButton) {
                const vrbutton = this.getButtonsFactory().createVRButton();
                this.addButton(vrbutton, xrButtonsPriority);
            }
        }

        if (this.createSendToQuestButton && !DeviceUtilities.isQuest()) {
            NeedleXRSession.isVRSupported().then(supported => {
                if (!supported) {
                    const button = this.getButtonsFactory().createSendToQuestButton();
                    this.addButton(button, xrButtonsPriority);
                }
            });
        }

        if (this.createQRCode) {
            const menu = findObjectOfType(NeedleMenu);
            if (menu && menu.createQRCodeButton === false) {
                // If the menu exists and the QRCode option is disabled we dont create it (NE-4919)
                if (isDevEnvironment()) console.warn("WebXR: QRCode button is disabled in the Needle Menu component")
            }
            else if (!DeviceUtilities.isMobileDevice()) {
                const qrCode = ButtonsFactory.getOrCreate().createQRCode();
                this.addButton(qrCode, xrButtonsPriority);
            }
        }
    }

    private readonly _buttons: HTMLElement[] = [];

    private addButton(button: HTMLElement, priority: number) {
        this._buttons.push(button);
        button.setAttribute("priority", priority.toString());
        this.context.menu.appendChild(button);
    }

    private removeButtons() {
        for (const button of this._buttons) {
            button.remove();
        }
        this._buttons.length = 0;
    }

}
