
import { Quaternion, Vector3 } from "three";

import { serializable } from "../../../engine/engine_serialization_decorator.js";
import { getTempVector } from "../../../engine/engine_three_utils.js";
import type { NeedleXREventArgs } from "../../../engine/engine_xr.js";
import { Behaviour } from "../../Component.js";


/** 
 * Add this script to an object and set `side` to make the object follow a specific controller.   
 * 
 * This can be useful to attach objects to controllers, for example a laser pointer or a 3D model of a tool.
 * 
 * @example Make an object follow the right controller
 * ```ts
 * import { onStart, XRControllerFollow } from "@needle-tools/engine";
 * onStart(context => {
 *   const obj = context.scene.getObjectByName("MyObject");
 *   obj?.addComponent(XRControllerFollow, { side: "right", controller: true, hands: true });
 * });
 * ```
 * 
 * @summary Makes the object follow a specific XR controller or hand
 * @category XR
 * @group Components
 * */
export class XRControllerFollow extends Behaviour {

    // override active and enabled here so that we always receive xr update events
    get activeAndEnabled() {
        return true;
    }

    /** Should this object follow a right hand/controller or left hand/controller.
     * When a number is provided, the controller with that index is followed. 
     * @default "none"
     **/
    @serializable()
    side: XRHandedness | number = "none";

    /** should it follow controllers (the physics controller) 
     * @default true
    */
    @serializable()
    controller: boolean = true;

    /** should it follow hands (when using hand tracking in WebXR) 
     * @default false
    */
    @serializable()
    hands: boolean = false;

    /** Disable if you don't want this script to modify the object's visibility  
     * If enabled the object will be hidden when the configured controller or hand is not available  
     * If disabled this script will not modify the object's visibility
     * @default true
     */
    @serializable()
    controlVisibility: boolean = true;

    /** when true it will use the grip space, otherwise the ray space 
     * @default false
    */
    @serializable()
    useGripSpace: boolean = false;

    /** when enabled the position, rotation and scale of this object will be set to the position it was at when it entered the XR session 
     * @default true
    */
    @serializable()
    resetTransformAfterXRSession: boolean = true;

    private readonly _startPosition: Vector3 = new Vector3();
    private readonly _startRotation: Quaternion = new Quaternion();
    private readonly _startScale: Vector3 = new Vector3();

    /** @internal */
    onEnterXR(_args: NeedleXREventArgs): void {
        this._startPosition.copy(this.gameObject.position);
        this._startRotation.copy(this.gameObject.quaternion);
        this._startScale.copy(this.gameObject.scale);
    }

    /** @internal */
    onUpdateXR(args: NeedleXREventArgs): void {
        // explicit check since we're overriding activeAndEnabled
        if (!this.enabled) return;

        // try to get the controller
        const ctrl = args.xr.getController(this.side);
        if (ctrl) {
            // check if this is a hand and hands are allowed
            if (ctrl.hand && !this.hands) {
                if (this.controlVisibility)
                    this.gameObject.visible = false;
                return;
            }
            // check if this is a controller and controllers are allowed
            else if (!this.controller) {
                if (this.controlVisibility)
                    this.gameObject.visible = false;
                return;
            }
            // we're following a controller (or hand)
            if (this.controlVisibility)
                this.gameObject.visible = true;

            if (this.useGripSpace || ctrl.targetRayMode === "transient-pointer") {
                this.gameObject.worldPosition = ctrl.gripWorldPosition;
                this.gameObject.worldQuaternion = ctrl.gripWorldQuaternion;
                this.gameObject.worldScale = getTempVector(ctrl.xr.rigScale, ctrl.xr.rigScale, ctrl.xr.rigScale)
                    .multiply(this._startScale);
            }
            else {
                this.gameObject.worldPosition = ctrl.rayWorldPosition;
                this.gameObject.worldQuaternion = ctrl.rayWorldQuaternion;
                this.gameObject.worldScale = getTempVector(ctrl.xr.rigScale, ctrl.xr.rigScale, ctrl.xr.rigScale)
                    .multiply(this._startScale);
            }
        }
    }

    /** @internal */
    onLeaveXR(_args: NeedleXREventArgs): void {
        if (this.resetTransformAfterXRSession) {
            this.gameObject.position.copy(this._startPosition);
            this.gameObject.quaternion.copy(this._startRotation);
            this.gameObject.scale.copy(this._startScale);
        }
    }
}