/**
 * The suspension-bridge kit.
 *
 * Two of the objects on the Bay Area board are worth flying to and both are the
 * same problem: a repeated tower, a main cable hanging in a parabola between the
 * tower tops, a regular series of hangers down to a deck, and — for every metre
 * of the crossing that is not suspended from anything — a viaduct standing on
 * piers. The Golden Gate is that kit with two towers and one main span; the Bay
 * Bridge is the same kit with a suspended half, a crossing of Yerba Buena that
 * cannot be suspended from anything, and a single-tower span on the Oakland
 * side. Neither is special-cased. What separates them is arithmetic over the
 * `Bridge` record the city pack already writes, which is the point: a pack
 * author adds a crossing by writing a path, some towers and two heights, and
 * gets the right *kind* of bridge back without naming it.
 *
 * ### What the classifier does, and why a bridge needs one
 *
 * A `Bridge` says where the deck runs and where the towers stand. It does not
 * say which parts hang from a cable, and it cannot: the same five fields
 * describe the Golden Gate, an eleven-kilometre trestle across the South Bay
 * with one hump in it for the ship channel, and a crossing that dives through an
 * island. Drawing all three as "a catenary between every pair of towers" is what
 * the previous builder did, and it is why the Bay Bridge hung a two-and-a-half
 * kilometre cable over the top of Yerba Buena — a span half again longer than
 * any suspension span ever built, over dry land.
 *
 * So each reach of deck between two stations — a path end or a tower — is
 * classified by whether a cable could actually hold it up:
 *
 *   - **main**: tower to tower, no longer than a tower can carry. Full
 *     catenary, hangers the whole way.
 *   - **side**: an anchorage to a tower, at the same limit. Same cable, less sag.
 *   - **approach**: everything else. A deck on piers, with a pier skipped
 *     wherever the ground has already come up to meet it — which is what makes
 *     the Yerba Buena crossing land on the island instead of standing on stilts
 *     over it.
 *
 * A tower with no suspended reach on either side — the ship-channel tower of a
 * long trestle — gets a **local** chain: anchorages placed on the deck a span
 * either side of it, so the cable is a hump over the channel rather than a wire
 * stretched the length of the bay. That is one more use of the same catenary,
 * not a fourth kind of bridge.
 *
 * ### Everything comes out as geometry, not meshes
 *
 * Nothing here constructs a `Mesh` or a `Material`. Parts are handed to a
 * `GeometrySink` — `structures.ts`'s `Batch` — which caches one material per
 * colour and merges every part of a bridge into a single buffer. That is why a
 * bridge with two towers, two cables, seventy hangers and thirty piers is two
 * draw calls: the painted structure, and the roadway on top of it.
 *
 * The corollary is the one that bites: every geometry returned from here must
 * carry **position, normal, uv and an index**, because `mergeGeometries` refuses
 * a bucket whose attribute sets disagree and drops it without throwing. `strip`
 * below sets UVs it has no texture for, for exactly that reason — and the
 * roadway, which does have one, is the reason the convention is u across and v
 * in metres along.
 *
 * ### Members are sized in metres, with a floor
 *
 * `socal.ts` asked for this in a comment: bridge members used to be constants in
 * scene units tuned at San Francisco's 94 m per unit, so at Los Angeles' 391 the
 * Vincent Thomas came out four times too heavy. Every cross-section here is
 * `metres / metresPerUnit` instead — with a **metre** floor, because a floor in
 * scene units is a floor that assumes one board. 0.09 units is 8.5 m on the Bay
 * and 173 m on merged California: the Golden Gate's deck became a highway.
 * Heights stay on `world.metres()`, which carries the board's vertical
 * exaggeration: a deck has to sit at the same exaggerated height as the hills it
 * lands on or it lands inside them.
 */

import * as THREE from "three";

import type { Bridge, LatLng } from "./types.ts";
import type { World } from "./world.ts";

/**
 * The three ways a surface out here is shaded — see `structures.ts`, which owns
 * the materials this names. Declared here rather than there because the sink
 * below is the boundary between the two modules and a boundary that names a
 * type should own it.
 */
export type SurfaceKind = "deck" | "solid" | "marking";

/**
 * Where parts go. `structures.ts`'s `Batch` satisfies this structurally; nothing
 * here needs to know that a bucket exists, only that a part with a name, a
 * geometry and a material is somebody else's problem after this.
 */
export interface GeometrySink {
  material(kind: SurfaceKind, color: number): THREE.Material;
  add(
    name: string,
    geometry: THREE.BufferGeometry,
    material: THREE.Material,
    shadows?: { cast?: boolean; receive?: boolean },
  ): void;
}

/** What a bridge needs from the board it stands on. */
type BridgeWorld = Pick<World, "project" | "groundAt" | "metres" | "metresPerUnit">;

// ---- Geometry primitives ---------------------------------------------------

/**
 * A quad strip between two rails, with UVs running 0..1 across and in **metres**
 * along.
 *
 * Winding is `left → left+1 → right`, which puts the face normal on the side the
 * left rail is anti-clockwise from — up, for a deck whose left rail is the
 * left-hand one. A downward or inward face is the same call with the rails
 * swapped, which is how the four faces of a deck box are built from one helper.
 */
function strip(left: readonly THREE.Vector3[], right: readonly THREE.Vector3[]): THREE.BufferGeometry {
  const positions: number[] = [];
  const uvs: number[] = [];
  const indices: number[] = [];
  let along = 0;
  const count = Math.min(left.length, right.length);
  for (let i = 0; i < count; i += 1) {
    const l = left[i];
    const r = right[i];
    if (!l || !r) continue;
    const previous = left[i - 1];
    if (i > 0 && previous) along += l.distanceTo(previous);
    positions.push(l.x, l.y, l.z, r.x, r.y, r.z);
    uvs.push(0, along, 1, along);
    if (i < count - 1) {
      const a = i * 2;
      indices.push(a, a + 2, a + 1, a + 2, a + 3, a + 1);
    }
  }
  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
  geometry.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2));
  geometry.setIndex(indices);
  geometry.computeVertexNormals();
  return geometry;
}

/** A box with its transform baked in, ready to merge. */
function block(
  x: number,
  y: number,
  z: number,
  width: number,
  height: number,
  depth: number,
  yaw = 0,
): THREE.BufferGeometry {
  const geometry = new THREE.BoxGeometry(width, height, depth);
  if (yaw !== 0) geometry.rotateY(yaw);
  geometry.translate(x, y, z);
  return geometry;
}

/**
 * A four-sided frustum: the same box, narrower at the top.
 *
 * This is what makes a tower leg read as a tower leg rather than a post. A
 * suspension tower carries its own weight plus half the cable load, so it is
 * visibly fatter at the waterline than at the saddle, and the taper is most of
 * what the eye uses to tell a 227 m tower from a 60 m one when both are a
 * hundred pixels tall.
 */
function taper(
  x: number,
  z: number,
  bottomY: number,
  topY: number,
  bottomWidth: number,
  topWidth: number,
  bottomDepth: number,
  topDepth: number,
  yaw: number,
): THREE.BufferGeometry {
  const geometry = new THREE.BoxGeometry(1, 1, 1);
  const position = geometry.getAttribute("position");
  const height = topY - bottomY;
  for (let i = 0; i < position.count; i += 1) {
    const up = position.getY(i) > 0;
    const width = up ? topWidth : bottomWidth;
    const depth = up ? topDepth : bottomDepth;
    position.setX(i, position.getX(i) * width);
    position.setZ(i, position.getZ(i) * depth);
    position.setY(i, up ? height / 2 : -height / 2);
  }
  geometry.computeVertexNormals();
  if (yaw !== 0) geometry.rotateY(yaw);
  geometry.translate(x, bottomY + height / 2, z);
  return geometry;
}

/** A swept tube — a main cable, a hanger, a stay. */
function cord(points: readonly THREE.Vector3[], radius: number, radial = 4): THREE.BufferGeometry {
  const curve = new THREE.CatmullRomCurve3([...points]);
  return new THREE.TubeGeometry(curve, Math.max(2, points.length - 1), radius, radial, false);
}

// ---- The deck --------------------------------------------------------------

interface Station {
  /** Centre of the deck, at the top of the deck slab. */
  point: THREE.Vector3;
  /** Unit tangent along the deck, in the ground plane. */
  tangent: THREE.Vector3;
  /** Unit left-of-travel normal, in the ground plane. */
  left: THREE.Vector3;
  /** Ground height under this sample, in scene units. */
  ground: number;
  /** Distance from the start of the deck, in scene units. */
  along: number;
}

/**
 * Resample the authored path evenly along its length.
 *
 * The packs write a bridge as four to seven points, which is enough to say where
 * it goes and nowhere near enough to hang anything off: the old builder took
 * those points literally, so the Golden Gate's cable had three control points
 * and its deck was a straight pipe between them. Everything below — hanger
 * spacing, pier spacing, the point a tower stands at — is expressed in distance
 * along the deck, and this is what makes distance along the deck mean something.
 */
function stations(world: BridgeWorld, path: LatLng[], spacing: number): Station[] {
  const raw: { point: THREE.Vector3; ground: number }[] = [];
  for (let i = 0; i < path.length - 1; i += 1) {
    const from = path[i];
    const to = path[i + 1];
    if (!from || !to) continue;
    const [x0, z0] = world.project(from[0], from[1]);
    const [x1, z1] = world.project(to[0], to[1]);
    const legLength = Math.hypot(x1 - x0, z1 - z0);
    const steps = Math.max(1, Math.round(legLength / spacing));
    const last = i === path.length - 2 ? steps : steps - 1;
    for (let s = 0; s <= last; s += 1) {
      const t = s / steps;
      const lat = from[0] + (to[0] - from[0]) * t;
      const lng = from[1] + (to[1] - from[1]) * t;
      const [x, z] = world.project(lat, lng);
      raw.push({ point: new THREE.Vector3(x, 0, z), ground: world.groundAt(lat, lng) });
    }
  }

  const out: Station[] = [];
  let along = 0;
  for (let i = 0; i < raw.length; i += 1) {
    const here = raw[i];
    if (!here) continue;
    const previous = raw[Math.max(0, i - 1)] ?? here;
    const next = raw[Math.min(raw.length - 1, i + 1)] ?? here;
    const dx = next.point.x - previous.point.x;
    const dz = next.point.z - previous.point.z;
    const length = Math.hypot(dx, dz) || 1;
    if (i > 0) along += here.point.distanceTo(previous.point);
    out.push({
      point: here.point.clone(),
      tangent: new THREE.Vector3(dx / length, 0, dz / length),
      left: new THREE.Vector3(-dz / length, 0, dx / length),
      ground: here.ground,
      along,
    });
  }
  return out;
}

/**
 * How finely the deck is sampled, in scene units.
 *
 * Four deck-widths — 60 m on the Bay Area board — is set by the suspended spans
 * rather than by the deck: this is also the grid the cable parabola and the
 * hangers are quantised to, and a coarser one leaves the Golden Gate with nine
 * hangers a span and a cable made of straight lines.
 */
function stationSpacing(deckHalf: number, perUnit: number): number {
  // 26 m was 0.28 scene units on the Bay board, the floor this was tuned on.
  return Math.max(deckHalf * 4, 26 / perUnit);
}

/** A point offset laterally from a station, at a given height. */
function offset(station: Station, lateral: number, y: number): THREE.Vector3 {
  return new THREE.Vector3(
    station.point.x + station.left.x * lateral,
    y,
    station.point.z + station.left.z * lateral,
  );
}

// ---- Reaches ---------------------------------------------------------------

type ReachKind = "main" | "side" | "approach";

interface Reach {
  kind: ReachKind;
  /** Station indices, inclusive. */
  from: number;
  to: number;
}

/** The station nearest a lat/lng — where a tower actually meets the deck. */
function nearestStation(world: BridgeWorld, list: Station[], at: LatLng): number {
  const [x, z] = world.project(at[0], at[1]);
  let best = 0;
  let bestDistance = Infinity;
  for (let i = 0; i < list.length; i += 1) {
    const station = list[i];
    if (!station) continue;
    const distance = Math.hypot(station.point.x - x, station.point.z - z);
    if (distance < bestDistance) {
      bestDistance = distance;
      best = i;
    }
  }
  return best;
}

/**
 * How long a reach a cable on this bridge could plausibly hold, in scene units.
 *
 * Nine tower-heights is not a structural formula, it is a fit to what has been
 * built: the Golden Gate's towers stand 227 m over a 1,280 m main span (5.6),
 * the Verrazzano 211 over 1,298 (6.2), the Akashi Kaikyō 297 over 1,991 (6.7).
 * Nothing reaches nine. The Bay Bridge's two-and-a-half kilometre gap between
 * its west-span towers and its east-span one is 15, which is the number that
 * tells you those three towers are not one bridge — and that is the whole job of
 * this constant.
 *
 * The height has to come in as **metres**, not as the scene-unit `towerY`. A
 * board exaggerates height and does not exaggerate distance — 3.6× here, 13× on
 * the California board — so comparing an exaggerated height against an
 * unexaggerated length said the Bay Bridge could suspend two and a half
 * kilometres, and it drew exactly that.
 */
function suspendableSpan(towerHeightM: number, metresPerUnit: number): number {
  return (towerHeightM * 9) / metresPerUnit;
}

/**
 * Split the deck into reaches, and decide which of them a cable holds up.
 *
 * The subtlety is the terminal reach. A pack writes both ends of a crossing well
 * inland so the span has something to land on — the Golden Gate's path runs from
 * the Presidio to Fort Baker, 1.4 km past the towers at each end — but a real
 * anchorage sits about half a main span beyond the tower and the rest of that
 * distance is approach viaduct. Anchoring the cable at the end of the path
 * instead is what used to run the Golden Gate's side cables up the hillside.
 */
function classify(
  list: Station[],
  towerAt: number[],
  towerHeightM: number,
  metresPerUnit: number,
): Reach[] {
  const limit = suspendableSpan(towerHeightM, metresPerUnit);
  // A side span is held from one end only, so it gets the shorter allowance:
  // the Golden Gate's are 343 m against a 1,280 m main span, and a reach that
  // long from a single tower is an approach viaduct in every real crossing.
  const sideLimit = limit * 0.55;
  const marks = [0, ...towerAt, list.length - 1];
  const reaches: Reach[] = [];
  const stationAt = (from: number, to: number, distance: number): number => {
    const target = (list[from]?.along ?? 0) + distance;
    for (let at = from; at <= to; at += 1) if ((list[at]?.along ?? 0) >= target) return at;
    return to;
  };
  for (let i = 0; i < marks.length - 1; i += 1) {
    const from = marks[i];
    const to = marks[i + 1];
    if (from === undefined || to === undefined || to <= from) continue;
    const length = (list[to]?.along ?? 0) - (list[from]?.along ?? 0);
    const startsOnTower = i > 0;
    const endsOnTower = i < marks.length - 2;
    if (startsOnTower && endsOnTower) {
      reaches.push({ kind: length > limit ? "approach" : "main", from, to });
      continue;
    }
    if (!startsOnTower && !endsOnTower) {
      reaches.push({ kind: "approach", from, to });
      continue;
    }
    if (length <= sideLimit) {
      reaches.push({ kind: "side", from, to });
      continue;
    }
    // Too long to be one side span: viaduct out to the anchorage, cable in.
    if (endsOnTower) {
      const anchor = stationAt(from, to, length - sideLimit);
      if (anchor > from) reaches.push({ kind: "approach", from, to: anchor });
      reaches.push({ kind: "side", from: anchor, to });
    } else {
      const anchor = stationAt(from, to, sideLimit);
      reaches.push({ kind: "side", from, to: anchor });
      if (to > anchor) reaches.push({ kind: "approach", from: anchor, to });
    }
  }
  return reaches;
}

// ---- Cable chains ----------------------------------------------------------

interface Chain {
  /** Station indices the cable passes over: anchorage, tower(s), anchorage. */
  nodes: number[];
  /** Which of those are towers, and so carry the cable at tower height. */
  towers: Set<number>;
}

/**
 * Group suspended reaches into cable runs.
 *
 * A main cable is continuous from one anchorage, over every tower it crosses, to
 * the next: it is not per-span. Breaking the run wherever an approach interrupts
 * it is what gives the Bay Bridge two cables — one over the pair of west-span
 * towers, one over the single east-span tower — instead of one improbable wire
 * from San Francisco to Oakland.
 */
function chains(reaches: Reach[], towerAt: number[]): Chain[] {
  const towers = new Set(towerAt);
  const out: Chain[] = [];
  let open: number[] | null = null;
  for (const reach of reaches) {
    if (reach.kind === "approach") {
      if (open) {
        out.push({ nodes: open, towers });
        open = null;
      }
      continue;
    }
    if (!open) open = [reach.from];
    open.push(reach.to);
  }
  if (open) out.push({ nodes: open, towers });
  return out;
}

/**
 * The channel tower: a tower no cable reached, given a cable of its own.
 *
 * The South Bay crossings are eleven kilometres of low trestle with one raised
 * span in the middle for shipping, and the packs write that as a single tower on
 * a very long path. Both reaches either side are approaches, so `chains` returns
 * nothing and the tower stands there holding air. This puts an anchorage on the
 * deck a span either side of it, which reads from the air as the hump those
 * bridges actually have.
 */
function localChains(
  list: Station[],
  reaches: Reach[],
  towerAt: number[],
  towerHeightM: number,
  metresPerUnit: number,
): Chain[] {
  const reached = new Set<number>();
  for (const reach of reaches) {
    if (reach.kind === "approach") continue;
    reached.add(reach.from);
    reached.add(reach.to);
  }
  const reach = suspendableSpan(towerHeightM, metresPerUnit) * 0.45;
  const out: Chain[] = [];
  for (const at of towerAt) {
    if (reached.has(at)) continue;
    const centre = list[at]?.along ?? 0;
    let from = at;
    let to = at;
    while (from > 0 && centre - (list[from - 1]?.along ?? 0) < reach) from -= 1;
    while (to < list.length - 1 && (list[to + 1]?.along ?? 0) - centre < reach) to += 1;
    if (to > from) out.push({ nodes: [from, at, to], towers: new Set([at]) });
  }
  return out;
}

// ---- The build -------------------------------------------------------------

export interface BridgeParts {
  /** Painted structure: deck box, towers, cables, hangers, piers. */
  structure: number;
  /** The running surface on top of the deck. */
  roadway: number;
  /** Triangles by part — deck, tower, cable, hanger, pier. For the census. */
  byPart: Record<string, number>;
}

/**
 * Everything about a bridge that is decided before a single triangle is made:
 * the member sizes, the deck heights, the stations, the classification, and the
 * cable runs.
 *
 * It is a separate function because two callers need the *same* answer.
 * `buildBridge` turns it into geometry; `bridgeLights` hangs lamps off the deck
 * and the tower heads, and a lamp that is not on the deck this module actually
 * drew is a row of lights floating beside a bridge. Recomputing the deck profile
 * in a second place is exactly how that happens — the ramp at each end alone is
 * five lines of arithmetic nobody would keep in step by hand.
 */
export interface BridgeLayout {
  /** Evenly resampled deck stations. Empty when the path is degenerate. */
  list: Station[];
  /** Station index of each authored tower. */
  towerAt: number[];
  reaches: Reach[];
  /** Cable runs, including the lone channel tower’s local hump. */
  runs: Chain[];
  /** Top of the deck slab at every station, in scene units. */
  deckTop: number[];
  deckHalf: number;
  deckDepth: number;
  legWidth: number;
  legDepth: number;
  cableRadius: number;
  hangerHalf: number;
  pierHalf: number;
  deckY: number;
  towerY: number;
  /** Metres to scene units across the bridge, with the legibility floor. */
  size(metres: number, floor: number): number;
  /** Where a leg or a pier starts, given the ground under it. */
  footing(ground: number, margin: number): number;
}

function layout(world: BridgeWorld, bridge: Bridge): BridgeLayout {
  // ---- Sizes ----
  //
  // Cross-sections in true metres with a legibility floor; heights through
  // `world.metres`, which carries the board's vertical exaggeration. See the
  // module comment for why those two are not the same conversion.
  const perUnit = world.metresPerUnit;
  /**
   * Cross-section in metres, never thinner than `floorM` metres.
   *
   * The second argument used to be scene units, tuned at 94 m/unit. On a board
   * 1,919 m to the unit those floors always won: a 21 m deck half became 173 m.
   * Floors are the metres those old constants measured on the Bay, so that
   * board does not move and every other board draws the same steel.
   */
  const size = (metres: number, floorM: number) => Math.max(metres, floorM) / perUnit;
  // Three of these are wider than the real member, and the reason is the
  // board's vertical exaggeration. San Francisco draws height at 3.6× and distance at
  // 1×, so a tower at true scale is 227 m tall and 12 m thick — a 62:1 needle
  // where the real thing is 19:1, and the first render of this kit had two
  // wires standing in the strait. Widening the tower back to 19:1 across the
  // deck would make it wider than the deck it carries, so the stoutness goes
  // into **depth along the bridge** instead, which is where a real tower is
  // already deeper than it is wide (10.4 m × 17.7 m at the Golden Gate's
  // saddle) and where nothing else needs the room. The deck follows at 42 m
  // against a true 27, which is the same allowance `socal.ts` records for its
  // landmarks: at this scale a true-width deck is under two pixels.
  const deckHalf = size(21, 8.5);
  const deckDepth = size(14, 3.8);
  const legWidth = size(20, 4.7);
  const legDepth = size(46, 9.4);
  const cableRadius = size(8, 1.4);
  const hangerHalf = size(1.9, 0.5);
  const pierHalf = size(8, 2.4);

  const deckY = world.metres(bridge.deckHeight);
  const towerY = world.metres(bridge.towerHeight);

  /**
   * Where a leg or a pier starts, given the ground under it.
   *
   * On land that is the ground, buried a little. Over water it is the seabed —
   * but only down to about 25 m, because the bay has trenches in it and a
   * column drawn to the bottom of one is a long thin thing nobody can see doing
   * nothing. The clamp is also what stops a foot appearing to float: the
   * surface is at zero, so anything below it is hidden by the water.
   *
   * This started as `Math.min(ground, 0)`, which is right in water and wrong on
   * land: it sank a pier through Yerba Buena to sea level and stood the island
   * crossing on stilts.
   */
  const seabed = -world.metres(25);
  const footing = (ground: number, margin: number) =>
    Math.max(ground, seabed) - world.metres(margin);

  const list = stations(world, bridge.path, stationSpacing(deckHalf, perUnit));
  const towerAt = bridge.towers.map((tower) => nearestStation(world, list, tower));
  const reaches = classify(list, towerAt, bridge.towerHeight, perUnit);
  const runs = [
    ...chains(reaches, towerAt),
    ...localChains(list, reaches, towerAt, bridge.towerHeight, perUnit),
  ];

  // ---- The deck ----
  //
  // Flat at `deckHeight` for its whole length, except the last stretch at each
  // end, which ramps down onto whatever the shore is. That ramp is not a
  // flourish: both ends of the Golden Gate are written well inland so the span
  // has something to land on, and a deck held rigidly at 67 m simply disappeared
  // into the Presidio bluff with no visible touchdown.
  const total = list[list.length - 1]?.along ?? 0;
  const rampLength = Math.min(total * 0.18, Math.max(deckHalf * 12, 113 / perUnit));
  const deckTopAt = (station: Station): number => {
    const fromEnd = Math.min(station.along, total - station.along);
    if (fromEnd >= rampLength) return deckY;
    const landing = Math.max(station.ground + deckDepth + size(4, 0.9), 0);
    if (landing >= deckY) return deckY;
    const t = rampLength <= 0 ? 1 : fromEnd / rampLength;
    // Smoothstep, so the deck leaves the shore tangentially rather than kinking.
    return landing + (deckY - landing) * (t * t * (3 - 2 * t));
  };

  const deckTop = list.map(deckTopAt);

  return {
    list,
    towerAt,
    reaches,
    runs,
    deckTop,
    deckHalf,
    deckDepth,
    legWidth,
    legDepth,
    cableRadius,
    hangerHalf,
    pierHalf,
    deckY,
    towerY,
    size,
    footing,
  };
}

/**
 * Build one bridge into `sink`, and report what it cost in triangles.
 *
 * The count is returned rather than measured afterwards because the merged mesh
 * cannot tell you which bridge paid for what, and the triangle budget on the Bay
 * Area board is the constraint this whole kit is written against.
 */
export function buildBridge(
  world: BridgeWorld,
  bridge: Bridge,
  sink: GeometrySink,
  roadwayMaterial: THREE.Material,
): BridgeParts {
  const paint = sink.material("solid", bridge.color);
  const cost: BridgeParts = { structure: 0, roadway: 0, byPart: {} };
  // `part` names what is being added, for the triangle census only. Everything
  // painted goes into one bucket keyed on the bridge's name, because one bridge
  // is one colour and one colour is one draw call — naming the buckets per part
  // would put the Golden Gate back to six meshes for one orange object.
  const add = (part: string, geometry: THREE.BufferGeometry, roadway = false) => {
    const index = geometry.getIndex();
    const triangles = index ? index.count / 3 : (geometry.getAttribute("position")?.count ?? 0) / 3;
    if (roadway) cost.roadway += triangles;
    else cost.structure += triangles;
    cost.byPart[part] = (cost.byPart[part] ?? 0) + triangles;
    sink.add(
      roadway ? "bridge:roadway" : bridge.name,
      geometry,
      roadway ? roadwayMaterial : paint,
      { cast: !roadway },
    );
  };

  const {
    list,
    towerAt,
    reaches,
    runs,
    deckTop,
    deckHalf,
    deckDepth,
    legWidth,
    legDepth,
    cableRadius,
    hangerHalf,
    pierHalf,
    deckY,
    towerY,
    size,
    footing,
  } = layout(world, bridge);
  if (list.length < 2) return cost;

  /**
   * The deck box, and the one place this module spends triangles on distance
   * rather than on detail.
   *
   * Every station carries a quad on four faces — 32 triangles a station — and
   * the station grid is set by what a suspended span needs. A causeway needs
   * nothing of the sort: the San Mateo–Hayward crossing is eleven kilometres of
   * dead-straight trestle, and sampling it at 60 m spent 1,800 triangles
   * subdividing a straight line. So an approach is emitted at every third
   * station and a suspended reach at every one, which took the five Bay Area
   * crossings from 18,044 triangles to 12,318 with nothing visible changing.
   * Reach boundaries are always kept, or the deck would tear at the anchorage.
   */
  const wanted = new Set<number>([0, list.length - 1]);
  for (const reach of reaches) {
    wanted.add(reach.from);
    wanted.add(reach.to);
    const stride = reach.kind === "approach" ? 3 : 1;
    for (let at = reach.from; at <= reach.to; at += stride) wanted.add(at);
  }
  for (const at of towerAt) {
    // Both sides of a tower, so a leg never lands between two deck samples.
    wanted.add(Math.max(0, at - 1));
    wanted.add(at);
    wanted.add(Math.min(list.length - 1, at + 1));
  }
  const deckAt = [...wanted].sort((a, b) => a - b);

  const topLeft: THREE.Vector3[] = [];
  const topRight: THREE.Vector3[] = [];
  const underLeft: THREE.Vector3[] = [];
  const underRight: THREE.Vector3[] = [];
  for (const at of deckAt) {
    const station = list[at];
    const y = deckTop[at];
    if (!station || y === undefined) continue;
    topLeft.push(offset(station, deckHalf, y));
    topRight.push(offset(station, -deckHalf, y));
    underLeft.push(offset(station, deckHalf, y - deckDepth));
    underRight.push(offset(station, -deckHalf, y - deckDepth));
  }

  add("deck", strip(topLeft, topRight), true);
  add("deck", strip(underRight, underLeft));
  add("deck", strip(underLeft, topLeft));
  add("deck", strip(topRight, underRight));

  // ---- Towers ----
  //
  // Two legs and a ladder of portal struts between them, not one post. The
  // openings between those struts are the single most recognisable thing about
  // the Golden Gate's towers, and a solid box has none of them.
  const towerTops = new Map<number, number>();
  for (const at of towerAt) {
    const station = list[at];
    if (!station) continue;
    const base = footing(station.ground, 8);
    const yaw = Math.atan2(station.tangent.x, station.tangent.z);
    const height = towerY - base;
    towerTops.set(at, towerY);
    for (const side of [-1, 1] as const) {
      const leg = offset(station, side * deckHalf, 0);
      // Three stacked frusta rather than one: real towers step in at intervals
      // rather than tapering evenly, and three steps is where the silhouette
      // stops changing.
      const steps = [0, 0.42, 0.76, 1];
      for (let i = 0; i < steps.length - 1; i += 1) {
        const a = steps[i] ?? 0;
        const b = steps[i + 1] ?? 1;
        const wide = (t: number) => legWidth * (1 - 0.34 * t);
        const deep = (t: number) => legDepth * (1 - 0.38 * t);
        add(
          "tower",
          taper(leg.x, leg.z, base + height * a, base + height * b, wide(a), wide(b), deep(a), deep(b), yaw),
        );
      }
      // A fender at the waterline. The south tower of the Golden Gate stands in
      // open water inside one, and without it a tower leg appears to be stuck
      // through the surface like a pin.
      add("tower", taper(leg.x, leg.z, base, deckY * 0.14, legWidth * 1.5, legWidth * 1.25, legDepth * 1.45, legDepth * 1.2, yaw));
    }
    // Portal struts. The lowest sits under the deck, the rest climb to the
    // saddle; the spacing tightens upward, which is what the real bracing does.
    const centre = offset(station, 0, 0);
    for (const frac of [0.2, 0.52, 0.71, 0.86, 0.985]) {
      const y = base + height * frac;
      const strutDepth = legDepth * (1 - 0.3 * frac);
      add("tower", block(centre.x, y, centre.z, deckHalf * 2 + legWidth, legWidth * 0.62, strutDepth, yaw));
    }
  }

  // ---- Cables and hangers ----
  const hangerSpacing = Math.max(deckHalf * 3.4, size(60, 21));
  for (const chain of runs) {
    for (const side of [-1, 1] as const) {
      const lateral = side * deckHalf;
      const points: THREE.Vector3[] = [];
      const hangers: { at: number; top: number }[] = [];
      for (let i = 0; i < chain.nodes.length - 1; i += 1) {
        const fromAt = chain.nodes[i];
        const toAt = chain.nodes[i + 1];
        if (fromAt === undefined || toAt === undefined) continue;
        const fromTower = chain.towers.has(fromAt);
        const toTower = chain.towers.has(toAt);
        const fromY = fromTower ? towerY : (deckTop[fromAt] ?? deckY) + deckDepth * 0.6;
        const toY = toTower ? towerY : (deckTop[toAt] ?? deckY) + deckDepth * 0.6;
        /**
         * Sag, measured against the drop from the saddle to the deck rather
         * than against tower height.
         *
         * `sag` is authored as a fraction of tower height, and read literally
         * it puts the Bay Bridge's cable a third of a tower above its own deck
         * at midspan — which is a trestle with a curve in it, not a suspension
         * bridge. On every real one the main cable comes down to within a few
         * metres of the deck at the centre of the main span; that near-touch is
         * the silhouette, and it is what the eye is actually recognising from
         * eight kilometres up. So the authored number is kept as the thing that
         * separates one bridge from another — a deeper cable on the Golden Gate
         * than on the shallow-sagging Long Beach Gateway — and mapped onto the
         * range a cable actually occupies.
         *
         * A side span drops most of a tower's height on its own and needs only
         * enough curve not to look like a guy wire.
         */
        const drop = Math.max(towerY - deckY, 0);
        const main = fromTower && toTower;
        const sag = main
          ? drop * Math.min(0.95, Math.max(0.55, 0.55 + bridge.sag * 0.7))
          : drop * 0.12;
        const fromAlong = list[fromAt]?.along ?? 0;
        const toAlong = list[toAt]?.along ?? 0;
        for (let at = fromAt; at <= toAt; at += 1) {
          const station = list[at];
          if (!station) continue;
          const t = toAlong === fromAlong ? 0 : (station.along - fromAlong) / (toAlong - fromAlong);
          // A parabola, not a sine: a cable under a deck of uniform weight
          // hangs in one, and the difference is visible at the tower, where a
          // sine leaves the saddle horizontally and a real cable does not.
          const y = fromY + (toY - fromY) * t - 4 * sag * t * (1 - t);
          const floorY = (deckTop[at] ?? deckY) + deckDepth * 0.35;
          const clamped = Math.max(y, floorY);
          if (at > fromAt || i === 0) points.push(offset(station, lateral, clamped));
          const carriesHanger = at !== fromAt && at !== toAt && !chain.towers.has(at);
          if (carriesHanger) hangers.push({ at, top: clamped });
        }
      }
      if (points.length < 2) continue;
      add("cable", cord(points, cableRadius, 4));

      // Anchorages. A main cable does not stop in mid-air: it runs into a block
      // of concrete, and without one the side spans ended in two red wires
      // pointing at the Presidio. One at each end of the run that is not a
      // tower — the tower ends are where the cable crosses a saddle and carries
      // on.
      for (const end of [chain.nodes[0], chain.nodes[chain.nodes.length - 1]]) {
        if (end === undefined || chain.towers.has(end)) continue;
        const station = list[end];
        const point = points[end === chain.nodes[0] ? 0 : points.length - 1];
        if (!station || !point) continue;
        const yaw = Math.atan2(station.tangent.x, station.tangent.z);
        add(
          "anchorage",
          block(point.x, point.y - deckDepth * 0.3, point.z, legWidth * 1.4, deckDepth * 2.2, legDepth * 0.7, yaw),
        );
      }

      let lastHanger = -Infinity;
      for (const hanger of hangers) {
        const station = list[hanger.at];
        if (!station) continue;
        if (station.along - lastHanger < hangerSpacing) continue;
        const deckAt = deckTop[hanger.at] ?? deckY;
        const height = hanger.top - deckAt;
        if (height <= deckDepth) continue;
        lastHanger = station.along;
        const point = offset(station, lateral, deckAt + height / 2);
        add("hanger", block(point.x, point.y, point.z, hangerHalf * 2, height, hangerHalf * 2));
      }
    }
  }

  // ---- Piers ----
  //
  // One every so often under an approach, and none at all where the ground has
  // already risen to the deck: that is what walks the Bay Bridge onto Yerba
  // Buena instead of standing it on stilts over the top of the island.
  const pierSpacing = Math.max(deckHalf * 9, size(240, 66));
  for (const reach of reaches) {
    if (reach.kind !== "approach") continue;
    let last = -Infinity;
    for (let at = reach.from + 1; at < reach.to; at += 1) {
      const station = list[at];
      if (!station) continue;
      if (station.along - last < pierSpacing) continue;
      const top = (deckTop[at] ?? deckY) - deckDepth;
      const foot = footing(station.ground, 6);
      if (top - foot < deckDepth) continue;
      last = station.along;
      const yaw = Math.atan2(station.tangent.x, station.tangent.z);
      const centre = offset(station, 0, 0);
      // One column, not a pair of legs. Two legs at deck width made the Bay
      // Bridge's skyway look like a viaduct carried on a wall — and the real
      // skyway is single-column piers, which is also two thirds fewer triangles
      // over eleven kilometres of South Bay trestle.
      add(
        "pier",
        taper(centre.x, centre.z, foot, top, pierHalf * 1.5, pierHalf, pierHalf * 2.6, pierHalf * 1.8, yaw),
      );
      // The pier cap: the crosshead the deck actually sits on, and the thing
      // that stops a column appearing to be pushed through the deck like a nail.
      add("pier", block(centre.x, top - deckDepth * 0.2, centre.z, deckHalf * 1.5, deckDepth * 0.5, pierHalf * 2.2, yaw));
    }
  }

  return cost;
}

/**
 * How a bridge came out, for tests and for the census. Exported because the
 * classifier is the part of this module with real judgement in it, and a picture
 * cannot tell you that the Yerba Buena crossing was classified as an approach —
 * it can only tell you the cable is gone, which is also what a bug looks like.
 */
export interface BridgePlan {
  reaches: Reach[];
  chains: number;
  towerStations: number[];
  stationCount: number;
  deckLength: number;
  /** Deck half-width in true metres. Same on every board; see `bridgeScale.test.ts`. */
  deckHalfMetres: number;
}

export function planBridge(world: BridgeWorld, bridge: Bridge): BridgePlan {
  const perUnit = world.metresPerUnit;
  // Same 21 m half as `layout`. A second, smaller number here meant the
  // classifier sampled a different deck from the one that was drawn.
  const deckHalf = Math.max(21, 8.5) / perUnit;
  const list = stations(world, bridge.path, stationSpacing(deckHalf, perUnit));
  const towerAt = bridge.towers.map((tower) => nearestStation(world, list, tower));
  const reaches = classify(list, towerAt, bridge.towerHeight, perUnit);
  return {
    reaches,
    chains:
      chains(reaches, towerAt).length +
      localChains(list, reaches, towerAt, bridge.towerHeight, perUnit).length,
    towerStations: towerAt,
    stationCount: list.length,
    deckLength: list[list.length - 1]?.along ?? 0,
    deckHalfMetres: deckHalf * perUnit,
  };
}

// ---- Lighting the kit ------------------------------------------------------

/**
 * Where a crossing's lamps go, as bare positions.
 *
 * A bay at night is black, and a bridge over it is the most legible object in
 * the frame — not because of its shape, which has nothing behind it to be a
 * silhouette against, but because it is *a line of lights over water*. Before
 * this the kit drew a beautiful crossing that after sunset was a dark line on a
 * dark bay: at 21:35 the Bay Bridge's whole canvas region measured 9.3 mean
 * luminance and the towers could not be found at all.
 *
 * **These are positions, not lights.** CONTRACT.md §4 gives `Atmosphere` the
 * only light rig there is, and the ceiling on real lights is a few dozen for the
 * whole scene — a deck lamp every 50 m over five Bay Area crossings is nearly
 * three thousand of them. `nightlights.ts` turns what comes back from here into
 * one additive point cloud, one draw call, exactly as it already does for the
 * street lamps: the lamps do not illuminate the deck, they *are* the deck as far
 * as the eye at this distance is concerned. See the header of that file.
 *
 * Three runs come back, because they are three different lamps and want three
 * different colours:
 *
 *   - **deck** — the roadway lighting, one per kerb, sodium. Additive blending
 *     is what makes a run of them saturate into the continuous line a lit deck
 *     actually is rather than staying a row of separate dots.
 *   - **heads** — the aviation obstruction lights at the tower tops. Two per
 *     tower, one on each leg, and red, which is both what is really up there and
 *     the one colour that cannot be confused with the deck run below it. This is
 *     the part that makes a tower *findable*: without it a 227 m tower at night
 *     is a hole in the sky.
 *   - **piers** — nothing. A pier is a navigation hazard with a light on it in
 *     reality, but at any framing this board is looked at from those sit inside
 *     the deck run and add nothing but count.
 *
 * Every position is a pure function of the pack's own path, so it is identical
 * across frames, reloads and the two resolutions the capture scripts compare.
 */
export interface BridgeLights {
  /** Kerb lamps down the deck: flat xyz triples in scene units. */
  deck: number[];
  /** Obstruction lights at the tower heads: flat xyz triples. */
  heads: number[];
  /**
   * Half the deck width, in scene units — the one length that says how big this
   * board draws a bridge.
   *
   * It comes back with the positions because a sprite is sized once for a whole
   * point cloud and the caller has no other honest way to pick that number. A
   * constant would be tuned at San Francisco's 94 m to the unit and then be
   * three times the width of the deck it sits on at Los Angeles' 391 — which is
   * the exact bug `socal.ts` asked this module to stop having with its members.
   */
  scale: number;
}

/**
 * Metres between deck lamps.
 *
 * The Golden Gate's are about 50 m apart, which at 94 m to the unit is 0.53
 * scene units — some 55 a side over the crossing, and against a `LAMP_SIZE` of
 * 0.3 that is close enough spacing for the additive glows to run together into a
 * line rather than reading as beads. The floor is what keeps that true on the
 * SoCal board, where 391 m to the unit would otherwise put four lamps on the
 * whole Vincent Thomas.
 */
const DECK_LAMP_SPACING_M = 50;
/** 0.16 scene units was 15 m on the Bay; a scene-unit min is 307 m on California. */
const DECK_LAMP_MIN_SPACING_M = 15;

/** Lamp height above the deck slab, and the head light's clearance over the saddle. */
const DECK_LAMP_HEIGHT_M = 12;
const HEAD_LIGHT_CLEARANCE_M = 6;

export function bridgeLights(world: BridgeWorld, bridge: Bridge): BridgeLights {
  const deck: number[] = [];
  const heads: number[] = [];
  const plan = layout(world, bridge);
  const { list, deckTop, deckHalf, towerAt, towerY } = plan;
  if (list.length < 2) return { deck, heads, scale: deckHalf };

  // Lamps stand on the kerb, just inboard of the deck edge, and the run is
  // walked in distance along the deck rather than per station — the station grid
  // is set by what a *cable* needs and is three times coarser under an approach,
  // so one lamp per station would thin out over exactly the causeways that have
  // nothing else to look at.
  const spacing = Math.max(DECK_LAMP_SPACING_M, DECK_LAMP_MIN_SPACING_M) / world.metresPerUnit;
  const lift = world.metres(DECK_LAMP_HEIGHT_M);
  const total = list[list.length - 1]?.along ?? 0;
  const kerb = deckHalf * 0.92;

  for (let distance = spacing * 0.5; distance < total; distance += spacing) {
    // The station whose interval contains this distance, and the fraction
    // across it. Linear between two stations is right: the deck itself is a
    // straight strip between the same pair.
    let at = 0;
    while (at < list.length - 2 && (list[at + 1]?.along ?? 0) < distance) at += 1;
    const from = list[at];
    const to = list[at + 1];
    if (!from || !to) continue;
    const span = to.along - from.along;
    const t = span <= 0 ? 0 : (distance - from.along) / span;
    const y = (deckTop[at] ?? 0) + ((deckTop[at + 1] ?? 0) - (deckTop[at] ?? 0)) * t + lift;
    const x = from.point.x + (to.point.x - from.point.x) * t;
    const z = from.point.z + (to.point.z - from.point.z) * t;
    for (const side of [-1, 1] as const) {
      deck.push(x + from.left.x * kerb * side, y, z + from.left.z * kerb * side);
    }
  }

  const clearance = world.metres(HEAD_LIGHT_CLEARANCE_M);
  for (const at of towerAt) {
    const station = list[at];
    if (!station) continue;
    for (const side of [-1, 1] as const) {
      heads.push(
        station.point.x + station.left.x * deckHalf * side,
        towerY + clearance,
        station.point.z + station.left.z * deckHalf * side,
      );
    }
  }

  return { deck, heads, scale: deckHalf };
}
