/**
 * Lot size follows the camera.
 *
 * `DETAIL_LOT_TIERS` used to be one number, 160 m, chosen as a compromise for a
 * board that was looked at from one distance. The merged board is not: at 2.5 km
 * over San Francisco it drew 1,987 buildings with 184,000 triangles of the
 * budget unspent, and at 7.7 km over Los Angeles it drew 40,000 and measured
 * 621,866 triangles against a 400,000 cap. Both are the same fault, and the fix
 * is that a detail district is built at several lot sizes and the board picks
 * the finest one whose *visible* lots fit a budget.
 *
 * Four things hold that honest, and none of them is a number out of the merged
 * pack — the board is synthetic for the reason `seaAndTerrain.test.ts` gives:
 *
 *   - the ladder is a ladder, so a smaller budget never draws more;
 *   - the budget is obeyed, and where it cannot be — the coarsest rung is over
 *     it — the mesh still has room, which is the allocation's whole claim;
 *   - **the buildings actually get smaller**, which is the product claim and
 *     the one a count alone does not make: a board that drew the same lots and
 *     merely hid some of them would pass the first two;
 *   - it is still one mesh, one geometry and one material, because the board it
 *     runs on has twenty-two spare draw calls out of four hundred and sixty and
 *     a second instance set would spend one of them.
 */

import assert from "node:assert/strict";
import test from "node:test";
import * as THREE from "three";

import { setReconcile } from "../../cities/reconcile.ts";
import {
  createBlocks,
  detailLotMetres,
  LOT_BUDGET,
  NEIGHBOURHOOD_LOT_METRES,
  storedLotCount,
  updateBlocksDetail,
} from "../../engine/blocks.ts";
import type { City, District } from "../../engine/types.ts";
import { World } from "../../engine/world.ts";

setReconcile(false);

/** A square district, `half` degrees to a side, centred on (lat, lng). */
function district(id: string, lat: number, lng: number, half: number, detail?: true): District {
  return {
    id,
    name: id,
    polygon: [
      [lat - half, lng - half],
      [lat + half, lng - half],
      [lat + half, lng + half],
      [lat - half, lng + half],
    ],
    gridAngle: 0,
    minHeight: 20,
    maxHeight: 120,
    towerChance: 0.05,
    palette: "downtown",
    ...(detail === true ? { detail } : {}),
  };
}

/**
 * One base district and two metro ones of deliberately different sizes.
 *
 * The size difference is the fixture's whole point, because it is the merged
 * board's own shape: `TIER_DISTRICT_LOTS` refuses a fine rung to a district
 * that would be enormous at it, so `downtown` — two kilometres across, like the
 * Financial District — gets the whole ladder and `sprawl` gets only its coarse
 * end, exactly as West Covina does. A fixture with one district size would
 * never exercise the coarse-fill that lets the two be drawn in the same frame.
 */
const CITY: City = {
  id: "ladder-board",
  name: "Ladder Board",
  center: { lat: 37, lng: -122 },
  bounds: { minLat: 36, maxLat: 38, minLng: -123, maxLng: -121 },
  latScale: 100,
  verticalExaggeration: 2,
  cellLat: 0.05,
  cellLng: 0.05,
  coastFalloff: 0.02,
  landmasses: [
    [
      [36.1, -122.9],
      [37.9, -122.9],
      [37.9, -121.1],
      [36.1, -121.1],
    ],
  ],
  parks: [],
  inlandWater: [],
  districts: [
    district("statewide", 37, -122, 0.08),
    district("downtown", 37.4, -122.4, 0.012, true),
    district("sprawl", 37.5, -121.6, 0.05, true),
  ],
  landmarks: [],
  bridges: [],
  roads: [],
  chapters: [],
  hills: [{ name: "swell", lat: 37, lng: -122, elevation: 200, radius: 0.5 }],
};

async function built(): Promise<World> {
  const world = new World(CITY);
  assert.equal(await world.ready(), true, "the synthetic board failed to build a heightfield");
  return world;
}

/** The smallest building footprint currently packed, in scene units. */
function smallestFootprint(mesh: THREE.InstancedMesh): number {
  const m = mesh.instanceMatrix.array as Float32Array;
  let smallest = Infinity;
  for (let i = 0; i < mesh.count; i++) {
    const at = i * 16;
    const width = Math.hypot(m[at]!, m[at + 1]!, m[at + 2]!);
    if (width < smallest) smallest = width;
  }
  return smallest;
}

/** Everything in reach, no frustum, at one lot budget. */
function packAt(world: World, mesh: THREE.InstancedMesh, budget: number): number {
  const [x, z] = world.project(37, -122);
  updateBlocksDetail(mesh, x, z, 1_000, undefined, 0, budget);
  return mesh.count;
}

test("a smaller lot budget never draws more of the city", async () => {
  const world = await built();
  const blocks = createBlocks(world);
  const capacity = (blocks.instanceMatrix.array.length / 16) | 0;

  /*
   * A sweep rather than two points, because the claim is that this is a ladder
   * and not a switch: several rungs have to be reachable, in order, or the
   * mechanism is a boolean with extra steps.
   */
  const budgets = [LOT_BUDGET, 6_000, 4_000, 3_000, 2_000, 100];
  const counts = budgets.map((b) => packAt(world, blocks, b));

  for (let i = 1; i < counts.length; i += 1) {
    assert.ok(
      counts[i]! <= counts[i - 1]!,
      `budget ${budgets[i]} drew ${counts[i]} against ${counts[i - 1]} at ${budgets[i - 1]}`,
    );
  }
  assert.ok(
    new Set(counts).size >= 3,
    `the ladder has one rung in practice: ${[...new Set(counts)].join(", ")}`,
  );
  assert.ok(
    counts[0]! > counts[counts.length - 1]! * 2,
    `the ladder spans nothing: ${counts[0]} at the top and ${counts[counts.length - 1]} at the bottom`,
  );
  for (const count of counts) {
    assert.ok(count <= capacity, `${count} instances is past the end of a ${capacity} buffer`);
  }
});

test("the budget is a budget, and the mesh has room when it cannot be met", async () => {
  const world = await built();
  const blocks = createBlocks(world);
  const capacity = (blocks.instanceMatrix.array.length / 16) | 0;

  /*
   * Above the coarsest rung's own total there is nothing left to give, so the
   * board draws it and goes over — which is legal and is why the capacity is
   * `max(budget, the coarsest rung)` rather than the budget. What is *not*
   * legal is a rung that fits being drawn past the budget.
   */
  const floor = packAt(world, blocks, 1);
  for (const budget of [2_000, 4_000, 8_000, LOT_BUDGET]) {
    const count = packAt(world, blocks, budget);
    assert.ok(
      count <= Math.max(budget, floor),
      `budget ${budget} packed ${count}, past both it and the ${floor}-lot floor`,
    );
    assert.ok(count <= capacity, `${count} instances is past the end of a ${capacity} buffer`);
  }
});

test("the buildings themselves get smaller, which is the whole claim", async () => {
  const world = await built();
  const blocks = createBlocks(world);

  packAt(world, blocks, 100);
  const coarse = smallestFootprint(blocks);
  packAt(world, blocks, LOT_BUDGET);
  const fine = smallestFootprint(blocks);

  /*
   * A count can fall for two reasons and only one of them is this one: a board
   * that kept 160 m lots and merely stopped drawing some of them would pass
   * every assertion above. So read the instance matrices and measure a
   * building. The rungs are at least 1.25x apart in lot and the ladder spans
   * 40 m to 400 m, so half is a wide margin around a real effect.
   */
  assert.ok(
    fine < coarse * 0.5,
    `the smallest building is ${fine.toFixed(4)} units at a full budget and ${coarse.toFixed(4)} at none`,
  );
  assert.equal(detailLotMetres(0) < detailLotMetres(5), true, "the ladder is not finest-first");
});

test("every rung is drawn from the same mesh, geometry and material", async () => {
  const world = await built();
  const blocks = createBlocks(world);
  const geometry = blocks.geometry;
  const material = blocks.material;

  /*
   * The board this ships on has 438 draw calls against a 460 cap on the desktop
   * profile and 434 against 455 on mobile. `mesh.count` is the level of detail
   * precisely because a second `InstancedMesh` — a fine tier and a coarse tier
   * cross-fading, say — would be non-empty at the same time as the first and
   * cost one of the twenty-two that are left. So: the ladder may move `count`
   * and rewrite the attribute buffers, and may not acquire an object.
   */
  for (const budget of [LOT_BUDGET, 3_000, 100, LOT_BUDGET]) {
    packAt(world, blocks, budget);
    assert.equal(blocks.geometry, geometry, "a rung swapped the geometry out");
    assert.equal(blocks.material, material, "a rung swapped the material out");
    assert.equal(blocks.boundingSphere, null, "a stale sphere would hide the city it was not built at");
  }
});

/**
 * The largest building footprint currently packed, in scene units.
 *
 * Same extract as `smallestFootprint`: the first three entries of each 4×4
 * instance matrix are the X-axis of the building, whose length is its width.
 * Depth is the Z-axis at offsets 8, 9, 10.
 */
function largestFootprintM(mesh: THREE.InstancedMesh, metresPerUnit: number): number {
  const m = mesh.instanceMatrix.array as Float32Array;
  let largest = 0;
  for (let i = 0; i < mesh.count; i++) {
    const at = i * 16;
    const width = Math.hypot(m[at]!, m[at + 1]!, m[at + 2]!);
    const depth = Math.hypot(m[at + 8]!, m[at + 9]!, m[at + 10]!);
    const span = Math.max(width, depth) * metresPerUnit;
    if (span > largest) largest = span;
  }
  return largest;
}

test("a coarse lot is a parcel, not a 700 m building", async () => {
  const world = await built();
  const blocks = createBlocks(world);
  /*
   * Budget 100 forces the coarsest rung — 400 m lots on this fixture, the
   * same rung the merged board picked over FiDi when the 700 m slab was
   * photographed. Without the cap, tower fill of 1.5–2.2 on a 400 m lot is
   * a 600–880 m cube. With it, nothing on the board is wider than a block.
   */
  packAt(world, blocks, 100);
  assert.ok(blocks.count > 0, "the coarsest rung packed nothing");
  const widest = largestFootprintM(blocks, world.metresPerUnit);
  assert.ok(
    widest <= NEIGHBOURHOOD_LOT_METRES + 1,
    `a ${widest.toFixed(0)} m building on a coarse lot is the FiDi slab`,
  );
});

test("fine rungs wait for a pose that would pick them", async () => {
  const world = await built();
  const blocks = createBlocks(world);
  const atBoot = storedLotCount(blocks);
  packAt(world, blocks, 100);
  assert.equal(
    storedLotCount(blocks),
    atBoot,
    "a coarse pose walked a fine rung the camera never asked for",
  );
  packAt(world, blocks, LOT_BUDGET);
  const afterFine = storedLotCount(blocks);
  assert.ok(
    afterFine > atBoot,
    `a full-budget pose did not walk a fine rung: store stayed at ${atBoot}`,
  );
  packAt(world, blocks, LOT_BUDGET);
  assert.equal(
    storedLotCount(blocks),
    afterFine,
    "the second close pose walked the same rung again",
  );
});
