/**
 * 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 ?? {}) };
}

/**
 * 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(new THREE.Color(pal.parkHigh), Math.min(1, elevation / 180));
  }
  if (elevation < 3) {
    return scratch.setHex(pal.sand).lerp(new THREE.Color(pal.flats), elevation / 3);
  }
  return scratch
    .setHex(pal.flats)
    .lerp(new THREE.Color(pal.upland), Math.min(1, (elevation - 3) / 150));
}

/** 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;
  mesh.name = "shorePlates";
  return mesh;
}

/**
 * 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);
  const { latSteps, lngSteps, lats, lngs, height, land, 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;
  };

  for (let i = 0; i < latSteps; i++) {
    for (let j = 0; j < lngSteps; j++) {
      const a = i * (lngSteps + 1) + j;
      const b = a + 1;
      const c = a + (lngSteps + 1);
      const d = c + 1;
      if (!land[a] || !land[b] || !land[c] || !land[d]) continue;
      indices.push(vertex(i, j), vertex(i + 1, j), vertex(i, j + 1));
      indices.push(vertex(i, j + 1), vertex(i + 1, j), vertex(i + 1, j + 1));
    }
  }

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

  const mesh = new THREE.Mesh(
    geo,
    new THREE.MeshLambertMaterial({ vertexColors: true, side: THREE.DoubleSide }),
  );
  mesh.receiveShadow = true;
  mesh.name = "terrain";
  return mesh;
}

/** 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 sea = new THREE.Mesh(
    new THREE.PlaneGeometry(Math.abs(x1 - x0) * 1.8, Math.abs(z1 - z0) * 1.8),
    new THREE.MeshLambertMaterial({ color: pal.sea }),
  );
  sea.rotation.x = -Math.PI / 2;
  sea.position.set((x0 + x1) / 2, -0.06, (z0 + z1) / 2);
  sea.receiveShadow = true;
  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);
    const lake = new THREE.Mesh(
      geo,
      new THREE.MeshLambertMaterial({ color: pal.lake, side: THREE.DoubleSide }),
    );
    lake.position.y = 0.05;
    group.add(lake);
  }

  return group;
}
