import { Intersection, Object3D, SkinnedMesh } from "three";

import { type IRaycastOptions, RaycastOptions } from "../../engine/engine_physics.js";
import { serializable } from "../../engine/engine_serialization.js";
import { NeedleXRSession } from "../../engine/engine_xr.js";
import { Behaviour } from "../Component.js";
import { EventSystem } from "./EventSystem.js";


/**
 * [ObjectRaycaster](https://engine.needle.tools/docs/api/ObjectRaycaster) Base class for raycasters that detect pointer interactions.  
 * Derive from this class to create custom raycasting logic. 
 *
 * **Built-in raycasters:**  
 * - {@link ObjectRaycaster} - Raycasts against 3D objects
 * - {@link GraphicRaycaster} - Raycasts against UI elements
 * - {@link SpatialGrabRaycaster} - Sphere overlap for XR grab
 *
 * **Important:** If you override `awake`, `onEnable`, or `onDisable`,  
 * call the base class methods to ensure proper registration with {@link EventSystem}.
 *
 * @category Interactivity
 * @group Components
 * @see {@link EventSystem} for the event dispatch system
 */
export abstract class Raycaster extends Behaviour {
    awake(): void {
        EventSystem.createIfNoneExists(this.context);
    }

    onEnable(): void {
        EventSystem.get(this.context)?.register(this);
    }

    onDisable(): void {
        EventSystem.get(this.context)?.unregister(this);
    }

    abstract performRaycast(_opts?: IRaycastOptions | RaycastOptions | null): Intersection[] | null;
}


/**
 * ObjectRaycaster enables pointer interactions with 3D objects.  
 * Add this component to any object that needs click/hover detection.  
 *
 * **Usage:**  
 * Objects with ObjectRaycaster will receive pointer events when  
 * they implement interfaces like {@link IPointerClickHandler}.
 * 
 * **Note:**  
 * In older Needle Engine versions the ObjectRaycaster was required to be added to the Scene.  
 * This is no longer the case - the EventSystem will automatically handle raycasts.  
 *
 *
 * @category Interactivity
 * @group Components
 * @see {@link IPointerClickHandler} for click events
 * @see {@link DragControls} for drag interactions
 */
export class ObjectRaycaster extends Raycaster {
    private targets: Object3D[] | null = null;
    private raycastHits: Intersection[] = [];

    @serializable()
    ignoreSkinnedMeshes: boolean = false;

    start(): void {
        this.targets = [this.gameObject];
    }

    performRaycast(opts: IRaycastOptions | RaycastOptions | null = null): Intersection[] | null {
        if (!this.targets) return null;
        opts ??= new RaycastOptions();
        opts.targets = this.targets;
        opts.results = this.raycastHits;
        opts.useAcceleratedRaycast = true;

        const orig = opts.testObject;
        if (this.ignoreSkinnedMeshes) {
            opts.testObject = obj => {
                // if we are set to ignore skinned meshes, we return false for them
                if (obj instanceof SkinnedMesh) {
                    return "continue in children";
                }
                // call the original testObject function
                if (orig) return orig(obj);
                // otherwise allow raycasting
                return true;
            };
        }
        const hits = this.context.physics.raycast(opts);
        opts.testObject = orig;
        return hits;
    }
}


/**
 * GraphicRaycaster enables pointer interactions with UI elements.
 * Add this to a {@link Canvas} or UI hierarchy to enable button clicks,
 * hover effects, and other UI interactions.
 *
 * **Requirements:**
 * - Must be on the same object as a Canvas or on a parent
 * - UI elements need proper RectTransform setup
 *
 * @example Enable UI interaction
 * ```ts
 * // Add to Canvas object
 * canvas.addComponent(GraphicRaycaster);
 * // Now buttons and other UI elements will respond to clicks
 * ```
 *
 * @summary Raycaster for UI elements
 * @category User Interface
 * @group Components
 * @see {@link Canvas} for UI root
 * @see {@link Button} for clickable UI
 * @see {@link EventSystem} for event handling
 */
export class GraphicRaycaster extends ObjectRaycaster {
    // eventCamera: Camera | null = null;
    // ignoreReversedGraphics: boolean = false;
    // rootRaycaster: GraphicRaycaster | null = null;

    constructor() {
        super();
        this.ignoreSkinnedMeshes = true;
    }
}

/**
 * SpatialGrabRaycaster enables direct grab interactions in VR/AR.
 * Uses sphere overlap detection around the controller/hand position
 * to allow grabbing objects by reaching into them.
 *
 * **Features:**
 * - Active only during XR sessions
 * - Can be globally disabled via `SpatialGrabRaycaster.allow`
 * - Works alongside ray-based interaction
 *
 * @category XR
 * @group Components
 * @see {@link WebXR} for XR session management
 * @see {@link DragControls} for object manipulation
 */
export class SpatialGrabRaycaster extends Raycaster {

    /**
     * Use to disable SpatialGrabRaycaster globally
     */
    static allow: boolean = true;

    performRaycast(_opts?: IRaycastOptions | RaycastOptions | null): Intersection[] | null {
        // ensure we're in XR, otherwise return
        if (!NeedleXRSession.active) return null;
        if (!SpatialGrabRaycaster.allow) return null;
        if (!_opts?.ray) return null;

        // TODO this raycast should actually start from gripWorldPosition, not the ray origin, for
        // cases like transient-pointer on VisionOS where the ray starts at the head and not the hand
        const rayOrigin = _opts.ray.origin;
        const radius = 0.015;

        // TODO if needed, check if the input source is a XR controller or hand
        // draw gizmo around ray origin
        // Gizmos.DrawSphere(rayOrigin, radius, 0x00ff0022);

        return this.context.physics.sphereOverlap(rayOrigin, radius, false, true);
    }
}