// No-code 2D scenario aspects (the gl/scenarios.ts precedent): behaviors a scene builder attaches
// in a `.scene2d.ts` file without writing aspect code. First resident: CameraFollow — the eased,
// pixel-snapped follow camera every top-down/side-scroller needs (it absorbs the reference app's
// ~50 lines of snap/follow plumbing).
//
// Conventions: update() runs in the LATE phase (after physics, render-synced when the engine
// provides the hooks), so followers read final, drawn-this-frame positions.

import { Aspect } from "../core/Aspect"
import type { Camera2D } from "./Camera2D"
import type { Node2D } from "./Node2D"
import type { Scene2D } from "./Scene2D"

// SCREEN pixels per world unit, MEASURED from the native projection (zoom + world→screen mapping
// are applied natively, so "1 world unit = 1 px" can't be assumed — and worldToScreen is affine,
// so the per-unit delta is position-independent). Re-measured only when zoom changes.
type PixelGrid = { sx: number, sy: number, zoomAt: number }

const snapToGrid = (cam: Camera2D, grid: PixelGrid, x: number, y: number): [number, number] => {
  if (grid.sx === 0 || cam.zoom !== grid.zoomAt) {
    const o = cam.worldToScreen(0, 0)
    grid.sx = Math.abs(cam.worldToScreen(1, 0).x - o.x) || 1
    grid.sy = Math.abs(cam.worldToScreen(0, 1).y - o.y) || 1
    grid.zoomAt = cam.zoom
  }
  return [ Math.round(x * grid.sx) / grid.sx, Math.round(y * grid.sy) / grid.sy ]
}

/**
 * Ease the scene camera toward this node every frame (attach it to the player). In a `.scene2d.ts`
 * file the loader wires `scene` automatically; from hand-written code pass it yourself:
 * `hero.aspect(CameraFollow, { scene })`.
 *
 * The eased position accumulates at full precision and only the value PUSHED to the camera is
 * snapped — reading the snapped camera back as the ease-from value would let tiny sub-pixel steps
 * round to zero near settle (the follow would stall a few px off-centre, then jump).
 */
export class CameraFollow extends Aspect<"cameraFollow", Node2D> {
  static readonly aspect = "cameraFollow"
  /** The scene2d loader injects the scene into aspects that declare this. */
  static readonly wantsScene = true

  /** The scene whose camera follows (wired by the scene file loader). */
  scene: Scene2D | null = null
  /** Exponential time constant in seconds; smaller = snappier, 0 = pin the node to centre. */
  smooth = 0.12
  /** Camera offset from the node, in world units. */
  offset: [number, number] = [ 0, 0 ]
  /** Quantize the camera to whole screen pixels so eased motion never shimmers pixel art.
   *  Defaults to the scene's `env.pixelPerfect` (true when unset). */
  pixelSnap = true

  private _x = NaN
  private _y = NaN
  private readonly _grid: PixelGrid = { sx: 0, sy: 0, zoomAt: NaN }

  update(dt: number): void {
    const scene = this.scene
    if (!scene) return
    const cam = scene.camera
    const p = this.node.worldPosition
    const tx = p.x + this.offset[0], ty = p.y + this.offset[1]
    if (Number.isNaN(this._x)) { const c = cam.position; this._x = c.x; this._y = c.y } // seed from the camera's pose
    const k = this.smooth > 0 ? 1 - Math.exp(-dt / this.smooth) : 1
    this._x += (tx - this._x) * k
    this._y += (ty - this._y) * k
    cam.position = this.pixelSnap ? snapToGrid(cam, this._grid, this._x, this._y) : [ this._x, this._y ]
  }
}
