/**
 * The conventions every asset in this directory agrees on, and the three
 * helpers that would otherwise be copied into seventeen files.
 *
 * ### Which way an asset faces
 *
 * An asset is built in its own frame with the **origin at the centre of its
 * footprint, on the floor**, and at yaw zero it faces **−Z** — the same sense as
 * `Yaw` in `interiors/types.ts`, which is `object.rotation.y` with no
 * conversion. "Faces −Z" here means what it means for a person: the direction
 * the thing is pointed, not the side you see. A desk, the chair pulled up to it
 * and the person in the chair therefore all carry **one rotation**, which is
 * what lets `DeskBank` hand the same `rotation` to its desk and its chair.
 *
 * The consequence, and it is worth stating because it is the opposite of what
 * you might guess: the *used* side of an asset is at **+Z**, because that is
 * where the user is. Drawer fronts, the open front of a shelf, the face of a
 * monitor and the writing side of a whiteboard all point +Z, and the back of a
 * thing that stands against a wall is at −Z.
 *
 * ### Two things that are not on the floor
 *
 * `light.pendant` and `light.troffer` hang from a ceiling, and a ceiling is
 * their datum the way the floor is everything else's. They are authored with
 * the origin at the **mounting plane** and their geometry below it, `y ≤ 0`, so
 * a pack writes `elevation: 2.9` and gets a lamp hanging at 2.9 m rather than a
 * lamp whose author had to know the ceiling height. `footprint().height` is the
 * total drop. Nothing else in the library does this.
 *
 * Anything that stands on a desk or hangs on a wall — a monitor, a wall display,
 * a whiteboard — is still authored on the floor. The monitor's foot sits at
 * `y = 0` and the pack raises it with `Prop.elevation`; the wall-mounted things
 * carry their own `mount` parameter, because the height of a whiteboard is a
 * property of the whiteboard and not of the room.
 *
 * ### One rule that will bite you
 *
 * Every part an asset puts under a given material must be **either all indexed
 * or all non-indexed**. `mergeGeometries` refuses a mixture, `MeshBin` treats
 * the refusal as "skip this material", and the result is not an error but a
 * chair with no shell on it — which is a lot harder to notice than a crash.
 *
 * In practice: `roundedBox` and `roundedBoxOf` are `ExtrudeGeometry` and carry
 * no index, while every other part in `parts.ts` does. So a material is a
 * rounded material or a boxy one, and where that forces a choice the honest fix
 * is to move the part to the material it belongs to anyway — a task chair's arm
 * pads are upholstery as readily as they are shell.
 *
 * `slab`'s `chamfer` option is the one place that rule had to be worked around
 * rather than worked with, and `nonIndexed` below is how. A chamfered desktop
 * wants an extruded body *and* a metric top face in the same material, and those
 * two are on opposite sides of the rule; converting the quad is four vertices of
 * cost and keeps the whole desktop in one merge.
 *
 * ### Light fixtures emit no light
 *
 * A luminaire here is geometry with a glowing diffuser and nothing else. The
 * office's lighting is a fixed rig owned by the scene (CONTRACT.md §4); a
 * hundred props each adding a `PointLight` is both the wrong owner and, at four
 * shadow-casting lights, the end of the frame budget.
 */

import * as THREE from "three";
import { tintFor, type AssetContext } from "../kit.ts";
import type { SurfaceMaterial, SurfaceRole } from "../materials.ts";
import type { MeshBin } from "../parts.ts";

/**
 * A de-indexed copy of one of the shared parts, memoised per source geometry.
 *
 * `mergeGeometries` refuses a mixture of indexed and non-indexed inputs, so a
 * material carrying an `ExtrudeGeometry` cannot also carry a `PlaneGeometry`.
 * Rather than give up either the chamfer or the metric UVs, the quad is
 * converted once and cached.
 *
 * A `WeakMap` rather than a `Map` because the key is a geometry owned by
 * `PartBin`: if the bin is ever disposed and rebuilt, the derived copies become
 * unreachable with their sources instead of pinning a disposed buffer forever.
 */
const NON_INDEXED = new WeakMap<THREE.BufferGeometry, THREE.BufferGeometry>();

function nonIndexed(source: THREE.BufferGeometry): THREE.BufferGeometry {
  if (source.getIndex() === null) return source;
  const hit = NON_INDEXED.get(source);
  if (hit) return hit;
  const made = source.toNonIndexed();
  made.name = `${source.name || "part"}:flat`;
  NON_INDEXED.set(source, made);
  return made;
}

/**
 * The material for the one part of an asset that answers to `Prop.colorKey` —
 * a chair's fabric, a locker's doors, a rug's pile. Every asset names its
 * tintable role in its own comment; there is at most one per asset, because
 * "the blue meeting room" wants one thing to be blue and not six.
 *
 * Falling back to the shared role material rather than `tinted(role,
 * palette[role])` is not a micro-optimisation: an identical-but-distinct
 * material is a second merge bucket and a second draw call on every instance,
 * for a colour nobody can tell apart from the one next to it.
 */
export function tintable(ctx: AssetContext, role: SurfaceRole): SurfaceMaterial {
  const color = tintFor(ctx, role);
  return color === ctx.palette[role] ? ctx.materials.get(role) : ctx.materials.tinted(role, color);
}

/**
 * A horizontal slab — a desktop, a tabletop, a shelf board — with the grain the
 * right size on the face you actually look at.
 *
 * The body is a scaled unit box, whose 0..1 UVs stretch; the top face is a
 * `metricQuad`, whose UVs are in metres. Without the second part a 1.6 m desk
 * and a 2.4 m table would each show exactly one repeat of the wood and read as
 * two different materials (see the UV note in `parts.ts`). The quad sits 0.6 mm
 * proud of the box so the two never z-fight.
 *
 * ### `chamfer`, and why three millimetres is worth a whole code path
 *
 * A sharp arris is the single most reliable tell that a surface was rendered
 * rather than built. Every real desktop, worktop and shelf board has a small
 * radius on its edge, and what that radius does is catch a *line* of specular
 * highlight along the whole length of the board — one bright edge that separates
 * the top from the front and tells you where the object stops. Without it a
 * desktop and the wall behind it meet in a hard colour change and the desk reads
 * as a decal.
 *
 * Three to six millimetres is the range; past that it starts reading as a
 * moulded plastic table. `roundedBoxOf` takes the radius in metres and applies it
 * after the proportions are known, which is the only way to get a circular
 * corner on a board that is fifty times wider than it is thick.
 *
 * It is opt-in rather than the default because it changes the primitive class of
 * the whole material (see the header): every other part an asset draws in the
 * same material has to become an extrusion too, and for a shelf board carrying
 * books that trade is not worth making.
 *
 * `y` is the underside of the slab.
 */
export function slab(
  bin: MeshBin,
  ctx: AssetContext,
  material: SurfaceMaterial,
  s: {
    x?: number;
    y: number;
    z?: number;
    width: number;
    depth: number;
    thickness: number;
    /** Edge radius in metres. 0.003–0.006 for a board; omit for a sharp edge. */
    chamfer?: number;
  },
): void {
  const x = s.x ?? 0;
  const z = s.z ?? 0;
  const chamfer = s.chamfer ?? 0;
  if (chamfer > 0) {
    bin.add(ctx.parts.roundedBoxOf(s.width, s.thickness, s.depth, chamfer), material, {
      x,
      y: s.y,
      z,
    });
    bin.add(nonIndexed(ctx.parts.metricQuad(s.width - chamfer * 2, s.depth - chamfer * 2)), material, {
      x,
      y: s.y + s.thickness + 0.0006,
      z,
    });
    return;
  }
  bin.add(ctx.parts.box(), material, {
    x,
    y: s.y,
    z,
    size: [s.width, s.thickness, s.depth],
  });
  bin.add(ctx.parts.metricQuad(s.width, s.depth), material, {
    x,
    y: s.y + s.thickness + 0.0006,
    z,
  });
}

/**
 * A standing panel — a partition, a modesty panel, a board — with metric UVs on
 * the faces.
 *
 * The only trick is how a floor-plane `metricQuad` is stood up: pitching it by
 * +π/2 sends its up-normal to +Z and its depth extent to Y, which gives a
 * vertical rectangle whose UVs are still in metres. `panel()` would have been
 * shorter and would have smeared one tile of felt across a 1.4 m screen.
 *
 * `y` is the bottom edge; the panel is centred on `z`.
 */
export function panelSlab(
  bin: MeshBin,
  ctx: AssetContext,
  material: SurfaceMaterial,
  s: {
    x?: number;
    y: number;
    z?: number;
    width: number;
    height: number;
    thickness: number;
    /** `"front"` skips the −Z face, for a panel hung flat against a wall. */
    faces?: "both" | "front";
  },
): void {
  const x = s.x ?? 0;
  const z = s.z ?? 0;
  bin.add(ctx.parts.box(), material, { x, y: s.y, z, size: [s.width, s.height, s.thickness] });

  const face = ctx.parts.metricQuad(s.width, s.height);
  const yMid = s.y + s.height / 2;
  bin.add(face, material, {
    x,
    y: yMid,
    z: z + s.thickness / 2 + 0.0006,
    pitch: Math.PI / 2,
  });
  if (s.faces !== "front") {
    bin.add(face, material, {
      x,
      y: yMid,
      z: z - s.thickness / 2 - 0.0006,
      pitch: -Math.PI / 2,
    });
  }
}

/**
 * Where a point `d` in front of a pitched part ends up.
 *
 * `Placement` applies its offset in the parent frame and its rotation about the
 * part's own base, so gluing a screen to a tilted bezel by writing `z: 0.013`
 * leaves the screen poking through the top of the bezel by `height × sin(tilt)`.
 * Rotating the offset first is the fix, and it is small enough that doing it by
 * hand twice would have been two chances to get the sign wrong.
 */
export function alongFacing(pitch: number, d: number): { y: number; z: number } {
  return { y: -d * Math.sin(pitch), z: d * Math.cos(pitch) };
}

/** Symmetric jitter of ±`amount`, for the small deliberate untidiness. */
export function jitter(rand: () => number, amount: number): number {
  return (rand() - 0.5) * 2 * amount;
}

export function clamp(v: number, lo: number, hi: number): number {
  return v < lo ? lo : v > hi ? hi : v;
}
