// ui/comms.js (Lane E) — the surgical team on the other end of the feed.
//
// GDD calls the tone "Star Fox arcade flow" and "playful-gross medical sci-fi" but the game
// had nobody in it except you. This is the comms window: a voice that tutorialises L1, calls
// your position so the tube stops being anonymous black, and carries the humour the docs
// describe but never sited anywhere.
//
// No portrait art (D has a hero-mesh queue and doesn't need faces on it) — a signal panel
// with a line-work waveform reads more "instrument feed" than a face would anyway.
//
// Clock: its own timer, NOT player:state. player.js:103 early-returns before the emit while
// dead, so player:state stops exactly when the death line needs to show and dismiss.
// Round-3 note filed to B in NOTES.

const WHO = {
  voss:    { name: 'voss',    role: 'attending',  color: '#39e6ff' },  // terse; the calm one
  park:    { name: 'park',    role: 'resident',   color: '#7dffb0' },  // too excited, always
  adeyemi: { name: 'adeyemi', role: 'immunology', color: '#ffb13a' },  // apologetic — and amber,
};                                                                     // the colour of the cells
                                                                       // attacking you. His fault.
// Lines are grouped by trigger key. Delivery rotates through each group deterministically —
// no RNG at all: this repo hashes and seeds everything, and round-robin has the nicer
// property anyway that it cannot repeat a line back-to-back.
const LINES = {
  start: [
    ['voss', 'endo-1, you are in. mind the walls — that is a patient.'],
  ],
  checkpoint: [
    ['voss', (n) => `mark. ${n}.`],
    ['park', (n) => `${n}! i have only ever seen that in the atlas.`],
    ['voss', (n) => `${n}. on schedule.`],
  ],
  warn_aortic_squeeze: [
    ['voss', 'aortic arch — the wall squeezes on the beat. thread it.'],
  ],
  warn_reflux_surge: [
    ['voss', 'reflux. it is coming up behind you. go. GO.'],
  ],
  // keys are the real HAZARDS ids (levels/enemies.js) — checked, not guessed. L2 authors
  // exactly three. The lines teach what C's registry note says the hazard IS; this is the
  // tutorialisation channel the GDD asks for and L1 has no other vehicle for.
  warn_ring_gate: [
    ['voss', 'ring gate. it opens on a beat — match the beat, do not ram it.'],
    ['park', 'peristaltic ring! ease the throttle, arrive when it opens!'],
  ],
  warn: [
    ['voss', (k) => `hazard ahead — ${String(k).replace(/_/g, ' ')}.`],
  ],
  hull_hit: [
    ['park', 'that was hull! that was the actual ship!'],
    ['voss', 'you are bleeding structure. do not trade hits.'],
  ],
  coat_hit: [
    ['adeyemi', 'coat is taking it. that is what it is for.'],
  ],
  coat_low: [
    ['adeyemi', 'your coat is nearly gone. find mucin, please.'],
    ['voss', 'coat critical. next hit is hull.'],
  ],
  sample: [
    ['park', 'specimen! oh, that is going straight in my thesis.'],
    ['park', 'got it! that is three years of somebody’s grant.'],
  ],
  surf: [
    ['park', 'he is riding the peristalsis. he is RIDING it.'],
  ],
  combo: [
    ['park', (n) => `${n} in a row! did everyone see that?`],
    ['voss', 'do not showboat in a live patient.'],
  ],
  death: [
    ['voss', 'we lost the feed. re-acquiring.'],
    ['adeyemi', 'that was the immune system. i am sorry. they think you are a parasite.'],
  ],
  boss: [
    ['voss', 'that is the pylorus. it does not open for you.'],
  ],
  complete: [
    ['voss', 'clear. next segment.'],
  ],
};

// seconds a line holds before it yields; higher pri interrupts a lower one mid-line
const HOLD = 3.0;
const PRI = { death: 9, boss: 8, warn: 7, complete: 6, coat_low: 5, hull_hit: 4, checkpoint: 3 };
// per-key silence so the crew doesn't chatter over a firefight
const COOLDOWN = { coat_hit: 12, hull_hit: 9, combo: 8, surf: 20, checkpoint: 0, sample: 0 };

// Keys are `family` or `family_variant` (`warn` / `warn_reflux_surge`). Look up the exact key
// first, then fall back to its family — otherwise every bespoke hazard line inherits the
// default priority of 1 and the surge warning, the single most urgent line in the level, can
// be talked over by a resident admiring a specimen. Exact keys still win: `hull_hit` is 4, not
// whatever `hull` would be.
const family = (key) => key.slice(0, key.indexOf('_') + 1 || key.length).replace(/_$/, '');
const priOf = (key) => PRI[key] ?? PRI[family(key)] ?? 1;
const cdOf = (key) => COOLDOWN[key] ?? COOLDOWN[family(key)] ?? 0;

const CSS = `
.comms { position:absolute; left:18px; bottom:70px; width:330px;
  font:11px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing:.1em; text-transform:uppercase;
  border-left:1px solid currentColor; padding:6px 0 6px 9px;
  opacity:0; transition:opacity .18s ease-out; text-shadow:0 0 6px rgba(0,0,0,.95); }
.comms.on { opacity:.95; }
.comms .who { display:flex; align-items:center; gap:7px; font-size:9px; letter-spacing:.22em; }
.comms .role { opacity:.45; }
.comms .txt { color:#dff2ff; margin-top:3px; letter-spacing:.08em; opacity:.9;
  text-transform:none; font-size:12px; }
/* the "carrier" — line-work waveform, CSS-animated so a talking head costs zero JS/frame */
.comms .wave { display:flex; align-items:flex-end; gap:2px; height:9px; }
.comms .wave i { width:2px; height:100%; background:currentColor; transform-origin:bottom;
  animation:commsWave .52s ease-in-out infinite; }
.comms .wave i:nth-child(2){ animation-delay:.09s } .comms .wave i:nth-child(3){ animation-delay:.18s }
.comms .wave i:nth-child(4){ animation-delay:.27s } .comms .wave i:nth-child(5){ animation-delay:.36s }
@keyframes commsWave { 0%,100%{ transform:scaleY(.25) } 50%{ transform:scaleY(1) } }
@media (prefers-reduced-motion:reduce){ .comms .wave i{ animation:none; transform:scaleY(.6) } }
`;

export function createComms({ bus, flags = {}, mount = null } = {}) {
  const host = mount ?? document.getElementById('ui');
  if (!host || flags.shots) return { dispose() {} };   // ?shots=1 = clean plate, same as the HUD

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

  const root = document.createElement('div');
  root.className = 'comms';
  root.innerHTML = `<div class="who"><span class="wave"><i></i><i></i><i></i><i></i><i></i></span>` +
    `<span class="name"></span><span class="role"></span></div><div class="txt"></div>`;
  host.appendChild(root);
  const nameEl = root.querySelector('.name');
  const roleEl = root.querySelector('.role');
  const txtEl = root.querySelector('.txt');

  const rot = new Map();      // key -> next index (deterministic rotation)
  const last = new Map();     // key -> clock reading at last delivery

  // Wall-clock, read — never accumulated. Counting `+= 0.1` per timer tick assumes the timer
  // is punctual; a backgrounded tab throttles setTimeout to ~1s and the count silently becomes
  // fiction (lines then hold ~10x too long). It must be wall time and not sim time because the
  // one moment this box MUST work — death — is the moment player:state stops emitting.
  const now = () => performance.now() / 1000;
  const t0 = now();
  let clock = 0, current = null, timer = null, disposed = false;

  const tick = () => {
    if (disposed) return;
    clock = now() - t0;
    if (current && clock >= current.until) hide();
    timer = setTimeout(tick, 100);
  };
  timer = setTimeout(tick, 100);

  function hide() {
    current = null;
    root.classList.remove('on');
  }

  function say(key, arg) {
    const group = LINES[key];
    if (!group?.length) return;
    const cd = cdOf(key);
    if (cd && clock - (last.get(key) ?? -1e9) < cd) return;
    const pri = priOf(key);
    if (current && pri < current.pri) return;          // never talk over something worse

    const i = rot.get(key) ?? 0;
    rot.set(key, (i + 1) % group.length);
    const [whoId, line] = group[i];
    const who = WHO[whoId];

    nameEl.textContent = who.name;
    roleEl.textContent = who.role;
    root.style.color = who.color;
    txtEl.textContent = typeof line === 'function' ? line(arg) : line;
    root.classList.add('on');

    last.set(key, clock);
    current = { pri, until: clock + HOLD };
    bus.emit('audio:cue', { name: 'comms_open' });     // E's own audio engine picks this up
  }

  const offs = [];
  offs.push(bus.on('level:event', (ev) => {
    if (ev.type === 'checkpoint' && ev.name) say('checkpoint', ev.name);
    else if (ev.type === 'boss') say('boss');
  }));
  offs.push(bus.on('hazard:warn', (e) => {
    const k = `warn_${e?.kind}`;
    say(LINES[k] ? k : 'warn', e?.kind);
  }));
  offs.push(bus.on('player:damage', (e) => say(e?.kind === 'hull_hit' ? 'hull_hit' : 'coat_hit')));
  offs.push(bus.on('player:death', () => say('death')));
  offs.push(bus.on('level:complete', () => say('complete')));
  offs.push(bus.on('pickup', (e) => { if (e?.kind === 'biopsy_sample') say('sample'); }));
  offs.push(bus.on('combo', (e) => { if ((e?.n ?? 0) >= 5) say('combo', e.n); }));
  offs.push(bus.on('player:surf', (e) => { if (e?.active) say('surf'); }));

  // coat_low is the one continuous signal worth a line — edge-triggered here so it fires on
  // the crossing, not every frame we're under the threshold.
  let wasLow = false;
  offs.push(bus.on('player:state', (p) => {
    const low = p.coatMax ? p.coat / p.coatMax < 0.25 : false;
    if (low && !wasLow) say('coat_low');
    wasLow = low;
  }));

  say('start');

  return {
    say,                                   // exposed for the ?fakebus harness + round-3 scripting
    dispose() {
      disposed = true;
      clearTimeout(timer);
      for (const off of offs) off();
      root.remove();
      style.remove();
    },
  };
}
