/**
 * The two readouts that sit *over* the scene rather than beside it: the play HUD
 * and the aircraft detail card.
 *
 * Both were formatting code inlined into `main.ts` — `renderPlayHud` was a
 * four-branch `if` chain writing three `textContent`s and toggling a class, and
 * the aircraft card did not exist at all: clicking a plane produced the same
 * one-line `showDetail(string)` a company marker produced, so the callsign,
 * transponder hex, altitude and heading the ADS-B feed had already parsed were
 * thrown away one line before they could be shown.
 *
 * Everything here is a pure formatter plus one small renderer. No THREE, no
 * feed, no clock: hand it numbers, get strings. That is what lets the units be
 * tested, which matters more than it sounds — a HUD is the one part of an
 * interface that is *only* units, and `2.23694` in the wrong place is a bug that
 * looks like a working product.
 *
 * ### The aircraft card is open to everyone
 *
 * Per the owner's first decision: the signed-out visitor is the audience this is
 * designed for, not a degraded tier. Aircraft positions arrive from ADS-B, which
 * is an unencrypted broadcast anybody with a forty-dollar receiver can hear —
 * there is nothing here an account could grant access to, because nothing is
 * withheld from anyone. Gating it would have traded the most legible "this is
 * real" moment in the product for a rule with nothing behind it.
 */

import type { ControlMode } from "../play/controlMode.ts";

// ---- Units ---------------------------------------------------------------

const METRES_PER_SECOND_TO_MPH = 2.23694;
const METRES_PER_SECOND_TO_KNOTS = 1.94384;
const METRES_TO_FEET = 3.28084;

/** The sixteen-point compass, so a heading reads as a direction and not a number. */
const COMPASS: readonly string[] = [
  "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
  "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW",
];

/** `47` → `"NE"`. Degrees clockwise from true north; anything is accepted and wrapped. */
export function compassPoint(headingDeg: number): string {
  if (!Number.isFinite(headingDeg)) return "—";
  const wrapped = ((headingDeg % 360) + 360) % 360;
  return COMPASS[Math.round(wrapped / 22.5) % 16] ?? "N";
}

/**
 * A latitude or longitude as a person reads it: three decimal places and a
 * hemisphere letter rather than a sign.
 *
 * Three places is about 110 m, which is the right precision for an aircraft
 * position that was observed some seconds ago and is being drawn interpolated.
 * Six places would be a claim about accuracy the feed never made.
 */
export function formatCoordinate(value: number, axis: "lat" | "lng"): string {
  if (!Number.isFinite(value)) return "—";
  const hemisphere = axis === "lat" ? (value >= 0 ? "N" : "S") : value >= 0 ? "E" : "W";
  return `${Math.abs(value).toFixed(3)}° ${hemisphere}`;
}

// ---- The play HUD --------------------------------------------------------

export interface DrivePlayTelemetry {
  kind: "drive";
  speedMps: number;
  roadName: string;
  driveMode: string;
  progress: number;
  camera: string;
  guardrailContact: boolean;
  collisionRisk: number;
}

export interface ActorPlayTelemetry {
  kind: "actor";
  actor: "crow" | "dog" | "humanoid";
  speedMps: number;
  altitudeM: number;
  distanceM: number;
  pose: string;
  flightEnergy: number;
  displayName: string;
  atAltitudeBound: boolean;
}

export interface AircraftPlayTelemetry {
  kind: "aircraft";
  speedMps: number;
  altitudeM: number;
  flightMode: string;
  batteryWh: number;
  /** Pack size, so the HUD can show a fraction rather than a raw watt-hour. */
  batteryCapacityWh: number;
  stalled: boolean;
  hardLanding: boolean;
  envelopeContact: boolean;
}

export interface WalkPlayTelemetry {
  kind: "office-walk";
  officeLabel: string;
  distanceM: number;
  x: number;
  z: number;
}

export type PlayTelemetry =
  | DrivePlayTelemetry
  | ActorPlayTelemetry
  | AircraftPlayTelemetry
  | WalkPlayTelemetry;

export interface PlayHudView {
  /** The mode chip: two syllables, upper-cased by the stylesheet. */
  mode: string;
  /** The number that matters most in this mode, first. */
  primary: string;
  /** State and secondary numbers, right-aligned. */
  status: string;
  /**
   * Something is wrong enough to colour.
   *
   * Deliberately narrow: a guardrail scrape, a stall, an altitude bound. A HUD
   * that goes orange for anything interesting goes orange permanently.
   */
  warning: boolean;
}

/**
 * The HUD for one mode, as three strings and a flag.
 *
 * Returns `null` for the two orbit modes, which is what the applier keys the
 * card's visibility off — one decision in one place, rather than a `hidden =`
 * assignment above the branch that computes what to hide.
 */
export function formatPlayHud(telemetry: PlayTelemetry | null): PlayHudView | null {
  if (telemetry === null) return null;
  if (telemetry.kind === "drive") {
    return {
      mode: "Drive",
      primary: `${Math.round(telemetry.speedMps * METRES_PER_SECOND_TO_MPH)} mph · ${telemetry.roadName}`,
      status: `${telemetry.driveMode} · ${Math.round(telemetry.progress * 100)}% · ${telemetry.camera}`,
      warning: telemetry.guardrailContact || telemetry.collisionRisk > 0.55,
    };
  }
  if (telemetry.kind === "actor") {
    const crow = telemetry.actor === "crow";
    return {
      mode: crow ? "Crow" : "Explore",
      primary: crow
        ? `${telemetry.speedMps.toFixed(1)} m/s · ${Math.max(0, telemetry.altitudeM).toFixed(0)} m alt`
        : `${telemetry.speedMps.toFixed(1)} m/s · ${telemetry.distanceM.toFixed(0)} m travelled`,
      status: crow
        ? `${telemetry.pose} · ${Math.round(telemetry.flightEnergy * 100)}% energy`
        : `${telemetry.pose} · ${telemetry.displayName}`,
      warning: telemetry.atAltitudeBound,
    };
  }
  if (telemetry.kind === "aircraft") {
    const pack = Math.max(1, telemetry.batteryCapacityWh);
    const pct = Math.round((100 * Math.max(0, telemetry.batteryWh)) / pack);
    return {
      mode: "Flight",
      primary: `${Math.round(telemetry.speedMps * METRES_PER_SECOND_TO_KNOTS)} kt · ${Math.round(telemetry.altitudeM).toLocaleString()} m`,
      status: `${telemetry.flightMode} · ${pct}% pack${telemetry.stalled ? " · STALL" : ""}`,
      warning: telemetry.stalled || telemetry.hardLanding || telemetry.envelopeContact || pct < 15,
    };
  }
  return {
    mode: "Studio",
    primary: `${telemetry.officeLabel} · ${telemetry.distanceM.toFixed(0)} m walked`,
    status: `${telemetry.x.toFixed(1)}, ${telemetry.z.toFixed(1)} m`,
    warning: false,
  };
}

/** Which telemetry kind a control mode expects, or `null` for the orbit modes. */
export function playHudKindFor(mode: ControlMode): PlayTelemetry["kind"] | null {
  if (mode === "drive" || mode === "actor" || mode === "aircraft") return mode;
  if (mode === "office-walk") return "office-walk";
  return null;
}

// ---- The aircraft detail card --------------------------------------------

/**
 * One aircraft, as much of it as the feed knew.
 *
 * Structurally the engine's `Aircraft` plus two facts the engine has and the
 * card needs: whether the track was invented, and what the feed asks to be
 * credited as. Restated rather than imported because `engine/types.ts` pulls the
 * renderer's whole vocabulary behind it and this module must stay importable in
 * a test with no THREE.
 */
export interface AircraftDetailInput {
  /**
   * The ADS-B transponder address, lower-case hex, for a real track — `"a1b2c3"`.
   * A simulated one carries its own id and is labelled as simulated instead.
   */
  id: string;
  callsign?: string | null;
  lat: number;
  lng: number;
  /** Barometric altitude, metres. */
  altitude: number;
  /** Degrees clockwise from true north. */
  heading: number;
  /**
   * ICAO type designator — `"B739"` — and tail number — `"N68834"`, or absent.
   *
   * Optional, and every caller may leave all four of these out: the simulator
   * knows none of them, and neither does a server one version behind. The rows
   * simply do not appear, which is why there is no "unknown" string here.
   */
  type?: string | null;
  registration?: string | null;
  /** Ground speed in knots. */
  groundSpeedKt?: number | null;
  /** Climb rate in feet per minute, positive up. */
  verticalRateFpm?: number | null;
  /** The track came out of the bundled simulator, not out of a receiver. */
  synthetic?: boolean;
  /** The feed's own credit line, shown on the card that displays its data. */
  attribution?: string | null;
}

export interface AircraftDetailRow {
  label: string;
  value: string;
}

export interface AircraftDetailView {
  /** The callsign if the aircraft broadcast one, else its hex. Never empty. */
  title: string;
  /** "Simulated track" or the transponder address, under the title. */
  subtitle: string;
  rows: readonly AircraftDetailRow[];
  /** True when nothing here was observed. The card says so rather than implying it. */
  synthetic: boolean;
  attribution: string | null;
}

/**
 * Below this, in feet per minute, an aircraft is flying level.
 *
 * A cruising airliner's reported vertical rate wanders either side of zero by a
 * few tens of feet a minute — pressure noise, not a manoeuvre — and drawing
 * "+64 ft/min" for it says *climbing* about an aeroplane that is not. 100 is
 * comfortably inside that noise and comfortably below anything deliberate.
 */
const LEVEL_FPM = 100;

/** `-1240` → `"−1,240 ft/min"`; anything inside the noise band → `"Level"`. */
function formatVerticalRate(fpm: number): string {
  if (Math.abs(fpm) < LEVEL_FPM) return "Level";
  // U+2212, not a hyphen: this sits in a tabular-numeric column next to a
  // heading and an altitude, and a hyphen is half the width of the digits.
  const sign = fpm > 0 ? "+" : "\u2212";
  return `${sign}${Math.round(Math.abs(fpm)).toLocaleString()} ft/min`;
}

/**
 * Format one aircraft for the card.
 *
 * Every value is stated in both the unit the feed reports and the unit aviation
 * uses, because the two audiences for this card are "someone who wants to see a
 * real plane" and "someone who knows what FL350 means", and neither is served by
 * the other's unit alone.
 */
export function formatAircraftDetail(aircraft: AircraftDetailInput): AircraftDetailView {
  const callsign = (aircraft.callsign ?? "").trim();
  const hex = aircraft.id.trim();
  const synthetic = aircraft.synthetic === true;
  const feet = Number.isFinite(aircraft.altitude) ? aircraft.altitude * METRES_TO_FEET : Number.NaN;

  const rows: AircraftDetailRow[] = [
    {
      label: "Altitude",
      value: Number.isFinite(feet)
        ? `${Math.round(feet).toLocaleString()} ft · ${Math.round(aircraft.altitude).toLocaleString()} m`
        : "—",
    },
    {
      label: "Heading",
      value: Number.isFinite(aircraft.heading)
        ? `${Math.round(((aircraft.heading % 360) + 360) % 360)}° ${compassPoint(aircraft.heading)}`
        : "—",
    },
  ];

  /**
   * What it is, above what it is doing.
   *
   * The type designator and the tail number are the two things that turn a dart
   * into an aeroplane — "B739 · N68834" is a specific 737 with a history, and a
   * hex address is a number. Both community feeds have carried them all along,
   * under the same terms as the position, so this row costs an anonymous
   * visitor nothing and is the half of the card they actually read.
   *
   * First, and `unshift` rather than a fourth entry, because identity reads
   * before state — and absent entirely rather than "unknown" when the feed said
   * nothing, which is every simulated track.
   */
  const airframe = [aircraft.type, aircraft.registration]
    .map((value) => (value ?? "").trim())
    .filter((value) => value !== "");
  if (airframe.length > 0) rows.unshift({ label: "Aircraft", value: airframe.join(" · ") });

  /**
   * Speed and climb, which are what say *landing at SFO* rather than *over the
   * Peninsula*. Knots and feet per minute because those are the units the
   * numbers are read in; a signed climb because the sign is the whole message.
   *
   * `typeof` rather than a truthiness test: a stationary aircraft reports zero
   * knots and a cruising one reports zero feet per minute, and both are facts.
   */
  if (typeof aircraft.groundSpeedKt === "number" && Number.isFinite(aircraft.groundSpeedKt)) {
    rows.push({
      label: "Ground speed",
      value: `${Math.round(aircraft.groundSpeedKt).toLocaleString()} kt`,
    });
  }
  if (typeof aircraft.verticalRateFpm === "number" && Number.isFinite(aircraft.verticalRateFpm)) {
    rows.push({ label: "Climb", value: formatVerticalRate(aircraft.verticalRateFpm) });
  }

  rows.push({
    label: "Position",
    value: `${formatCoordinate(aircraft.lat, "lat")} · ${formatCoordinate(aircraft.lng, "lng")}`,
  });

  return {
    title: callsign !== "" ? callsign : hex !== "" ? hex.toUpperCase() : "Unknown aircraft",
    subtitle: synthetic
      ? "Simulated track · no receiver involved"
      : hex !== ""
        ? `Mode S ${hex.toUpperCase()}`
        : "No transponder address reported",
    rows,
    synthetic,
    attribution: aircraft.attribution ?? null,
  };
}

/**
 * Draw the card into a host element.
 *
 * The host is `#detail-text` — the same slot a company marker's one-liner uses —
 * because the rail already solves the hard parts of putting a card over a 3D
 * scene on both layouts: it is safe-area inset, it is full width on a phone, and
 * it already has a dismiss the thumb can find. A second card type would have had
 * to solve all three again.
 */
export function renderAircraftDetail(
  host: HTMLElement,
  view: AircraftDetailView,
): void {
  const doc = host.ownerDocument;
  const root = doc.createElement("div");
  root.className = "aircraft-card";

  const title = doc.createElement("p");
  title.className = "aircraft-card__title";
  title.textContent = view.title;

  const subtitle = doc.createElement("p");
  subtitle.className = "aircraft-card__subtitle t-caps";
  subtitle.textContent = view.subtitle;

  const list = doc.createElement("dl");
  list.className = "aircraft-card__rows";
  for (const row of view.rows) {
    const term = doc.createElement("dt");
    term.textContent = row.label;
    const value = doc.createElement("dd");
    value.textContent = row.value;
    list.append(term, value);
  }

  root.append(title, subtitle, list);

  if (view.attribution !== null && view.attribution !== "") {
    const credit = doc.createElement("p");
    credit.className = "aircraft-card__credit";
    credit.textContent = view.attribution;
    root.append(credit);
  }

  host.replaceChildren(root);
}
