/**
 * The heightfield, built off the main thread.
 *
 * This worker exists for one number: on the Bay Area, producing the lattice is
 * about 730 ms of unbroken synchronous work, and SoCal's is around 390 ms on
 * top of a switch that already blocks for four seconds. Nothing paints and
 * nothing responds while it runs — not the boot card, not the progress line
 * explaining why the boot card is still up.
 *
 * It is deliberately thin. All the geography lives in `world.ts` and this file
 * calls `computeField` exactly as the main thread would; the only thing here
 * that is not in `world.ts` is the message plumbing. The alternative — a second
 * copy of the sampling loop, tuned separately — produces two maps of the same
 * city that differ in the fourth decimal place and agree on nothing that would
 * make the difference visible.
 *
 * ## Why the `City` is sent whole
 *
 * A `City` is pure data by contract (`types.ts`), so structured clone carries it
 * across as-is. That costs something — the Bay Area pack's polygons are a few
 * hundred kilobytes — but it is paid once per city, against a field that comes
 * back as several megabytes of transferred buffer. Sending a city *id* and
 * importing the pack in here instead would drag both city modules into the
 * worker chunk and break the rule that a pack is data the engine is handed,
 * not data the engine knows about.
 */

import { World, computeField, type FieldMessage, type FieldRequest } from "./world.ts";

/**
 * `DedicatedWorkerGlobalScope` is not in `lib.dom.d.ts` and this project's
 * `tsconfig.json` belongs to another session, so the two members this file
 * actually touches are declared here rather than by adding `WebWorker` to
 * `lib`. Narrow on purpose: if this grows a third member, that is the moment to
 * ask for the lib entry instead.
 */
declare const self: {
  onmessage: ((event: MessageEvent<FieldRequest>) => void) | null;
  postMessage(message: FieldMessage, transfer?: Transferable[]): void;
};

/**
 * How often progress goes back over the wire.
 *
 * Per-row would be 656 messages for San Francisco, and every one of them is a
 * task queued on the main thread — the thread this whole file exists to leave
 * alone. Eight a second is enough for a bar that moves and cheap enough to be
 * invisible.
 */
const PROGRESS_INTERVAL_MS = 125;

self.onmessage = (event: MessageEvent<FieldRequest>) => {
  const { city } = event.data;
  try {
    const world = new World(city);
    let last = 0;
    const field = computeField(world, (done, rows) => {
      const now = performance.now();
      if (done < rows && now - last < PROGRESS_INTERVAL_MS) return;
      last = now;
      self.postMessage({ type: "progress", done, rows });
    });

    // Transfer, do not copy. The Bay Area's field is a 2.1 MB `Float32Array`
    // and two 533 kB `Uint8Array`s, plus the two axes; structured-cloning that
    // back hands the main thread a memcpy and an allocation of everything the
    // worker just saved it. After this the worker's own views are detached,
    // which is fine because it is about to be terminated.
    //
    // Every buffer in the field is listed. A buffer left off this list is
    // silently *copied* instead of moved, which is invisible in behaviour and
    // is exactly the cost this postMessage exists to avoid.
    self.postMessage({ type: "field", ...field }, [
      field.lats.buffer,
      field.lngs.buffer,
      field.height.buffer,
      field.land.buffer,
      field.park.buffer,
    ]);
  } catch (err) {
    // Report rather than throw. An uncaught error in here reaches the main
    // thread as an `ErrorEvent` with no message under most cross-origin rules,
    // and "something went wrong somewhere" is not worth the fallback path being
    // silent about.
    self.postMessage({ type: "failed", message: err instanceof Error ? err.message : String(err) });
  }
};
