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, InputComponent, ViewComponent, Emitter, } from '@wonderlandengine/api';
import { property } from '@wonderlandengine/api/decorators.js';
import { vec3, mat4 } from 'gl-matrix';
import { CursorTarget } from './cursor-target.js';
import { HitTestLocation } from './hit-test-location.js';
const tempVec = new Float32Array(3);
const ZERO = [0, 0, 0];
/** Global target for {@link Cursor} */
class CursorTargetEmitters {
    /** Emitter for events when the target is hovered */
    onHover = new Emitter();
    /** Emitter for events when the target is unhovered */
    onUnhover = new Emitter();
    /** Emitter for events when the target is clicked */
    onClick = new Emitter();
    /** Emitter for events when the cursor moves on the target */
    onMove = new Emitter();
    /** Emitter for events when the user pressed the select button on the target */
    onDown = new Emitter();
    /** Emitter for events when the user unpressed the select button on the target */
    onUp = new Emitter();
}
/**
 * 3D cursor for desktop/mobile/VR.
 *
 * Implements a ray-casting cursor into the scene. To react to
 * clicking/hover/unhover/cursor down/cursor up/move use a
 * [cursor-target](#cursor-target).
 *
 * For VR, the ray is cast in direction of
 * [this.object.getForward()](/jsapi/object/#getforward). For desktop and mobile, the
 * forward vector is inverse-projected to account for where on screen the user clicked.
 *
 * `.globalTarget` can be used to call callbacks for all objects, even those that
 * do not have a cursor target attached, but match the collision group.
 *
 * `.hitTestTarget` can be used to call callbacks WebXR hit test results,
 *
 * See [Animation Example](/showcase/animation).
 */
class Cursor extends Component {
    static TypeName = 'cursor';
    /* Dependencies is deprecated, but we keep it here for compatibility
     * with 1.0.0-rc2 until 1.0.0 is released */
    static Dependencies = [HitTestLocation];
    static onRegister(engine) {
        engine.registerComponent(HitTestLocation);
    }
    _collisionMask = 0;
    _onDeactivateCallbacks = [];
    _input = null;
    _origin = new Float32Array(3);
    _cursorObjScale = new Float32Array(3);
    _direction = new Float32Array(3);
    _projectionMatrix = new Float32Array(16);
    _viewComponent = null;
    _isDown = false;
    _lastIsDown = false;
    _arTouchDown = false;
    _lastPointerPos = new Float32Array(2);
    _lastCursorPosOnTarget = new Float32Array(3);
    _hitTestLocation = null;
    _hitTestObject = null;
    _onSessionStartCallback = null;
    /**
     * Whether the cursor (and cursorObject) is visible, i.e. pointing at an object
     * that matches the collision group
     */
    visible = true;
    /** Currently hovered object */
    hoveringObject = null;
    /** CursorTarget component of the currently hovered object */
    hoveringObjectTarget = null;
    /** Whether the cursor is hovering reality via hit-test */
    hoveringReality = false;
    /**
     * Global target lets you receive global cursor events on any object.
     */
    globalTarget = new CursorTargetEmitters();
    /**
     * Hit test target lets you receive cursor events for "reality", if
     * `useWebXRHitTest` is set to `true`.
     *
     * @example
     * ```js
     * cursor.hitTestTarget.onClick.add((hit, cursor) => {
     *     // User clicked on reality
     * });
     * ```
     */
    hitTestTarget = new CursorTargetEmitters();
    /** World position of the cursor */
    cursorPos = new Float32Array(3);
    /** Collision group for the ray cast. Only objects in this group will be affected by this cursor. */
    collisionGroup = 1;
    /** (optional) Object that visualizes the cursor's ray. */
    cursorRayObject = null;
    /** Axis along which to scale the `cursorRayObject`. */
    cursorRayScalingAxis = 2;
    /** (optional) Object that visualizes the cursor's hit location. */
    cursorObject = null;
    /** Handedness for VR cursors to accept trigger events only from respective controller. */
    handedness = 0;
    /** Object that has an input component. */
    inputObject = null;
    /** Object that has a view component. */
    viewObject = null;
    /** Mode for raycasting, whether to use PhysX or simple collision components */
    rayCastMode = 0;
    /** Maximum distance for the cursor's ray cast. */
    maxDistance = 100;
    /** Whether to set the CSS style of the mouse cursor on desktop */
    styleCursor = true;
    /**
     * Use WebXR hit-test if available.
     *
     * Attaches a hit-test-location component to the cursorObject, which will be used
     * by the cursor to send events to the hitTestTarget with HitTestResult.
     */
    useWebXRHitTest = false;
    _onViewportResize = () => {
        if (!this._viewComponent)
            return;
        /* Projection matrix will change if the viewport is resized, which will affect the
         * projection matrix because of the aspect ratio. */
        mat4.invert(this._projectionMatrix, this._viewComponent.projectionMatrix);
    };
    start() {
        this._collisionMask = 1 << this.collisionGroup;
        if (this.handedness == 0) {
            const inputCompObj = this.inputObject || this.object;
            const inputComp = inputCompObj.getComponent(InputComponent);
            if (!inputComp) {
                console.warn('cursor component on object', this.object.name, 'was configured with handedness "input component", ' +
                    'but object has no input component.');
            }
            else {
                this.handedness = inputComp.handedness || 'none';
                this._input = inputComp;
            }
        }
        else {
            this.handedness = ['left', 'right', 'none'][this.handedness - 1];
        }
        const viewObject = this.viewObject || this.object;
        this._viewComponent = viewObject.getComponent(ViewComponent);
        if (this.useWebXRHitTest) {
            this._hitTestObject = this.engine.scene.addObject(this.object);
            this._hitTestLocation =
                this._hitTestObject.addComponent(HitTestLocation, {
                    scaleObject: false,
                }) ?? null;
        }
        this._onSessionStartCallback = this.setupVREvents.bind(this);
    }
    onActivate() {
        this.engine.onXRSessionStart.add(this._onSessionStartCallback);
        this.engine.onResize.add(this._onViewportResize);
        this._setCursorVisibility(true);
        /* If this object also has a view component, we will enable inverse-projected mouse clicks,
         * otherwise just use the objects transformation */
        if (this._viewComponent != null) {
            const canvas = this.engine.canvas;
            const onClick = this.onClick.bind(this);
            const onPointerMove = this.onPointerMove.bind(this);
            const onPointerDown = this.onPointerDown.bind(this);
            const onPointerUp = this.onPointerUp.bind(this);
            canvas.addEventListener('click', onClick);
            canvas.addEventListener('pointermove', onPointerMove);
            canvas.addEventListener('pointerdown', onPointerDown);
            canvas.addEventListener('pointerup', onPointerUp);
            this._onDeactivateCallbacks.push(() => {
                canvas.removeEventListener('click', onClick);
                canvas.removeEventListener('pointermove', onPointerMove);
                canvas.removeEventListener('pointerdown', onPointerDown);
                canvas.removeEventListener('pointerup', onPointerUp);
            });
        }
        this._onViewportResize();
    }
    _setCursorRayTransform(hitPosition) {
        if (!this.cursorRayObject)
            return;
        const dist = vec3.dist(this._origin, hitPosition);
        this.cursorRayObject.setPositionLocal([0.0, 0.0, -dist / 2]);
        if (this.cursorRayScalingAxis != 4) {
            /* Scale only the requested axis, relative to the original scaling */
            tempVec.fill(1);
            tempVec[this.cursorRayScalingAxis] = dist / 2;
            this.cursorRayObject.setScalingLocal(tempVec);
        }
    }
    _setCursorVisibility(visible) {
        if (this.visible == visible)
            return;
        this.visible = visible;
        if (!this.cursorObject)
            return;
        if (visible) {
            this.cursorObject.setScalingWorld(this._cursorObjScale);
        }
        else {
            this.cursorObject.getScalingWorld(this._cursorObjScale);
            this.cursorObject.scaleLocal([0, 0, 0]);
        }
    }
    update() {
        /* If in VR, set the cursor ray based on object transform */
        /* Since Google Cardboard tap is registered as arTouchDown without a gamepad, we need to check for gamepad presence */
        if (this.engine.xr &&
            this._arTouchDown &&
            this._input &&
            this.engine.xr.session.inputSources[0].handedness === 'none' &&
            this.engine.xr.session.inputSources[0].gamepad) {
            /* WebXR AR input */
            const p = this.engine.xr.session.inputSources[0].gamepad.axes;
            /* Screenspace Y is inverted */
            this._direction[0] = p[0];
            this._direction[1] = -p[1];
            this._direction[2] = -1.0;
            this.applyTransformAndProjectDirection();
        }
        else if (this.engine.xr && this._input && this._input.xrInputSource) {
            /* WebXR VR input */
            this._direction[0] = 0;
            this._direction[1] = 0;
            this._direction[2] = -1.0;
            this.applyTransformToDirection();
        }
        else if (this._viewComponent) {
            /* Apply potentially changed transform to last stored pointer
             * position */
            this.updateDirection();
        }
        this.rayCast(null, this.engine.xr?.frame);
        if (this.cursorObject) {
            if (this.hoveringObject &&
                (this.cursorPos[0] != 0 || this.cursorPos[1] != 0 || this.cursorPos[2] != 0)) {
                this._setCursorVisibility(true);
                this.cursorObject.setPositionWorld(this.cursorPos);
                this._setCursorRayTransform(this.cursorPos);
            }
            else {
                this._setCursorVisibility(false);
            }
        }
    }
    /* Returns the hovered cursor target, if available */
    notify(event, originalEvent) {
        const target = this.hoveringObject;
        if (target) {
            const cursorTarget = this.hoveringObjectTarget;
            if (cursorTarget)
                cursorTarget[event].notify(target, this, originalEvent ?? undefined);
            this.globalTarget[event].notify(target, this, originalEvent ?? undefined);
        }
    }
    hoverBehaviour(rayHit, hitTestResult, doClick, originalEvent) {
        /* Old API version does not return null for objects[0] if no hit */
        const hit = !this.hoveringReality && rayHit.hitCount > 0 ? rayHit.objects[0] : null;
        if (hit) {
            if (!this.hoveringObject || !this.hoveringObject.equals(hit)) {
                /* Unhover previous, if exists */
                if (this.hoveringObject) {
                    this.notify('onUnhover', originalEvent);
                }
                /* Hover new object */
                this.hoveringObject = hit;
                this.hoveringObjectTarget = this.hoveringObject.getComponent(CursorTarget);
                if (this.styleCursor)
                    this.engine.canvas.style.cursor = 'pointer';
                this.notify('onHover', originalEvent);
            }
        }
        else if (this.hoveringObject) {
            /* Previously hovering object, now hovering nothing */
            this.notify('onUnhover', originalEvent);
            this.hoveringObject = null;
            this.hoveringObjectTarget = null;
            if (this.styleCursor)
                this.engine.canvas.style.cursor = 'default';
        }
        if (this.hoveringObject) {
            /* onDown/onUp for object */
            if (this._isDown !== this._lastIsDown) {
                this.notify(this._isDown ? 'onDown' : 'onUp', originalEvent);
            }
            /* onClick for object */
            if (doClick)
                this.notify('onClick', originalEvent);
        }
        else if (this.hoveringReality) {
            /* onDown/onUp for hit test */
            if (this._isDown !== this._lastIsDown) {
                (this._isDown ? this.hitTestTarget.onDown : this.hitTestTarget.onUp).notify(hitTestResult, this, originalEvent ?? undefined);
            }
            /* onClick for hit test */
            if (doClick)
                this.hitTestTarget.onClick.notify(hitTestResult, this, originalEvent ?? undefined);
        }
        /* onMove */
        if (hit) {
            if (this.hoveringObject) {
                this.hoveringObject.transformPointInverseWorld(tempVec, this.cursorPos);
            }
            else {
                tempVec.set(this.cursorPos);
            }
            if (!vec3.equals(this._lastCursorPosOnTarget, tempVec)) {
                this.notify('onMove', originalEvent);
                this._lastCursorPosOnTarget.set(tempVec);
            }
        }
        else if (this.hoveringReality) {
            if (!vec3.equals(this._lastCursorPosOnTarget, this.cursorPos)) {
                this.hitTestTarget.onMove.notify(hitTestResult, this, originalEvent ?? undefined);
                this._lastCursorPosOnTarget.set(this.cursorPos);
            }
        }
        else {
            this._lastCursorPosOnTarget.set(this.cursorPos);
        }
        this._lastIsDown = this._isDown;
    }
    /**
     * Setup event listeners on session object
     * @param s WebXR session
     *
     * Sets up 'select' and 'end' events.
     */
    setupVREvents(s) {
        if (!s)
            console.error('setupVREvents called without a valid session');
        /* Because we remove the callback in .active and another component might
         * deactivate the cursor during onXRSessionStart, we make sure the cursor
         * really is active when we run this */
        if (!this.active)
            return;
        /* If in VR, one-time bind the listener */
        const onSelect = this.onSelect.bind(this);
        s.addEventListener('select', onSelect);
        const onSelectStart = this.onSelectStart.bind(this);
        s.addEventListener('selectstart', onSelectStart);
        const onSelectEnd = this.onSelectEnd.bind(this);
        s.addEventListener('selectend', onSelectEnd);
        this._onDeactivateCallbacks.push(() => {
            if (!this.engine.xr)
                return;
            s.removeEventListener('select', onSelect);
            s.removeEventListener('selectstart', onSelectStart);
            s.removeEventListener('selectend', onSelectEnd);
        });
        /* After AR session was entered, the projection matrix changed */
        this._onViewportResize();
    }
    onDeactivate() {
        this.engine.onXRSessionStart.remove(this._onSessionStartCallback);
        this.engine.onResize.remove(this._onViewportResize);
        this._setCursorVisibility(false);
        if (this.hoveringObject)
            this.notify('onUnhover', null);
        if (this.cursorRayObject)
            this.cursorRayObject.setScalingLocal(ZERO);
        /* Ensure all event listeners are removed */
        for (const f of this._onDeactivateCallbacks)
            f();
        this._onDeactivateCallbacks.length = 0;
    }
    onDestroy() {
        this._hitTestObject?.destroy();
    }
    /** 'select' event listener */
    onSelect(e) {
        if (e.inputSource.handedness != this.handedness)
            return;
        this.rayCast(e, e.frame, true);
    }
    /** 'selectstart' event listener */
    onSelectStart(e) {
        this._arTouchDown = true;
        if (e.inputSource.handedness == this.handedness) {
            this._isDown = true;
            this.rayCast(e, e.frame);
        }
    }
    /** 'selectend' event listener */
    onSelectEnd(e) {
        this._arTouchDown = false;
        if (e.inputSource.handedness == this.handedness) {
            this._isDown = false;
            this.rayCast(e, e.frame);
        }
    }
    /** 'pointermove' event listener */
    onPointerMove(e) {
        /* Don't care about secondary pointers */
        if (!e.isPrimary)
            return;
        this.updateMousePos(e);
        this.rayCast(e, null);
    }
    /** 'click' event listener */
    onClick(e) {
        this.updateMousePos(e);
        this.rayCast(e, null, true);
    }
    /** 'pointerdown' event listener */
    onPointerDown(e) {
        /* Don't care about secondary pointers or non-left clicks */
        if (!e.isPrimary || e.button !== 0)
            return;
        this.updateMousePos(e);
        this._isDown = true;
        this.rayCast(e);
    }
    /** 'pointerup' event listener */
    onPointerUp(e) {
        /* Don't care about secondary pointers or non-left clicks */
        if (!e.isPrimary || e.button !== 0)
            return;
        this.updateMousePos(e);
        this._isDown = false;
        this.rayCast(e);
    }
    /**
     * Update mouse position in non-VR mode and raycast for new position
     * @returns @ref RayHit for new position.
     */
    updateMousePos(e) {
        this._lastPointerPos[0] = e.clientX;
        this._lastPointerPos[1] = e.clientY;
        this.updateDirection();
    }
    updateDirection() {
        const bounds = this.engine.canvas.getBoundingClientRect();
        /* Get direction in normalized device coordinate space from mouse position */
        const left = this._lastPointerPos[0] / bounds.width;
        const top = this._lastPointerPos[1] / bounds.height;
        this._direction[0] = left * 2 - 1;
        this._direction[1] = -top * 2 + 1;
        this._direction[2] = -1.0;
        this.applyTransformAndProjectDirection();
    }
    applyTransformAndProjectDirection() {
        /* Reverse-project the direction into view space */
        vec3.transformMat4(this._direction, this._direction, this._projectionMatrix);
        vec3.normalize(this._direction, this._direction);
        this.applyTransformToDirection();
    }
    applyTransformToDirection() {
        this.object.transformVectorWorld(this._direction, this._direction);
        this.object.getPositionWorld(this._origin);
    }
    rayCast(originalEvent, frame = null, doClick = false) {
        const rayHit = this.rayCastMode == 0
            ? this.engine.scene.rayCast(this._origin, this._direction, this._collisionMask)
            : this.engine.physics.rayCast(this._origin, this._direction, this._collisionMask, this.maxDistance);
        let hitResultDistance = Infinity;
        let hitTestResult = null;
        if (this._hitTestLocation?.visible) {
            this._hitTestObject.getPositionWorld(this.cursorPos);
            hitResultDistance = vec3.distance(this.object.getPositionWorld(tempVec), this.cursorPos);
            hitTestResult = this._hitTestLocation?.getHitTestResults(frame)[0];
        }
        let hoveringReality = false;
        if (rayHit.hitCount > 0) {
            const d = rayHit.distances[0];
            if (hitResultDistance >= d) {
                /* Override cursorPos set by hit test location */
                this.cursorPos.set(rayHit.locations[0]);
            }
            else {
                hoveringReality = true;
            }
        }
        else if (hitResultDistance < Infinity) {
            /* cursorPos already set */
        }
        else {
            this.cursorPos.fill(0);
        }
        if (hoveringReality && !this.hoveringReality) {
            this.hitTestTarget.onHover.notify(hitTestResult, this);
        }
        else if (!hoveringReality && this.hoveringReality) {
            this.hitTestTarget.onUnhover.notify(hitTestResult, this);
        }
        this.hoveringReality = hoveringReality;
        this.hoverBehaviour(rayHit, hitTestResult, doClick, originalEvent);
        return rayHit;
    }
}
__decorate([
    property.int(1)
], Cursor.prototype, "collisionGroup", void 0);
__decorate([
    property.object()
], Cursor.prototype, "cursorRayObject", void 0);
__decorate([
    property.enum(['x', 'y', 'z', 'none'], 'z')
], Cursor.prototype, "cursorRayScalingAxis", void 0);
__decorate([
    property.object()
], Cursor.prototype, "cursorObject", void 0);
__decorate([
    property.enum(['input component', 'left', 'right', 'none'], 'input component')
], Cursor.prototype, "handedness", void 0);
__decorate([
    property.object()
], Cursor.prototype, "inputObject", void 0);
__decorate([
    property.object()
], Cursor.prototype, "viewObject", void 0);
__decorate([
    property.enum(['collision', 'physx'], 'collision')
], Cursor.prototype, "rayCastMode", void 0);
__decorate([
    property.float(100)
], Cursor.prototype, "maxDistance", void 0);
__decorate([
    property.bool(true)
], Cursor.prototype, "styleCursor", void 0);
__decorate([
    property.bool(false)
], Cursor.prototype, "useWebXRHitTest", void 0);
export { Cursor };
//# sourceMappingURL=cursor.js.map