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 { Component, Emitter } from '@wonderlandengine/api';
import { HandTracking } from './hand-tracking.js';
import { vec3, quat } from 'gl-matrix';
import { property } from '@wonderlandengine/api/decorators.js';
import { VrModeActiveSwitch } from './vr-mode-active-switch.js';
const _tempVec = vec3.create();
const _tempQuat = quat.create();
const _tempRotation1 = new Float32Array(4);
const _tempRotation2 = new Float32Array(4);
const minTemp = new Float32Array(3);
const maxTemp = new Float32Array(3);
const hands = ['left', 'right'];
/**
 * Dynamically load and map input profiles for XR controllers.
 */
class InputProfile extends Component {
    static TypeName = 'input-profile';
    /**
     * A cache to store loaded profiles for reuse.
     */
    static Cache = new Map();
    _gamepadObjects = {};
    _controllerModel = null;
    _defaultControllerComponents;
    _handedness;
    _profileJSON = null;
    _buttons = [];
    _axes = [];
    /**
     * The XR gamepad associated with the current input source.
     */
    gamepad;
    /**
     * A reference to the emitter which triggered on model lodaed event.
     */
    onModelLoaded = new Emitter();
    /**
     * Returns url of input profile json file
     */
    url;
    /**
     * A set of components to filter during component retrieval.
     */
    toFilter = new Set(['vr-mode-active-mode-switch']);
    /**
     * The index representing the handedness of the controller (0 for left, 1 for right).
     */
    handedness = 0;
    /**
     * The base path where XR input profiles are stored.
     */
    defaultBasePath;
    /**
     * An optional folder path for loading custom XR input profiles.
     */
    customBasePath;
    /**
     * The default 3D controller model used when a custom model fails to load.
     */
    defaultController;
    /**
     * The object which has HandTracking component added to it.
     */
    trackedHand;
    /**
     * If true, the input profile will be mapped to the default controller, and no dynamic 3D model of controller will be loaded.
     */
    mapToDefaultController;
    /**
     * If true, adds a VR mode switch component to the loaded controller model.
     */
    addVrModeSwitch;
    onActivate() {
        this._handedness = hands[this.handedness];
        const defaultHandName = 'Hand' + this._handedness.charAt(0).toUpperCase() + this._handedness.slice(1);
        this.trackedHand =
            this.trackedHand ?? this.object.parent?.findByNameRecursive(defaultHandName)[0];
        this.defaultController = this.defaultController || this.object.children[0];
        this._defaultControllerComponents = this._getComponents(this.defaultController);
        this.engine.onXRSessionStart.add(() => {
            this.engine.xr?.session.addEventListener('inputsourceschange', this._onInputSourcesChange.bind(this));
        });
    }
    onDeactivate() {
        this.engine.xr?.session?.removeEventListener('inputsourceschange', this._onInputSourcesChange.bind(this));
    }
    /**
     * Sets newly loaded controllers for the HandTracking component to proper switching.
     * @param controllerObject The controller object.
     * @hidden
     */
    _setHandTrackingControllers(controllerObject) {
        const handtrackingComponent = this.trackedHand.getComponent(HandTracking);
        if (!handtrackingComponent)
            return;
        handtrackingComponent.controllerToDeactivate = controllerObject;
    }
    /**
     * Retrieves all components from the specified object and its children.
     * @param obj The object to retrieve components from.
     * @return An array of components.
     * @hidden
     */
    _getComponents(obj) {
        const components = [];
        if (obj == null)
            return components;
        const stack = [obj];
        while (stack.length > 0) {
            const currentObj = stack.pop();
            const comps = currentObj
                .getComponents()
                .filter((c) => !this.toFilter.has(c.type));
            components.push(...comps);
            const children = currentObj.children;
            // Push children onto the stack in reverse order to maintain the correct order
            for (let i = children.length - 1; i >= 0; --i) {
                stack.push(children[i]);
            }
        }
        return components;
    }
    /**
     * Activates or deactivates components based on the specified boolean value.
     * @param active If true, components are set to active; otherwise, they are set to inactive.
     * @hidden
     */
    _setComponentsActive(active) {
        const comps = this._defaultControllerComponents;
        if (comps == undefined)
            return;
        for (let i = 0; i < comps.length; ++i) {
            comps[i].active = active;
        }
    }
    /**
     * Event handler triggered when XR input sources change.
     * Detects new XR input sources and initiates the loading of input profiles.
     * @param event The XR input source change event.
     * @hidden
     */
    _onInputSourcesChange(event) {
        if (this._isModelLoaded() && !this.mapToDefaultController) {
            this._setComponentsActive(false);
        }
        event.added.forEach((xrInputSource) => {
            if (xrInputSource.hand != null)
                return;
            if (this._handedness != xrInputSource.handedness)
                return;
            this.gamepad = xrInputSource.gamepad;
            const profile = this.customBasePath !== ''
                ? this.customBasePath
                : this.defaultBasePath + xrInputSource.profiles[0];
            this.url = profile + '/profile.json';
            this._profileJSON = InputProfile.Cache.get(this.url) ?? null;
            if (this._profileJSON != null)
                return;
            fetch(this.url)
                .then((res) => res.json())
                .then((out) => {
                this._profileJSON = out;
                InputProfile.Cache.set(this.url, this._profileJSON);
                if (!this._isModelLoaded())
                    this._loadAndMapGamepad(profile);
            })
                .catch((e) => {
                console.error(`Failed to load profile from ${this.url}. Reason:`, e);
            });
        });
    }
    /**
     * Checks if the 3D controller model is loaded.
     * @return True if the model is loaded; otherwise, false.
     * @hidden
     */
    _isModelLoaded() {
        return this._controllerModel !== null;
    }
    /**
     * Loads the 3D controller model and caches the mapping to the gamepad.
     * @param profile The path to the input profile.
     * @hidden
     */
    async _loadAndMapGamepad(profile) {
        const assetPath = profile + '/' + this._handedness + '.glb';
        this._controllerModel = this.defaultController;
        if (!this.mapToDefaultController) {
            /** load 3d model in the runtime with profile url */
            try {
                this._controllerModel = (await this.engine.scene.append(assetPath));
            }
            catch (e) {
                console.error(`Failed to load i-p controller model. Reason:`, e, `Continuing with ${this._handedness} default controller.`);
                this._setComponentsActive(true);
            }
            this._controllerModel.parent = this.object;
            this._controllerModel.setPositionLocal([0, 0, 0]);
            this._setComponentsActive(false);
            if (this.addVrModeSwitch)
                this._controllerModel.addComponent(VrModeActiveSwitch);
            this.onModelLoaded.notify();
        }
        this._cacheGamepadObjectsFromProfile(this._profileJSON, this._controllerModel);
        this._setHandTrackingControllers(this._controllerModel);
        this.update = () => this._mapGamepadInput();
    }
    /**
     * Caches gamepad objects (buttons, axes) from the loaded input profile.
     * @hidden
     */
    _cacheGamepadObjectsFromProfile(profile, obj) {
        const components = profile.layouts[this._handedness].components;
        if (!components)
            return;
        this._buttons = [];
        this._axes = [];
        for (const i in components) {
            const visualResponses = components[i].visualResponses;
            if (components[i].rootNodeName === 'menu')
                continue;
            for (const j in visualResponses) {
                // update buttons with new interface of current visual response
                const visualResponse = visualResponses[j];
                const valueNode = visualResponse.valueNodeName;
                const minNode = visualResponse.minNodeName;
                const maxNode = visualResponse.maxNodeName;
                this._gamepadObjects[valueNode] = obj.findByNameRecursive(valueNode)[0];
                this._gamepadObjects[minNode] = obj.findByNameRecursive(minNode)[0];
                this._gamepadObjects[maxNode] = obj.findByNameRecursive(maxNode)[0];
                const prop = visualResponses[j].componentProperty;
                const response = {
                    target: this._gamepadObjects[valueNode],
                    min: this._gamepadObjects[minNode],
                    max: this._gamepadObjects[maxNode],
                    id: components[i].gamepadIndices[prop], // Assign a unique ID
                };
                switch (prop) {
                    case 'button':
                        this._buttons.push(response);
                        break;
                    case 'xAxis':
                    case 'yAxis':
                        this._axes.push(response);
                        break;
                }
            }
        }
    }
    /**
     * Assigns a transformed position and rotation to the target based on minimum and maximum values and a normalized input value.
     * @param target The target object to be transformed.
     * @param min The minimum object providing transformation limits.
     * @param max The maximum object providing transformation limits.
     * @param value The normalized input value.
     * @hidden
     */
    _assignTransform(target, min, max, value) {
        vec3.lerp(_tempVec, min.getPositionWorld(minTemp), max.getPositionWorld(maxTemp), value);
        target.setPositionWorld(_tempVec);
        quat.lerp(_tempQuat, min.getRotationWorld(_tempRotation1), max.getRotationWorld(_tempRotation2), value);
        quat.normalize(_tempQuat, _tempQuat);
        target.setRotationWorld(_tempQuat);
    }
    /**
     * Maps input values (buttons, axes) to the 3D controller model.
     * @hidden
     */
    _mapGamepadInput() {
        for (const button of this._buttons) {
            const buttonValue = this.gamepad.buttons[button.id].value;
            this._assignTransform(button.target, button.min, button.max, buttonValue);
        }
        for (const axis of this._axes) {
            const axisValue = this.gamepad.axes[axis.id];
            const normalizedAxisValue = (axisValue + 1) / 2;
            this._assignTransform(axis.target, axis.min, axis.max, normalizedAxisValue);
        }
    }
}
__decorate([
    property.enum(hands, 0)
], InputProfile.prototype, "handedness", void 0);
__decorate([
    property.string('https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@latest/dist/profiles/')
], InputProfile.prototype, "defaultBasePath", void 0);
__decorate([
    property.string()
], InputProfile.prototype, "customBasePath", void 0);
__decorate([
    property.object()
], InputProfile.prototype, "defaultController", void 0);
__decorate([
    property.object()
], InputProfile.prototype, "trackedHand", void 0);
__decorate([
    property.bool(false)
], InputProfile.prototype, "mapToDefaultController", void 0);
__decorate([
    property.bool(true)
], InputProfile.prototype, "addVrModeSwitch", void 0);
export { InputProfile };
//# sourceMappingURL=input-profile.js.map