/**
 * The ground: water, coastline, and relief.
 *
 * Drawn as two meshes rather than one, deliberately:
 *
 *   1. A **shore plate** per landmass — the smooth polygon from the city pack,
 *      triangulated flat at y=0. This is what gives the coastline a crisp edge
 *      at any zoom.
 *   2. A **terrain grid** on top, clipped to land and displaced. Its edge is
 *      necessarily stair-stepped at cell size, which is why the world's coastal
 *      falloff ramps every height to zero near the water: the grid's blocky rim
 *      ends up flat, at y≈0, exactly where the smooth plate already is and in
 *      the same colour. The steps vanish.
 *
 * One mesh would have to choose between a crisp coastline and cheap relief.
 */

import * as THREE from "three";
import type { ScenePalette } from "./types.ts";
import type { World } from "./world.ts";

export const DEFAULT_PALETTE: ScenePalette = {
  skyTop: 0x8fb8d8,
  skyHorizon: 0xd9e6ee,
  sea: 0x4a7a99,
  lake: 0x527f9c,
  shore: 0xa8a495,
  sand: 0xc4b79b,
  flats: 0x9d9c93,
  upland: 0x8f9084,
  park: 0x6f8a5c,
  parkHigh: 0x5d7a4c,
};

export function paletteFor(world: World): ScenePalette {
  return { ...DEFAULT_PALETTE, ...(world.city.palette ?? {}) };
}

/**
 * Where `palette.alpine` starts and where it wins, in metres.
 *
 * Only consulted for a pack that declares the colour; see `ScenePalette`.
 *
 * These are a *snow* line, not a tree line, and the difference is the whole
 * tuning. The first pair was 900 and 2,300 — a tree line — and it was tried and
 * photographed: at 900 every range in the Mojave has its top third above the
 * threshold, so a desert two hundred kilometres wide came out with white caps on
 * it and the Panamints, the Providences and the New York Mountains all read as
 * small Sierras. The Mojave ranges are dark rock and the Sierra crest is bare
 * granite and old snow, and only one of the two is pale.
 *
 * At 1,900 the desert keeps `upland` almost everywhere — the tallest thing in
 * the eastern Mojave is 2,300 m and lands a fifth of the way along — while the
 * Sierra crest at 3,150 to 4,300, the White Mountains, Telescope Peak and San
 * Gorgonio, which are the four places in California that hold snow into the
 * summer, are most of the way to it. That is the correct list.
 *
 * Wildland is unaffected either way: `groundColor` answers `inPark` first, so a
 * forest belt is green to whatever height its envelope reaches and this ramp
 * only ever paints ground that no park covers.
 */
const ALPINE_FROM = 1_900;
const ALPINE_TO = 3_300;

/**
 * Ramp endpoints, held rather than allocated.
 *
 * `groundColor` runs once per emitted lattice vertex — 85,000 times on the
 * California board and 294,000 on the Bay Area — and the two `new THREE.Color`
 * calls it used to make per vertex were three quarters of a million short-lived
 * objects on one board build, all of them the same three values. Module scope
 * is safe here for the reason the `scratch` argument already is: this is
 * single-threaded, synchronous, and the result is read into a flat array before
 * the next call.
 */
const RAMP_TO = new THREE.Color();
const RAMP_TOP = new THREE.Color();

/**
 * Ground colour is about land *use*, not altitude.
 *
 * An earlier version ramped green with elevation, which turned every hill into
 * a meadow — and in San Francisco the hills are the most thoroughly built-on
 * part of the city. Nob Hill, Pacific Heights, Bernal and Potrero are houses to
 * the summit; the genuinely green high ground is Twin Peaks, Sutro, Davidson
 * and McLaren, all of which are parks and get their green from being in
 * `city.parks`. Everywhere else stays city-coloured however high it goes, and
 * the buildings do the rest of the talking.
 *
 * `inPark` arrives as an argument rather than being worked out here. This used
 * to call `world.pointInAny(lat, lng, world.city.parks)` itself, once per
 * emitted vertex, which on the Bay Area is 294k walks of twenty-four park
 * polygons — 72 ms of main thread, on a desktop, recomputing a fact the Worker
 * had already established at exactly these points on its way past. The lattice
 * now carries it (`Field.park`), and the caller has the index in hand.
 */
function groundColor(
  pal: ScenePalette,
  scratch: THREE.Color,
  inPark: boolean,
  elevation: number,
): THREE.Color {
  if (inPark) {
    return scratch.setHex(pal.park).lerp(RAMP_TO.setHex(pal.parkHigh), Math.min(1, elevation / 180));
  }
  if (elevation < 3) {
    return scratch.setHex(pal.sand).lerp(RAMP_TO.setHex(pal.flats), elevation / 3);
  }
  const ground = scratch
    .setHex(pal.flats)
    .lerp(RAMP_TO.setHex(pal.upland), Math.min(1, (elevation - 3) / 150));
  if (pal.alpine === undefined || elevation <= ALPINE_FROM) return ground;
  return ground.lerp(
    RAMP_TOP.setHex(pal.alpine),
    Math.min(1, (elevation - ALPINE_FROM) / (ALPINE_TO - ALPINE_FROM)),
  );
}

/** The smooth flat polygon under each landmass — the crisp coastline. */
export function createShorePlates(world: World): THREE.Mesh {
  const pal = paletteFor(world);
  const positions: number[] = [];

  for (const poly of world.city.landmasses) {
    const pts = world.projectPolygon(poly).map(([x, z]) => new THREE.Vector2(x, z));
    const geo = new THREE.ShapeGeometry(new THREE.Shape(pts));
    geo.rotateX(Math.PI / 2); // the shape's XY plane onto the scene's XZ ground
    const pos = geo.getAttribute("position");
    const index = geo.getIndex();
    if (index) {
      for (let i = 0; i < index.count; i++) {
        const k = index.getX(i);
        positions.push(pos.getX(k), pos.getY(k), pos.getZ(k));
      }
    } else {
      for (let i = 0; i < pos.count; i++) positions.push(pos.getX(i), pos.getY(i), pos.getZ(i));
    }
    geo.dispose();
  }

  const geo = new THREE.BufferGeometry();
  geo.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
  geo.computeVertexNormals();

  const mesh = new THREE.Mesh(
    geo,
    new THREE.MeshLambertMaterial({ color: pal.shore, side: THREE.DoubleSide }),
  );
  mesh.receiveShadow = true;
  /**
   * Receives, and deliberately does not cast.
   *
   * The plate is the landmass polygon lying flat at y=0 with the sea six
   * hundredths of a unit beneath it — at California's 1.9 km per unit, a step
   * of about a hundred metres. A caster that thin is a caster with no volume:
   * at a low sun it would throw the whole coastline's silhouette out across the
   * water as a hard-edged slab shadow, which is a rendering of the step and not
   * of anything in the world. `createTerrain` below is the mesh with relief in
   * it, and relief is the only thing here worth a shadow.
   */
  mesh.castShadow = false;
  mesh.name = "shorePlates";
  return mesh;
}

/**
 * Patch sizes the visible surface is allowed to collapse to, largest first, in
 * lattice cells per side.
 *
 * Powers of two and aligned to their own multiple, which is what makes the
 * bookkeeping in `lodPatches` a single lookup: a level-2 patch is either wholly
 * inside a level-4 patch or wholly outside one, so a patch is rejected by
 * testing its first cell rather than all of them.
 *
 * Stopping at 8 is a measurement, not a limit of the method. Each level costs a
 * pass over the lattice, and the levels pay less as they get coarser: against
 * the cell-by-cell surface, going to 4 takes the Bay Area from 581,008
 * triangles to 171,124 and going to 8 takes it to 157,378, while a fifth level
 * would be scanning for flat ground that levels 4 and 8 have already claimed.
 * The whole pass is *cheaper* than the surface it replaces, because the
 * vertices it stops emitting cost more than the flatness test costs to run:
 * `createTerrain` on the Bay Area went from 260 ms to 159 ms.
 */
const LOD_LEVELS = [8, 4, 2] as const;

/**
 * The same, for the shadow caster appended to the visible surface.
 *
 * It ends one level coarser and begins one level coarser, because the caster
 * already starts at `SHADOW_CASTER_STRIDE` rather than at a single cell — the
 * levels here are the *merges above* that stride, not a second decimation of
 * the same ground. A 16 was measured and left out: it takes the Bay Area's
 * caster from 53,806 triangles to 51,238 and California's from 17,632 to
 * 17,602, which is not worth a fourth pass over the lattice.
 */
const LOD_CASTER_LEVELS = [8, 4] as const;

/**
 * How far the collapsed surface may sit from the one it replaces, in **scene
 * units** of height.
 *
 * Scene units rather than metres deliberately: the boards disagree about metres
 * by a factor of twenty — one unit of height is 148 m on California, 65 m in
 * Southern California and 26 m on the Bay Area — and what has to stay invisible
 * is a number of *pixels*, which is a scene-unit quantity. The three boards are
 * 1003, 308 and 284 units across and are all looked at from a standoff that
 * puts the board across most of a 1440-pixel viewport, so a unit is roughly
 * 1.4, 4.5 and 4.9 pixels. 0.1 units is therefore a seventh of a pixel on the
 * board with the loosest scale and half a pixel on the tightest.
 *
 * It is deliberately not tuned per board. A self-hoster's pack gets the same
 * guarantee without having to declare anything, for the same reason
 * `NEIGHBOURHOOD_LOT_METRES` in `blocks.ts` is a measurement of the ground
 * rather than a list of city ids.
 */
export const LOD_HEIGHT_TOLERANCE = 0.1;
//         ^ exported, because it is a floor under anything laid flat on the
// ground. `airports.ts` derives its field lift from it: a plate laid closer to
// the surface than this can be pierced by the collapsed quad that replaced the
// lattice under it, which showed as a wedge of bare ground through the middle
// of Van Nuys. Two constants that must not drift apart are one constant.

/**
 * How far the collapsed surface's colour may sit from the one it replaces, as a
 * distance in linear RGB.
 *
 * Height is not enough on its own and the coast is where that shows.
 * `groundColor` ramps `sand` into `flats` over the **first three metres** of
 * elevation — a swing of 0.15 in red across a band the coastal falloff makes
 * tens of kilometres wide — so a height tolerance loose enough to be free
 * everywhere else is loose enough to walk the beach inland. Bounding the colour
 * as well keeps the shoreline's pale rim exactly where it was and leaves the
 * ramp free to be as steep as a pack likes.
 *
 * 0.012 is three parts in 255, which is under one step of an 8-bit framebuffer
 * once the tone map has been through it.
 */
const LOD_COLOUR_TOLERANCE = 0.012;

/**
 * Which lattice cells of the visible surface are drawn, and at what size.
 *
 * Returns `[i, j, s]` triples: the patch rooted at lattice corner `(i, j)`,
 * `s` cells on a side, drawn as one quad.
 *
 * **Why this exists.** The lattice is sized for the roughest ground on the
 * board, and most of a board is not that. California's Central Valley is four
 * hundred kilometres of ground that never leaves a hundred-metre band, and it
 * was being spent at the same triangle density as the Sierra crest. Southern
 * California's terrain measured 213,968 triangles and the Bay Area's 581,008
 * for exactly that reason: the floor of the LA basin and the floor of the bay's
 * south valley are planes, and they were being tessellated like mountains.
 *
 * **Why it cannot be seen.** A patch is only collapsed when every lattice point
 * inside it lies within `LOD_HEIGHT_TOLERANCE` of the quad that would replace
 * it *and* within `LOD_COLOUR_TOLERANCE` of the colour that quad interpolates —
 * both measured against the very diagonal `createTerrain` draws, so the test
 * and the mesh agree. Flat ground collapses; a ridge cannot, because a ridge is
 * exactly the case the bilinear surface gets wrong.
 *
 * **Why the coastline is untouched.** A patch is also only collapsed when every
 * one of its lattice points is on land and all of them agree about `park`. Any
 * patch straddling either boundary falls back to `base` cells, which are
 * emitted by the same four-corner test the surface used before this existed. So
 * the *set of ground covered* is identical to the cell-by-cell version, down to
 * the last stair-step, and only the interior of a flat run is redrawn.
 *
 * The seam between a collapsed patch and its finer neighbour is a T-junction,
 * and it is bounded by the same tolerance: the two surfaces share the patch's
 * corners, cover the same footprint in x/z — the projection is linear in
 * latitude and longitude, so a lattice rectangle stays a rectangle — and differ
 * along the shared edge by at most `LOD_HEIGHT_TOLERANCE`. There is no hole,
 * only a sliver thinner than a pixel.
 */
function lodPatches(
  world: World,
  pal: ScenePalette,
  levels: readonly number[],
  base: number,
  matchColour: boolean,
): Int32Array {
  const { latSteps, lngSteps, lats, lngs, height, land, park } = world.lattice();
  const w = lngSteps + 1;
  const tolerance = world.unitsToMetres(LOD_HEIGHT_TOLERANCE);
  const taken = new Uint8Array(latSteps * lngSteps);
  const out: number[] = [];

  // Held rather than allocated: `fits` runs a few million times on the Bay
  // Area, and `groundColor` wants somewhere to put its answer.
  const c00 = new THREE.Color();
  const c01 = new THREE.Color();
  const c10 = new THREE.Color();
  const c11 = new THREE.Color();
  const here = new THREE.Color();

  const fits = (i0: number, j0: number, s: number): boolean => {
    const parkAt = park[i0 * w + j0];
    for (let i = i0; i <= i0 + s; i++) {
      const row = i * w;
      for (let j = j0; j <= j0 + s; j++) {
        const k = row + j;
        if (!land[k]) return false;
        // Park membership is a *colour* boundary and nothing else, so the depth
        // pass has no opinion about it.
        if (matchColour && park[k] !== parkAt) return false;
      }
    }

    const inPark = parkAt === 1;
    const h00 = height[i0 * w + j0] as number;
    const h01 = height[i0 * w + j0 + s] as number;
    const h10 = height[(i0 + s) * w + j0] as number;
    const h11 = height[(i0 + s) * w + j0 + s] as number;
    if (matchColour) {
      groundColor(pal, c00, inPark, h00);
      groundColor(pal, c01, inPark, h01);
      groundColor(pal, c10, inPark, h10);
      groundColor(pal, c11, inPark, h11);
    }

    const lat0 = lats[i0] as number;
    const lat1 = lats[i0 + s] as number;
    const lng0 = lngs[j0] as number;
    const lng1 = lngs[j0 + s] as number;

    for (let i = i0; i <= i0 + s; i++) {
      // The axes are not uniformly spaced — `buildAxis` runs a focus region
      // fine and the rest coarse — so the interpolant comes off the coordinate
      // rather than off the index.
      const u = ((lats[i] as number) - lat0) / (lat1 - lat0);
      const row = i * w;
      for (let j = j0; j <= j0 + s; j++) {
        const v = ((lngs[j] as number) - lng0) / (lng1 - lng0);
        // Which of the patch's two triangles this point lands in. The diagonal
        // joins (i0+s, j0) to (i0, j0+s), so u + v = 1 is the seam.
        const near = u + v <= 1;
        const a = near ? 1 - u - v : 1 - u;
        const b = near ? u : 1 - v;
        const cc = 1 - a - b;
        const ha = near ? h00 : h01;
        const hc = near ? h01 : h11;
        const flat = a * ha + b * h10 + cc * hc;
        const k = row + j;
        const real = height[k] as number;
        if (Math.abs(flat - real) > tolerance) return false;
        if (!matchColour) continue;
        const ca = near ? c00 : c01;
        const ccol = near ? c01 : c11;
        groundColor(pal, here, inPark, real);
        const dr = a * ca.r + b * c10.r + cc * ccol.r - here.r;
        const dg = a * ca.g + b * c10.g + cc * ccol.g - here.g;
        const db = a * ca.b + b * c10.b + cc * ccol.b - here.b;
        if (dr * dr + dg * dg + db * db > LOD_COLOUR_TOLERANCE * LOD_COLOUR_TOLERANCE) {
          return false;
        }
      }
    }
    return true;
  };

  for (const s of levels) {
    for (let i = 0; i + s <= latSteps; i += s) {
      for (let j = 0; j + s <= lngSteps; j += s) {
        if (taken[i * lngSteps + j]) continue; // inside a coarser patch already
        if (!fits(i, j, s)) continue;
        for (let a = i; a < i + s; a++) {
          for (let b = j; b < j + s; b++) taken[a * lngSteps + b] = 1;
        }
        out.push(i, j, s);
      }
    }
  }

  // Everything a patch did not claim, at `base`, by the original four-corner
  // land test — which is what keeps the covered ground identical to the version
  // before any of this existed.
  for (let i = 0; i + base <= latSteps; i += base) {
    for (let j = 0; j + base <= lngSteps; j += base) {
      if (taken[i * lngSteps + j]) continue;
      const a = i * w + j;
      if (
        !land[a] ||
        !land[a + base] ||
        !land[a + base * w] ||
        !land[a + base * w + base]
      ) {
        continue;
      }
      out.push(i, j, base);
    }
  }
  return Int32Array.from(out);
}

/**
 * The displaced ground. Indexed, and holding only the cells that are fully on
 * land — a partial cell would poke a stair-step out over the water that the
 * shore plate cannot hide.
 */
export function createTerrain(world: World): THREE.Mesh {
  const pal = paletteFor(world);
  // Which cells are drawn, and how big, is `lodPatches`' answer; this function
  // only turns a corner into a vertex.
  const { latSteps, lngSteps, lats, lngs, height, park } = world.lattice();

  const positions: number[] = [];
  const colors: number[] = [];
  const indices: number[] = [];
  const scratch = new THREE.Color();

  // Lattice corner -> emitted vertex, so the four cells sharing a corner share
  // its vertex. Non-indexed, SF's terrain was 724k vertices for 241k triangles.
  const vertexAt = new Int32Array((latSteps + 1) * (lngSteps + 1)).fill(-1);

  const vertex = (i: number, j: number): number => {
    const k = i * (lngSteps + 1) + j;
    const existing = vertexAt[k];
    if (existing !== undefined && existing >= 0) return existing;
    const lat = lats[i] as number;
    const lng = lngs[j] as number;
    const e = height[k] ?? 0;
    const [x, z] = world.project(lat, lng);
    positions.push(x, world.metres(e) + 0.012, z);
    const c = groundColor(pal, scratch, park[k] === 1, e);
    colors.push(c.r, c.g, c.b);
    const id = positions.length / 3 - 1;
    vertexAt[k] = id;
    return id;
  };

  const patches = lodPatches(world, pal, LOD_LEVELS, 1, true);
  for (let p = 0; p < patches.length; p += 3) {
    const i = patches[p] as number;
    const j = patches[p + 1] as number;
    const s = patches[p + 2] as number;
    // The diagonal runs from (i+s, j) to (i, j+s). `lodPatches` splits its
    // error test along the same one, so what it measured is what is drawn.
    indices.push(vertex(i, j), vertex(i + s, j), vertex(i, j + s));
    indices.push(vertex(i, j + s), vertex(i + s, j), vertex(i + s, j + s));
  }

  const geo = new THREE.BufferGeometry();
  geo.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
  geo.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
  // Normals first, and from the visible triangles alone. The caster's coarse
  // triangles are about to join the same index buffer, and a vertex normal
  // averaged over both would be a normal for neither.
  geo.setIndex(indices);
  geo.computeVertexNormals();

  /**
   * The same relief again at a quarter of the triangles, appended to the same
   * index buffer, and drawn only by the depth pass.
   *
   * The visible triangles occupy `[0, seen)` of the index and the caster's
   * occupy the tail; `Mesh.onBeforeShadow` swings `drawRange` onto the tail and
   * `onAfterShadow` swings it back. Both hooks exist for exactly this and fire
   * either side of the one `renderBufferDirect` the shadow pass makes for this
   * mesh, and the shadow pass runs before the colour pass, so the range is
   * always right for whoever is reading it.
   *
   * **The obvious alternatives were tried and neither works.** A second mesh on
   * a rendering layer only the shadow camera can see: `WebGLShadowMap.
   * renderObject` tests `object.layers.test( camera.layers )` against the
   * **scene** camera it was handed, not against `shadow.camera`, so a mesh
   * hidden from the viewer is hidden from the depth pass by the same line — this
   * was written, measured, and cast nothing at all. A second mesh with
   * `colorWrite` off: `visible`, `material.visible` and `frustumCulled` are each
   * one flag consulted identically by both passes, so it stays in the colour
   * pass and `renderer.info` counts its triangles twice, which is the whole
   * cost this exists to avoid. One geometry with two ranges is what is left, and
   * it is also the cheapest: no second draw call, no second vertex buffer.
   */
  const seen = indices.length;
  /*
   * The caster is `lodPatches` again, one level coarser at both ends.
   *
   * `SHADOW_CASTER_STRIDE` is its *floor* rather than its spacing: rugged
   * ground still writes a facet every two lattice cells, which is the
   * resolution the round before this one measured against full resolution and
   * shipped. What changes is the flat ground, which used to pay the same
   * stride and cannot record a shadow edge at any resolution — the Central
   * Valley's depth is a plane, and a plane is two triangles whether it is
   * drawn as two or as eight thousand.
   *
   * It runs with `matchColour` off. A depth pass reads no colour, so bounding
   * one would only stop the caster merging across a park boundary that the
   * shadow map cannot see. The height tolerance still applies, and it is the
   * one that decides whether a shadow lands where it did.
   */
  const casterPatches = lodPatches(world, pal, LOD_CASTER_LEVELS, SHADOW_CASTER_STRIDE, false);
  for (let p = 0; p < casterPatches.length; p += 3) {
    const i = casterPatches[p] as number;
    const j = casterPatches[p + 1] as number;
    const s = casterPatches[p + 2] as number;
    indices.push(vertex(i, j), vertex(i + s, j), vertex(i, j + s));
    indices.push(vertex(i, j + s), vertex(i + s, j), vertex(i + s, j + s));
  }
  const cast = indices.length - seen;
  // `vertex()` may have emitted a few lattice corners the visible surface never
  // needed, so the position and colour attributes are rebuilt alongside the
  // index rather than reused from above.
  geo.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
  geo.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
  const normal = geo.getAttribute("normal") as THREE.BufferAttribute;
  if (normal.count < positions.length / 3) {
    const grown = new Float32Array(positions.length);
    grown.set(normal.array as Float32Array);
    // A corner only the caster uses is never shaded, so any unit normal will
    // do; up is the one that cannot be mistaken for a bug.
    for (let k = normal.count * 3; k < grown.length; k += 3) grown[k + 1] = 1;
    geo.setAttribute("normal", new THREE.BufferAttribute(grown, 3));
  }
  geo.setIndex(indices);
  geo.setDrawRange(0, seen);

  const material = new THREE.MeshLambertMaterial({ vertexColors: true, side: THREE.DoubleSide });
  /**
   * `shadowSide = BackSide` is the acne cure, and it is exactly right for a
   * heightfield.
   *
   * The depth pass then culls every face turned *toward* the sun — precisely
   * the set of faces that were shadowing themselves — and keeps the faces
   * turned away from it. The boundary between the two is the terminator, so the
   * depth recorded along a ridge starts at the crest and runs down its far
   * slope, and the valley floor beyond, which is still front-facing and
   * therefore writes nothing, tests against it and lands in shadow. A lit slope
   * has nothing in the map above it and cannot stipple.
   *
   * Without it, a constant `shadow.bias` has to cover a depth-per-texel that
   * grows as 1/tan(elevation) — one texel is 0.21 scene units on California and
   * 0.73 on the Bay Area — and there is no single value that is free of acne at
   * 40° and free of peter-panning at 8°. That is why this was left off through
   * the previous round, and it is checked here by photographing the boards at
   * a sun of 12° and of 2°.
   */
  material.shadowSide = THREE.BackSide;

  const mesh = new THREE.Mesh(geo, material);
  mesh.receiveShadow = true;
  /**
   * The relief casts at last.
   *
   * Until it did, a ridge shaded its own back slope through the Lambert N·L
   * term and then darkened nothing beside it: the valley next to a mountain
   * range stayed fully lit at every hour of the day, and a range read as a bump
   * map rather than as geography. `stage.ts` states the omission and this is
   * the line it was waiting on.
   */
  mesh.castShadow = true;
  mesh.onBeforeShadow = () => geo.setDrawRange(seen, cast);
  mesh.onAfterShadow = () => geo.setDrawRange(0, seen);
  mesh.name = "terrain";
  return mesh;
}

/**
 * How many lattice cells of the visible terrain go into one cell of the caster
 * appended to it, per axis.
 *
 * Bounded below by the shadow map's own resolution and above by the board's
 * triangle budget, and the budget is much the tighter of the two.
 *
 * *Below*: a caster finer than a shadow texel is detail the map cannot record.
 * The city hands `SceneKit` a box of 1.5 board spans over 2048 texels — 0.21
 * scene units on California — against a terrain lattice of about 1.2 units, so
 * a stride of 2 still writes a facet several texels across.
 *
 * *Above*: triangles, and the number is unforgiving. California's terrain is
 * 65,566 triangles; with a stride of 2 the whole board measures 732,206 against
 * a budget of 750,000, so the depth pass has about 18,000 in hand. A stride of
 * 1 would spend 65,566 and miss the budget by 47,000; a stride of 3 would spend
 * 7,285 and buy back nothing worth having. Both were measured.
 *
 * If the board's triangle count ever comes back down, this is the first number
 * worth spending it on — a stride of 1 puts a shadow edge on every ridge the
 * map can resolve and costs nothing else.
 *
 * Since `lodPatches` arrived this is a **floor** rather than a spacing: rugged
 * ground still writes a facet every two cells, and flat ground merges above it.
 * The numbers above are what a uniform stride cost, and they are why the floor
 * is 2 rather than 1; what the merge changed is that California's caster came
 * down from 20,716 triangles to 17,632 and the Bay Area's from 143,704 to
 * 53,806 without touching the resolution anywhere a shadow edge can exist.
 */
const SHADOW_CASTER_STRIDE = 2;

// ---- The sea --------------------------------------------------------------

/**
 * How far the sea reaches, in board spans, measured from the board's centre.
 *
 * This number is the fix for the hard diamond the world used to end in. The
 * plane was 1.8 times the board across — 0.9 spans from the middle — while
 * `main.ts` gives the atmosphere a clear-day fog that closes at 3.9 spans and
 * `scene.ts` gives the camera a far plane at 4. The sea therefore ran out
 * roughly a fifth of the way to the fog, in full contrast, and what you saw was
 * its silhouette: a blue quad with two straight edges meeting in a point, and
 * the state of California apparently floating on it.
 *
 * The fade already existed and nothing was reaching it. This is the plane's
 * *width*, so 18 puts its rim 9 spans from the middle, and the worst case it
 * has to beat is about 6.4: the camera can retreat 2 spans from the scene
 * origin, the origin can itself be half a span off the middle of the bounds,
 * and the fog closes 3.9 spans beyond wherever the camera is. Past the camera's
 * own far plane the water is clipped rather than drawn, and by the far plane it
 * is 100% fog, and
 * `atmosphere.ts` makes the fog colour the sky's own horizon colour on a clear
 * day. So the horizon is where the sea's colour and the sky's colour have
 * already become the same number: there is nothing there to see an edge in.
 *
 * It costs one quad. The plane is two triangles at any size.
 */
const SEA_SPAN_MULTIPLE = 18;

/**
 * The swell's wavelength, as a fraction of the board.
 *
 * Not a physical number and it should not be read as one. Real ocean swell is
 * 100–200 m crest to crest, which at California's 1.9 km per scene unit is a
 * tenth of a unit — below one screen pixel from any pose that has the board in
 * frame, so a physically-scaled sea is a sea with no visible surface at all,
 * which is where this started.
 *
 * What the number actually has to serve is the *glitter*: the eye reads water
 * as water because the sun's reflection is broken into a shifting path rather
 * than a mirror disc, and that path needs its texture to land at a handful of
 * pixels per tile at board distance. It was tuned by photographing it. A board
 * span over 20 puts a tile at some ninety screen pixels from a whole-board pose,
 * which is large enough that the eye finds the repeat and the sea reads as
 * woven fabric; over 45 the tile lands at forty and the mip chain has eaten it
 * before it says anything. 34 is the value in between, where the swell is
 * legible as swell when you look at it and never resolves into a pattern.
 */
const SWELL_TILE_SPANS = 1 / 34;

/** Tiles per second each swell layer drifts. Slow: this is weather, not surf. */
const SWELL_DRIFT_A = new THREE.Vector2(0.031, 0.017);
const SWELL_DRIFT_B = new THREE.Vector2(-0.019, 0.024);

/**
 * How much smaller the second swell layer's tile is than the first, and how far
 * round it is turned.
 *
 * Two samples of one map at two scales, two headings and two drift directions,
 * for the price of one texture. A single scrolling normal map reads as a
 * conveyor belt — the pattern is recognisable and it translates rigidly — and
 * the cheapest cure is a second copy that is not in step with it, so that no
 * crest keeps its shape for longer than the two layers stay aligned. 2.6 rather than a round
 * number because an integer ratio re-aligns on a short period and you can see
 * it happen, and 63° rather than nothing because the two copies otherwise share a
 * dominant swell direction and read as one sea seen twice.
 */
const SWELL_LAYER_B_SCALE = 2.6;
const SWELL_LAYER_B_TURN = (63 * Math.PI) / 180;

/**
 * The Fresnel term, as the fraction of the body colour left when you look
 * straight down into the water.
 *
 * `MeshStandardMaterial` already has the *reflective* half of Fresnel: its
 * environment term brightens toward grazing, so with `environmentRig.ts` on the
 * scene the far water already picks up the horizon sky. What it has no notion
 * of is the other half — that the light coming *out* of the water is a body
 * colour seen through a surface that reflects less and less of the sky the more
 * squarely you look at it, so the sea directly below the camera should be the
 * deep colour and the sea at the horizon should be mostly sky.
 *
 * Multiplying the diffuse by 0.6 looking straight down and by 1 at grazing is
 * that, cheaply and in the right direction. Without it the ocean is one value
 * across the whole frame, which is the single loudest way water reads as a
 * painted card.
 */
const SEA_DEEP_FACTOR = 0.6;

/**
 * Where the far ocean gives up its specular, in board spans of view distance.
 *
 * The glitter is a per-pixel normal against a narrow specular lobe, and at some
 * distance one pixel covers more swell than the mip chain can average without
 * flickering as the camera moves. Rather than let that flicker happen, the
 * roughness is walked up toward `SEA_FAR_ROUGHNESS` past `SEA_CALM_NEAR`, which
 * is beyond the board — the water that matters keeps every bit of its response
 * and the water at the horizon becomes the flat hazy sheet it looks like from
 * an aeroplane anyway.
 */
const SEA_CALM_NEAR = 1.6;
const SEA_CALM_FAR = 5;
const SEA_FAR_ROUGHNESS = 0.42;

/**
 * A tiling tangent-space normal map for the swell, as raw RGBA bytes.
 *
 * Returned as data rather than drawn on a canvas, and this is the one texture
 * in the repo that is not a `TextureBin` drawing. A 2D canvas is the right tool
 * for anything with *shapes* in it — a screen, a leaf, a carpet — and the wrong
 * one here: the value being encoded is a surface derivative, `ctx` has no way
 * to express one, and going through a canvas would mean rasterising a height
 * field only to read it straight back out with `getImageData`. It also keeps
 * this module free of the DOM, so a test can call it directly.
 *
 * The height field is a sum of sine waves whose wave numbers are **integers**,
 * which is what makes the result tile: every component completes a whole number
 * of cycles across the map, so the left edge and the right edge are the same
 * sample. Phases are fixed constants rather than `Math.random`, so a board looks
 * the same on every reload and two people looking at it see one sea.
 *
 * **Amplitude falls as 1/k², and the exponent is the whole difference between
 * water and corduroy.** What a normal map encodes is not height but *slope*,
 * and slope is amplitude times wave number — so the obvious 1/k spectrum gives
 * every component in the sum exactly the same slope, the shortest one wins on
 * sheer count of edges, and the sea comes out as one hard diagonal rib. This
 * was tried and photographed. At 1/k² the slope falls as 1/k instead: the two
 * long components carry the shape, the short ones sit on top as sparkle, and
 * the eight directions stay spread far enough apart that no single one is
 * legible as a stripe.
 */
export function swellNormalData(size = 128, strength = 6): Uint8Array {
  /** `[wave number x, wave number y, phase in turns]`, amplitude is 1/|k|². */
  const waves: readonly (readonly [number, number, number])[] = [
    [2, 1, 0.13],
    [-1, 2, 0.61],
    [3, -2, 0.29],
    [2, 4, 0.87],
    [-5, 2, 0.44],
    [4, 5, 0.07],
    [-3, -7, 0.72],
    [8, -2, 0.35],
  ];

  const height = new Float32Array(size * size);
  for (const [kx, ky, phase] of waves) {
    const amplitude = 1 / (kx * kx + ky * ky);
    const p = phase * Math.PI * 2;
    for (let y = 0; y < size; y++) {
      const v = y / size;
      for (let x = 0; x < size; x++) {
        const u = x / size;
        height[y * size + x] =
          (height[y * size + x] as number) +
          amplitude * Math.sin(2 * Math.PI * (kx * u + ky * v) + p);
      }
    }
  }

  // Central differences, wrapped, so the derivative tiles as cleanly as the
  // height does. `size` scales the difference back into per-tile slope, which
  // is what keeps `strength` independent of the resolution.
  const data = new Uint8Array(size * size * 4);
  const at = (x: number, y: number): number =>
    height[((y + size) % size) * size + ((x + size) % size)] as number;

  for (let y = 0; y < size; y++) {
    for (let x = 0; x < size; x++) {
      const dx = (at(x + 1, y) - at(x - 1, y)) * 0.5 * strength;
      const dy = (at(x, y + 1) - at(x, y - 1)) * 0.5 * strength;
      // OpenGL convention: +Y in the map is +V in the texture, which is how
      // three's `getTangentFrame` builds the bitangent.
      const length = Math.hypot(-dx, -dy, 1);
      const i = (y * size + x) * 4;
      data[i] = Math.round(((-dx / length) * 0.5 + 0.5) * 255);
      data[i + 1] = Math.round(((-dy / length) * 0.5 + 0.5) * 255);
      data[i + 2] = Math.round((1 / length) * 0.5 * 255 + 127.5);
      data[i + 3] = 255;
    }
  }
  return data;
}

function swellNormalTexture(): THREE.DataTexture {
  const size = 128;
  const texture = new THREE.DataTexture(swellNormalData(size), size, size);
  texture.wrapS = THREE.RepeatWrapping;
  texture.wrapT = THREE.RepeatWrapping;
  texture.magFilter = THREE.LinearFilter;
  // Mipmapped, and it is doing real work: the mip chain of a normal map
  // converges on "flat", so the sea a long way off stops perturbing its normal
  // without anything having to decide when. Anisotropy is what keeps the water
  // at a grazing angle — which is most of the frame from a map pose — from
  // blurring to that flat mip several times too early.
  texture.minFilter = THREE.LinearMipmapLinearFilter;
  texture.generateMipmaps = true;
  texture.anisotropy = 8;
  texture.needsUpdate = true;
  texture.name = "swellNormal";
  return texture;
}

/** Ocean and bay: one plane under everything, plus any inland water. */
export function createWater(world: World): THREE.Group {
  const pal = paletteFor(world);
  const group = new THREE.Group();
  group.name = "water";

  const { bounds } = world.city;
  const [x0, z0] = world.project(bounds.minLat, bounds.minLng);
  const [x1, z1] = world.project(bounds.maxLat, bounds.maxLng);
  const span = Math.max(Math.abs(x1 - x0), Math.abs(z1 - z0));
  const size = span * SEA_SPAN_MULTIPLE;

  /**
   * Standard rather than Lambert, and it is the whole difference between an
   * ocean and a blue card.
   *
   * `MeshLambertMaterial` has no specular term at all — none, by construction —
   * so the Pacific, which is between a third and a half of the California
   * board's frame, rendered as one flat value at every hour and from every
   * angle. A low roughness gives it the sun's glint back, and now that the
   * scene carries an environment map (`engine/environmentRig.ts`) it also gives
   * it the sky: `MeshStandardMaterial` reads `scene.environment`, so the water
   * reflects whatever the atmosphere decided the sky is, for free and with no
   * second pass.
   *
   * `metalness: 0` is stated rather than defaulted because water is a
   * dielectric: its reflection is a Fresnel term over a coloured body, which is
   * exactly what metalness 0 with low roughness produces, and a metallic water
   * would lose `pal.sea` entirely.
   *
   * The material on its own is still not a sea, and the three things below are
   * why: a mirror-flat plane has one specular *point* rather than a glitter
   * path, nothing about it moves, and its body colour does not change with the
   * angle you look into it. `seaSwell` supplies all three.
   */
  const material = new THREE.MeshStandardMaterial({
    color: pal.sea,
    /**
     * Rougher than a mirror, and the number is what sets the width of the sun's
     * path across the water.
     *
     * At 0.14 the specular lobe is about as tight as a smooth plane can make
     * it, so a flat sea returns the sun as a small hard disc that is either in
     * frame or not. The swell normals below spread it, and 0.2 spreads it
     * further: together they give the long shivering streak toward the sun that
     * is the single most recognisable thing about looking at the sea.
     */
    roughness: 0.2,
    metalness: 0,
    normalMap: swellNormalTexture(),
    /**
     * Pushed to the back of every depth argument it is in, and this is not
     * cosmetic — without it the Bay Area and Southern California come out with
     * the sea dithered across their flats.
     *
     * The sea sits six hundredths of a unit below y=0 and the terrain's coastal
     * rim sits twelve thousandths above it, so seventy-two thousandths of a unit
     * is the whole separation between an ocean and the ground it is supposed to
     * be under. Ask what that is worth in the depth buffer at board distance:
     * with `near` at 0.1 and the camera 800 units out — an ordinary whole-board
     * pose on the Bay Area, which is 1,003 units across — one unit of world
     * depth is about two and a half of the twenty-four-bit buffer's steps, so
     * the entire gap is **a fifth of one step**. The two surfaces are the same
     * number.
     *
     * Which of them wins is then decided by float error, and the plane is
     * `SEA_SPAN_MULTIPLE` — eighteen board spans, two triangles, corners nine
     * thousand units from the middle. Interpolating a depth near the board from
     * vertex values of that magnitude leaves a few steps of noise in it, and a
     * few steps of noise on top of a fifth of a step of signal is the dapple of
     * blue over every flat piece of ground on the board. It arrived with the
     * bigger plane, it is invisible on California — whose valley floor stands at
     * 28 m, three and a half times the gap — and it is the reason to state the
     * numbers rather than to nudge the `y` until a screenshot looks right.
     *
     * Polygon offset is the fix that does not need a per-board number: it is
     * expressed in units of whatever the depth buffer can resolve *there*, so
     * thirty-two of them is thirty-two steps on any board at any zoom, and the
     * slope term covers the grazing angles where the noise is worst. It moves
     * no pixel and changes no colour. It only ever loses the sea an argument it
     * should never have been winning.
     */
    polygonOffset: true,
    polygonOffsetFactor: 2,
    polygonOffsetUnits: 32,
  });
  material.normalScale.set(0.95, 0.95);

  const swellA = new THREE.Vector3(0, 0, size / (span * SWELL_TILE_SPANS));
  const swellB = new THREE.Vector3(0, 0, swellA.z * SWELL_LAYER_B_SCALE);
  const turn = new THREE.Matrix3().set(
    Math.cos(SWELL_LAYER_B_TURN), -Math.sin(SWELL_LAYER_B_TURN), 0,
    Math.sin(SWELL_LAYER_B_TURN), Math.cos(SWELL_LAYER_B_TURN), 0,
    0, 0, 1,
  );
  const uniforms = {
    uSwellA: { value: swellA },
    uSwellB: { value: swellB },
    uSwellTurn: { value: turn },
    uSeaDeep: { value: SEA_DEEP_FACTOR },
    uSeaCalm: { value: new THREE.Vector2(span * SEA_CALM_NEAR, span * SEA_CALM_FAR) },
    uSeaFarRough: { value: SEA_FAR_ROUGHNESS },
  };

  /**
   * Three edits to the standard shader, each of which the material has no dial
   * for and each of which the water needs.
   *
   * A patch rather than a `ShaderMaterial` written from scratch, because the
   * lighting this surface has to obey — a sun, a hemisphere, an ambient, a
   * shadow, a PMREM environment and a fog, all of them owned elsewhere and all
   * of them changing with the hour — is exactly what `MeshStandardMaterial`
   * already implements correctly. Reimplementing it to add a second texture
   * lookup would mean owning a second copy of the light rig, which CONTRACT.md
   * §4 spends its whole length arguing against.
   */
  material.onBeforeCompile = (shader) => {
    Object.assign(shader.uniforms, uniforms);
    shader.fragmentShader = shader.fragmentShader
      .replace(
        "#include <common>",
        `#include <common>
uniform vec3 uSwellA;
uniform vec3 uSwellB;
uniform mat3 uSwellTurn;
uniform float uSeaDeep;
uniform vec2 uSeaCalm;
uniform float uSeaFarRough;`,
      )
      // 1. The horizon gives up its specular before it can alias. See
      //    `SEA_CALM_NEAR`.
      .replace(
        "#include <roughnessmap_fragment>",
        `#include <roughnessmap_fragment>
float seaFar = smoothstep( uSeaCalm.x, uSeaCalm.y, length( vViewPosition ) );
roughnessFactor = mix( roughnessFactor, uSeaFarRough, seaFar );`,
      )
      // 2. Two drifting samples of the one swell map instead of the single
      //    static one the material would take, and 3. the Fresnel term on the
      //    body colour. Both live here because both need `tbn` and the
      //    unperturbed normal, which exist only between these two chunks.
      .replace(
        "#include <normal_fragment_maps>",
        `vec3 seaFlatNormal = normal;
mat2 seaTurn = mat2( uSwellTurn[0].xy, uSwellTurn[1].xy );
vec3 seaNormalA = texture2D( normalMap, vNormalMapUv * uSwellA.z + uSwellA.xy ).xyz * 2.0 - 1.0;
vec3 seaNormalB = texture2D( normalMap, seaTurn * ( vNormalMapUv * uSwellB.z ) + uSwellB.xy ).xyz * 2.0 - 1.0;
// The second layer was sampled through a rotation, so its slope arrived in the
// rotated frame; \`v * M\` is \`transpose(M) * v\`, which is the inverse of a
// rotation and turns that slope back into this surface's own frame.
seaNormalB.xy = seaNormalB.xy * seaTurn;
vec3 mapN = normalize( seaNormalA + seaNormalB );
mapN.xy *= normalScale * ( 1.0 - 0.9 * seaFar );
normal = normalize( tbn * mapN );
float seaFacing = saturate( dot( normalize( vViewPosition ), seaFlatNormal ) );
diffuseColor.rgb *= mix( 1.0, uSeaDeep, seaFacing );`,
      );
  };
  // Two materials that compile to different programs must not share a cache
  // key, and `onBeforeCompile` is invisible to three's default key.
  material.customProgramCacheKey = () => "tera:sea";

  const sea = new THREE.Mesh(new THREE.PlaneGeometry(size, size), material);
  sea.rotation.x = -Math.PI / 2;
  sea.position.set((x0 + x1) / 2, -0.06, (z0 + z1) / 2);
  sea.receiveShadow = true;
  sea.name = "sea";
  /**
   * The swell drifts from the wall clock, on the mesh's own render hook.
   *
   * `createWater` returns a `Group` and its caller adds it to a scene; there is
   * no `tick` on the way in and adding one would mean a new seam through
   * `scene.ts` for two uniform writes. `Object3D.onBeforeRender` is the hook
   * three already runs immediately before this mesh is drawn, which is the only
   * moment these two values are read.
   *
   * `performance.now()` rather than a delta accumulated per frame, because a
   * phase that is a pure function of the clock cannot drift, cannot double up
   * if the mesh is ever drawn twice in a frame, and resumes correctly after a
   * paused tab — a scene that has been backgrounded for a minute wakes up with
   * the sea where it should be rather than a minute behind.
   */
  sea.onBeforeRender = () => {
    const t = performance.now() / 1000;
    swellA.x = SWELL_DRIFT_A.x * t;
    swellA.y = SWELL_DRIFT_A.y * t;
    swellB.x = SWELL_DRIFT_B.x * t;
    swellB.y = SWELL_DRIFT_B.y * t;
  };
  group.add(sea);

  for (const poly of world.city.inlandWater) {
    const pts = world.projectPolygon(poly).map(([x, z]) => new THREE.Vector2(x, z));
    const geo = new THREE.ShapeGeometry(new THREE.Shape(pts));
    geo.rotateX(Math.PI / 2);
    // The same change as the sea above, and for the same reason. Slightly
    // rougher: an inland lake is sheltered, and a mirror-smooth bay next to a
    // wind-roughened ocean reads as the wrong way round.
    const lake = new THREE.Mesh(
      geo,
      new THREE.MeshStandardMaterial({
        color: pal.lake,
        roughness: 0.24,
        metalness: 0,
        side: THREE.DoubleSide,
        /**
         * The five-hundredths below is not a separation, at board scale.
         *
         * A lake floats over the shore plate, which is the same landmass
         * polygon lying flat at y=0, and 0.05 units is all there is between
         * them. On San Francisco — 230 units across, camera a couple of hundred
         * out — that is comfortably more than one step of the depth buffer and
         * the two never argue. On the California board the camera stands 570
         * units off and the depth buffer's resolution *there* is about 0.15
         * units, three times the gap: the Salton Sea came out banded in
         * alternating stripes of lake and shore, which reads as a rendering
         * artefact because it is one.
         *
         * Polygon offset is the fix rather than a bigger `y`, because it is
         * expressed in units of whatever the depth buffer can currently resolve
         * — it scales itself with distance, where a hard-coded lift would have
         * to be tuned per board and would leave the lake visibly hovering on the
         * two boards that never needed it.
         */
        polygonOffset: true,
        polygonOffsetFactor: -2,
        polygonOffsetUnits: -4,
      }),
    );
    lake.position.y = 0.05;
    group.add(lake);
  }

  return group;
}

