// Field introspection for the scene-editor inspector: what's editable on an aspect, and with which
// editor widget. Engine-agnostic (works for 2D and 3D aspects alike).
//
// The editable surface of an aspect is its public class fields with defaults — the same convention
// aspects already follow for `node.aspect(Ctor, opts)` configuration. Editors are layered:
//   1. inference from the default value's shape (number → number field, '#rrggbb' → color, …), free
//      for every user aspect with zero ceremony;
//   2. optional `static fields: FieldMeta<T>` on the class for ranges/labels/options/overrides
//      (mirrors how `static aspect` declares the accessor name).

/** Editor widget kinds the inspector knows how to render. `"node"` = a scene-node reference
 *  (`ref('name')` in the file, name dropdown + pick-in-viewport in the inspector) — declare it via
 *  `static fields` on fields typed `Node | null`; a `Node | null` type annotation is also picked
 *  up syntactically by the editor's source scan. `"asset"` = a project-asset path (the editor
 *  fills the dropdown with the project's GLBs — `ui.asset()` in windows/tools/cards). */
export type FieldEditor =
  | "number" | "text" | "color" | "switch" | "select"
  | "vec2" | "vec3" | "vec4" | "node" | "asset"

export type FieldMetaEntry = {
  /** Inspector label (default: the field name). */
  label?: string
  min?: number
  max?: number
  step?: number
  /** Allowed values — renders a dropdown (editor "select"). */
  options?: readonly (string | number)[]
  /** Override the editor inferred from the default value. */
  editor?: FieldEditor
  /** Exclude the field from the inspector. */
  hidden?: boolean
}

/** Declared on an aspect class as `static fields: FieldMeta<MyAspect> = { … }`. All optional. */
export type FieldMeta<T = unknown> = { [K in Extract<keyof T, string>]?: FieldMetaEntry }

/** One editable field: key + default value + resolved editor + merged metadata. */
export type FieldDescriptor = {
  key: string
  label: string
  /** The default value (from a freshly constructed instance; undefined if only declared in meta). */
  value: unknown
  /** Resolved editor kind; undefined = no editor known (inspector shows it read-only). */
  editor: FieldEditor | undefined
  min?: number
  max?: number
  step?: number
  options?: readonly (string | number)[]
}

export type AspectClassInfo = {
  /** The accessor name (`static aspect`, injected by chisel for user aspects). */
  name: string
  className: string
  fields: FieldDescriptor[]
  /** True for editor-run classes (`static editor` — generators). The editor routes their
   *  structural changes through a re-run instead of a live patch. */
  editor?: boolean
  /** True when the class declares a custom `static inspector(ui, aspect)` card. */
  inspector?: boolean
}

// Instance fields every aspect inherits from the Aspect base — configuration plumbing, not content.
// `generated` is declare-only in TS, but the bundler lowers it to a real (undefined) class field.
const BASE_FIELDS = new Set([ "node", "generated", "updateOrder", "updateWhilePaused", "updateWhenVisible" ])

/** Infer the editor widget from a default value's shape. */
export const inferFieldEditor = (value: unknown): FieldEditor | undefined => {
  if (typeof value === "boolean") return "switch"
  if (typeof value === "number") return "number"
  if (typeof value === "string") return value.startsWith("#") ? "color" : "text"
  if (Array.isArray(value) && value.length >= 2 && value.length <= 4 && value.every((n) => typeof n === "number")) {
    return value.length === 2 ? "vec2" : value.length === 3 ? "vec3" : "vec4"
  }
  return undefined
}

const cloneValue = (v: unknown): unknown => (Array.isArray(v) ? [ ...v ] : v)

/**
 * Enumerate the editable fields of a class: construct a default instance, take its own enumerable
 * non-underscore fields (minus the Aspect base plumbing), infer editors from the default values, and
 * merge the class's optional `static fields` metadata (which can also add keys that have no runtime
 * default, e.g. fields declared without an initializer).
 */
export const describeFields = (ctor: abstract new () => unknown): FieldDescriptor[] => {
  let defaults: Record<string, unknown> = {}
  // Aspects have no-arg constructors by convention (the engine creates them via node.aspect()); a
  // class that throws here just loses default-value inference, it doesn't break the inspector.
  try { defaults = new (ctor as new () => Record<string, unknown>)() } catch { /* meta-only */ }

  const meta = ((ctor as { fields?: FieldMeta }).fields ?? {}) as Record<string, FieldMetaEntry>

  const keys = new Set<string>()
  for (const k of Object.keys(defaults)) {
    if (BASE_FIELDS.has(k) || k.startsWith("_")) continue
    if (typeof defaults[k] === "function") continue
    keys.add(k)
  }
  for (const k of Object.keys(meta)) keys.add(k)

  const out: FieldDescriptor[] = []
  for (const key of keys) {
    const m = meta[key] ?? {}
    if (m.hidden) continue
    const value = defaults[key]
    const editor = m.editor ?? (m.options ? "select" : inferFieldEditor(value))
    out.push({ key, label: m.label ?? key, value: cloneValue(value), editor, min: m.min, max: m.max, step: m.step, options: m.options })
  }
  return out
}

/** Describe an aspect class for the inspector: accessor name + class name + editable fields. */
export const describeAspect = (ctor: abstract new () => unknown): AspectClassInfo => ({
  name: (ctor as { aspect?: string }).aspect ?? "",
  className: (ctor as { name?: string }).name ?? "",
  fields: describeFields(ctor),
  editor: (ctor as { editor?: unknown }).editor ? true : undefined,
  inspector: typeof (ctor as { inspector?: unknown }).inspector === "function" ? true : undefined,
})
