/**
 * The contract between the engine and everything else.
 *
 * The engine renders a `City` and a list of `Marker`s. It does not know what a
 * marker *is* — not that markers are companies, not that a red one means a
 * rejection. That mapping lives in an adapter, outside this package, which is
 * what lets one renderer serve a private career map, a public sector map, and
 * whatever anyone else builds, without any of them being a fork.
 *
 * See ARCHITECTURE.md §3.3.
 */

/** `[latitude, longitude]`, always in that order. */
export type LatLng = [number, number];

// ---- Geography ------------------------------------------------------------

/**
 * A hill, as a radial peak summed into the heightfield.
 *
 * `elevation` is metres above sea level at the summit. `radius` is roughly
 * where the hill meets the flats, in degrees of latitude.
 */
export interface Hill {
  name: string;
  lat: number;
  lng: number;
  elevation: number;
  radius: number;
}

/**
 * Where buildings go, how tall, and on what street grid.
 *
 * `gridAngle` is the district's street bearing in radians. It is per-district
 * rather than per-city because that is the fact on the ground in San Francisco:
 * the grid north of Market and the grid south of it are 46° out of true, and
 * reproducing that is most of what makes the city recognisable from above.
 */
export interface District {
  id: string;
  name: string;
  polygon: LatLng[];
  /** Street bearing, radians clockwise from true north. */
  gridAngle: number;
  minHeight: number;
  maxHeight: number;
  /** Chance a given lot gets a tower rather than a low-rise. */
  towerChance: number;
  /** Facade palette key; see `blocks.ts`. */
  palette: "downtown" | "residential" | "industrial";
  /** Fraction of lots that get built on at all. Defaults to 0.88. */
  coverage?: number;
  /**
   * Metro detail on a merged board: drawn only when the camera is close enough
   * to resolve it.
   *
   * Absent on every hand-authored pack and set only by `cities/unify.ts`, which
   * folds two metros into the state board. One California carries 107 districts
   * against the state pack's 17, and drawn unconditionally that measured
   * 1,157,802 triangles at the whole-board pose against a 440,000 cap — for
   * buildings that are comfortably sub-pixel at 1,919 m to the unit. The flag is
   * what lets `createBlocks` order those lots last, so a single `count` on one
   * `InstancedMesh` is the whole of the level of detail: no second mesh, no
   * second draw call, and nothing to dispose.
   */
  detail?: true;
}

/** A building placed by hand because the eye goes looking for it. */
export interface Landmark {
  name: string;
  lat: number;
  lng: number;
  /** Roof height in metres. */
  height: number;
  /** Half-width in degrees of longitude. */
  footprint: number;
  shape: "box" | "pyramid" | "tower" | "cylinder";
  /** Metro detail on a merged board; see `District.detail`. */
  detail?: true;
  color?: number;
  label?: boolean;
}

export interface Bridge {
  name: string;
  /** Deck centreline. Both ends should run onto land. */
  path: LatLng[];
  towers: LatLng[];
  towerHeight: number;
  deckHeight: number;
  /**
   * How deeply the main cable sags, 0..1. Scaled onto the drop from the
   * saddle to the deck, not read literally as a fraction of tower height —
   * see `bridges.ts`. Read literally it left the Bay Bridge's cable a third of
   * a tower above its own deck at midspan, which is a trestle with a curve in it.
   */
  sag: number;
  color: number;
}

export interface Road {
  path: LatLng[];
  /**
   * Half-width to either side of the centreline, in **scene units** — which is
   * to say, at whatever this board's metres-per-unit happens to be.
   *
   * Both metro packs authored roughly true widths here: 19–32 m on the Bay Area
   * and 31–55 m on Southern California. The state board did not, and could not
   * sensibly — a 40 m freeway is 0.02 units on a board 554 units across — so its
   * two corridors are authored as 1,919 m and 2,034 m **symbols**, which is why
   * they draw as ribbons wider than the cities they join.
   */
  width: number;
  /**
   * What the road really is, in metres, for a pack whose `width` is a symbol.
   *
   * Read by `cities/reconcile.ts` and by nothing else: the engine draws `width`,
   * always. A pack that omits this is taken to have authored true metres in
   * `width` already, which both metro packs did.
   */
  widthM?: number;
  kind: "street" | "freeway";
}

/**
 * A named destination, as the interface knows it: what the legend prints and
 * what `flyTo` is keyed on.
 *
 * Split out of `Chapter` because an office has exactly the same idea — a short
 * list of places you can jump to — but positions them in metres, not in
 * latitude and longitude. Only this half of a chapter is shared; the pose is
 * not. `number` and `description` are optional here and required on `Chapter`,
 * because a city's chapters are a numbered tour with a sentence each and an
 * office's views are usually just "Reception" and "The desk bay".
 */
// ---- Airports -------------------------------------------------------------
//
// Plain data, JSON-serialisable, no functions and no THREE types — a city pack
// is posted to the terrain worker as a structured clone, so an airport that
// acquired a method would stop the whole pack being sendable. Same rule as
// `City` itself; see ARCHITECTURE.md §5.1.
//
// `engine/airports.ts` is the renderer for these and re-exports every name, so
// a pack may import them from either place.

/**
 * One runway, given as its centre, its bearing and its size.
 *
 * **Centre and heading rather than two thresholds**, which is the opposite of
 * how `Bridge` and `Road` are authored, and the reason is that a runway's
 * heading is the fact worth being exact about. Two hand-typed thresholds encode
 * a bearing implicitly and to whatever precision the fifth decimal place of a
 * latitude happens to give you; a stated bearing can be checked against the
 * published one by reading it. `runwayThresholds()` converts, for the callers
 * that want the ends.
 *
 * `heading` is **true**, not magnetic. The two-digit designator painted on a
 * runway is magnetic and rounded to ten degrees, so it is not the number to
 * build geometry from: San Francisco's declination is about 13.5° east, which
 * means "28R" is a runway pointing 298.6° true, and thirteen degrees is the
 * difference between SFO's pattern and a plausible-looking airfield.
 */
export interface Runway {
  /** Both designators, as they are said: `"10L/28R"`. */
  id: string;
  /** Midpoint of the centreline. */
  lat: number;
  lng: number;
  /** TRUE bearing of the low-numbered end's direction, degrees clockwise from north. */
  heading: number;
  /** Threshold to threshold, metres. */
  length: number;
  /** Metres. */
  width: number;
  /** The numbers painted on the two thresholds, low end first. */
  designators?: [string, string];
}

/** A taxiway centreline. Drawn as a flat strip; it does not follow terrain. */
export interface Taxiway {
  id?: string;
  path: LatLng[];
  /** Metres. Defaults to 25, which is a Group V taxiway. */
  width?: number;
}

/** A paved area: a terminal apron, a cargo ramp, a maintenance pad. */
export interface Apron {
  id?: string;
  polygon: LatLng[];
}

/**
 * A terminal mass — a building, not a plan.
 *
 * `gates` is what turns the box into an airport: the kit lines that many parked
 * airliners nose-in along one long face. Aeroplanes on stand are the cheapest
 * thing in this file and the most legible; a terminal without them is a
 * warehouse.
 */
export interface Terminal {
  id?: string;
  lat: number;
  lng: number;
  /** Along `heading`, metres. */
  length: number;
  /** Across `heading`, metres. */
  width: number;
  /** Metres. */
  height: number;
  /** TRUE bearing of the long axis, degrees clockwise from north. */
  heading: number;
  /** Aircraft on stand, and which side of the long axis they park on. */
  gates?: { count: number; side: 1 | -1 };
}

/** The control tower. One per field; it is a landmark, not a category. */
export interface Tower {
  lat: number;
  lng: number;
  /** To the top of the cab, metres. */
  height: number;
}

export interface Airport {
  /** ICAO where there is one — `"KSFO"`. Only ever an id. */
  id: string;
  name: string;
  /** The airport reference point. */
  lat: number;
  lng: number;
  /** Field elevation, metres above sea level. Data, not a placement; see above. */
  elevation: number;
  /**
   * The graded outline — the fill, the fence line, whatever the airport's edge
   * actually is. Optional: an airport with no outline is four bars on the bare
   * terrain, which is right for a strip in a desert and wrong for SFO, whose
   * pale rectangle of bay fill is half of what you recognise.
   */
  field?: LatLng[];
  runways: Runway[];
  taxiways?: Taxiway[];
  aprons?: Apron[];
  terminals?: Terminal[];
  tower?: Tower;
}

// ---- Ports ----------------------------------------------------------------
//
// Plain data, JSON-serialisable, no functions and no THREE types, for exactly
// the reason the airport block above gives: a city pack is posted to the
// terrain worker as a structured clone, and `barrel.test.ts` reads the import
// graph as source. A port record that acquired a method would stop the pack
// being sendable; a port type that lived in `engine/ports.ts` would put three.js
// on the package surface for anyone who wanted to name a berth.
//
// The whole of this block is authored by hand. `ports.sqlite` has a `ports`
// table with seven rows and it is not usable for placement: every row sits on an
// exact arc-minute grid, up to 1,852 m from the water it claims to be, and
// `channel_depth_ft` is a binned WPI code that says Los Angeles is fourteen feet
// deep. The figures on `PortThroughput` are the half of that store that *is*
// worth having.

/**
 * The quay wall itself: a slab of stone with a vertical face down to the water.
 *
 * A polygon rather than a centreline and a width, which is the opposite of how
 * `Road` and `Bridge` are authored, and the reason is that a container terminal
 * is not a ribbon — it is a corner of reclaimed land with slips cut into it, and
 * the shape of that corner is what the eye reads as a port from a hundred
 * kilometres up. `deckHeight` is metres above chart datum, so the kit can drop a
 * skirt from the deck to the waterline rather than floating the slab.
 */
export interface Quay {
  id: string;
  polygon: LatLng[];
  /** Metres above the waterline. Around 4 m on a modern container quay. */
  deckHeight: number;
}

/**
 * One place a ship ties up, and the only thing that knows which way it faces.
 *
 * **`bearing` is authored here rather than read off the wire, and that is not a
 * shortcut.** Of 150 vessels in the store whose latest fix is under half a knot,
 * only 75 report a usable heading and 21 report neither heading nor course; a
 * layer that oriented moored hulls from AIS would leave a third of a harbour
 * pointing due north in a row. A berth, by contrast, has one answer and it never
 * changes: a hull alongside is parallel to the wall it is alongside.
 *
 * `maxLength` is the length class, in metres, and it does two jobs — it is what
 * lets a berth refuse a hull it could not physically take, and it is the scale
 * the kit uses when it has to draw a berth with nothing in it.
 */
export interface Berth {
  id: string;
  lat: number;
  lng: number;
  /** TRUE bearing of the bow of a hull lying alongside, degrees clockwise from north. */
  bearing: number;
  /** Longest vessel this berth takes, metres. */
  maxLength: number;
  /** The quay this berth is cut into, when it is on one. */
  quayId?: string;
}

/**
 * A row of ship-to-shore gantries, as a count and a line.
 *
 * A row and not a crane, because that is how they are built and because it is
 * what keeps the instancing honest: eighteen gantries on Pier 400 are one record
 * and eighteen matrices in the board's single crane `InstancedMesh`, never
 * eighteen `Group`s. Vertical exaggeration is what makes this the highest
 * legibility-per-triangle object in the whole feature — at SoCal's 3.4x a 130 m
 * gantry stands 1.13 scene units while a 400 m ship is 1.02 units long, so the
 * crane is taller than the ship is long.
 *
 * `idle` is free storytelling and costs one number: a boom down over a berth
 * reads as working and a boom raised to the vertical reads as idle, and it is a
 * per-instance rotation either way.
 */
export interface Crane {
  id?: string;
  /** Start of the rail. */
  from: LatLng;
  /** End of the rail. Gantries are spread evenly between the two. */
  to: LatLng;
  /** How many gantries stand on this rail. */
  count: number;
  /** TRUE bearing the boom points along when it is down, degrees from north. */
  bearing: number;
  /** Rail to the top of the portal beam, metres. ~82 m; ~130 m to the raised boom tip. */
  height: number;
  /** How far the boom reaches over the water, metres. ~70 m for a post-panamax gantry. */
  outreach: number;
  /**
   * Fraction of this row with the boom raised, 0..1. Absent means none of it —
   * every crane working, which is the picture a port at rest does *not* have.
   */
  idleFraction?: number;
}

/**
 * A container yard: a rectangle of stacked boxes, drawn as **pixels**.
 *
 * This is the counterintuitive call in the whole kit and the arithmetic belongs
 * next to the type or it will be "improved" into instances by the next reader. A
 * 40-foot container is 12.2 m long, which at SoCal's 390.6 m per scene unit is
 * **0.0312 units** — sub-pixel from every camera pose the board allows, and
 * there are tens of thousands of them. A yard is therefore one ground quad
 * textured from a canvas atlas banded per terminal, exactly the way
 * `markingsAtlas` bands runway paint, and the loaded/empty proportion is drawn
 * into those pixels rather than counted in geometry.
 */
export interface Yard {
  id?: string;
  lat: number;
  lng: number;
  /** Along `bearing`, metres. */
  length: number;
  /** Across `bearing`, metres. */
  width: number;
  /** TRUE bearing of the long axis — the direction the container rows run. */
  bearing: number;
  /**
   * Share of the boxes stacked here that are empties, 0..1, for the atlas to
   * draw in the empty palette at its true proportion. Absent means unknown, and
   * unknown is drawn as one colour rather than as a guessed mix.
   */
  emptyShare?: number;
}

/**
 * What went across a quay in a month, and how much it cost to send it.
 *
 * The honest answer to "are they empty or full", and it is a fact about a
 * **port**, not about a ship. There is no draught column in `vessels` and the
 * AIS static message that would carry one is absent for most hulls most of the
 * time, so a per-hull laden state would be an invention of exactly the kind the
 * fire layer nearly shipped. This is the same question answered from data that
 * exists: 348,691 of 460,467 boxes left Los Angeles **empty** in July 2026.
 *
 * `asOf` is a month, `YYYY-MM`, and it is required. A throughput figure with no
 * month on it is a claim about now, and this one is never about now.
 */
export interface PortThroughput {
  /** The month these counts describe, `YYYY-MM`. Never "now", never a fetch time. */
  asOf: string;
  loadedExport: number;
  emptyExport: number;
  loadedImport: number;
  emptyImport: number;
}

/**
 * A freight index on one lane, in dollars per forty-foot equivalent.
 *
 * Carried beside the throughput because it is the *explanation* for it: FBX01
 * China-to-US-West-Coast at $7,491 against FBX02 back the other way at $347 is
 * why three quarters of the boxes leaving Los Angeles have nothing in them.
 *
 * There is deliberately no observation timestamp. The store's `observed_at` is
 * our own read clock — Freightos publishes none — and a card that renders it as
 * "as of" is lying about precision.
 */
export interface PortFreightRate {
  /** The published index code: `FBX01`. An id, not a label. */
  id: string;
  /** What the lane is, in words: "China / East Asia to North America West Coast". */
  lane: string;
  usdPerFeu: number;
}

/**
 * A working port, as the board draws it.
 *
 * `harborType` decides the single biggest object on the plate and is therefore
 * not decoration: `CB` is a coastal breakwater harbour and `CN` is a coastal
 * natural one. San Pedro and Long Beach are `CB` and share the thirteen and a
 * half kilometres of federal breakwater that turns a bight into a harbour from a
 * hundred kilometres up; Oakland is `CN` and must never be given one.
 */
export interface Port {
  /** UN/LOCODE where there is one — `"USLAX"`. Only ever an id. */
  id: string;
  name: string;
  /**
   * A hand-traced anchor for the plate and the card, **not** a row out of
   * `ports.sqlite`. See the note at the head of this block.
   */
  lat: number;
  lng: number;
  /** `CB` coastal breakwater, `CN` coastal natural, `RN` river natural. */
  harborType: "CB" | "CN" | "RN";
  /**
   * The breakwater, as one continuous path per arm. Absent on a natural
   * harbour, and absence is the correct picture there rather than a gap.
   */
  breakwater?: LatLng[][];
  /** The dredged channel centreline, drawn as a darker strip on the sea. */
  channel?: LatLng[];
  quays?: Quay[];
  berths?: Berth[];
  cranes?: Crane[];
  yards?: Yard[];
  throughput?: PortThroughput;
  rates?: PortFreightRate[];
}

export interface View {
  id: string;
  label: string;
  shortLabel: string;
  number?: string;
  description?: string;
}

/** A camera destination, and a sentence about why it is on the map. */
export interface Chapter extends View {
  number: string;
  description: string;
  focus: {
    lat: number;
    lng: number;
    distance: number;
    height: number;
    rotation: number;
  };
}

/**
 * A rectangle rendered at fine terrain resolution.
 *
 * SF declares one covering the whole city and behaves as if this did not exist.
 * LA needs six — DTLA, Santa Monica, Culver, Irvine, Pasadena, downtown
 * Riverside — with the basin between them coarse, because LA/OC/Riverside is
 * roughly fourteen times SF's area and a uniform 45 m lattice over it would be
 * 4.6M points. See ARCHITECTURE.md §5.
 */
export interface FocusRegion {
  minLat: number;
  maxLat: number;
  minLng: number;
  maxLng: number;
}

/**
 * Everything the engine needs to draw a place. Pure data — a city pack must
 * contain no code, so that adding one is a contribution anybody can review.
 */
export interface City {
  id: string;
  name: string;

  /** Map centre, and the origin of scene space. */
  center: { lat: number; lng: number };
  /** Scene bounds. Everything outside this is open water or off-frame. */
  bounds: { minLat: number; maxLat: number; minLng: number; maxLng: number };

  /**
   * Degrees to scene units, for latitude. Longitude is derived as
   * `latScale * cos(center.lat)` so the place keeps its true proportions.
   */
  latScale: number;

  /**
   * Degrees to scene units for longitude, when the derivation above is not what
   * the board wants.
   *
   * Absent on every authored pack, and present on a pack that
   * `cities/reconcile.ts` has rewritten: three boards each squashing longitude
   * by the cosine of their own centre latitude disagree about where a place is
   * by up to 4 km, and the reconciliation puts all three on one reference
   * latitude. Optional rather than required so that "a city pack is a data file
   * anybody can review" keeps meaning what it meant.
   */
  lngScale?: number;

  /**
   * How much taller than life the vertical is. Terrain and buildings share it,
   * so they stay honest relative to each other.
   *
   * This is the exaggeration the board is **built** at, and on every
   * hand-authored pack it is the only one there is. See
   * `nearVerticalExaggeration` for the boards where it cannot be the only one.
   */
  verticalExaggeration: number;

  /**
   * The exaggeration this board wants when the camera is *in* it, if that is a
   * different number from the one it wants when the camera is looking at the
   * whole of it.
   *
   * ## Why a board would need two
   *
   * `reconciledExaggeration` derives one figure per board, so the tallest
   * blended peak fills a fixed fraction of the board span: 15.00 for
   * California, 5.78 for the Bay Area, 3.41 for the Southland. Each is right
   * *for framing that board whole*, and while a board was only ever looked at
   * from its own stand-off, one number was all a board could want.
   *
   * The merged California is the first board a visitor can traverse from
   * 1,551 km down to 1.9 km, and one number cannot serve both ends of that. At
   * 15x the Sierra reads from orbit and San Francisco's own relief is drawn 2.6x
   * taller than on the board named after it — Twin Peaks becomes 2.19 units of
   * drawn height against a 2.26-unit stand-off at the FiDi rung, which is a
   * camera inside a hill. At 5.78x the city is right and the state is a plate.
   *
   * So a board that declares this is asking for the exaggeration to **ramp with
   * the camera**, between `verticalExaggeration` far out and this near in. The
   * ramp is `engine/scene.ts`'s, it is applied as one scale on the ground group
   * rather than by rebuilding anything, and the note there is where the
   * mechanism and its limits are written down.
   *
   * Absent on every authored pack, which is the honest default: a board you can
   * only look at from one distance does not have this problem and should not
   * pay for machinery that solves it.
   */
  nearVerticalExaggeration?: number;

  /** Ground-cell size inside a focus region, in degrees. */
  cellLat: number;
  cellLng: number;
  /** Multiplier applied to cell size outside every focus region. 1 = uniform. */
  coarseFactor?: number;
  focusRegions?: FocusRegion[];

  /** Distance from open water, in degrees, over which relief ramps to zero. */
  coastFalloff: number;

  landmasses: LatLng[][];
  parks: LatLng[][];
  inlandWater: LatLng[][];
  hills: Hill[];
  districts: District[];
  landmarks: Landmark[];
  bridges: Bridge[];
  roads: Road[];
  chapters: Chapter[];

  /**
   * Airfields, drawn by `engine/airports.ts` as graded plates with real
   * runway headings. Optional because a pack that has none should not have to
   * say so, and because every pack predates it.
   */
  airports?: Airport[];

  /**
   * Working ports, drawn by `engine/ports.ts` as quays, breakwaters, yards and
   * crane rows. Optional for the same reason `airports` is: a pack with no port
   * should not have to say so, and every pack predates it.
   */
  ports?: Port[];

  /** Palette overrides; every field is optional. */
  palette?: Partial<ScenePalette>;

  /**
   * Set by `cities/reconcile.ts` on a pack it has already rewritten.
   *
   * `World` posts its pack to the terrain worker as a structured clone and the
   * worker builds a second `World` from it. This is what makes that second pass
   * a no-op instead of a second reconciliation of an already-reconciled board.
   * Never authored by hand.
   */
  reconciled?: true;
}

export interface ScenePalette {
  skyTop: number;
  skyHorizon: number;
  sea: number;
  lake: number;
  shore: number;
  sand: number;
  flats: number;
  upland: number;
  park: number;
  parkHigh: number;
  /**
   * Bare high ground, and **optional on purpose**.
   *
   * The unpainted ramp is `flats → upland` over the first 150 m and then flat
   * forever, which is right for a city — a hill in San Francisco is built to its
   * summit and the buildings do the talking. On a board measured in hundreds of
   * kilometres it is not: the Sierra crest at 4,000 m and the Mojave floor at
   * 600 come out the same number, so a granite skyline and a creosote flat are
   * one colour and the only thing separating them is the shading.
   *
   * A pack that declares this gets a second stop above `upland` — see
   * `groundColor` in `terrain.ts` for the two elevations it ramps between. A
   * pack that does not is rendered exactly as before, which is why this is
   * optional rather than a tenth required colour that every existing pack would
   * have to answer for.
   */
  alpine?: number;
}

// ---- Lighting -------------------------------------------------------------

/**
 * Everything the light rig needs, as plain numbers.
 *
 * This is a *state*, not an observation: `Environment` — `{ time, sun, weather }`
 * — is what the world is doing, and a `LightingState` is what that means for the
 * rig. `Atmosphere` owns the conversion and is the only thing allowed to make
 * one; a scene applies it and never writes back. Two modules both constructing
 * and mutating the same three lights is the failure this shape exists to
 * prevent. See CONTRACT.md §4.
 *
 * Colours are `0xrrggbb`, matching `ScenePalette` and three.js.
 */
export interface LightingState {
  sun: {
    /**
     * Unit vector from the scene toward the sun. Distance is deliberately
     * absent: how far away to place the light is a fact about the scale of the
     * scene, and the sun does not know whether it is shining on 94 m per unit
     * or on 1 m per unit.
     */
    direction: [number, number, number];
    color: number;
    intensity: number;
  };
  hemisphere: { sky: number; ground: number; intensity: number };
  ambient: { color: number; intensity: number };
  /**
   * Background gradient, or `null` to leave the background alone — which is
   * what an interior wants, since it has walls and no horizon.
   */
  sky: { top: number; horizon: number } | null;
  /** `null` for no fog at all. An office gets none. */
  fog: { color: number; near: number; far: number } | null;
  /**
   * The moon as a **drawn object**, or absent when nobody modelled one.
   *
   * The only field on a `LightingState` that is not a light, and the exception
   * is deliberate and narrow. `atmosphere.ts` computes the moon's position to a
   * hundredth of a degree — four hundred lines of Meeus — for the sole purpose
   * of deciding the key light after sunset, and then throws the position away.
   * The disc itself was never drawn, and the moonrise the file documents at
   * length was something nobody could watch happen.
   *
   * It stays on this type rather than becoming a second channel because
   * CONTRACT.md §4's rule is about *direction*, not about cardinality:
   * `Atmosphere` computes it, a scene applies it, and nothing writes back. A
   * separate `moonPosition()` call in the renderer would be a second opinion
   * about where the moon is, computed from a clock the scrubber does not own.
   *
   * **Drawing it must not light anything.** The key light after dark is already
   * `sun`, which `combineKey` has handed over to the moon; a second light here
   * would be the same photons twice and would break the one-owner rule outright.
   *
   * Optional, and `null` is as meaningful as absent: an office rig has walls and
   * no sky, and a hard-coded fallback rig has no ephemeris behind it. Both
   * simply do not draw a moon.
   */
  moon?: LightingMoon | null;
}

/**
 * Where the moon is, how much of it is lit, and which way the lit side faces.
 *
 * Angles only. There is no distance here for the same reason `sun.direction`
 * carries none — how far away to put it is a fact about the scale of the scene —
 * and no colour, because a renderer that has the illuminated fraction can decide
 * for itself how a waning crescent should read against its own sky.
 */
export interface LightingMoon {
  /**
   * Unit vector from the scene toward the moon. **Not floored**, unlike
   * `sun.direction`: the floor exists so a shadow camera stays usable, and a
   * drawn disc lifted seven degrees off where the moon actually is would be a
   * visible lie about the one thing this record is for.
   */
  direction: [number, number, number];
  /**
   * Unit vector, perpendicular to `direction`, pointing at the middle of the lit
   * limb — the true sun's direction with the moon's own component taken out.
   *
   * Computed here because it needs the sun's *unfloored* position, which a
   * `LightingState` deliberately does not carry. Without it a renderer has to
   * either guess the phase's orientation or re-derive the sun, and a crescent
   * pointing the wrong way is the one error in this whole record that anybody
   * who has ever looked up will spot immediately.
   */
  brightLimb: [number, number, number];
  /** Fraction of the visible disc in sunlight: 0 at new, 1 at full. */
  illuminated: number;
  /** Angular radius of the disc in radians, from its real distance. ~0.0045. */
  angularRadius: number;
  /**
   * How much of the disc gets through, 0..1: below the horizon, behind cloud, or
   * washed out by daylight. `0` means do not draw it at all.
   *
   * A single scalar rather than three, because every consumer wants the product
   * and the three causes are `atmosphere.ts`'s to weigh — it is the module that
   * already holds the obscuration, the cover and the night factor.
   */
  visibility: number;
}

// ---- Markers --------------------------------------------------------------

/**
 * A thing worth pointing at, minus where it is.
 *
 * `colorKey` is deliberately opaque to the engine — it indexes into a palette
 * the caller supplies. The engine will not learn what "rejected" means.
 *
 * This is the half that survives a change of coordinate system: a pin on a city
 * at 37.79 N, -122.40 E and a pin on a desk 4.2 m along the east wall are the
 * same kind of thing to everything downstream of the geometry, so an office can
 * carry its own positions and still hand a `Pin` to the same detail card.
 */
export interface Pin {
  id: string;
  label: string;
  colorKey: string;
  /** Optional href for the detail card. */
  url?: string;
  /** Optional one-liner for the detail card. */
  blurb?: string;
}

/**
 * A small, map-scale building drawn in place of a pin.
 *
 * This is deliberately a glyph rather than an architectural model. A city is
 * normally viewed from kilometres away, so loading a façade kit with one mesh
 * per window buys triangles nobody can see and gives up the one-draw-call city
 * that `blocks.ts` works hard to preserve. The glyph keeps the useful grammar
 * — a ground floor, repeated bays, a roof line and a deterministic silhouette
 * — and expresses it in a handful of procedural meshes.
 *
 * Metres are used here because these values describe a real building even
 * though the city scene does not: `World` converts horizontal metres with
 * `metresPerUnit` and vertical metres with the city's exaggeration.
 */
export interface BuildingGlyph {
  kind: "building";
  width: number;
  depth: number;
  height: number;
  /** Approximate occupied floors; used to choose the façade rhythm. */
  storeys: number;
  /** Compass bearing of local −Z, degrees clockwise from true north. */
  heading: number;
  /** The silhouette family, not a tenant or product category. */
  profile: "tower" | "hangar" | "courtyard" | "block";
  /** Stable variation for bay widths and lit panes. */
  seed?: number;
  /** Neutral shell colour. The marker palette still supplies the door/accent. */
  bodyColor?: number;
}

/** A `Pin` placed on a city, in degrees. */
export interface Marker extends Pin {
  lat: number;
  lng: number;
  /**
   * False when the position is a placeholder rather than a real address.
   * Rendered distinctly, because inventing a location on a map whose premise
   * is that it is real is worse than admitting the gap.
   */
  located?: boolean;
  /** Optional map-scale representation. Omit it for the ordinary pin. */
  glyph?: BuildingGlyph;
}

/** Caller-supplied `colorKey` -> colour. */
export type MarkerPalette = Record<string, number>;

// ---- Flights --------------------------------------------------------------

export interface Aircraft {
  id: string;
  lat: number;
  lng: number;
  /** Barometric altitude in metres. */
  altitude: number;
  /** Degrees clockwise from true north. */
  heading: number;
  callsign?: string;
  /**
   * Metres per second over the ground, when the source knew.
   *
   * The three fields below are what let `createFlightLayer` **dead-reckon**
   * rather than merely interpolate, and the difference between the two is the
   * difference between a sky and a photograph of one. A layer given positions
   * alone can only replay the leg between the last two of them: it arrives at
   * the newest known point and then sits motionless for the five to fifteen
   * seconds until the next snapshot, which is exactly what a live ADS-B board
   * looked like here for as long as this type had four numbers in it.
   *
   * Optional, and the layer's behaviour when they are absent is the old
   * behaviour unchanged — interpolate between observations, hold still at the
   * end. That is deliberate: a source that does not know how fast something is
   * going must not have a speed guessed for it, and a stationary ground vehicle
   * is a thing this feed genuinely reports.
   */
  groundSpeed?: number;
  /** Metres per second, positive climbing, when the source knew. */
  verticalRate?: number;
  /**
   * How old the position already was when the source handed it over, in
   * seconds.
   *
   * A fix crosses a receiver, a cache and a browser hold before it is drawn, so
   * the coordinates describe an instant several seconds in the past. A
   * dead-reckoner that starts from them as though they were current draws the
   * whole sky that far behind; one that is told the age advances the position to
   * now first. Absent means "as of when you were told", which is the safe
   * reading and the one every source that cannot answer honestly gets.
   */
  ageSeconds?: number;
}

/**
 * Where aircraft come from.
 *
 * An interface rather than a client, because this repo must not ship one for the
 * obvious source: FlightRadar24's terms do not permit scraping and do not permit
 * redistributing the data, so a client for it in an Apache-2.0 repo would be
 * publishing instructions for violating a ToS. This package ships a simulator
 * and open community sources; anything commercial is an adapter in a private
 * deployment. See ARCHITECTURE.md §4.
 */
export interface FlightSource {
  /** Current traffic. Called on a timer; must be cheap and must not throw. */
  poll(): Promise<Aircraft[]> | Aircraft[];
  /** Seconds between polls. */
  interval: number;
  dispose?(): void;
}

// ---- Satellites -----------------------------------------------------------

/**
 * Which constellation a satellite belongs to, as far as anyone looking up cares.
 *
 * A **display bucket and not a taxonomy**: no orbital regime, no operator, no
 * launch date. `engine/satellites.ts` picks a colour from it and nothing else
 * reads it, and a field that carried more would be a field that got wrong.
 *
 * `starlink` is broken out from `comms` because it is the reason the layer
 * exists — it is the constellation people can see with their eyes, in a train,
 * forty minutes after sunset. `other` is not a failure; most of the catalogue is
 * other.
 *
 * This lives here rather than in `server/wire.ts` for the same reason `Marker`
 * does: the renderer owns the vocabulary and the wire carries it, so a body off
 * the network is handed to the engine as-is with no adapter in between.
 */
export type SatelliteGroup =
  | "starlink"
  | "comms"
  | "navigation"
  | "station"
  | "weather"
  | "other";

// ---- Vessels --------------------------------------------------------------

/**
 * What kind of hull this is, as far as anyone looking down at it cares.
 *
 * A **display bucket and not a taxonomy**, the same argument `SatelliteGroup`
 * makes: the geometry factory switches on this and nothing else reads it. It is
 * an enumerated union rather than a hardcoded builder for a specific reason —
 * better hull geometry is being modelled separately, and a new case here is how
 * it arrives, rather than a rewrite of the instancing path.
 */
export type VesselKind =
  | "container"
  | "tanker"
  | "bulk"
  | "vehicle-carrier"
  | "tug"
  | "ferry"
  | "fishing"
  | "other";

/**
 * What the hull is doing, from AIS `nav_status`.
 *
 * **This labels a card and never gates motion.** 83 of 197 vessels claiming
 * "under way using engine" in the store are sitting still; speed over ground is
 * the only thing that knows whether something is moving. Kept as a small union
 * rather than the raw 0-15 code because fifteen of those sixteen values are
 * distinctions no renderer can draw.
 */
export type VesselStatus = "under-way" | "at-anchor" | "moored" | "unknown";

/**
 * One hull on the board.
 *
 * ## What is deliberately absent
 *
 * **There is no name and no MMSI**, and their absence is the contract rather
 * than an omission. A layer that hardcoded the hulls sitting in the store right
 * now would be the fire layer's twenty-two orange marks in a nicer costume:
 * plausible, specific, and a claim about a named commercial vessel that this
 * deployment has no licensed feed behind. Identity arrives with a licensed live
 * feed and a written licence entry, or it does not arrive.
 *
 * **There is no laden state.** `vessels` carries no draught column, the AIS
 * static message that would carry one is absent for most hulls most of the time,
 * and "empty or full" is answered at the port instead — see `PortThroughput`.
 * `draught` below is the *hull's* depth as a scale for the geometry, authored
 * with the vessel, and it is never an observation and never a cargo claim.
 *
 * ## Scale
 *
 * Sizes are true metres and are drawn true. That **inverts** the deliberate
 * oversizing in `aircraftGeometry.ts`, where a map symbol is held at 0.42 units
 * whatever the board, and the reason for the inversion is that a ship is almost
 * always alongside a quay drawn at true scale while an aeroplane is alone in the
 * sky with nothing to be wrong against. 400 m is 1.024 units on SoCal and 4.24
 * on the Bay.
 */
export interface Vessel {
  /** Stable for the life of a fix set. An opaque token, never an MMSI. */
  id: string;
  kind: VesselKind;
  lat: number;
  lng: number;
  /**
   * Which way the bow points, degrees clockwise from TRUE north.
   *
   * Already resolved by the gate, and that is the point of it being non-null: a
   * moored hull takes its berth's authored bearing, a moving one takes its
   * course over ground, and the caller never has to know which. See
   * `Berth.bearing`.
   */
  bearing: number;
  /** Overall, metres. */
  length: number;
  /** Metres. */
  beam: number;
  /** Hull depth for the geometry, metres. Authored, never observed, never cargo. */
  draught?: number;
  /**
   * Metres per second over the ground.
   *
   * Past the gate this is a real speed or zero, never a sentinel: AIS reports
   * 102.3 knots for "not available", which dead-reckons a hull 3.2 km — eight
   * SoCal units — in sixty seconds. The sentinel is stripped upstream; it is
   * never NULL, so a null-check catches nothing.
   */
  speed: number;
  /**
   * Course over ground, degrees clockwise from TRUE north, or `null` when the
   * source did not report a usable one.
   *
   * **`cog % 360` is a booby trap and it will look correct.** Real course
   * reaches 358.7 and the "not available" sentinel is exactly 360.0, so the
   * obvious normalisation turns every unknown course into due north and the
   * whole fleet quietly faces the same way with no test that notices.
   */
  course: number | null;
  /** What it says it is doing. Labels the card; never gates the motion. */
  status: VesselStatus;
  /** The berth it is lying alongside, when it is lying alongside one. */
  berthId?: string;
  /**
   * How old the fix already was when the source handed it over, in seconds —
   * the same field and the same reading as `Aircraft.ageSeconds`.
   *
   * It matters more here. `vessel_pos` is a thirty-second listen every fifteen
   * minutes, so a hull under way has moved about five kilometres between
   * samples. Dead-reckoning along the reported course from a fix whose age is
   * known is honest; splining between two fixes is not, and no code path may
   * produce a position on the chord between them.
   */
  ageSeconds?: number;
}

// ---- The sky: radar and migration -----------------------------------------

/**
 * One statewide reflectivity raster, as a regular lattice.
 *
 * A lattice rather than a list of cells, because `echo_cells` turns out to be a
 * regular 0.25-degree grid and saying so in the type is what makes the layer one
 * quad and one `DataTexture` instead of geometry. On the extended state board
 * the whole of California is about 41x40 = 1,640 texels, roughly 6.5 KB as
 * RGBA.
 *
 * **`null` in `dbz` is not zero.** A cell inside the coverage radius of a
 * station whose RDA is inoperable is *unknown*, and a hole drawn as a hole is
 * the difference between "nothing is falling there" and "nobody is looking
 * there". That distinction is the whole reason this array is nullable rather
 * than a `Float32Array`, and it is worth the boxing.
 */
export interface RadarField {
  /** Centre of the south-west cell. */
  minLat: number;
  minLng: number;
  /** Cell pitch in degrees. 0.25 for the NEXRAD composite — about 27.8 km. */
  cellLat: number;
  cellLng: number;
  /** Lattice size. `dbz.length` must equal `rows * cols`. */
  rows: number;
  cols: number;
  /** Reflectivity in dBZ, row-major from the south-west. `null` where unknown. */
  dbz: (number | null)[];
  /** ISO-8601 of the volume scan these cells came from. */
  observedAt: string;
  /** Fraction of the lattice at or above the draw threshold, 0..1. */
  wetFraction: number;
  /** Radars that answered, and radars whose RDA is down. For the panel. */
  stations: number;
  stationsDown: number;
}

/** Nocturnal migration over one county, for one ten-minute granule. */
export interface MigrationCounty {
  /** FIPS or slug. An id; the layer never renders it. */
  id: string;
  name: string;
  /** The county's internal point — a point known to be inside it. */
  lat: number;
  lng: number;
  /**
   * True county area, km². The motes scatter in a **disc** of this area, and a
   * disc rather than a polygon because the data has no structure finer than a
   * county and an outline would claim one it does not have.
   */
  areaKm2: number;
  /** Birds aloft over this county at `MigrationField.observedAt`. */
  aloft: number;
  /** Mean flight altitude above ground, metres. Statewide mean is about 726. */
  altitude: number;
  /** Direction of travel — the bearing they are heading TOWARD, degrees true. */
  direction: number;
  /** Ground speed, metres per second. */
  speed: number;
}

/**
 * Last night, as one sentence's worth of numbers.
 *
 * `crossed` and `peakAloft` come from the **state row** and never from a sum
 * over counties. Summing all 58 gives 2,360,086 against an authoritative
 * 393,290 — a 6x error waiting for the first person who writes `SELECT SUM`,
 * because a bird crossing four counties is counted four times.
 */
export interface MigrationNight {
  crossed: number;
  peakAloft: number;
  /** ISO-8601 of the peak. */
  peakAt: string;
  /** Metres above ground. */
  meanAltitude: number;
  /** Where they were going, in words: "south-east". */
  heading: string;
}

/** Why the sky is empty, when it is — which is most of the time. */
export interface MigrationQuiet {
  reason: "daylight" | "off-season" | "no-data";
  /** One true, specific sentence. Never a blank panel and never "no data". */
  message: string;
}

/**
 * The migration layer's whole input, including an empty one.
 *
 * **The empty state is the layer**, for most visitors: 168 of 297 granules are
 * daytime, BirdCast measures only after dark, and the layer is therefore absent
 * about fourteen hours in every twenty-four by construction. `quiet` is
 * non-optional for the same reason `FiresBody.fetchedAt` is — an empty
 * `counties` array with nothing beside it is indistinguishable from a dead feed.
 */
export interface MigrationField {
  counties: MigrationCounty[];
  /** ISO-8601 of the ten-minute granule. */
  observedAt: string;
  /** The statewide headline, from the state row. `null` when there is no night yet. */
  statewide: MigrationNight | null;
  /** `null` when something is genuinely aloft. Never `null` beside an empty set. */
  quiet: MigrationQuiet | null;
}
