import { Body, Box, Vec3 } from "cannon-es";
import { PhysicsC, PhysicsLayer } from "./PhysicsC";

/**
 * A single invisible trigger zone.
 *
 * It is a cannon body flagged `isTrigger`: the physics world still detects when
 * something overlaps it (so we get events), but it produces NO push — the
 * player walks straight through. Use it for "player entered this area" logic
 * (resource pickups, gather zones, etc.).
 */
export class Trigger {
  readonly body: Body;
  onEnter?: () => void;
  onExit?: () => void;

  constructor(
    center: { x: number; y: number; z: number },
    halfExtents: { x: number; y: number; z: number },
    handlers: { onEnter?: () => void; onExit?: () => void } = {}
  ) {
    this.onEnter = handlers.onEnter;
    this.onExit = handlers.onExit;

    this.body = new Body({
      isTrigger: true,
      type: Body.STATIC,
      shape: new Box(new Vec3(halfExtents.x, halfExtents.y, halfExtents.z)),
      collisionFilterGroup: PhysicsLayer.Trigger,
      collisionFilterMask: PhysicsLayer.Player, // only reacts to the player
    });
    this.body.position.set(center.x, center.y, center.z);

    PhysicsC.addBody(this.body);
    TriggerC.register(this);
  }

  destroy() {
    TriggerC.unregister(this);
    PhysicsC.removeBody(this.body);
  }
}

/**
 * Central trigger dispatcher.
 *
 * Instead of attaching a `collide` handler to every body, we listen ONCE to the
 * world's `beginContact` / `endContact` events. Each event gives us the two
 * bodies that started/stopped touching; if one of them is the player and the
 * other is a registered trigger, we fire that trigger's enter/exit callback.
 * `endContact` is what makes a clean "player left the zone" (stop) event easy.
 */
export class TriggerC {
  private static byBodyId = new Map<number, Trigger>();
  private static playerBody: Body | null = null;
  private static started = false;

  static init(playerBody: Body) {
    this.playerBody = playerBody;
    if (this.started) return;

    if (!PhysicsC.world) return;
    PhysicsC.onContactBegin(this.onBegin);
    PhysicsC.onContactEnd(this.onEnd);
    this.started = true;
  }

  static register(t: Trigger) { this.byBodyId.set(t.body.id, t); }
  static unregister(t: Trigger) { this.byBodyId.delete(t.body.id); }

  private static onBegin = (e: any) => this.dispatch(e.bodyA, e.bodyB, true);
  private static onEnd   = (e: any) => this.dispatch(e.bodyA, e.bodyB, false);

  private static dispatch(a: Body, b: Body, enter: boolean) {
    // When a trigger body is removed from the world (e.g. a crate breaks)
    // cannon emits an endContact whose other body can be undefined. Guard it,
    // otherwise the throw aborts the physics step and FREEZES the whole world.
    if (!this.playerBody || !a || !b) return;

    // Exactly one of the two bodies must be the player; the other must be a
    // registered trigger — otherwise this contact isn't ours.
    let other: Body | null = null;
    if (a === this.playerBody) other = b;
    else if (b === this.playerBody) other = a;
    else return;

    const trigger = other ? this.byBodyId.get(other.id) : undefined;
    if (!trigger) return;

    if (enter) trigger.onEnter?.();
    else trigger.onExit?.();
  }
}
