// A trigger zone, as an aspect on a Node2D. Requires a Shape2D (its geometry); creates a SENSOR body
// from it, so a physics body overlapping the zone fires the node's 'enter' / 'exit' events instead of
// colliding. A trigger does not block movement.
//
//   const goal = new Node2D()
//     .aspect(Shape2D, { box: [32, 64] })
//     .aspect(Trigger2D)
//   goal.addEventListener('enter', other => win(other))
//   goal.position = [x, y]        // the zone follows — it is kinematic, not static
//   goal.trigger.enabled = false  // switch it off instead of parking it off-screen
//
// (A trigger is still pointer-pickable — clicks hit sensor shapes too.)
//
// Keep triggers on ROOT nodes: a sensor parented to a moving node does not follow it (see
// docs/2d-physics-plan.md §6).

import { Aspect } from "../core/Aspect"
import { ensurePhysicsEvents } from "./loop"
import { Shape2D } from "./Shape2D"
import { PhysicsGroup2D, _watchGroupChanges, _unwatchGroupChanges, categoryOf, sensorMask } from "./groups2d"
import type { Node2D } from "./Node2D"

export class Trigger2D extends Aspect<"trigger", Node2D> {
  static readonly aspect = "trigger"

  private _h = 0
  private _enabled = true
  private _group: PhysicsGroup2D | undefined
  private _repush = (): void => {
    if (this._h) _creator2d.physicsSetFilter(this._h, categoryOf(this._group), sensorMask(this._group))
  }

  /** Which collision group this zone belongs to. Live. */
  get group(): PhysicsGroup2D | undefined { return this._group }
  set group(g: PhysicsGroup2D | undefined) {
    this._group = g
    this.node.get(Shape2D)?._setFilter(categoryOf(g), sensorMask(g))
    this._repush()
  }

  /** Native physics body id (0 if no physics support). Mirrors 3D `Trigger.id`. */
  get id(): number { return this._h }

  onAttach(): void {
    if (!_creator2d.physicsHasSupport || !_creator2d.physicsHasSupport()) return
    const shape = this.node.get(Shape2D)
    if (!shape) {
      throw new Error("Trigger2D requires a Shape2D aspect — add it first: node.aspect(Shape2D, {…}).aspect(Trigger2D)")
    }
    // KINEMATIC, not static: a zone has to be able to ride an elevator, a patrolling enemy or a
    // fighter's fist. It used to be static, which is why games parked spent triggers at [-9999,-9999].
    this._h = _creator2d.physicsCreateBody(this.node.id, 1 /* kinematic */)
    if (!this._h) return
    // sensorMask always keeps the character bit: a kinematic mover is not a Box2D body, so its shadow
    // body is what triggers actually see, and no game should have to know that.
    shape._addTo(this._h, { sensor: true, friction: 0, bounce: 0, category: categoryOf(this.group), mask: sensorMask(this.group) })
    if (this._group) _watchGroupChanges(this._repush)
    this.node._xf = this._h        // node.position moves the zone
    this.node._xfKind = 2
    if (!this.enabled) _creator2d.physicsSetEnabled(this._h, false)   // via the accessor on purpose — see Physics2D.onAttach
    ensurePhysicsEvents()   // routes contact/sensor events → the node's 'enter' / 'exit'
  }

  onDetach(): void {
    _unwatchGroupChanges(this._repush)
    if (this._h) { _creator2d.physicsRemoveBody(this._h); this._h = 0 }
    this.node._xf = 0
    this.node._xfKind = 0
  }

  /** Switch the zone off and on — a spent pickup, a hitbox that is only live during a swing. */
  get enabled(): boolean { return this._enabled }
  set enabled(v: boolean) {
    this._enabled = v
    if (this._h) _creator2d.physicsSetEnabled(this._h, v)
  }
}
