import { Layers, Object3D } from "three";

import { serializable } from "../engine/engine_serialization_decorator.js";
import { getParam } from "../engine/engine_utils.js";
import { BoxHelperComponent } from "./BoxHelperComponent.js"
import { Behaviour, GameObject } from "./Component.js";
import { EventList } from "./EventList.js";

const debug = getParam("debugspatialtrigger");

/** Layer instances used for mask comparison */
const layer1 = new Layers();
const layer2 = new Layers();
/**
 * Tests if two layer masks intersect
 * @param mask1 First layer mask
 * @param mask2 Second layer mask
 * @returns True if the layers intersect
 */
function testMask(mask1, mask2) {
    layer1.mask = mask1;
    layer2.mask = mask2;
    return layer1.test(layer2);
}

/**
 * Component that receives and responds to spatial events, like entering or exiting a trigger zone.  
 * Used in conjunction with {@link SpatialTrigger} to create interactive spatial events.  
 *
 * Place this on objects that should react when entering trigger zones. The receiver checks  
 * against all active SpatialTriggers each frame and fires events when intersections change.  
 *
 * Events can be connected via {@link EventList} in the editor or listened to in code.  
 *
 * @example Listen to trigger events
 * ```ts
 * export class DoorTrigger extends Behaviour {
 *   @serializable(SpatialTriggerReceiver)
 *   receiver?: SpatialTriggerReceiver;
 *
 *   start() {
 *     this.receiver?.onEnter?.addEventListener(() => {
 *       console.log("Player entered door zone");
 *     });
 *   }
 * }
 * ```
 *
 * @summary Receives spatial trigger events
 * @category Interactivity
 * @group Components
 * @see {@link SpatialTrigger} for defining trigger zones
 * @see {@link EventList} for event handling
 * @link https://engine.needle.tools/samples/spatial-triggers/
 */
export class SpatialTriggerReceiver extends Behaviour {

    /**
     * Bitmask determining which triggers this receiver responds to
     * Only triggers with matching masks will interact with this receiver
     */
    @serializable()
    triggerMask: number = 0;
    
    /** Event invoked when this object enters a trigger zone */
    @serializable(EventList)
    onEnter?: EventList<any>;
    
    /** Event invoked continuously while this object is inside a trigger zone */
    @serializable(EventList)
    onStay?: EventList<any>;
    
    /** Event invoked when this object exits a trigger zone */
    @serializable(EventList)
    onExit?: EventList<any>;

    /** 
     * Initializes the receiver and logs debug info if enabled
     * @internal
     */
    start() {
        if (debug) console.log(this.name, this.triggerMask, this);
    }

    /**
     * Checks for intersections with spatial triggers and fires appropriate events
     * Handles enter, stay, and exit events for all relevant triggers
     * @internal
     */
    update(): void {
        this.currentIntersected.length = 0;
        for (const trigger of SpatialTrigger.triggers) {
            if (testMask(trigger.triggerMask, this.triggerMask)) {
                if (trigger.test(this.gameObject)) {
                    this.currentIntersected.push(trigger);
                }
            }
        }
        for (let i = this.lastIntersected.length - 1; i >= 0; i--) {
            const last = this.lastIntersected[i]
            if (this.currentIntersected.indexOf(last) < 0) {
                this.onExitTrigger(last);
                this.lastIntersected.splice(i, 1);
            }
        }
        for (const cur of this.currentIntersected) {
            if (this.lastIntersected.indexOf(cur) < 0)
                this.onEnterTrigger(cur);
            this.onStayTrigger(cur);
        }
        this.lastIntersected.length = 0;
        this.lastIntersected.push(...this.currentIntersected);
    }

    /** Array of triggers currently intersecting with this receiver */
    readonly currentIntersected: SpatialTrigger[] = [];
    
    /** Array of triggers that intersected with this receiver in the previous frame */
    readonly lastIntersected: SpatialTrigger[] = [];

    /**
     * Handles trigger enter events.
     * @param trigger The spatial trigger that was entered
     */
    onEnterTrigger(trigger: SpatialTrigger): void {
        if(debug) console.log("ENTER TRIGGER", this.name, trigger.name, this, trigger);
        trigger.raiseOnEnterEvent(this);
        this.onEnter?.invoke();
    }
    
    /**
     * Handles trigger exit events.
     * @param trigger The spatial trigger that was exited
     */
    onExitTrigger(trigger: SpatialTrigger): void {
        if(debug) console.log("EXIT TRIGGER", this.name, trigger.name, );
        trigger.raiseOnExitEvent(this);
        this.onExit?.invoke();
    }

    /**
     * Handles trigger stay events.
     * @param trigger The spatial trigger that the receiver is staying in
     */
    onStayTrigger(trigger: SpatialTrigger): void {
        trigger.raiseOnStayEvent(this);
        this.onStay?.invoke();
    }
}

/**
 * A spatial trigger component that detects objects within a box-shaped area.  
 * Used to trigger events when objects enter, stay in, or exit the defined area.  
 *
 * The trigger area is defined by the GameObject's bounding box (uses {@link BoxHelperComponent}).  
 * Objects with {@link SpatialTriggerReceiver} components are tested against this area.  
 *
 * **Mask system:** Both trigger and receiver have a `triggerMask` - they only interact  
 * when their masks have overlapping bits set. This allows selective triggering.  
 *
 * **Debug:** Use `?debugspatialtrigger` URL parameter to visualize trigger zones.  
 *
 * @example Create a pickup zone
 * ```ts
 * // On the pickup zone object
 * const trigger = pickupZone.addComponent(SpatialTrigger);
 * trigger.triggerMask = 1; // Layer 1 for pickups
 *
 * // On the player
 * const receiver = player.addComponent(SpatialTriggerReceiver);
 * receiver.triggerMask = 1; // Match the pickup layer
 * ```
 *
 * @summary Define a trigger zone that detects entering objects
 * @category Interactivity
 * @group Components
 * @see {@link SpatialTriggerReceiver} for objects that respond to triggers
 * @see {@link BoxHelperComponent} for the underlying box used to define the trigger area
 * @link https://engine.needle.tools/samples/spatial-triggers/
 */
export class SpatialTrigger extends Behaviour {

    /** Global registry of all active spatial triggers in the scene */
    static triggers: SpatialTrigger[] = [];

    /** 
     * Bitmask determining which receivers this trigger affects.
     * Only receivers with matching masks will be triggered.
     */
    // currently Layers in unity but maybe this should be a string or plane number? Or should it be a bitmask to allow receivers use multiple triggers?
    @serializable()
    triggerMask?: number;

    /** Box helper component used to visualize and calculate the trigger area */
    private boxHelper?: BoxHelperComponent;

    /**
     * Initializes the trigger and logs debug info if enabled
     */
    start() {
        if (debug)
            console.log(this.name, this.triggerMask, this);
    }

    /**
     * Registers this trigger in the global registry and sets up debug visualization if enabled
     */
    onEnable(): void {
        SpatialTrigger.triggers.push(this);
        if (!this.boxHelper) {
            this.boxHelper = GameObject.addComponent(this.gameObject, BoxHelperComponent);
            this.boxHelper?.showHelper(null, debug as boolean);
        }
    }
    
    /**
     * Removes this trigger from the global registry when disabled
     */
    onDisable(): void {
        SpatialTrigger.triggers.splice(SpatialTrigger.triggers.indexOf(this), 1);
    }

    /**
     * Tests if an object is inside this trigger's box
     * @param obj The object to test against this trigger
     * @returns True if the object is inside the trigger box
     */
    test(obj: Object3D): boolean {
        if (!this.boxHelper) return false;
        return this.boxHelper.isInBox(obj) ?? false;
    }

    // private args: SpatialTriggerEventArgs = new SpatialTriggerEventArgs();

    /**
     * Raises the onEnter event on any SpatialTriggerReceiver components attached to this trigger's GameObject
     * @param rec The receiver that entered this trigger
     */
    raiseOnEnterEvent(rec: SpatialTriggerReceiver) {
        // this.args.trigger = this;
        // this.args.source = rec;
        GameObject.foreachComponent(this.gameObject, c => {
            if (c === rec) return;
            if(c instanceof SpatialTriggerReceiver) {
                c.onEnterTrigger(this);
            }
        }, false);
    }

    /**
     * Raises the onStay event on any SpatialTriggerReceiver components attached to this trigger's GameObject
     * @param rec The receiver that is staying in this trigger
     */
    raiseOnStayEvent(rec: SpatialTriggerReceiver) {
        // this.args.trigger = this;
        // this.args.source = rec;
        GameObject.foreachComponent(this.gameObject, c => {
            if (c === rec) return;
            if(c instanceof SpatialTriggerReceiver) {
                c.onStayTrigger(this);
            }
        }, false);
    }

    /**
     * Raises the onExit event on any SpatialTriggerReceiver components attached to this trigger's GameObject
     * @param rec The receiver that exited this trigger
     */
    raiseOnExitEvent(rec: SpatialTriggerReceiver) {
        GameObject.foreachComponent(this.gameObject, c => {
            if (c === rec) return;
            if(c instanceof SpatialTriggerReceiver) {
                c.onExitTrigger(this);
            }
        }, false);
    }
}
