// Editor plugins: `registerEditorWindow` / `registerEditorTool` (docs/scene-editor-plan.md,
// phase 11). Registrations live in `*.editor.ts` files — compiled and executed ONLY in
// scene-editor bundles (phase 10), so none of this reaches shipped app.js (nothing references it
// there, and method-granular DCE drops it).
//
// The scene-editor harness (packages/projects/src/scene-editor) reads the registry through the
// injected `__editorPlugins` global — the harness, the scene, and the editor files compile into
// ONE bundle, so they share this module instance. Windows and tools describe their UI through the
// same immediate-mode `InspectorUI` protocol custom inspector cards use (one widget vocabulary,
// three mount points), and mutate the scene ONLY through the `editor` doc API — every plugin write
// lands in the scene document (undoable, diffable), never in live engine state.

import type { InspectorUI } from "../core/InspectorUI"

/** A geometry hit under the viewport pointer, handed to tools. Physics builds raycast the scene's
 *  meshes (precise); otherwise (and on misses) the hit falls back to the ground plane (y = 0). */
export type EditorRayHit = {
  point: [number, number, number]
  normal: [number, number, number]
  /** Absolute path of the scene-file node the hit belongs to ("city/in1/pt1") — null for
   *  ground-plane fallback hits. */
  node: string | null
}

/**
 * The editor scripting API handed to windows and tools. Nodes are addressed by ABSOLUTE PATH —
 * '/'-joined names from the scene root; a root node's path is its bare name. Doc-op methods write
 * the scene DOCUMENT through the editor's normal commit path (undo, file, live patching all
 * included); they return false / no-op when the document can't take the edit (sibling-name
 * collision, unknown path).
 */
export type EditorApi = {
  /** The currently selected node path (or `path::part` key for asset internals), null when
   *  nothing is selected. */
  readonly selection: string | null
  select(path: string | null): void
  /** The scene document's nodes: absolute path, sibling-unique display name, source kind
   *  (mesh / model / light / group / …). */
  nodes(): { path: string, name: string, kind: string }[]
  /** First unused "base", "base2", "base3", … name among the SIBLINGS under `parentPath`
   *  (scene root when omitted). */
  uniqueName(base: string, parentPath?: string): string
  /** Add a ROOT node from plain def data (scene-file grammar; a string `model` value means an
   *  asset path). `name` may not contain '/' or ':'. One undo step unless grouped by `transact`. */
  addNode(name: string, def: Record<string, unknown>): boolean
  /** Write one def prop (transforms apply live; anything else patches/re-runs the node). */
  setProp(path: string, key: string, value: unknown): boolean
  removeNode(path: string): void
  /** Duplicate a node; returns the copy's path (null when it can't). */
  duplicate(path: string): string | null
  /** Raycast the scene under a viewport pixel (same hit rules as tool clicks). */
  raycast(screenX: number, screenY: number): EditorRayHit | null
  /** Group every doc edit inside `fn` into ONE undo step. */
  transact(fn: () => void): void
  /** Read one def prop of a node as document data (`asset()` refs come as `{ $asset: "./x" }`);
   *  undefined when the node or key doesn't exist. */
  getProp(path: string, key: string): unknown
  /** Write a BINARY asset file for a node (a terrain's `.terrain`, a baked mask…). `ref` = the
   *  node's existing def-relative `asset()` ref to overwrite, or null to create `./<name>` at the
   *  project root (a free name is picked on collisions). Resolves to the ref to store in the node's
   *  def (`{ $asset: ref }` via `setProp`), null when the host can't write files. Not a doc edit —
   *  it is not undoable; keep the doc pointing at the file. */
  writeAsset(path: string, ref: string | null, name: string, bytes: Uint8Array): Promise<string | null>
}

export type EditorWindowFn = (ui: InspectorUI, editor: EditorApi) => void

export type EditorToolHooks = {
  /** CSS cursor for the viewport while this tool is active (default "crosshair"). */
  cursor?: string
  /** A short glyph (one character / emoji) for the tool's toolbar button — tools without one all
   *  share the generic wand icon, so any editor with two or more tools should set it. */
  icon?: string
  /** A viewport click while the tool is active — `hit` is the raycast result under the pointer.
   *  Viewport clicks route to the tool while it is active. */
  onViewportClick?(hit: EditorRayHit, editor: EditorApi): void
  /** Drag tools (brushes): a primary-button drag in the viewport routes here instead of orbiting
   *  the camera — start / every pointer move (with the hit under it, null while off-world) /
   *  release. A tool with `onDragStart` gets NO `onViewportClick` for the same gesture. */
  onDragStart?(hit: EditorRayHit, editor: EditorApi): void
  onDrag?(hit: EditorRayHit | null, editor: EditorApi): void
  onDragEnd?(editor: EditorApi): void
  /** The pointer moved over the viewport with no button down (throttled to the frame) — draw a
   *  brush cursor through `Gizmos`; null when nothing is under it. */
  onHover?(hit: EditorRayHit | null, editor: EditorApi): void
}

/** @internal The registry the scene-editor harness reads (windows/tools in registration order). */
export const __editorPlugins = {
  windows: [] as { title: string, render: EditorWindowFn }[],
  tools: [] as { name: string, hooks: EditorToolHooks }[],
}

/** Register an editor panel — a collapsible overlay docked over the viewport (the inspector rail
 *  stays selection-scoped). A window that emits `ui.toolButton` for a tool becomes that tool's
 *  settings panel: activating the tool expands and highlights it. `render` re-runs immediate-mode
 *  on every interaction — same protocol as `static inspector` cards. */
export const registerEditorWindow = (title: string, render: EditorWindowFn): void => {
  const existing = __editorPlugins.windows.find((w) => w.title === title)
  if (existing) existing.render = render
  else __editorPlugins.windows.push({ title, render })
}

/** Register a viewport tool — a toolbar entry beside move/rotate/scale (activate it there or via
 *  `ui.toolButton(label, name)`). While active, viewport clicks arrive as raycast hits. */
export const registerEditorTool = (name: string, hooks: EditorToolHooks): void => {
  const existing = __editorPlugins.tools.find((t) => t.name === name)
  if (existing) existing.hooks = hooks
  else __editorPlugins.tools.push({ name, hooks })
}
