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 { vec3, quat2, quat } from 'gl-matrix';
import { clamp, property, Component, Emitter, Property, } from '@wonderlandengine/api';
import { HistoryTracker } from '../history-tracker.js';
import { computeRelativeTransform, isPointEqual, toRad } from '../utils/math.js';
import { GrabPoint, GrabSnapMode, InteractorVisualState, InteractorVisualStateNames, } from './grab-point.js';
import { computeLocalPositionForPivot, rotateAroundPivot, rotateFreeDual, } from './providers.js';
import { FORWARD, RIGHT, UP } from '../constants.js';
import { TempDualQuat, TempQuat, TempVec3 } from '../internal-constants.js';
import { componentError, enumStringKeys } from '../utils/wle.js';
/* Constants */
const MAX_GRABS = 2;
const GRAB_EPSILON_DIST = 0.005; /* Half centimeter. */
const GRAB_EPSILON_ANGLE = toRad(0.5); /* Half a degree */
export var GrabTransformType;
(function (GrabTransformType) {
    GrabTransformType[GrabTransformType["Hand"] = 0] = "Hand";
    GrabTransformType[GrabTransformType["AroundPivot"] = 1] = "AroundPivot";
})(GrabTransformType || (GrabTransformType = {}));
/** List of string keys for {@link GrabTransformType}. */
const GrabTransformTypeNames = enumStringKeys(GrabTransformType);
export var PivotAxis;
(function (PivotAxis) {
    PivotAxis[PivotAxis["X"] = 0] = "X";
    PivotAxis[PivotAxis["Y"] = 1] = "Y";
    PivotAxis[PivotAxis["Z"] = 2] = "Z";
})(PivotAxis || (PivotAxis = {}));
/** List of string keys for {@link PivotAxis}. */
export const PivotAxisNames = enumStringKeys(PivotAxis);
function axis(axis) {
    switch (axis) {
        case PivotAxis.X:
            return RIGHT;
        case PivotAxis.Y:
            return UP;
        case PivotAxis.Z:
            return FORWARD;
    }
}
/**
 * Enables objects to be interactively grabbed and manipulated in a virtual environment.
 *
 * The `Grabbable` class extends the basic functionality provided by the {@link Interactable}
 * component to allow objects to be picked up, held, and potentially thrown by the user. It
 * facilitates the creation of immersive and interactive experiences by providing an intuitive
 * interface for object manipulation within a 3D scene.
 */
export class Grabbable extends Component {
    static TypeName = 'grabbable';
    /** @override */
    static onRegister(engine) {
        engine.registerComponent(GrabPoint);
    }
    /** Properties */
    /**
     * List of objects to use as grab points.
     *
     * @note If no object is provided, the grabbable is treated as its own grab point.
     * @note Objects with no {@link GrabPoint} component will have a default one created.
     */
    handleObjects = [];
    /**
     * Whether the object can be thrown with physics or not.
     *
     * When the interactor releases this grabbable, it will be thrown based on
     * the velocity the interactor had.
     */
    canThrow = true;
    /**
     * Linear multiplier for the throwing speed.
     *
     * By default, throws at the controller speed.
     */
    throwLinearIntensity = 1.0;
    /**
     * Linear multiplier for the throwing angular  speed.
     *
     * By default, throws at the controller speed.
     */
    throwAngularIntensity = 1.0;
    /**
     * If `true`, the grabbable will be updated based on the controller
     * velocity data, if available.
     *
     * When `false`, the linear and angular velocities will be emulated based on
     * the grabbable previous orientation and position.
     *
     * For more information, have a look at:
     * - [linearVelocity](https://developer.mozilla.org/en-US/docs/Web/API/XRPose/linearVelocity)
     * - [angularVelocity](https://developer.mozilla.org/en-US/docs/Web/API/XRPose/angularVelocity)
     */
    useControllerVelocityData = true;
    /**
     * Max distance to automatically stop grabbing.
     *
     * @note Set a negative value to disable.
     */
    releaseDistance = 0.25;
    /**
     * The distance marker to use when distance-grabbed.
     */
    distanceMarker = null;
    /**
     * The index of the handle to use when distance-grabbed.
     *
     * Use `0` for {@link handle} and `1` for {@link handleSecondary}.
     */
    distanceHandle = 0;
    transformType = GrabTransformType.Hand;
    /** Pivot axis used when {@link transformType} is set to {@link GrabRotationType.AroundPivot} */
    pivotAxis = PivotAxis.Y;
    secondaryPivot = null;
    /** Pivot axis used when {@link transformType} is set to {@link GrabRotationType.AroundPivot} */
    secondaryPivotAxis = PivotAxis.Y;
    /**
     * Visual state to apply to the interactor once interaction occurs.
     *
     * @note Behavior overriden by {@link GrabPoint.interactorVisualState} if set.
     */
    interactorVisualState = InteractorVisualState.None;
    /** Public Attributes */
    grabPoints = [];
    /** Notifies once a grab point is selected for interaction. */
    onGrabPointSelect = new Emitter();
    /** Notifies once a grab point is released. */
    onGrabPointRelease = new Emitter();
    /**
     * Notifies once this object is grabbed.
     *
     * @note The notification only occurs when first grabbed.
     */
    onGrabStart = new Emitter();
    /**
     * Notifies once this object is released.
     *
     * @note The notification only occurs when both grip point are free.
     */
    onGrabEnd = new Emitter();
    /** Private Attributes. */
    /** Cached currently grabbed data. */
    _grabData = [];
    /**
     * Squared distance to automatically stop a grab when:
     * - Using dual grabbing
     * - Moving away from a locked grab
     */
    _history = new HistoryTracker();
    _physx = null;
    /**
     * Relative grab transform to apply every update, used
     * to maintain the object transform when grab starts.
     *
     * @note Can be local or world based on the transformation type.
     */
    _relativeGrabTransform = quat2.create();
    /**
     * Relative pivot's grab transform to apply every update, used
     * to maintain the object transform when grab starts.
     */
    _pivotGrabTransform = quat2.create();
    _useUpOrientation = false;
    /** `true` if the transform is computed and applied in world space, `false` otherwise. */
    _computeWorldSpace = false;
    /** `true` if the grabbable should continue lerping to the target rotation / position. */
    _lerp = false;
    init() {
        this.grabPoints = this.handleObjects.map((o) => {
            return o.getComponent(GrabPoint) ?? o.addComponent(GrabPoint);
        });
        if (this.grabPoints.length === 0) {
            const handle = this.object.getComponent(GrabPoint) ?? this.object.addComponent(GrabPoint);
            this.grabPoints.push(handle);
        }
    }
    start() {
        this._physx = this.object.getComponent('physx');
    }
    onActivate() {
        this._computeWorldSpace = this.transformType === GrabTransformType.Hand;
    }
    update(dt) {
        for (let i = this._grabData.length - 1; i >= 0; --i) {
            const grab = this._grabData[i];
            const target = this.object.transformPointWorld(TempVec3.get(), grab.localAnchor);
            const source = grab.interactor.object.getPositionWorld(TempVec3.get());
            const squaredDistance = vec3.squaredDistance(source, target);
            TempVec3.free(2);
            if (squaredDistance <= this.releaseDistance * this.releaseDistance)
                continue;
            /* Hands are too far apart, release the second handle. */
            this.release(grab.interactor);
        }
        if (!this.isGrabbed)
            return;
        const primaryInteractor = this.primaryGrab.interactor.object;
        const secondaryInteractor = this.secondaryGrab?.interactor.object ?? null;
        const currentPos = TempVec3.get();
        const currentRot = TempQuat.get();
        if (this._computeWorldSpace) {
            this.object.getPositionWorld(currentPos);
            this.object.getRotationWorld(currentRot);
        }
        else {
            this.object.getPositionLocal(currentPos);
            this.object.getRotationLocal(currentRot);
        }
        const rotation = TempQuat.get();
        vec3.copy(rotation, currentRot);
        const position = TempVec3.get();
        vec3.copy(position, currentPos);
        const transform = TempDualQuat.get();
        const pivotRot = TempQuat.get();
        this.computeTransform(transform, pivotRot, primaryInteractor, secondaryInteractor);
        quat2.getTranslation(position, transform);
        quat2.getReal(rotation, transform);
        quat.normalize(rotation, rotation);
        if (this._lerp) {
            const primaryHandle = this.grabPoints[this.primaryGrab.handleId];
            let lerp = primaryHandle.snapLerp;
            if (this.secondaryGrab) {
                const secondaryHandle = this.grabPoints[this.secondaryGrab.handleId];
                lerp = Math.max(lerp, secondaryHandle.snapLerp);
            }
            lerp = clamp(lerp, 0, 1);
            vec3.lerp(position, currentPos, position, lerp);
            quat.slerp(rotation, currentRot, rotation, lerp);
            quat.normalize(rotation, rotation);
            this._lerp =
                !isPointEqual(position, currentPos, GRAB_EPSILON_DIST) ||
                    !(quat.getAngle(rotation, currentRot) < GRAB_EPSILON_ANGLE);
        }
        if (this._computeWorldSpace) {
            this.object.setPositionWorld(position);
            this.object.setRotationWorld(rotation);
        }
        else {
            this.object.setPositionLocal(position);
            this.object.setRotationLocal(rotation);
        }
        if (this.secondaryPivot) {
            this.secondaryPivot.setRotationLocal(pivotRot);
        }
        TempVec3.free(2);
        TempQuat.free(3);
        TempDualQuat.free();
        const xrPose = this.primaryGrab.interactor.input.xrPose;
        if (xrPose && this.useControllerVelocityData) {
            this._history.updateFromPose(xrPose, this.primaryGrab.interactor.trackedSpace, this.object, dt);
        }
        else {
            this._history.update(this.object, dt);
        }
    }
    /**
     * Throws the grabbable.
     */
    throw(interactor) {
        if (!this._physx) {
            return;
        }
        this._setKinematicState(false);
        const angular = this._history.angular(TempVec3.get());
        vec3.scale(angular, angular, this.throwAngularIntensity);
        const velocity = this._history.velocity(TempVec3.get());
        vec3.scale(velocity, velocity, this.throwLinearIntensity);
        const radius = vec3.subtract(TempVec3.get(), this.object.getPositionWorld(TempVec3.get()), interactor.object.getPositionWorld(TempVec3.get()));
        vec3.cross(radius, angular, radius);
        vec3.add(velocity, velocity, radius);
        this._physx.angularVelocity = angular;
        this._physx.linearVelocity = velocity;
        TempVec3.free(5);
    }
    /**
     * Programmatically grab an interactable.
     *
     * @remarks
     * The interactable must be one of {@link Grabbable.handle}
     * or {@link Grabbable.handleSecondary}.
     *
     * This method is useful for grab emulation for non-VR applications.
     * In general, you will not call this method but rather rely on collision
     * checks between the {@link Interactor} and the {@link Interactable}.
     *
     * @param interactor The interactor issuing the interaction.
     * @param interactable The interactable undergoing the action.
     */
    grab(interactor, handleId) {
        if (this._grabData.length === MAX_GRABS)
            return;
        const grab = {
            interactor,
            handleId,
            localAnchor: vec3.create(),
        };
        this._grabData.push(grab);
        const dual = this._grabData.length > 1;
        if (dual && handleId === 0) {
            /* Ensure primary grab is always first */
            const second = this._grabData[0];
            this._grabData[0] = this._grabData[1];
            this._grabData[1] = second;
        }
        const handle = this.grabPoints[handleId];
        const source = handle.snap != GrabSnapMode.None ? handle.object : interactor.object;
        source.getPositionWorld(grab.localAnchor);
        this.object.transformPointInverseWorld(grab.localAnchor);
        this._history.reset(this.object);
        this.initializeGrab();
        this.onGrabPointSelect.notify(this, handle);
        if (!dual) {
            this.onGrabStart.notify(this);
        }
    }
    /**
     * Programmatically release an interactable.
     *
     * @remarks
     * The interactable must be one of {@link Grabbable.handle}
     * or {@link Grabbable.handleSecondary}.
     *
     * This method is useful for grab emulation for non-VR applications.
     * In general, you will not call this method but rather rely on collision
     * checks between the {@link Interactor} and the {@link Interactable}.
     *
     * @param interactor The interactor issuing the interaction.
     * @param interactable The interactable undergoing the action.
     */
    release(interactor) {
        const index = this._grabData.findIndex((v) => v.interactor === interactor);
        const grab = this._grabData[index];
        if (!grab)
            return;
        const handle = this.grabPoints[grab.handleId];
        handle._interactor = null;
        this._grabData.splice(index, 1);
        const released = !this._grabData.length;
        if (!released) {
            this.initializeGrab();
        }
        else if (this.canThrow) {
            this.throw(interactor);
        }
        this.onGrabPointRelease.notify(this, handle);
        if (released) {
            this.onGrabEnd.notify(this);
        }
    }
    /** `true` is any of the two handles is currently grabbed. */
    get isGrabbed() {
        return !!this.primaryGrab || !!this.secondaryGrab;
    }
    /** `true` if the primary handle is grabbed, the object pointer by {@link handle}. */
    get primaryGrab() {
        return this._grabData[0];
    }
    /** `true` if the secondary handle is grabbed, the object pointer by {@link handleSecondary}. */
    get secondaryGrab() {
        return this._grabData[1];
    }
    computeTransform(out, pivotOut, primary, secondary) {
        quat2.identity(out);
        quat.identity(pivotOut);
        const source = primary.getPositionWorld(TempVec3.get());
        const target = vec3.copy(TempVec3.get(), source);
        if (secondary) {
            secondary.getPositionWorld(target);
        }
        switch (this.transformType) {
            case GrabTransformType.Hand: {
                if (secondary) {
                    const interactor = this.secondaryGrab?.interactor.object ?? secondary;
                    this.transformDualHand(out, interactor, source, target);
                }
                else {
                    this.transformHand(out, primary);
                }
                break;
            }
            case GrabTransformType.AroundPivot: {
                const pos = vec3.lerp(TempVec3.get(), source, target, 0.5);
                this.rotationAroundPivot(out, pivotOut, pos);
                quat2.fromRotationTranslation(out, out, this.object.getPositionLocal(pos));
                TempVec3.free();
                break;
            }
            default:
                console.warn(componentError(this, `Unrecognized type ${this.transformType}`));
                break;
        }
        TempVec3.free(2);
    }
    transformHand(out, source) {
        /* `_relativeGrabTransform` is in the hand space, thus multiplyig
         * the hand transform by the object's transform leads to
         * its world space transform. */
        source.getTransformWorld(out);
        quat2.multiply(out, out, this._relativeGrabTransform);
        return out;
    }
    /**
     * Compute the transform of this grabbable based on both handles.
     */
    transformDualHand(out, interactorUp, source, target) {
        const primaryHandle = this.grabPoints[this.primaryGrab.handleId];
        /* Pivot */
        const up = TempVec3.get();
        if (this._useUpOrientation) {
            interactorUp.getUpWorld(up);
        }
        else {
            interactorUp.getForwardWorld(up);
        }
        const pivotRotation = rotateFreeDual(TempQuat.get(), source, target, up);
        quat.multiply(pivotRotation, pivotRotation, this._relativeGrabTransform);
        const pivotToWorld = quat2.fromRotationTranslation(TempDualQuat.get(), pivotRotation, source);
        /* Object to handle */
        const objectToPivotVec = vec3.subtract(TempVec3.get(), this.object.getPositionWorld(), primaryHandle.object.getPositionWorld());
        const objectToPivot = quat2.fromTranslation(out, objectToPivotVec);
        quat2.multiply(objectToPivot, objectToPivot, pivotToWorld);
        TempVec3.free(2);
        TempDualQuat.free();
        TempQuat.free();
        return out;
    }
    /**
     * Rotate the grabable around an origin.
     *
     * @param out Destination quaternion
     * @param positionWorld Anchor point, in **world space**.
     */
    rotationAroundPivot(out, pivotOut, positionWorld) {
        const localPos = computeLocalPositionForPivot(TempVec3.get(), this.object, positionWorld);
        vec3.normalize(localPos, localPos);
        rotateAroundPivot(out, axis(this.pivotAxis), localPos);
        quat.multiply(out, out, this._relativeGrabTransform);
        TempVec3.free();
        if (!this.secondaryPivot) {
            quat.identity(pivotOut);
            return;
        }
        const pos = computeLocalPositionForPivot(TempVec3.get(), this.secondaryPivot, positionWorld);
        vec3.normalize(pos, pos);
        rotateAroundPivot(pivotOut, axis(this.secondaryPivotAxis), pos);
        quat.multiply(pivotOut, pivotOut, this._pivotGrabTransform);
        TempVec3.free();
    }
    /**
     * Initializes grab state.
     *
     * @note Triggered when single grab occurs, or when second hand is released.
     */
    initializeGrab() {
        this._setKinematicState(true);
        this._lerp = true;
        quat2.identity(this._relativeGrabTransform);
        quat.identity(this._pivotGrabTransform);
        const primaryHandle = this.grabPoints[this._grabData[0].handleId];
        const primaryInteractor = this._grabData[0].interactor.object;
        /* Switch between handle or interactor for snapping */
        const source = primaryHandle.snap != GrabSnapMode.None
            ? primaryHandle.object
            : primaryInteractor;
        let secondaryInteractor = null;
        let target = null;
        if (this._grabData.length > 1) {
            const secondaryHandle = this.grabPoints[this._grabData[1].handleId];
            secondaryInteractor = this._grabData[1].interactor.object;
            target =
                secondaryHandle.snap != GrabSnapMode.None
                    ? secondaryHandle.object
                    : secondaryInteractor;
            const interactorUp = secondaryInteractor.getUpWorld(TempVec3.get());
            this._useUpOrientation = vec3.dot(interactorUp, UP) >= 0.5;
            TempVec3.free();
        }
        /* Do not use `_relativeGrabTransform` and `_pivotGrabTransform` directly.
         * The inner computation relies on those variables, we want them to be identity at this point. */
        const transform = TempDualQuat.get();
        const pivotTransform = TempDualQuat.get();
        this.computeTransform(transform, pivotTransform, source, target);
        const current = this._relativeGrabTransform;
        if (this._computeWorldSpace) {
            this.object.getTransformWorld(current);
        }
        else {
            this.object.getTransformLocal(current);
        }
        computeRelativeTransform(this._relativeGrabTransform, current, transform);
        const pivotCurrent = this._pivotGrabTransform;
        this.secondaryPivot?.getRotationLocal(pivotCurrent);
        computeRelativeTransform(this._pivotGrabTransform, pivotCurrent, pivotTransform);
        TempDualQuat.free(2);
    }
    _setKinematicState(enable) {
        if (!this._physx)
            return;
        if (enable === this._physx.kinematic)
            return;
        this._physx.kinematic = enable;
        /* Required to change the physx object state */
        this._physx.active = false;
        this._physx.active = true;
    }
}
__decorate([
    property.array(Property.object())
], Grabbable.prototype, "handleObjects", void 0);
__decorate([
    property.bool(true)
], Grabbable.prototype, "canThrow", void 0);
__decorate([
    property.float(1.0)
], Grabbable.prototype, "throwLinearIntensity", void 0);
__decorate([
    property.float(1.0)
], Grabbable.prototype, "throwAngularIntensity", void 0);
__decorate([
    property.bool(true)
], Grabbable.prototype, "useControllerVelocityData", void 0);
__decorate([
    property.float(0.25)
], Grabbable.prototype, "releaseDistance", void 0);
__decorate([
    property.object()
], Grabbable.prototype, "distanceMarker", void 0);
__decorate([
    property.int(0)
], Grabbable.prototype, "distanceHandle", void 0);
__decorate([
    property.enum(GrabTransformTypeNames, GrabTransformType.Hand)
], Grabbable.prototype, "transformType", void 0);
__decorate([
    property.enum(PivotAxisNames, PivotAxis.Y)
], Grabbable.prototype, "pivotAxis", void 0);
__decorate([
    property.object()
], Grabbable.prototype, "secondaryPivot", void 0);
__decorate([
    property.enum(PivotAxisNames, PivotAxis.Y)
], Grabbable.prototype, "secondaryPivotAxis", void 0);
__decorate([
    property.enum(InteractorVisualStateNames, InteractorVisualState.None)
], Grabbable.prototype, "interactorVisualState", void 0);
