/**
 * The asset registry: what ids exist, what they build, and how a self-hoster
 * replaces one without forking the repo.
 *
 * There is **one** registry, not two. `Prop.kind` in an office pack is an
 * `AssetId` and resolves here (CONTRACT.md §3); there is no separate table of
 * things you are allowed to put in a room. An office pack is data, this is the
 * code that turns data into geometry, and the arrow only points one way —
 * `interiors/types.ts` declares `AssetId` as a loose `string` precisely so that
 * a pack can be parsed, validated and stored without importing a mesh library.
 *
 * ### Overrides are the point
 *
 * `tera:` is the namespace this repo ships. Somebody running their own office
 * registers `acme:desk.standing` with `overrides: "tera:desk.workstation"`, and
 * every desk in the reference pack becomes theirs — no fork, no edit to the
 * pack, no patch to keep rebasing. That is the difference between an open-source
 * asset library and an open-source asset library people actually use.
 *
 * ### Nothing throws
 *
 * An unregistered id builds a placeholder box instead of an exception. An office
 * pack with one typo in it should still open, and a missing desk should look
 * like a missing desk rather than a blank screen — the failure has to be visible
 * in the room, because that is where the person who can fix it is looking.
 */

import * as THREE from "three";
import type { MaterialQuality, MaterialRegistry } from "./materials.ts";
import type { InteriorPalette } from "./palette.ts";
import { MeshBin, parts, type PartBin } from "./parts.ts";

/**
 * A namespaced id — `"tera:desk.workstation"`, `"acme:desk.standing"`.
 *
 * The template type is enforced where assets are *registered*, so a typo in a
 * built-in is caught by the compiler. Lookups take a plain `string`, because
 * the ids arriving from an office pack are strings that came out of JSON and no
 * amount of type declaration makes them anything else. Validation is `get`
 * returning `undefined`, not a cast.
 */
export type AssetId = `${string}:${string}`;

/** What an asset's parameters may be. JSON values, for the same reason a pack is. */
export type AssetParamValue =
  | string
  | number
  | boolean
  | null
  | undefined
  | readonly AssetParamValue[];

export type AssetParams = Readonly<Record<string, AssetParamValue>>;

/**
 * How much room an asset needs, in metres, in its own local frame — X across,
 * Z deep, Y tall, with the origin on the floor at the centre of the footprint.
 *
 * Separate from `build` because layout wants the numbers without the geometry:
 * a desk bank spacing itself, a room checking a sofa fits, an editor drawing a
 * plan view. Building 1,200 objects to find out how big they are is the kind of
 * thing that is fine until the day it is not.
 */
export interface Footprint {
  width: number;
  depth: number;
  height: number;
  /**
   * Metres of floor the asset wants kept clear in front of it — a chair to pull
   * out, a drawer to open. Advisory; nothing enforces it.
   */
  clearance?: number;
}

/**
 * Everything an asset builder is handed.
 *
 * Note what is *not* here: no `Office`, no `Level`, no room, no seat, no
 * neighbours. An asset builds one object in its own local frame and knows
 * nothing about where it is going, which is what keeps assets independently
 * writable and independently testable.
 */
export interface AssetContext {
  materials: MaterialRegistry;
  /** The shared unit-primitive bin. Almost always the module-level `parts`. */
  parts: PartBin;
  palette: InteriorPalette;
  quality: MaterialQuality;
  /**
   * Deterministic randomness for this instance — book angles, cushion sag, the
   * rotation of a mug. Seeded per prop by whoever builds the context, so the
   * office looks the same on every reload. A world that reshuffles itself
   * between visits is a lava lamp.
   */
  rand: () => number;
  /** The placed instance's opaque palette key, if it had one. */
  colorKey?: string;
  /**
   * Resolve an opaque `colorKey` to a colour, or `undefined` for "no opinion".
   *
   * Supplied by the caller exactly as `MarkerPalette` is, and for the same
   * reason: the library will never learn that a colour key means a status, a
   * team or a company (ARCHITECTURE.md §3.3).
   */
  colorFor?: (key: string) => number | undefined;
  /** Build another asset — how a composite places a child. */
  build: (id: string, params?: AssetParams) => THREE.Object3D;
}

/**
 * One asset: an id, its parameters, how big it is, and how to build it.
 *
 * `footprint` and `build` are method shorthand rather than arrow properties so
 * that a concretely-parameterised def is assignable to the erased one the
 * registry stores. That is bivariance, and it is the intended amount of
 * looseness here — the registry cannot know every asset's parameter type and
 * should not pretend to.
 */
export interface AssetDef<P extends AssetParams = AssetParams> {
  id: AssetId;
  /**
   * The built-in id this asset stands in for. Registering with this set means
   * every request for the overridden id resolves here instead.
   */
  overrides?: AssetId;
  /** Human-readable, for editors and error messages. Never rendered. */
  label?: string;
  defaults: P;
  footprint(params: P): Footprint;
  build(params: P, ctx: AssetContext): THREE.Object3D;
}

/**
 * An asset with its parameter type erased — what the registry stores and hands
 * back. The `any` is deliberate and confined to this line: the registry cannot
 * know every asset's parameter type, and pretending otherwise with a union
 * would mean editing a central file every time somebody adds a chair.
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyAsset = AssetDef<any>;

/** Identity helper that keeps a def's parameter type inferred at the call site. */
export function defineAsset<P extends AssetParams>(def: AssetDef<P>): AssetDef<P> {
  return def;
}

const PLACEHOLDER_FOOTPRINT: Footprint = { width: 0.6, depth: 0.6, height: 0.6 };

export class AssetRegistry {
  private readonly defs = new Map<string, AnyAsset>();
  /** overridden id -> the id that replaced it. */
  private readonly overriddenBy = new Map<string, AssetId>();
  private readonly warned = new Set<string>();

  /**
   * Later registrations of the same id win, so a self-hoster can re-register a
   * built-in outright as well as override it. Both are supported because they
   * mean different things: re-registering replaces one asset, overriding
   * redirects one id at another asset that keeps its own name.
   */
  register<P extends AssetParams>(def: AssetDef<P>): this {
    this.defs.set(def.id, def as AnyAsset);
    if (def.overrides) this.overriddenBy.set(def.overrides, def.id);
    return this;
  }

  registerAll(defs: readonly AnyAsset[]): this {
    for (const def of defs) this.register(def);
    return this;
  }

  /**
   * Follow the override chain to the id that will actually be built.
   *
   * The visited set is not paranoia: `acme:a` overriding `tera:b` while
   * `acme:b` overrides `tera:a` is a plausible thing for two half-finished
   * packs to do between them, and an infinite loop inside a scene build is a
   * hung tab with no message in it.
   */
  resolveId(id: string): string {
    let current = id;
    const seen = new Set<string>([current]);
    for (;;) {
      const next = this.overriddenBy.get(current);
      if (!next || seen.has(next)) return current;
      seen.add(next);
      current = next;
    }
  }

  get(id: string): AnyAsset | undefined {
    return this.defs.get(this.resolveId(id));
  }

  has(id: string): boolean {
    return this.get(id) !== undefined;
  }

  /** Every registered id, including ones only reachable as an override target. */
  ids(): AssetId[] {
    return [...this.defs.keys()] as AssetId[];
  }

  /** Size without building. Unknown ids give the placeholder's size. */
  footprintOf(id: string, params?: AssetParams): Footprint {
    const def = this.get(id);
    if (!def) return PLACEHOLDER_FOOTPRINT;
    return def.footprint({ ...def.defaults, ...params });
  }

  /**
   * Build one asset. The returned object's origin sits on the floor at the
   * centre of its footprint, facing -Z at yaw zero, matching `Yaw` in
   * `interiors/types.ts`.
   */
  build(id: string, ctx: AssetContext, params?: AssetParams): THREE.Object3D {
    const def = this.get(id);
    if (!def) return this.placeholder(id, ctx);
    const object = def.build({ ...def.defaults, ...params }, ctx);
    object.userData.assetId = def.id;
    return object;
  }

  /**
   * A box, in the accent colour, where the asset should have been.
   *
   * Warned once per id rather than once per instance — a pack referring to a
   * missing desk 120 times should say so once.
   */
  private placeholder(id: string, ctx: AssetContext): THREE.Object3D {
    if (!this.warned.has(id)) {
      this.warned.add(id);
      console.warn(`[tera/assets] no asset registered for "${id}" — drawing a placeholder`);
    }
    const bin = new MeshBin();
    const { width, depth, height } = PLACEHOLDER_FOOTPRINT;
    bin.box(ctx.materials.get("accent"), { size: [width, height, depth] });
    const group = bin.build("placeholder");
    group.userData.assetId = id;
    group.userData.missing = true;
    return group;
  }
}

/**
 * The registry the built-in `tera:` assets register into and the one an office
 * uses unless it is handed another. One registry per page is the normal case;
 * the class is exported for tests and for anyone rendering two differently
 * skinned worlds side by side.
 */
export const kit = new AssetRegistry();

export interface AssetContextOptions {
  materials: MaterialRegistry;
  registry?: AssetRegistry;
  parts?: PartBin;
  rand?: () => number;
  colorKey?: string;
  colorFor?: (key: string) => number | undefined;
}

/**
 * Wire up an `AssetContext`, including the recursive `build` a composite asset
 * uses to place its children. Callers should not assemble one by hand — the
 * recursion is the fiddly part and it belongs in one place.
 */
export function createAssetContext(options: AssetContextOptions): AssetContext {
  const registry = options.registry ?? kit;
  const ctx: AssetContext = {
    materials: options.materials,
    parts: options.parts ?? parts,
    palette: options.materials.palette,
    quality: options.materials.quality,
    rand: options.rand ?? Math.random,
    colorKey: options.colorKey,
    colorFor: options.colorFor,
    build: (id, params) => registry.build(id, ctx, params),
  };
  return ctx;
}

/**
 * The colour an asset should use for a tintable part: the caller's answer for
 * this instance's `colorKey`, or the role's palette colour when there is no key
 * or the caller has no opinion about it.
 */
export function tintFor(ctx: AssetContext, fallbackRole: keyof InteriorPalette): number {
  const key = ctx.colorKey;
  const resolved = key !== undefined ? ctx.colorFor?.(key) : undefined;
  return resolved ?? ctx.palette[fallbackRole];
}
