/**
 * The city is packed by district, and now by frustum as well.
 *
 * `createBlocks` returns one `InstancedMesh` for every anonymous building on the
 * board, and three culls per *object* — so that mesh's bounding sphere contains
 * California and nothing about it is ever rejected at any pose. Standing 2.5 km
 * over Los Angeles the merged board packed 48,081 detail lots and 2,839 base
 * lots, 509,200 triangles against a 400,000 cap, two thirds of them behind the
 * camera or off the sides. No budget cell stands there, so nothing had ever
 * measured it.
 *
 * `updateBlocksDetail` takes a frustum for that, and the four facts below are
 * what hold it honest: that a board which passes no frustum is untouched, that
 * one which does packs strictly less, that the pad is a pad, and that neither
 * can ever pack more instances than the mesh has room for.
 *
 * `blocksLotLadder.test.ts` is the other half and came later: the frustum
 * decides *which* districts are packed and the lot budget decides how finely
 * each of them is. Everything here leaves the budget at its default, so what is
 * measured below is the frustum on its own.
 *
 * The board is synthetic for the reason `seaAndTerrain.test.ts` gives: none of
 * this is about California, and a real pack would couple this to a coastline.
 */

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

import { setReconcile } from "../../cities/reconcile.ts";
import { createBlocks, 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 } : {}),
  };
}

const CITY: City = {
  id: "cull-board",
  name: "Cull 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: [],
  // One base district and two metros, far enough apart that a camera over one
  // cannot see the other — which is the whole case being measured.
  districts: [
    district("statewide", 37, -122, 0.08),
    district("north", 37.7, -122.6, 0.06, true),
    district("south", 36.3, -121.4, 0.06, 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;
}

/** A camera looking straight down at (lat, lng) from `units` above it. */
function looking(world: World, lat: number, lng: number, units: number): THREE.Frustum {
  const [x, z] = world.project(lat, lng);
  const camera = new THREE.PerspectiveCamera(42, 1, 0.01, units * 10);
  camera.position.set(x, world.groundAt(lat, lng) + units, z);
  camera.lookAt(new THREE.Vector3(x, 0, z));
  camera.updateMatrixWorld(true);
  camera.updateProjectionMatrix();
  return new THREE.Frustum().setFromProjectionMatrix(
    new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse),
  );
}

const REACH = 40; // scene units; both metros are inside this of their own centre

test("without a frustum the packing is exactly what it always was", async () => {
  const world = await built();
  const blocks = createBlocks(world);
  /*
   * Capacity is no longer "every lot that exists": only one rung of each
   * district is drawn, and fine rungs are not walked at boot, so the mesh is
   * allocated against the lot budget (or the coarsest rung if that is larger).
   * On this fixture the detail districts are far too large for a rung finer
   * than the reference, so a full-reach pack draws the reference and leaves
   * room in the buffer.
   */
  const capacity = (blocks.instanceMatrix.array.length / 16) | 0;
  assert.ok(capacity > 200, `the fixture is too small to be a test: ${capacity} lots`);

  /*
   * **Born packed at zero**, which is the one thing that changed about the
   * returned mesh: the base lots stopped being a fixed prefix, so `count`
   * cannot start at `baseCount` any more. It still starts strictly below
   * capacity, which is the only property that line ever needed.
   */
  assert.equal(blocks.count, 0, "the mesh must not draw a district nobody has selected");

  // Out of reach of either metro: the base district and nothing else.
  const [x, z] = world.project(37, -122);
  updateBlocksDetail(blocks, x, z, 0);
  const base = blocks.count;
  assert.ok(base > 0, "the base district vanished");

  // In reach of both: every lot on the board, in one contiguous run — at the
  // finest rung it has, which on this fixture is the reference.
  updateBlocksDetail(blocks, x, z, 1_000);
  assert.ok(blocks.count > base, "the fixture has no detail lots to cull");
  assert.ok(
    blocks.count <= capacity,
    `reach packed ${blocks.count} past a ${capacity}-lot buffer`,
  );
});

test("a frustum packs strictly less, and never more than there is room for", async () => {
  const world = await built();
  const blocks = createBlocks(world);
  const capacity = (blocks.instanceMatrix.array.length / 16) | 0;
  const [nx, nz] = world.project(37.7, -122.6);

  updateBlocksDetail(blocks, nx, nz, REACH);
  const everything = blocks.count;

  updateBlocksDetail(blocks, nx, nz, REACH, looking(world, 37.7, -122.6, 6));
  const culled = blocks.count;

  assert.ok(
    culled < everything,
    `the frustum removed nothing: ${culled} against ${everything} in reach`,
  );
  assert.ok(culled > 0, "the district under the camera was culled away as well");
  assert.ok(culled <= capacity, `${culled} instances is past the end of the buffer`);

  /*
   * `InstancedMesh.boundingSphere` is computed once, lazily, over whatever
   * `count` held at the time and never again — so a sphere computed while the
   * camera stood over one metro would hide the other for the rest of the
   * session. `updateBlocksDetail` nulls it on every repack; this is that.
   */
  assert.equal(blocks.boundingSphere, null, "a stale sphere would hide the city it was not built at");
});

test("the pad keeps a district that is just off the edge of frame", async () => {
  const world = await built();
  const blocks = createBlocks(world);
  const [nx, nz] = world.project(37.7, -122.6);
  const view = looking(world, 37.7, -122.6, 6);

  updateBlocksDetail(blocks, nx, nz, REACH, view, 0);
  const bare = blocks.count;
  /*
   * The pad is a fraction of the stand-off and exists to cover one frame of
   * lag: `scene.ts` runs the level of detail before `kit.tick`, so the frustum
   * is last frame's. Handed a large enough stand-off it must admit a district
   * the bare frustum rejected — which is the mechanism, stated as the only
   * thing about it that can be asserted without pinning the constant.
   */
  updateBlocksDetail(blocks, nx, nz, REACH, view, 5_000);
  assert.ok(
    blocks.count > bare,
    `the pad admitted nothing: ${blocks.count} against ${bare} with no pad`,
  );
});

test("a board with no detail districts is never repacked at all", async () => {
  const plain = new World({ ...CITY, districts: [district("statewide", 37, -122, 0.08)] });
  assert.equal(await plain.ready(), true);
  const blocks = createBlocks(plain);
  const drawn = blocks.count;
  assert.ok(drawn > 0, "the plain board built nothing");
  assert.equal(blocks.userData.detail, undefined, "a board with no metros must carry no store");

  // Every argument, including a frustum that contains nothing: still a no-op.
  updateBlocksDetail(blocks, 0, 0, 0, looking(plain, 36.05, -122.95, 0.2), 1);
  assert.equal(blocks.count, drawn, "a board with no detail districts must not move");
});

/**
 * A district on high ground is culled by where it *is*, not by where its
 * footprint would be at sea level.
 *
 * This is a regression test for a real defect, and it is written as a separate
 * board because the fixture above cannot express it: that board exaggerates by
 * 2 and its tallest hill is 200 m, so every district's height above the ground
 * plane is a rounding error next to its own plan radius and a cull volume
 * pinned at y=0 contains it by accident.
 *
 * The merged California board exaggerates by 15. A lot's local y is the terrain
 * under it times that, so a district in the San Gabriel foothills stands
 * kilometres of scene height above y=0 while its plan radius stays a few units.
 * Tested against a sphere centred on the ground plane, **39 of the merged
 * board's 97 district ranges had lots outside their own cull volume** — the
 * worst by 4.78 units, 9.2 km — and the picture that produces is hillside
 * buildings vanishing while they are on screen.
 *
 * The board below reproduces that geometry rather than that city: one small
 * detail district on a 3,000 m peak, exaggerated 15x, so its lots sit about
 * forty units up on a footprint about three units across. A camera at the
 * summit looking at the summit contains every one of those lots and misses a
 * y=0 sphere entirely.
 */
const PEAK_CITY: City = {
  ...CITY,
  id: "peak-board",
  verticalExaggeration: 15,
  districts: [district("summit", 37, -122, 0.02, true)],
  hills: [{ name: "peak", lat: 37, lng: -122, elevation: 3_000, radius: 0.25 }],
};

test("a district on high ground survives a frustum that contains it", async () => {
  const world = new World(PEAK_CITY);
  assert.equal(await world.ready(), true, "the peak board failed to build a heightfield");
  const blocks = createBlocks(world);
  const store = blocks.userData.detail as { ranges: { r: number; y0: number; y1: number }[] };
  assert.ok(store !== undefined && store.ranges.length > 0, "the peak board built no district");

  /*
   * The fixture is only a test while this holds. If a pack edit ever flattens
   * this board, the assertion below would pass against a y=0 volume too and
   * would quietly stop testing anything.
   */
  const range = store.ranges[0]!;
  assert.ok(
    range.y0 > range.r,
    `the fixture is not off the ground plane: y0 ${range.y0.toFixed(2)} against r ${range.r.toFixed(2)}`,
  );

  /*
   * Aimed at the summit, and reaching only as far as the summit.
   *
   * `looking()` above aims every camera at `y = 0` with a far plane ten times
   * the stand-off, which is right for that board and useless here: it puts the
   * ground plane in the middle of the frustum, so a cull volume pinned to the
   * ground plane is contained whatever the terrain does and the defect cannot
   * be expressed. This camera hangs eight units over the peak, looks at the
   * peak, and stops twelve units short — so the lots at y 36.09-39.28 are in
   * frame and the plane at y = 0, thirty-seven units below them, is not.
   */
  const [x, z] = world.project(37, -122);
  const summitY = world.groundAt(37, -122);
  const camera = new THREE.PerspectiveCamera(42, 1, 0.01, 12);
  camera.position.set(x, summitY + 8, z);
  camera.lookAt(new THREE.Vector3(x, summitY, z));
  camera.updateMatrixWorld(true);
  camera.updateProjectionMatrix();
  const view = new THREE.Frustum().setFromProjectionMatrix(
    new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse),
  );

  updateBlocksDetail(blocks, x, z, REACH, view, 0);
  assert.ok(
    blocks.count > 0,
    "a district standing on a peak was culled by a frustum aimed straight at it",
  );
});
