var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { AxesHelper, Group, Material, Mesh, Object3D } from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { XRControllerModelFactory } from "three/examples/jsm/webxr/XRControllerModelFactory.js";
import { XRHandMeshModel } from "three/examples/jsm/webxr/XRHandMeshModel.js";
import { AssetReference } from "../../../engine/engine_addressables.js";
import { setDontDestroy } from "../../../engine/engine_gameobject.js";
import { Gizmos } from "../../../engine/engine_gizmos.js";
import { getLoader } from "../../../engine/engine_gltf.js";
import { addDracoAndKTX2Loaders } from "../../../engine/engine_loaders.gltf.js";
import { serializable } from "../../../engine/engine_serialization_decorator.js";
import { getParam } from "../../../engine/engine_utils.js";
import { NeedleXRSession } from "../../../engine/engine_xr.js";
import { registerComponentExtension, registerExtensions } from "../../../engine/extensions/extensions.js";
import { NEEDLE_progressive } from "../../../engine/extensions/NEEDLE_progressive.js";
import { flipForwardMatrix } from "../../../engine/xr/internal.js";
import { Behaviour } from "../../Component.js";
const debug = getParam("debugwebxr");
const handsJointBuffer = new Float32Array(16 * 25);
const renderingUpdateTimings = new Array();
/**
 * XRControllerModel is a component that allows to display controller models or hand models in an XR session.
 * It automatically loads the appropriate model for the connected controller or hand.
 *
 * You can configure if controller models or hand models should be created.
 */
export class XRControllerModel extends Behaviour {
    /**
     * If true, the controller model will be created when a controller is added/connected
     * @default true
     */
    createControllerModel = true;
    /**
     * If true, the hand model will be created when a hand is "added"/tracked
     * @default true
     */
    createHandModel = true;
    /** assign a model or model url to create custom hand models */
    customLeftHand;
    /** assign a model or model url to create custom hand models */
    customRightHand;
    static factory = new XRControllerModelFactory();
    supportsXR(mode) {
        return mode === "immersive-vr" || mode === "immersive-ar";
    }
    _models = new Array();
    async onXRControllerAdded(args) {
        // TODO we may want to treat controllers differently in AR/Passthrough mode
        const isSupportedSession = args.xr.isVR || args.xr.isPassThrough;
        if (!isSupportedSession)
            return;
        console.debug("XR Controller Added", args.controller.side, args.controller.index);
        // return;
        const { controller } = args;
        if (this.createControllerModel || this.createHandModel) {
            if (controller.hand) {
                if (this.createHandModel) {
                    const res = await this.loadHandModel(this, controller);
                    // check if the model doesnt exist, the hand disconnected or it's suddenly a controller
                    if (!res || !controller.connected || !controller.isHand) {
                        if (res?.handObject)
                            setDontDestroy(res.handObject, false);
                        res?.handObject?.destroy();
                        return;
                    }
                    this._models.push({ controller: controller, model: res.handObject, handmesh: res.handmesh });
                    this._models.sort((a, b) => a.controller.index - b.controller.index);
                    this.scene.add(res.handObject);
                    controller.model = res.handObject;
                }
            }
            else {
                if (this.createControllerModel) {
                    const assetUrl = await controller.getModelUrl();
                    if (assetUrl) {
                        const model = await this.loadModel(controller, assetUrl);
                        // check if the model doesnt exist, the controller disconnected or it's suddenly a hand
                        if (!model || !controller.connected || controller.isHand) {
                            return;
                        }
                        this._models.push({ controller: controller, model });
                        this._models.sort((a, b) => a.controller.index - b.controller.index);
                        this.scene.add(model);
                        // The controller mesh should by default inherit layers.
                        model.traverse(child => {
                            child.layers.set(2);
                            // disable auto update on controller objects. No need to do this every frame
                            child.matrixAutoUpdate = false;
                            child.updateMatrix();
                        });
                        controller.model = model;
                    }
                    else if (controller.targetRayMode !== "transient-pointer") {
                        console.warn("XRControllerModel: no model found for " + controller.side);
                    }
                }
            }
        }
    }
    onXRControllerRemoved(args) {
        console.debug("XR Controller Removed", args.controller.side, args.controller.index);
        // we need to find the index by the controller because if controller 0 is removed first then args.controller.index 1 will be at index 0
        const indexInArray = this._models.findIndex(m => m.controller === args.controller);
        const entry = this._models[indexInArray];
        if (!entry)
            return;
        this._models.splice(indexInArray, 1);
        if (entry.model) {
            setDontDestroy(entry.model, false);
            entry.model.destroy();
            entry.model = undefined;
        }
    }
    onBeforeXR(_mode, args) {
        // When a custom hand model is used, we want to ensure we're requesting hand tracking,
        // even when the platform default doesn't include it (for example, on VisionOS we don't
        // request hand tracking by default because there's an additional permissions dialogue.
        if (this.createHandModel && (this.customLeftHand || this.customRightHand)) {
            args.optionalFeatures = args.optionalFeatures || [];
            if (!args.optionalFeatures.includes("hand-tracking")) {
                args.optionalFeatures.push("hand-tracking");
            }
        }
    }
    onLeaveXR(_args) {
        for (const entry of this._models) {
            if (!entry)
                continue;
            if (entry.model) {
                setDontDestroy(entry.model, false);
                entry.model.destroy();
                entry.model = undefined;
            }
            // Unassign the model from the controller when this script becomes inactive
            if (entry.controller.model === entry.model) {
                entry.controller.model = null;
            }
        }
        this._models.length = 0;
    }
    onBeforeRender() {
        if (!NeedleXRSession.active)
            return;
        if (debug)
            renderingUpdateTimings[0] = Date.now();
        // update model
        this.updateRendering(NeedleXRSession.active);
        if (debug) {
            const dt = Date.now() - renderingUpdateTimings[0];
            renderingUpdateTimings.push(dt);
            if (renderingUpdateTimings.length >= 30) {
                renderingUpdateTimings[0] = 0;
                const avrg = renderingUpdateTimings.reduce((a, b) => a + b, 0) / renderingUpdateTimings.length;
                renderingUpdateTimings.length = 0;
                // console.log("[XRControllerModel] " + avrg.toFixed(2) + " ms");
            }
        }
    }
    updateRendering(xr) {
        for (let i = 0; i < this._models.length; i++) {
            const entry = this._models[i];
            if (!entry)
                continue;
            const ctrl = entry.controller;
            if (!ctrl.connected) {
                // the actual removal of the model happens in onXRControllerRemoved
                if (debug)
                    console.warn("XRControllerModel.onUpdateXR: controller is not connected anymore", ctrl.side, ctrl.hand);
                continue;
            }
            // do we have a controller model?
            if (entry.model && !entry.handmesh) {
                entry.model.matrixAutoUpdate = false;
                entry.model.matrix.copy(ctrl.gripMatrix);
                entry.model.visible = ctrl.isTracking;
                // ensure that controller models are in rig space
                xr.rig?.gameObject.add(entry.model);
            }
            // do we have a hand mesh?
            else if (ctrl.inputSource.hand && entry.handmesh) {
                const referenceSpace = xr.referenceSpace;
                const hand = this.context.renderer.xr.getHand(ctrl.index);
                // if (referenceSpace && xr.frame.fillPoses) {
                //     xr.frame.fillPoses(ctrl.inputSource.hand.values(), referenceSpace, handsJointBuffer);
                //     let j = 0;
                //     for (const space of ctrl.inputSource.hand.values()) {
                //         const joint = hand.joints[space.jointName];
                //         if (joint) {
                //             joint.matrix.fromArray(handsJointBuffer, j * 16);
                //             joint.matrix.decompose(joint.position, joint.quaternion, joint.scale);
                //             joint.visible = true;
                //         }
                //         j++;
                //     }
                // }
                // else 
                if (referenceSpace && xr.frame.getJointPose) {
                    for (const inputjoint of ctrl.inputSource.hand.values()) {
                        // The transform of this joint will be updated with the joint pose on each frame
                        const joint = hand.joints[inputjoint.jointName];
                        if (joint) {
                            // Update the joints groups with the XRJoint poses
                            const jointPose = ctrl.getHandJointPose(inputjoint);
                            if (jointPose) {
                                const position = jointPose.transform.position;
                                const quaternion = jointPose.transform.orientation;
                                joint.position.copy(position);
                                joint.quaternion.copy(quaternion);
                                joint.matrixAutoUpdate = false;
                            }
                            joint.visible = jointPose != null;
                        }
                    }
                    // ensure that the hand renders in rig space
                    if (entry.model) {
                        entry.model.visible = ctrl.isTracking;
                        if (entry.model.visible && entry.model.parent !== xr.rig?.gameObject) {
                            xr.rig?.gameObject.add(entry.model);
                        }
                    }
                    if (entry.model?.visible) {
                        entry.handmesh?.updateMesh();
                        entry.model.matrixAutoUpdate = false;
                        entry.model.matrix.identity();
                        entry.model.applyMatrix4(flipForwardMatrix);
                    }
                }
            }
        }
    }
    async loadModel(controller, url) {
        if (!controller.connected) {
            console.warn("XRControllerModel.onXRControllerAdded: controller is not connected anymore", controller.side);
            return null;
        }
        const assetReference = AssetReference.getOrCreate("", url);
        const model = await assetReference.instantiate();
        setDontDestroy(model);
        if (NeedleXRSession.active?.isPassThrough) {
            model.traverseVisible((obj) => {
                this.makeOccluder(obj);
            });
        }
        return model;
    }
    async loadHandModel(comp, controller) {
        const context = this.context;
        const hand = context.renderer.xr.getHand(controller.index);
        if (!hand) {
            if (debug)
                Gizmos.DrawLabel(controller.rayWorldPosition, "No hand found for index " + controller.index, .05, 5);
            else
                console.warn("No hand found for index " + controller.index);
        }
        const loader = new GLTFLoader();
        addDracoAndKTX2Loaders(loader, context);
        await registerExtensions(loader, context, this.sourceId ?? "");
        const componentsExtension = registerComponentExtension(loader);
        let filename = "";
        const customHand = controller.side === "left" ? this.customLeftHand : this.customRightHand;
        if (customHand) {
            const urlWithoutExtension = customHand.url.split('.').slice(0, -1).join('.');
            filename = urlWithoutExtension;
            loader.setPath("");
        }
        else {
            // DEFAULT hands
            // XRHandmeshModel is using "<handedness>.glb" for loading the file
            filename = controller.inputSource.handedness === "left" ? "left" : "right";
            loader.setPath('https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles/generic-hand/');
        }
        const handObject = new Object3D();
        setDontDestroy(handObject);
        // @ts-ignore
        const handmesh = new XRHandMeshModel(handObject, hand, loader.path, filename, loader, (object) => {
            const gltf = componentsExtension?.gltf;
            // The XRHandMeshController removes the hand from the gltf before calling this callback
            // we need this in the GLTF scene however for creating the builtin components 
            if (gltf?.scene.children?.length === 0) {
                gltf.scene.children[0] = object;
            }
            // console.log(controller.side, componentsExtension.gltf, object, componentsExtension.gltf.scene?.children)
            if (componentsExtension?.gltf)
                getLoader().createBuiltinComponents(comp.context, comp.sourceId || filename, componentsExtension.gltf, null, componentsExtension);
            // The hand mesh should not receive raycasts
            object.traverse(child => {
                child.layers.set(2);
                if (NeedleXRSession.active?.isPassThrough && !customHand)
                    this.makeOccluder(child);
                if (child instanceof Mesh) {
                    NEEDLE_progressive.assignMeshLOD(child, 0);
                }
            });
            if (!controller.connected) {
                if (debug)
                    Gizmos.DrawLabel(controller.rayWorldPosition, "Hand is loaded but not connected anymore", .05, 5);
                object.removeFromParent();
            }
        });
        if (debug)
            handObject.add(new AxesHelper(.5));
        if (controller.inputSource.hand) {
            if (debug)
                console.log(controller.inputSource.hand);
            for (const inputjoint of controller.inputSource.hand.values()) {
                if (hand.joints[inputjoint.jointName] === undefined) {
                    const joint = new Group();
                    joint.matrixAutoUpdate = false;
                    joint.visible = true;
                    // joint.jointRadius = 0.01;
                    // @ts-ignore
                    hand.joints[inputjoint.jointName] = joint;
                    hand.add(joint);
                }
            }
        }
        else {
            if (debug) {
                Gizmos.DrawLabel(controller.rayWorldPosition, "No inputSource.hand found for index " + controller.index, .05, 5);
            }
        }
        return { handObject: handObject, handmesh: handmesh };
    }
    makeOccluder(obj) {
        if (obj instanceof Mesh) {
            let mat = obj.material;
            if (mat instanceof Material) {
                mat = obj.material = mat.clone();
                // depth only
                mat.depthWrite = true;
                mat.depthTest = true;
                mat.colorWrite = false;
                obj.receiveShadow = false;
                obj.renderOrder = -100;
            }
        }
    }
}
__decorate([
    serializable()
], XRControllerModel.prototype, "createControllerModel", void 0);
__decorate([
    serializable()
], XRControllerModel.prototype, "createHandModel", void 0);
__decorate([
    serializable(AssetReference)
], XRControllerModel.prototype, "customLeftHand", void 0);
__decorate([
    serializable(AssetReference)
], XRControllerModel.prototype, "customRightHand", void 0);
//# sourceMappingURL=XRControllerModel.js.map