/**
 * Pins on the map.
 *
 * This module is deliberately ignorant. It renders `Marker[]` and looks colours
 * up by `colorKey` in a palette the caller supplies. It does not know that
 * markers are usually companies, and it will never learn that "rejected" is
 * red — that mapping belongs to the adapter in the consuming app, which is what
 * lets one engine serve a private career map and a public sector map without
 * either being a fork. See ARCHITECTURE.md §3.3.
 */

import * as THREE from "three";
import { createBuildingGlyph, type BuildingGlyphHandle } from "./buildingGlyph.ts";
import type { Marker, MarkerPalette } from "./types.ts";
import type { World } from "./world.ts";

const FALLBACK_COLOR = 0x9aa4ad;

export interface MarkerLayer {
  group: THREE.Group;
  /** Raycast targets, for hover and click. */
  pickables: THREE.Object3D[];
  /** Scene-space head position per marker id, for the HTML label layer. */
  anchors: Map<string, THREE.Vector3>;
  setMarkers(markers: Marker[]): void;
  dispose(): void;
}

/**
 * How high above the roofline a pin floats, in scene units. Enough to clear
 * a mid-rise; towers will still occlude, which is honest — a pin behind the
 * skyline should look like it is behind the skyline.
 */
/**
 * A pin, in **true metres** — because the boards are no longer one scale.
 *
 * These were 1.6 and 1.1 *scene units*, and on the board they were authored for
 * that is 151 m of lift and a 104 m pin: tall, deliberately, because a pin has
 * to clear a mid-rise. San Francisco is 94.3 m to the unit and every number in
 * this file was chosen against that.
 *
 * `cities/unify.ts` put San Francisco's districts on a board that is **1,919 m
 * to the unit**, and `main.ts` draws markers on any board carrying metro detail,
 * so the same constants became a 3.07 km lift under a 2.1 km pin with a 614 m
 * sphere on top of it — twenty times over, sitting on downtown like a moon.
 * Photographed at a 2.5 km stand-off it is the most prominent object in the
 * frame by an order of magnitude.
 *
 * This is the third time this exact failure has been found in this engine, which
 * is the reason for the length of this comment. `structures.ts`'s freeway
 * corridor is a 4.7 km ribbon for the same reason and `23e4bb8` had to withdraw
 * it; `engine/aircraftGeometry.ts` was given "a true-metre size" in the same
 * commit. **A constant in scene units is a constant that assumes one board.**
 * The engine has one board with two decades of scale in it now, and true metres
 * are the only units that mean the same thing on all of it.
 *
 * The metre values are exactly what the old constants measured on the Bay Area
 * board, so that board's appearance does not move by a pixel — and the merged
 * board, at the same true stand-off over the same city, now draws the same pin
 * at the same apparent size, which is the property that was wanted all along.
 */
const PIN_LIFT_M = 151;
const PIN_HEIGHT_M = 104;
const PIN_HEAD_RADIUS_M = 15.1;
const PIN_STEM_RADIUS_M = 4.2;
const GHOST_RADIUS_M = 14.1;

export function createMarkerLayer(world: World, palette: MarkerPalette): MarkerLayer {
  const group = new THREE.Group();
  group.name = "markers";
  const pickables: THREE.Object3D[] = [];
  const anchors = new Map<string, THREE.Vector3>();
  const buildings: BuildingGlyphHandle[] = [];

  // One shared geometry per shape; colour varies per instance material, which
  // is cheap enough at the scale markers live at (hundreds, not tens of
  // thousands — that is what `blocks` is for).
  /*
   * Every dimension below is a true metre put into this board's units. A pin is
   * *lifted* by the exaggeration the same way the ground under it is — it has to
   * clear a skyline that is itself exaggerated — but it is not *stretched* by it,
   * so height uses `world.metres` and the two radii do not.
   */
  const u = (m: number) => m / world.metresPerUnit;
  const PIN_LIFT = world.metres(PIN_LIFT_M);
  const PIN_HEIGHT = world.metres(PIN_HEIGHT_M);
  const stemGeo = new THREE.CylinderGeometry(
    u(PIN_STEM_RADIUS_M),
    u(PIN_STEM_RADIUS_M),
    PIN_HEIGHT,
    6,
  );
  stemGeo.translate(0, PIN_HEIGHT / 2, 0);
  const headGeo = new THREE.SphereGeometry(u(PIN_HEAD_RADIUS_M), 12, 10);
  const ghostGeo = new THREE.OctahedronGeometry(u(GHOST_RADIUS_M));

  const materials = new Map<string, THREE.Material>();
  const materialFor = (key: string, located: boolean): THREE.Material => {
    const id = `${key}:${located ? "solid" : "ghost"}`;
    const existing = materials.get(id);
    if (existing) return existing;
    const color = palette[key] ?? FALLBACK_COLOR;
    const mat = located
      ? new THREE.MeshLambertMaterial({ color, emissive: color, emissiveIntensity: 0.28 })
      : new THREE.MeshLambertMaterial({ color, transparent: true, opacity: 0.42 });
    materials.set(id, mat);
    return mat;
  };

  function clear() {
    for (const building of buildings) building.dispose();
    buildings.length = 0;
    for (const child of [...group.children]) group.remove(child);
    pickables.length = 0;
    anchors.clear();
  }

  function setMarkers(markers: Marker[]) {
    clear();
    for (const m of markers) {
      const located = m.located !== false;
      const [x, z] = world.project(m.lat, m.lng);
      const base = world.groundAt(m.lat, m.lng);

      if (located && m.glyph?.kind === "building") {
        const building = createBuildingGlyph(
          world,
          m.glyph,
          palette[m.colorKey] ?? FALLBACK_COLOR,
        );
        building.group.position.set(x, base, z);
        for (const target of building.pickables) {
          target.userData.marker = m;
          pickables.push(target);
        }
        anchors.set(m.id, new THREE.Vector3(x, base + building.anchorY, z));
        buildings.push(building);
        group.add(building.group);
        continue;
      }

      const pin = new THREE.Group();
      pin.position.set(x, base + PIN_LIFT, z);

      const stem = new THREE.Mesh(stemGeo, materialFor(m.colorKey, located));
      pin.add(stem);

      // Unplaced markers get a different silhouette as well as a different
      // opacity. Colour alone is not enough of a tell, and a map whose premise
      // is that it is real must not quietly invent addresses.
      const head = new THREE.Mesh(
        located ? headGeo : ghostGeo,
        materialFor(m.colorKey, located),
      );
      head.position.y = PIN_HEIGHT;
      head.userData.marker = m;
      pin.add(head);
      pickables.push(head);

      anchors.set(m.id, new THREE.Vector3(x, base + PIN_LIFT + PIN_HEIGHT, z));
      group.add(pin);
    }
  }

  return {
    group,
    pickables,
    anchors,
    setMarkers,
    dispose() {
      clear();
      stemGeo.dispose();
      headGeo.dispose();
      ghostGeo.dispose();
      for (const m of materials.values()) m.dispose();
      materials.clear();
    },
  };
}
