// Internal per-frame plumbing for the 2D engine. NOT user-facing — user game logic uses the host
// `setLoop` global. `onUpdate` taps the engine's render-synced pre-frame hook (_creator2d.onUpdate),
// for anything that must move in lockstep with the native render (no 1-frame lag).
//
// The animation-event registration is lazy (first use) so a 2D project that never animates a sprite
// pulls none of this in.

import { Vec2 } from "../math/vec"
import { node2dRegistry } from "./Node2D"

export const onUpdate = (callback: (dt: number) => void): void => _creator2d.onUpdate(callback)
export const offUpdate = (callback: (dt: number) => void): void => _creator2d.offUpdate(callback)

// Route native animation events (queued in C++, drained once per frame) to the owning node, which
// translates the native clip id to a clip name and emits 'loopReached' / 'completed'.
let animEventsRegistered = false
export const ensureAnimEvents = (): void => {
  if (animEventsRegistered) return
  animEventsRegistered = true
  _creator2d.setOnAnimEvent(({ entityId, clipId, type }) => {
    node2dRegistry.get(entityId)?._dispatchAnimEvent(clipId, type)
  })
}

// Route native physics contact/sensor events to the owning nodes' 'enter'/'exit' events. Each event
// is delivered to BOTH nodes with the other as the argument. type: 0 contactBegin, 1 contactEnd,
// 2 sensorBegin, 3 sensorEnd. Registered lazily by the first 'enter'/'exit' listener (Node2D) or the
// first Trigger2D — it used to hang off Trigger2D alone, so solid-vs-solid contacts silently never
// fired in a project without a single trigger.
//
// A contact BEGIN also carries the manifold, which the listener gets as a second argument: the world
// point, the surface normal, and the approach speed. The normal points from A to B, so it is FLIPPED
// for the second delivery — each side gets the normal pointing away from the thing it hit, which is
// what "did I land on top or hit a wall" needs.
let physicsEventsRegistered = false
export const ensurePhysicsEvents = (): void => {
  if (physicsEventsRegistered) return
  physicsEventsRegistered = true
  _creator2d.setOnPhysicsEvent((a, b, type, px, py, nx, ny, speed) => {
    const na = node2dRegistry.get(a)
    const nb = node2dRegistry.get(b)
    if (!na || !nb) return
    const channel = type === 1 || type === 3 ? "exit" : "enter"
    // Box2D's manifold normal points from A TOWARD B. Each listener wants the normal of the surface
    // it hit, pointing back at itself — so A gets it negated and B gets it as-is. Landing on a floor
    // then reads normal.y ≈ +1 for the falling body, which is the test a game actually writes.
    na._emitCollision(channel, nb, { point: new Vec2(px, py), normal: new Vec2(-nx, -ny), speed })
    nb._emitCollision(channel, na, { point: new Vec2(px, py), normal: new Vec2(nx, ny), speed })
  })
}
