// The scene editor's immediate-mode inspector protocol (docs/scene-editor-plan.md, phase 9).
//
// Custom editor UI never renders into the editor panel directly — the live data (aspect instances,
// nodes, `model.anim.clips`, generated meshes) lives in the SCENE WORLD (the compiled bundle), on
// the other side of the controller. So a custom view is an imgui-style function running in the
// world: it re-runs on every change/event and emits a WIDGET LIST (plain data); the editor panel is
// a dumb renderer of that list; user events `{ id, value }` come back and trigger the next run —
// `ui.button()` returns `true` on the run that consumes the click.
//
//   class Road extends Aspect<'road'> {
//     from: Node | null = null
//     to: Node | null = null
//     width = 2
//     static editor = { rebuild: true }
//     static inspector(ui: InspectorUI, road: Road) {
//       ui.auto()                                   // the inferred fields, as usual
//       if (road.from && road.to) ui.info(`${road.generated.children.length} pieces`)
//       else ui.warn('Assign both endpoints')
//       if (ui.button('Shuffle')) road.rebuild()
//     }
//   }
//
// BINDING RULE: a field whose key is a declared aspect field (a public class field) is DOC-BOUND —
// the editor persists edits to the scene file through its normal commit/undo path. Any other key is
// EDITOR STATE — transient, kept on this InspectorUI instance across runs, never written to a file
// (the animation card's clip choice, a tool's brush size). One vocabulary, two lifetimes.

import type { FieldDescriptor, FieldEditor } from "./fields"

/** One user interaction coming back from the editor panel: the widget's id (+ value for fields). */
export type InspectorEvent = { id: string, value?: unknown }

export type InspectorFieldWidget = {
  kind: "field"
  id: string
  /** The bound key — an aspect field (doc-bound) or an editor-state key. */
  key: string
  label: string
  editor: FieldEditor | undefined
  value: unknown
  /** True = persisted to the scene file by the editor; false = transient editor state. */
  doc: boolean
  /** True = emitted by `ui.auto()` — the editor may overlay its syntactic hints (options/node). */
  auto?: boolean
  min?: number
  max?: number
  step?: number
  options?: readonly (string | number)[]
}

export type InspectorWidget =
  | InspectorFieldWidget
  | { kind: "button", id: string, label: string }
  | { kind: "toolButton", id: string, label: string, tool: string }
  | { kind: "header", id: string, label: string }
  | { kind: "info" | "warn", id: string, text: string }

type FieldOpts = {
  label?: string
  min?: number
  max?: number
  step?: number
  /** Default for EDITOR-STATE keys (doc keys default from the class field). */
  value?: unknown
}

/**
 * The widget builder handed to `static inspector(ui, aspect)` (and to editor windows/tools later).
 * One instance lives per card and persists across runs — it carries the card's editor state.
 */
export class InspectorUI {
  /** @internal This run's emitted widgets. */
  _widgets: InspectorWidget[] = []
  /** @internal Transient editor state (non-doc field keys), kept across runs. */
  _state: Record<string, unknown> = {}
  /** @internal The aspect entry's doc props (live reference — the runner keeps it current). */
  _props: Record<string, unknown> = {}
  /** @internal Keys that are declared aspect fields → doc-bound. Empty for node/window cards. */
  _docKeys = new Set<string>()
  /** @internal Field descriptors for auto() + doc-key defaults (describeFields output). */
  _fields: FieldDescriptor[] = []
  /** @internal The unconsumed button event of the current run. */
  _event: InspectorEvent | null = null

  // ---- fields (each emits one widget AND returns the current value) ---------------------------

  /** Numeric field (min/max/step render as a slider-style scrub range). */
  number(key: string, opts?: FieldOpts): number {
    const v = this._field("number", key, opts)
    return typeof v === "number" ? v : 0
  }
  /** Alias of `number` — give it min/max/step and the editor renders a scrubable range. */
  slider(key: string, opts?: FieldOpts): number { return this.number(key, opts) }

  text(key: string, opts?: FieldOpts): string {
    const v = this._field("text", key, opts)
    return typeof v === "string" ? v : ""
  }

  color(key: string, opts?: FieldOpts): string {
    const v = this._field("color", key, opts)
    return typeof v === "string" ? v : "#ffffff"
  }

  switch(key: string, opts?: FieldOpts): boolean {
    return this._field("switch", key, opts) === true
  }

  /** Dropdown — `options` may be computed fresh every run (dynamic lists come free). */
  select(key: string, options: readonly (string | number)[], opts?: FieldOpts): string | number {
    const v = this._field("select", key, { ...opts, value: opts?.value ?? options[0] }, options)
    return (typeof v === "string" || typeof v === "number") && options.includes(v) ? v : options[0]
  }

  vec2(key: string, opts?: FieldOpts): [number, number] { return this._vec("vec2", key, 2, opts) as [number, number] }
  vec3(key: string, opts?: FieldOpts): [number, number, number] { return this._vec("vec3", key, 3, opts) as [number, number, number] }
  vec4(key: string, opts?: FieldOpts): [number, number, number, number] { return this._vec("vec4", key, 4, opts) as [number, number, number, number] }

  /** Scene-node reference (`ref()` fields): name dropdown + pick-in-viewport. Returns the RAW doc
   *  value (`{ $ref }` marker or null) — read the resolved live node off the aspect instance. */
  node(key: string, opts?: FieldOpts): unknown {
    return this._field("node", key, opts)
  }

  /** Project-asset path (the editor renders a dropdown of the project's GLBs). "" = none picked. */
  asset(key: string, opts?: FieldOpts): string {
    const v = this._field("asset", key, { ...opts, value: opts?.value ?? "" })
    return typeof v === "string" ? v : ""
  }

  // ---- everything else --------------------------------------------------------------------------

  /** True on the run that consumes this button's click — do the action right there. */
  button(label: string, opts?: { id?: string }): boolean {
    const id = opts?.id ?? `b:${label}`
    this._widgets.push({ kind: "button", id, label })
    if (this._event?.id === id) {
      this._event = null
      return true
    }
    return false
  }

  /** A toggle that activates/deactivates the named viewport tool (`registerEditorTool`). The
   *  editor owns the active-tool state — the click never round-trips into the scene world. */
  toolButton(label: string, tool: string): void {
    this._widgets.push({ kind: "toolButton", id: `t:${tool}`, label, tool })
  }

  header(label: string): void { this._widgets.push({ kind: "header", id: `h:${label}`, label }) }
  info(text: string): void { this._widgets.push({ kind: "info", id: `i:${this._widgets.length}`, text }) }
  warn(text: string): void { this._widgets.push({ kind: "warn", id: `w:${this._widgets.length}`, text }) }

  /** Emit the inferred field widgets (all declared aspect fields, or just the named ones) — the
   *  zero-ceremony baseline; append custom widgets around it. */
  auto(...keys: string[]): void {
    for (const f of this._fields) {
      if (keys.length > 0 && !keys.includes(f.key)) continue
      const value = this._props[f.key] !== undefined ? this._props[f.key] : f.value
      this._widgets.push({
        kind: "field", id: `f:${f.key}`, key: f.key, label: f.label, editor: f.editor,
        value, doc: true, auto: true, min: f.min, max: f.max, step: f.step, options: f.options,
      })
    }
  }

  // ---- internals ----------------------------------------------------------------------------

  private _vec(editor: FieldEditor, key: string, size: number, opts?: FieldOpts): number[] {
    const v = this._field(editor, key, opts)
    const arr = Array.isArray(v) ? v : []
    return Array.from({ length: size }, (_, i) => (typeof arr[i] === "number" ? arr[i] as number : 0))
  }

  private _field(
    editor: FieldEditor, key: string, opts?: FieldOpts, options?: readonly (string | number)[],
  ): unknown {
    const doc = this._docKeys.has(key)
    const fallback = doc ? this._fields.find((f) => f.key === key)?.value : opts?.value
    const stored = doc ? this._props[key] : this._state[key]
    const value = stored !== undefined ? stored : fallback
    this._widgets.push({
      kind: "field", id: `f:${key}`, key, label: opts?.label ?? key, editor, value, doc,
      min: opts?.min, max: opts?.max, step: opts?.step, options,
    })
    return value
  }

  /**
   * @internal One immediate-mode pass: deliver `event`, run `fn`, return the widget list.
   * Editor-state field events are applied here; DOC-key field events must be applied by the
   * caller beforehand (they go through the scene loader's prop machinery — rebuilds, ref
   * resolution); button events are consumed by the matching `button()` call during the run.
   * A throwing inspector logs and shows the error as a warn line — it can't take the editor down.
   */
  _run(fn: (ui: this) => void, event?: InspectorEvent): InspectorWidget[] {
    if (event && event.id.startsWith("f:") && !this._docKeys.has(event.id.slice(2))) {
      this._state[event.id.slice(2)] = event.value
      event = undefined
    }
    this._event = event ?? null
    this._widgets = []
    try {
      fn(this)
    } catch (e) {
      console.error("[scene] inspector render failed:", e)
      this.warn(String((e as Error)?.message ?? e))
    }
    this._event = null
    return this._widgets
  }
}
