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";


/** Derive from this class to create your own custom Raycaster  
 * If you override awake, onEnable or onDisable, be sure to call the base class methods  
 * Implement `performRaycast` to perform your custom raycasting logic
 */
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;
}


export class ObjectRaycaster extends Raycaster {
    private targets: Object3D[] | null = null;
    private raycastHits: Intersection[] = [];

    @serializable()
    ignoreSkinnedMeshes = 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;
    }
}

export class GraphicRaycaster extends ObjectRaycaster {
    // eventCamera: Camera | null = null;
    // ignoreReversedGraphics: boolean = false;
    // rootRaycaster: GraphicRaycaster | null = null;

    constructor() {
        super();
        this.ignoreSkinnedMeshes = true;
    }
}

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);
    }
}