import { Ray } from "three";

import { Gizmos } from "../../engine/engine_gizmos.js";
import { serializable } from "../../engine/engine_serialization_decorator.js";
import { getTempVector } from "../../engine/engine_three_utils.js";
import { getParam } from "../../engine/engine_utils.js";
import { Behaviour } from "../Component.js";


const debug = getParam("debugcursor");

/**
 * [CursorFollow](https://engine.needle.tools/docs/api/CursorFollow) makes an object smoothly follow the cursor or touch position in 3D space.  
 * The component tracks pointer movement and updates the object's position to follow it, with optional damping for smooth motion.  
 *
 * ![](https://cloud.needle.tools/-/media/GDspQGC_kB85Bc9IyEtr9Q.gif)  
 *
 * **How It Works:**  
 * The component creates a ray from the camera through the cursor position and places the object along that ray.  
 * By default, it maintains the object's initial distance from the camera, creating a natural cursor-following effect  
 * that works consistently regardless of camera movement.  
 *
 * **Key Features:**
 * - Smooth cursor following with configurable damping
 * - Works with both mouse and touch input
 * - Can follow cursor across the entire page or just within the canvas
 * - Maintains consistent distance from camera by default
 * - Optional surface snapping using raycasts
 * - Responds to camera movement automatically
 *
 * **Common Use Cases:**
 * - Interactive 3D cursors or pointers
 * - Look-at effects combined with {@link LookAtConstraint}
 * - Floating UI elements that track cursor
 * - Interactive product showcases
 * - 3D header effects and hero sections
 * - Virtual laser pointers in XR experiences
 *
 * @example Basic cursor follow with smooth damping
 * ```ts
 * const follower = new Object3D();
 * follower.position.set(0, 0, -5); // Initial position 5 units from camera
 * follower.addComponent(CursorFollow, {
 *   damping: 0.2,        // Smooth following with 200ms damping
 *   keepDistance: true,  // Maintain initial distance
 *   useFullPage: true    // Track cursor across entire page
 * });
 * scene.add(follower);
 * ```
 *
 * @example Surface-snapping cursor with raycast
 * ```ts
 * const cursor = new Object3D();
 * cursor.addComponent(CursorFollow, {
 *   snapToSurface: true,  // Snap to surfaces in the scene
 *   keepDistance: false,  // Don't maintain distance when snapping
 *   damping: 0.1          // Quick, responsive movement
 * });
 * scene.add(cursor);
 * ```
 *
 * @example Instant cursor following (no damping)
 * ```ts
 * gameObject.addComponent(CursorFollow, {
 *   damping: 0,           // Instant movement
 *   useFullPage: false    // Only track within canvas
 * });
 * ```
 *
 * @example Interactive 3D header that looks at cursor
 * ```ts
 * const character = loadModel("character.glb");
 * const lookTarget = new Object3D();
 * lookTarget.addComponent(CursorFollow, { damping: 0.3 });
 * character.addComponent(LookAtConstraint, { target: lookTarget });
 * scene.add(lookTarget, character);
 * ```
 *
 * - Example: [Look At Cursor sample](https://engine.needle.tools/samples/look-at-cursor-interactive-3d-header/) - Combines CursorFollow with LookAt for an interactive 3D header
 *
 * @see {@link PointerEvents} - For more complex pointer interaction handling
 * @see {@link DragControls} - For dragging objects in 3D space
 * @see {@link OrbitControls} - For camera controls that work alongside CursorFollow
 * @see {@link Context.input} - The input system that provides cursor position
 * @see {@link Context.physics.raycastFromRay} - Used when snapToSurface is enabled
 *
 * @summary Makes objects follow the cursor/touch position in 3D space
 * @category Interactivity
 * @category Web
 * @group Components
 * @component
 */
export class CursorFollow extends Behaviour {

    // testing this for compilation
    static readonly NAME = "CursorFollow";

    /**
     * Damping factor controlling how smoothly the object follows the cursor (in seconds).
     *
     * This value determines the "lag" or smoothness of the following motion:
     * - `0`: Instant movement, no damping (object snaps directly to cursor position)
     * - `0.1-0.2`: Quick, responsive following with slight smoothing
     * - `0.3-0.5`: Noticeable smooth trailing effect
     * - `1.0+`: Slow, heavily damped movement
     *
     * The damping uses delta time, so the movement speed is framerate-independent and
     * provides consistent behavior across different devices.
     *
     * **Tip:** For look-at effects, values between 0.2-0.4 typically feel most natural.
     * For cursor indicators, 0.1 or less provides better responsiveness.
     *
     * @default 0
     */
    @serializable()
    damping: number = 0;

    /**
     * Whether the object should track the cursor across the entire webpage or only within the canvas.
     *
     * **When `true` (default):**
     * - The object follows the cursor anywhere on the page, even outside the canvas bounds
     * - Perfect for look-at effects where you want continuous tracking
     * - Great for embedded 3D elements that should feel aware of the whole page
     * - Example: A 3D character in a hero section that watches the cursor as you scroll
     *
     * **When `false`:**
     * - The object only follows the cursor when it's inside the Needle Engine canvas
     * - Useful for contained experiences where the 3D element shouldn't react to external cursor movement
     * - Better for multi-canvas scenarios or when you want isolated 3D interactions
     *
     * **Note:** When enabled, the component listens to `window.pointermove` events to track the
     * full-page cursor position. When disabled, it uses the context's input system which is
     * canvas-relative.
     *
     * @see {@link Context.input.mousePositionRC} for canvas-relative cursor position
     * @default true
     */
    @serializable()
    useFullPage: boolean = true;

    /**
     * Whether to maintain the object's initial distance from the camera while following the cursor.
     *
     * **When `true` (default):**
     * - The object stays at a constant distance from the camera, moving in a spherical arc around it
     * - Creates a natural "floating at cursor position" effect
     * - The object's depth remains consistent as you move the cursor around
     * - Perfect for cursors, pointers, or look-at targets
     *
     * **When `false`:**
     * - The object's distance can change based on where the cursor projects in 3D space
     * - More useful when combined with {@link snapToSurface} to follow surface geometry
     * - Can create unusual depth behavior if not carefully configured
     *
     * **How it works:**
     * On the first update, the component measures the distance from the object to the camera.
     * This initial distance is then maintained throughout the object's lifetime (unless {@link updateDistance} is called).
     * The object moves along a ray from the camera through the cursor, staying at this fixed distance.
     *
     * @see {@link updateDistance} to manually recalculate the distance
     * @default true
     */
    @serializable()
    keepDistance: boolean = true;

    /**
     * When enabled, the object snaps to the surfaces of other objects in the scene using raycasting.
     *
     * **How it works:**
     * After positioning the object at the cursor location, a raycast is performed backwards toward the camera.
     * If the ray hits any surface, the object is moved to that hit point, effectively "snapping" to the surface.
     *
     * **Use cases:**
     * - 3D paint or decal placement tools
     * - Surface markers or waypoints
     * - Interactive object placement in AR/VR
     * - Cursor that follows terrain or mesh surfaces
     *
     * **Important notes:**
     * - Requires objects in the scene to have colliders for raycasting to work
     * - Works best with {@link keepDistance} set to `false` to allow depth changes
     * - Can be combined with {@link damping} for smooth surface following
     * - The raycast uses the physics system's raycast functionality
     *
     * **Debug mode:**
     * Add `?debugcursor` to your URL to visualize the raycast hits with green debug lines.
     *
     * @see {@link Context.physics.raycastFromRay} for the underlying raycast implementation
     * @see {@link keepDistance} should typically be false when using surface snapping
     * @default false
     */
    @serializable()
    snapToSurface: boolean = false;


    private _distance: number = -1;

    /**
     * Manually recalculates the distance between the object and the camera.
     *
     * By default, the distance is calculated once when the component starts and then maintained
     * when {@link keepDistance} is enabled. Use this method to update the reference distance
     * if the camera or object has moved significantly.
     *
     * **Use cases:**
     * - After teleporting the camera or object
     * - When switching between different camera positions
     * - After zoom operations that change the desired following distance
     * - Dynamically adjusting the cursor's depth in response to user input
     *
     * @param force - If `true`, forces a recalculation even if {@link keepDistance} is enabled and distance was already set
     *
     * @example Recalculate distance after camera movement
     * ```ts
     * const cursorFollow = gameObject.getComponent(CursorFollow);
     * camera.position.set(0, 0, 10); // Move camera
     * cursorFollow?.updateDistance(true); // Update the reference distance
     * ```
     */
    updateDistance(force: boolean = false) {
        if (!force && (this.keepDistance && this._distance !== -1)) {
            return;
        }
        this._distance = this.gameObject.worldPosition.distanceTo(this.context.mainCamera.worldPosition);
    }

    /** @internal */
    awake() {
        this._distance = -1;
    }

    /** @internal */
    onEnable(): void {
        this._distance = -1;
        window.addEventListener('pointermove', this._onPointerMove);
    }
    /** @internal */
    onDisable(): void {
        window.removeEventListener('pointermove', this._onPointerMove);
    }

    private _ndc_x = 0;
    private _ndc_y = 0;

    private _onPointerMove = (e: PointerEvent) => {
        if (!this.useFullPage) return;
        const x = e.clientX;
        const y = e.clientY;
        const domx = this.context.domX;
        const domy = this.context.domY;
        const domw = this.context.domWidth;
        const domh = this.context.domHeight;
        this._ndc_x = (x - domx) / domw * 2 - 1;
        this._ndc_y = - (y - domy) / domh * 2 + 1;
    }


    /** @internal */
    lateUpdate() {
        // continuously update distance in case camera or object moves
        this.updateDistance();

        const x = this.useFullPage ? this._ndc_x : this.context.input.mousePositionRC.x;
        const y = this.useFullPage ? this._ndc_y : this.context.input.mousePositionRC.y;

        // follow cursor in screenspace but maintain initial distance from camera
        const camera = this.context.mainCamera;
        const cameraPosition = camera.worldPosition;

        // create ray from camera through cursor position
        const rayDirection = getTempVector(x, y, 1).unproject(camera);
        rayDirection.sub(cameraPosition).normalize();

        // position object at initial distance along the ray
        const newPosition = getTempVector(rayDirection).multiplyScalar(this._distance).add(cameraPosition);
        let _position = newPosition;


        if (this.damping > 0) {
            const pos = this.gameObject.worldPosition;
            pos.lerp(newPosition, this.context.time.deltaTime / this.damping);
            this.gameObject.worldPosition = pos;
            _position = pos;
        }
        else {
            this.gameObject.worldPosition = newPosition;
        }


        if (this.snapToSurface) {
            ray.origin = _position;
            ray.direction = rayDirection.multiplyScalar(-1);
            const hits = this.context.physics.raycastFromRay(ray);
            if (hits?.length) {
                const hit = hits[0];
                if (this.damping > 0) {
                    this.gameObject.worldPosition = _position.lerp(hit.point, this.context.time.deltaTime / this.damping);
                }
                else {
                    this.gameObject.worldPosition = hit.point;
                }

                if(debug) {
                    Gizmos.DrawLine(hit.point, hit.normal!.add(hit.point), 0x00FF00);
                }
            }
        }

    }

}

const ray = new Ray();