// ui/hud.js (Lane E) — the instrument overlay. ART_BIBLE §Screens: thin cyan line-work,
// small caps, data-noise ticks. An instrument reading a live feed, not a game HUD.
//
// Bus-driven only (LANE_E charter): player:state + combat:state arrive every frame and ARE
// the clock — no rAF of our own, so the HUD cannot tick while the game is paused or absent.
// Level data comes from C's registry (sanctioned: ROUND2 §Lane E #2), never from B.
//
// 60fps law: build the DOM once, then only transform + textContent-on-change. Bars scale,
// they never resize — a width:% write per bar per frame is a layout pass we can't afford.

const C = {
  line: '#39e6ff',   // cyan line-work — ART_BIBLE emissive code, neutral/interactive
  good: '#7dffb0',
  warn: '#ffb13a',
  bad: '#ff5a2a',
  surf: '#b06aff',
  ally: '#33ffbe',   // teal — matches EMISSIVE.ally; the flora/phage reputation
  allyHot: '#c9fff0', // near-full: an ally is imminent
};

const RAIL_W = 190;   // px; shared by the CSS and the blip transform below

const CSS = `
.hud { position:absolute; inset:0; color:${C.line};
  font:11px/1.35 ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing:.14em; text-transform:uppercase;
  text-shadow:0 0 6px rgba(0,0,0,.95); opacity:.92; }
.hud .q { position:absolute; display:flex; flex-direction:column; gap:5px; }
.hud .tl { top:16px; left:18px; }
.hud .tr { top:16px; right:18px; align-items:flex-end; }
.hud .bl { bottom:18px; left:18px; }
.hud .br { bottom:18px; right:18px; align-items:flex-end; }
.hud .bc { bottom:18px; left:50%; transform:translateX(-50%); align-items:center; }
.hud .dim { opacity:.5; }
.hud .row { display:flex; align-items:center; gap:8px; white-space:nowrap; }
.hud .lbl { font-size:9px; opacity:.65; letter-spacing:.2em; }

/* bars: a hairline rail + a fill that only ever scales */
.hud .rail { position:relative; width:132px; height:5px;
  border:1px solid currentColor; opacity:.85; }
.hud .fill { position:absolute; inset:1px; background:currentColor;
  transform-origin:left center; transform:scaleX(1); }
.hud .rail.thin { height:3px; width:96px; }

/* progress rail — the gut-map's honest placeholder until the silhouette lands */
.hud .prog { position:relative; width:${RAIL_W}px; height:2px; background:currentColor;
  opacity:.35; margin-top:3px; }
.hud .blip { position:absolute; top:-3px; left:0; width:2px; height:8px;
  background:${C.line}; box-shadow:0 0 5px ${C.line}; transform:translateX(0); }
.hud .tick { position:absolute; top:-2px; width:1px; height:6px; background:${C.surf}; opacity:.6; }

.hud .big { font-size:15px; letter-spacing:.16em; }
.hud .chain { color:${C.warn}; font-size:12px; }
.hud .noise { font-size:9px; opacity:.28; letter-spacing:.3em; }
.hud .pips { display:flex; gap:3px; }
.hud .pip { width:6px; height:6px; border:1px solid currentColor; }
.hud .pip.on { background:currentColor; }
`;

const el = (tag, cls, parent) => {
  const n = document.createElement(tag);
  if (cls) n.className = cls;
  if (parent) parent.appendChild(n);
  return n;
};

// A label + rail whose fill scales. set() is the hot path: one transform write, and a color
// write only when the danger band actually changes.
function makeBar(parent, label, thin = false) {
  const row = el('div', 'row', parent);
  const lbl = el('span', 'lbl', row);
  lbl.textContent = label;
  const rail = el('div', `rail${thin ? ' thin' : ''}`, row);
  const fill = el('div', 'fill', rail);
  let lastV = -1, lastC = '';
  return {
    set(v, color) {
      v = v > 0 ? (v < 1 ? v : 1) : 0;
      if (v !== lastV) { fill.style.transform = `scaleX(${v})`; lastV = v; }
      if (color !== lastC) { rail.style.color = color; lastC = color; }
    },
  };
}

function makeText(parent, cls) {
  const n = el('span', cls, parent);
  let last = null;
  return { set(s) { if (s !== last) { n.textContent = s; last = s; } }, node: n };
}

export function createHUD({ bus, flags = {}, level = null, mount = null } = {}) {
  const host = mount ?? document.getElementById('ui');
  if (!host) return { dispose() {} };

  const style = el('style');
  style.textContent = CSS;
  document.head.appendChild(style);

  const root = el('div', 'hud', host);
  // ?shots=1 = clean plate for other lanes' world evidence (same contract boot's #dbg keeps).
  // E's own HUD shots are taken without the flag.
  if (flags.shots) root.style.display = 'none';

  const tl = el('div', 'q tl', root);
  const tr = el('div', 'q tr', root);
  const bl = el('div', 'q bl', root);
  const br = el('div', 'q br', root);
  const bc = el('div', 'q bc', root);

  // --- top-left: where we are -----------------------------------------------------------
  const where = makeText(tl, 'big');
  where.set(level?.name ?? '—');
  const depth = makeText(tl, 'dim');
  const progWrap = el('div', null, tl);
  const prog = el('div', 'prog', progWrap);
  const blip = el('div', 'blip', prog);

  // Checkpoint ticks, drawn once from C's authored events. Deferred to the first player:state
  // because the canal's length is a RUNTIME number (world.length, which B already divides into
  // p.progress) — the level object carries segments, not a total, and asking it for one is how
  // the rail silently rendered empty the first time.
  const checkpoints = (level?.events ?? []).filter((ev) => ev.type === 'checkpoint');
  let ticksPlaced = false;

  // --- top-right: the scoreboard --------------------------------------------------------
  const score = makeText(tr, 'big');
  score.set('000000');
  const chain = makeText(tr, 'chain');
  const noise = makeText(tr, 'noise');

  // --- bottom-left: what's keeping you alive --------------------------------------------
  const coat = makeBar(bl, 'coat');
  const hull = makeBar(bl, 'hull');
  // biome standing — the flora reputation. Fills as you tend flora; a full bar buds a phage
  // ally (it drops to the recharge floor). Starts hidden and reveals on the first `standing`
  // event, so a level with no flora never shows an empty bar the player can't act on.
  const biomeRow = el('div', 'row', bl);
  biomeRow.style.display = 'none';
  const biomeLbl = el('span', 'lbl', biomeRow);
  biomeLbl.textContent = 'biome';
  const biomeRail = el('div', 'rail thin', biomeRow);
  const biomeFill = el('div', 'fill', biomeRail);

  // --- bottom-right: what you're shooting with ------------------------------------------
  const heat = makeBar(br, 'heat', true);
  const ammoRow = el('div', 'row', br);
  const ammoLbl = el('span', 'lbl', ammoRow);
  ammoLbl.textContent = 'torpedo';
  const pips = el('div', 'pips', ammoRow);
  let pipEls = [];

  // --- bottom-centre: the flight strip --------------------------------------------------
  const strip = el('div', 'row', bc);
  const spd = makeText(strip, 'big');
  const flowT = makeText(strip, 'dim');
  const boostT = makeText(strip, null);

  // --- bus ------------------------------------------------------------------------------
  const offs = [];
  let frame = 0;

  offs.push(bus.on('player:state', (p) => {
    const coatF = p.coatMax ? p.coat / p.coatMax : 0;
    const hullF = p.hullMax ? p.hull / p.hullMax : 0;
    coat.set(coatF, coatF < 0.25 ? C.warn : C.line);
    hull.set(hullF, hullF < 0.34 ? C.bad : hullF < 0.67 ? C.warn : C.good);

    depth.set(`s ${(p.s | 0).toString().padStart(4, '0')} / ${p.length | 0} · ${p.biome ?? ''}`);
    blip.style.transform = `translateX(${(p.progress > 1 ? 1 : p.progress) * RAIL_W}px)`;

    if (!ticksPlaced && p.length > 0) {
      for (const ev of checkpoints) {
        el('div', 'tick', prog).style.left = `${(ev.s / p.length) * 100}%`;
      }
      ticksPlaced = true;
    }

    spd.set(`${p.speed.toFixed(1)}`);
    spd.node.style.color = p.surfing ? C.surf : C.line;
    flowT.set(`u/s · flow ${p.flow.toFixed(0)} · thr ${(p.throttle * 100) | 0}%`);
    if (p.boostReady) { boostT.set('· boost rdy'); boostT.node.style.color = C.good; }
    else {
      boostT.set(`· boost ${(p.boostCd ?? 0).toFixed(1)}`);
      boostT.node.style.color = C.line;
      boostT.node.style.opacity = '.45';
    }

    // data-noise ticks — the feed is alive even when nothing is happening. 4 Hz-ish, and
    // derived from s so it reads as telemetry rather than a random number generator.
    if ((frame++ & 15) === 0) {
      noise.set(`0x${(((p.s * 4099) ^ 0x5f3a) & 0xffff).toString(16).padStart(4, '0')}`);
    }
  }));

  offs.push(bus.on('combat:state', (c) => {
    const heatF = c.heatMax ? c.heat / c.heatMax : 0;
    heat.set(heatF, c.overheated ? C.bad : heatF > 0.7 ? C.warn : C.line);
    score.set(((c.score | 0) % 1000000).toString().padStart(6, '0'));

    if (pipEls.length !== (c.ammoMax | 0)) {           // rebuild only when the cap changes
      pips.textContent = '';
      pipEls = Array.from({ length: c.ammoMax | 0 }, () => el('div', 'pip', pips));
    }
    for (let i = 0; i < pipEls.length; i++) pipEls[i].classList.toggle('on', i < c.ammo);
  }));

  offs.push(bus.on('combo', (e) => chain.set(e.n > 1 ? `×${e.n} chain` : '')));

  // biome standing — reveal on first event, then scale-only (60fps law). Brightens near full
  // to telegraph an incoming ally.
  let biomeShown = false, lastStand = -1, lastStandC = '';
  offs.push(bus.on('standing', (e) => {
    if (!biomeShown) { biomeRow.style.display = ''; biomeShown = true; }
    const f = Math.max(0, Math.min(1, (e.value || 0) / 100));
    if (f !== lastStand) { biomeFill.style.transform = `scaleX(${f})`; lastStand = f; }
    const col = f >= 0.9 ? C.allyHot : C.ally;
    if (col !== lastStandC) { biomeRail.style.color = col; lastStandC = col; }
  }));

  offs.push(bus.on('level:event', (ev) => {
    if (ev.type === 'checkpoint' && ev.name) where.set(ev.name);
  }));

  return {
    dispose() {
      for (const off of offs) off();
      root.remove();
      style.remove();
    },
  };
}
