// Editor gizmos: immediate-mode line drawing for code that runs while a scene is EDITED — a
// generator aspect's `rebuild()`, a `make()` factory, an editor tool's hooks. Nothing here touches
// the scene: calls accumulate WORLD-space line segments into the buffer of the run that is
// currently executing (an ambient scope the scene loader opens around each synchronous call), and
// the scene editor pushes those buffers to the lite engine's overlay layer, which draws them over
// the picture — never outlined, never picked, never rendered by Filament, absent in play mode.
//
//   rebuild() {
//     Gizmos.polyline(points, { color: "#5b8ef0", closed: true })
//     for (const p of points) Gizmos.cross(p, 0.07)
//   }
//
// Every run starts with an EMPTY buffer — what a call draws is the whole picture (like
// `generated.clear()`, only implicit). Outside any scope (play mode, hand-attached aspects, a call
// after an `await` inside rebuild) the calls are no-ops: rebuild is synchronous by contract.
//
// ANCHORED gizmos (`{ node }` in the style): points are in that node's LOCAL frame and the engine
// follows the node live — through a gizmo drag, not only after it — with the node's scale
// stripped (an empty scaled ×10 for its children keeps a normal-sized cross). An anchored picture
// is the node's own: clicking it in the viewport selects the node, and it draws in the selection
// accent while selected. World-space calls (path lines) are never pickable.

import { Mat4, type Mat4Like } from "../math/mat4"
import type { Vec3Like } from "../math/vec"

/** The anchor of a gizmo call: any object with an engine entity id (a `Node`). */
export type GizmoAnchor = { readonly id: number }

export type GizmoStyle = {
  /** CSS hex color (`#rgb` / `#rrggbb`); default a neutral light gray. */
  color?: string
  /** 0..1, default 1. */
  alpha?: number
  /** Anchor: points are in this node's LOCAL frame (scale ignored); the lines follow the node
   *  live, are pickable (a click selects the node) and turn the selection accent when it is
   *  selected. Omit for world-space lines (not pickable). */
  node?: GizmoAnchor | null
}

/** One color batch of LINES (flat `[x,y,z, x,y,z]` per segment) — what a host hands the engine.
 *  With `entityId` the segments are in that entity's local frame (see GizmoStyle.node). */
export type GizmoBatch = { color: string, alpha: number, segments: number[], entityId?: number }

const DEFAULT_COLOR = "#cfd4dd"

/** The per-run collector (owned by the scene loader's EditorRun). Batches are keyed by style so a
 *  hundred same-colored segments cost one draw. */
export class GizmoBuffer {
  batches: GizmoBatch[] = []
  private _byStyle = new Map<string, GizmoBatch>()

  clear(): void {
    this.batches = []
    this._byStyle.clear()
  }

  segments(style: GizmoStyle | undefined): number[] {
    const color = style?.color ?? DEFAULT_COLOR
    const alpha = Math.max(0, Math.min(1, style?.alpha ?? 1))
    const entityId = style?.node?.id
    const key = `${color}@${alpha}@${entityId ?? ""}`
    let b = this._byStyle.get(key)
    if (!b) {
      b = entityId ? { color, alpha, segments: [], entityId } : { color, alpha, segments: [] }
      this._byStyle.set(key, b)
      this.batches.push(b)
    }
    return b.segments
  }
}

let current: GizmoBuffer | null = null
let warnedOutOfScope = false

const xyz = (p: Vec3Like): [number, number, number] =>
  Array.isArray(p) ? [ p[0] ?? 0, p[1] ?? 0, p[2] ?? 0 ] : [ (p as { x: number }).x, (p as { y: number }).y, (p as { z: number }).z ]

const target = (style: GizmoStyle | undefined): number[] | null => {
  if (current) return current.segments(style)
  if (!warnedOutOfScope && (globalThis as { __lecodesSceneEdit?: boolean }).__lecodesSceneEdit) {
    warnedOutOfScope = true
    console.warn("[Gizmos] draw call outside an editor run — gizmos must be drawn synchronously inside rebuild() / a make() factory / a tool hook")
  }
  return null
}

/** @internal Run `fn` with `buffer` as the ambient gizmo target (cleared first). Nested scopes
 *  restore the outer one. */
export const withGizmoScope = <T>(buffer: GizmoBuffer, fn: () => T): T => {
  const prev = current
  buffer.clear()
  current = buffer
  try { return fn() } finally { current = prev }
}

/**
 * Editor-only line drawing, available inside generator `rebuild()`, `make()` factories and editor
 * tool hooks. Points are WORLD space. Drawn by the scene editor's viewport overlay (depth-test
 * off — a path through a wall is still a path); invisible everywhere else.
 */
export const Gizmos = {
  /** One segment from `a` to `b`. */
  line(a: Vec3Like, b: Vec3Like, style?: GizmoStyle): void {
    const out = target(style)
    if (!out) return
    out.push(...xyz(a), ...xyz(b))
  },

  /** Consecutive segments through `points`; `closed` joins the last point back to the first. */
  polyline(points: readonly Vec3Like[], style?: GizmoStyle & { closed?: boolean }): void {
    if (points.length < 2) return
    const out = target(style)
    if (!out) return
    const n = points.length
    const last = style?.closed ? n : n - 1
    for (let i = 0; i < last; i++) out.push(...xyz(points[i]!), ...xyz(points[(i + 1) % n]!))
  },

  /** A wireframe camera frustum looking down −Z: the four edges from the origin to a rect
   *  `length` metres ahead sized by `fov` (vertical, degrees) × `aspect` (default 16:9), plus an
   *  "up" fin above the rect. Anchor it (`{ node }`) for a camera node's marker — the frustum
   *  then follows the node's pose; `matrix` places an unanchored one in world space. */
  frustum(fov: number, style?: GizmoStyle & { aspect?: number, length?: number, matrix?: Mat4Like }): void {
    const out = target(style)
    if (!out) return
    const aspect = style?.aspect ?? 16 / 9, length = style?.length ?? 0.6
    const m = style?.matrix ? new Mat4(style.matrix) : null
    const h = Math.tan((fov * Math.PI) / 360) * length, w = h * aspect, z = -length
    const P = (x: number, y: number, zz: number): [number, number, number] => {
      if (!m) return [ x, y, zz ]
      const v = m.transformPoint([ x, y, zz ])
      return [ v.x, v.y, v.z ]
    }
    const o = P(0, 0, 0)
    const c = [ P(-w, -h, z), P(w, -h, z), P(w, h, z), P(-w, h, z) ]
    for (const q of c) out.push(...o, ...q)
    for (let i = 0; i < 4; i++) out.push(...c[i]!, ...c[(i + 1) % 4]!)
    out.push(...P(-w * 0.4, h, z), ...P(0, h * 1.6, z), ...P(0, h * 1.6, z), ...P(w * 0.4, h, z))
  },

  /** A three-axis cross centred on `p` (a point marker); `size` = half extent, default 0.1. */
  cross(p: Vec3Like, size = 0.1, style?: GizmoStyle): void {
    const out = target(style)
    if (!out) return
    const [ x, y, z ] = xyz(p)
    out.push(x - size, y, z, x + size, y, z, x, y - size, z, x, y + size, z, x, y, z - size, x, y, z + size)
  },
}
