/**
 * One California, from three packs — as **one pack**, not three boards.
 *
 * ## Why this exists, and why it is not `reconcile.ts`
 *
 * `reconcile.ts` makes the three packs *agree*: one projection, one relief-in-
 * frame, one road width. It leaves three boards that draw the same California
 * and hands the camera between them. That was the answer to "the boards feel
 * like three products". It is not the answer to "I want one full California",
 * which is this file: a single `City` carrying the state's landform and both
 * metros' detail, so the engine builds **one world, one lattice and one terrain
 * mesh** and there is no seam anywhere to stitch.
 *
 * ## Why one pack rather than three scenes nested in each other
 *
 * The first attempt nested each metro's built scene inside the state board's
 * under a similarity transform. The transform was right — measured, both metros
 * landed where they belong at the size they should be — but two independently
 * built terrains then occupied the same ground and interpenetrated: a block sat
 * neatly on a coarse hill in one place and was half-buried in it three hundred
 * metres away. That is nested-grid stitching, and the way to not have the
 * problem is to not have two grids. One pack has one.
 *
 * ## The lattice is affordable, and the number that says so was measured
 *
 * `TODO.md` records a statewide lattice at metro fidelity as 23.7M points and
 * dead. That is true at San Francisco's own 45 m cell, because `buildAxis`
 * refines per **axis** — a focus region refines its whole row *and* its whole
 * column, a plus rather than a box — so two metros at opposite corners refine
 * most of the state. The blowup is a function of the fine cell, and it is
 * gentle until 45 m:
 *
 * | fine cell | coarse | lattice | points | memory |
 * | --- | --- | --- | --- | --- |
 * | 45 m | x16 | 6002 x 7600 | 45.6M | 411 MB |
 * | 100 m | x8 | 1676 x 2002 | 3.36M | 30 MB |
 * | **150 m** | **x8** | **1120 x 1349** | **1.51M** | **14 MB** |
 * | 200 m | x8 | 850 x 1013 | 0.86M | 8 MB |
 *
 * All three of today's boards together are 0.75M points. So 150 m costs about
 * twice the whole product's current terrain and buys ground **23 times finer
 * than the state board's 3,473 m cell** everywhere a city stands. No quadtree,
 * no per-rectangle refinement, no change to `buildAxis` at all.
 *
 * ## What this file must do beyond concatenating arrays
 *
 * Everything in a `City` that describes a place is absolute — hills, districts,
 * landmarks, bridges, ports and airports are all lat/lng — so most of the merge
 * is a union. Two things are not:
 *
 *   - **Hills sum.** `World.elevationAt` blends every hill that covers a point
 *     (tallest, plus 35% of the rest), so leaving the state's coarse hills under
 *     a metro's fine ones *adds* their heights and lifts the whole Bay. The
 *     state's hills are dropped where a metro replaces them.
 *   - **Districts overlap.** The state's seventeen districts include coarse
 *     stand-ins for Los Angeles and San Francisco; the metros carry 99 real
 *     ones. Both drawn is two cities in one place, so the coarse stand-ins go.
 *
 * Both filters are by centre-in-bounds, which is honest but not exact: a state
 * hill centred outside a metro with a radius reaching into it still contributes.
 * At the state's hill radii that is a gentle lift at the edge of a metro rather
 * than a doubled mountain, and it is recorded here rather than hidden.
 */

import type { City, District, FocusRegion, Hill, LatLng } from "../engine/types.ts";
import { reconciledCity } from "./reconcile.ts";
import CALIFORNIA from "./california.ts";
import SAN_FRANCISCO from "./sf.ts";
import SOCAL from "./socal.ts";

/**
 * The fine cell, in metres, and the factor the rest of the state is drawn at.
 *
 * **300 m, and it was measured down to that from 150.** The first pass used
 * 150 m / x8, which is 1.23M lattice points, and the merged board then rendered
 * **1,114,226 triangles at the whole-board pose against a 440,000 cap** — with
 * every metro building already hidden by the detail LOD. That number is almost
 * entirely *terrain*: hiding 99 districts' worth of lots moved it by 43,000
 * triangles and moved draw calls by 111, which is how it was established that
 * the buildings were never the problem.
 *
 * So the fine cell is what a statewide board can afford, and the honest ceiling
 * on this engine — with no terrain level of detail, which is the piece that does
 * not exist yet — is around 300 m / x16, or 0.18M points. That is **11.6x finer
 * than the state pack's 3,473 m cell** and 6.7x coarser than San Francisco's own
 * 45 m board.
 *
 * Closing that last gap is terrain LOD, and `TODO.md` already names its three
 * blockers: placement drift (everything is placed once against `groundAt`), the
 * shadow caster (15x triangles if selected independently), and determinism (the
 * budget harness assumes geometry is not a function of camera path). None of
 * them is in this file's way, and all of them are in that one's.
 */
export const UNIFIED_FINE_METRES = 300;
export const UNIFIED_COARSE_FACTOR = 16;

/** Is this point inside the rectangle? */
function within(lat: number, lng: number, r: FocusRegion): boolean {
  return lat >= r.minLat && lat <= r.maxLat && lng >= r.minLng && lng <= r.maxLng;
}

/**
 * The average of a polygon's vertices — enough to say which metro owns it.
 *
 * `LatLng` is a `[number, number]` tuple in this codebase and not an object, so
 * the destructuring below is the whole of the type story.
 */
function centroid(polygon: readonly LatLng[]): { lat: number; lng: number } {
  let lat = 0;
  let lng = 0;
  for (const [pLat, pLng] of polygon) {
    lat += pLat;
    lng += pLng;
  }
  const n = Math.max(1, polygon.length);
  return { lat: lat / n, lng: lng / n };
}

export interface UnifyReport {
  hills: { state: number; dropped: number; metro: number; total: number };
  districts: { state: number; dropped: number; metro: number; total: number };
  focusRegions: number;
  cellLat: number;
  coarseFactor: number;
}

let cached: { city: City; report: UnifyReport } | null = null;

/**
 * The one board. Memoised, because `World` builds one per mount and the merge
 * walks every district polygon in the product.
 */
/**
 * The detailed packs this board was built from.
 *
 * Exported because a board that spans more than one served region has to ask
 * the live feeds for each of them — `regionOf` on the merged board's own bounds
 * is a 402-nautical-mile circle centred on the middle of the state, and the API
 * refuses it. `main.ts` derives one sky region per entry here instead. Reading
 * it from the merge rather than hard-coding two imports is what keeps a third
 * metro from being a change in two files.
 */
export const UNIFIED_SOURCES: readonly City[] = [SAN_FRANCISCO, SOCAL];

export function unifiedCalifornia(): { city: City; report: UnifyReport } {
  if (cached !== null) return cached;

  const state = reconciledCity(CALIFORNIA);
  const metros = [reconciledCity(SAN_FRANCISCO), reconciledCity(SOCAL)];

  /*
   * A metro's *bounds* rather than its focus regions decide what the state stops
   * drawing, and the two are very different rectangles: San Francisco's focus
   * region is 9.2% of its own board and its bounds are all of it. The state's
   * coarse stand-in for a city covers the whole metro, so the whole metro is
   * what has to be cleared — clearing only the focus region would leave a coarse
   * Oakland beside a fine San Francisco.
   */
  const claimed: FocusRegion[] = metros.map((m) => ({ ...m.bounds }));
  const claims = (lat: number, lng: number): boolean =>
    claimed.some((r) => within(lat, lng, r));

  const stateHills = state.hills.filter((h: Hill) => !claims(h.lat, h.lng));
  const metroHills = metros.flatMap((m) => m.hills);

  const stateDistricts = state.districts.filter((d: District) => {
    const c = centroid(d.polygon);
    return !claims(c.lat, c.lng);
  });
  /*
   * Marked `detail`, which is the flag `createBlocks` orders on and `scene.ts`
   * reveals by camera stand-off. Without it the merged board measured 1,157,802
   * triangles at the whole-board pose against a 440,000 cap, for buildings that
   * are sub-pixel at 1,919 m to the unit. The lots exist either way; the flag
   * decides when they are drawn.
   */
  const metroDistricts: District[] = metros.flatMap((m) =>
    m.districts.map((d) => ({ ...d, detail: true as const })),
  );

  /*
   * ---- Chapters: the state's, and both metros' -----------------------------
   *
   * **Tried, photographed, reverted — and now restored, because the thing that
   * was wrong with it was fixed one commit later.**
   *
   * The revert was recorded here at `64618cb` and its reason was specific: at
   * FiDi's stand-off the frame filled with *state-scale content*, because
   * `createFreewayWorld` draws the corridor as a deliberate 4.7 km-wide atlas
   * glyph and a 4.7 km ribbon lying across a 12 km peninsula is a black slab.
   * That was a true report of the board as it stood.
   *
   * `23e4bb8` — the next commit — added `overviewLayers` to `engine/scene.ts`,
   * which **withdraws the corridor glyph** once the camera comes inside
   * `DETAIL_STANDOFF_M`, for exactly this reason and naming exactly this
   * photograph. The blocker was removed and nobody came back for the rungs it
   * was blocking, so the board kept six chapters and both metros stayed
   * unreachable on the one board built to hold them.
   *
   * What the revert note also said is still true and is *not* a blocker: the
   * terrain under a metro here is 300 m cells against that metro's own 45 m.
   * That is a quality gap, it is named in `UNIFIED_FINE_METRES`, and its fix is
   * terrain level of detail. A rung that arrives at coarse ground is a board
   * that needs finer ground; it is not a reason to have no rung.
   *
   * ## The conversion
   *
   * `focus.lat/lng` is absolute and carries over untouched. The other two are
   * lengths in the owning board's scene units and have to be restated in this
   * one's:
   *
   *   - **`distance` is horizontal**, so it keeps its true metres:
   *     `d x (mpu_src / mpu_here)`, and `mpu` is `111320 / latScale`, so the
   *     whole factor is `latScale_here / latScale_src`.
   *   - **`height` is vertical, and vertical is exaggerated per pack.** It has
   *     already been through the source board's `verticalExaggeration`, and
   *     this board draws its relief at a different one — 15x against San
   *     Francisco's 3.6. Converting on true metres alone would put the camera
   *     at the right altitude over the wrong-looking hills and flatten every
   *     authored angle. It carries the exaggeration ratio too, which keeps the
   *     camera at the same height *relative to the terrain it is looking at* —
   *     which is what makes the framing survive the move.
   *
   * Each metro's own whole-board chapter is dropped: `california-overview`
   * already is the whole board, and two rungs that frame the same thing at the
   * same range is a ladder with a repeated step.
   */
  const mergedChapters: City["chapters"] = [
    ...state.chapters,
    ...metros.flatMap((m) => {
      const horizontal = state.latScale / m.latScale;
      const vertical = horizontal * (state.verticalExaggeration / m.verticalExaggeration);
      return m.chapters
        .filter((chapter) => chapter.id !== "all")
        .map((chapter) => ({
          ...chapter,
          focus: {
            ...chapter.focus,
            distance: chapter.focus.distance * horizontal,
            height: chapter.focus.height * vertical,
          },
        }));
    }),
  ];

  const fine = UNIFIED_FINE_METRES / 111_320;

  const city: City = {
    ...state,
    /*
     * The state's own id, on purpose. Every consumer that keys off a board — the
     * fire gate, the ladder's region table, the budget harness's `data-board`
     * assertion, `?city=` — already knows "california", and this *is* California.
     * A new id would mean teaching all of them about a fourth board, which is
     * the opposite of the point.
     */
    id: state.id,
    name: state.name,
    cellLat: fine,
    // The same lat:lng cell ratio the state pack authored, so cells stay square
    // on the ground at this latitude rather than becoming letterboxes.
    cellLng: fine * (CALIFORNIA.cellLng / CALIFORNIA.cellLat),
    coarseFactor: UNIFIED_COARSE_FACTOR,
    focusRegions: metros.flatMap((m) => m.focusRegions ?? []),
    hills: [...stateHills, ...metroHills],
    districts: [...stateDistricts, ...metroDistricts],
    landmarks: [
      ...state.landmarks,
      ...metros.flatMap((m) => m.landmarks.map((l) => ({ ...l, detail: true as const }))),
    ],
    bridges: [...state.bridges, ...metros.flatMap((m) => m.bridges)],
    parks: [...state.parks, ...metros.flatMap((m) => m.parks)],
    inlandWater: [...state.inlandWater, ...metros.flatMap((m) => m.inlandWater)],
    airports: [...(state.airports ?? []), ...metros.flatMap((m) => m.airports ?? [])],
    ports: [...(state.ports ?? []), ...metros.flatMap((m) => m.ports ?? [])],
    chapters: mergedChapters,
    /*
     * The exaggeration to arrive at when the camera comes down into a metro,
     * taken from the metros themselves rather than chosen.
     *
     * `state.verticalExaggeration` is 15.00 and it is correctly derived — the
     * blended peak is Whitney's 5,132 m and 15x is what makes it fill the frame
     * from 1,551 km. It is also what puts the camera inside Twin Peaks at the
     * FiDi rung, because San Francisco's own board draws that hill at 5.78x and
     * this one draws it 2.6x taller.
     *
     * So the near end of the ramp is **the largest exaggeration any merged metro
     * asked for**, which is San Francisco's. The largest rather than the mean or
     * the smallest: it is the most dramatic relief either metro chose for
     * itself, both metros are known to read well at their own figure, and
     * picking the smaller would flatten the Bay to suit a basin four hundred
     * kilometres away. Derived from the packs, so a third metro moves it
     * without anybody editing a constant.
     */
    nearVerticalExaggeration: Math.max(...metros.map((m) => m.verticalExaggeration)),
    /*
     * Landmasses are NOT unioned. `isLand` is a point-in-any-polygon test, so a
     * metro's finer coastline can only ever add land the state's outline already
     * claimed — while costing a polygon test per sample over a 1.5M-point
     * lattice. The state's coastline is the state's coastline.
     */
    landmasses: state.landmasses,
    reconciled: true,
  };

  const report: UnifyReport = {
    hills: {
      state: state.hills.length,
      dropped: state.hills.length - stateHills.length,
      metro: metroHills.length,
      total: city.hills.length,
    },
    districts: {
      state: state.districts.length,
      dropped: state.districts.length - stateDistricts.length,
      metro: metroDistricts.length,
      total: city.districts.length,
    },
    focusRegions: city.focusRegions?.length ?? 0,
    cellLat: fine,
    coarseFactor: UNIFIED_COARSE_FACTOR,
  };

  cached = { city, report };
  return cached;
}
