/* audio.jsx — site-wide sound: sonified simulations + spoken narration.
   Loaded on every page after shared.jsx. Everything is OFF until the user
   flips the sound toggle (persisted in localStorage 'hte-sound'). No assets —
   sonification uses Web Audio, narration uses the browser speechSynthesis.

   Exports to window:
     useSound()            → { on, setOn }       global on/off (persisted)
     SoundToggleButton     → the fab button (added to the fab-stack)
     hteTone({...})        → play a short tone
     hteClick()            → a soft UI click
     useToneFollow(...)    → live-pitch a sim value (pitch=freq, gain=current)
     useBlinkBeep(...)     → click/beep in time with a blink rate (the 555!)
     NarrateButton         → a "Listen" button that reads given text aloud
*/

const { useState: useStateA, useEffect: useEffectA, useRef: useRefA } = React;

/* ─── shared AudioContext (created lazily on first real sound) ────────────── */
let _ac = null;
let _master = null;
function hteAC() {
  if (_ac) return _ac;
  const AC = window.AudioContext || window.webkitAudioContext;
  if (!AC) return null;
  _ac = new AC();
  _master = _ac.createGain();
  _master.gain.value = 0.5;
  _master.connect(_ac.destination);
  return _ac;
}
function hteResume() { const ac = hteAC(); if (ac && ac.state === "suspended") ac.resume(); }

/* ─── global sound state ─────────────────────────────────────────────────── */
function hteSoundOn() {
  try { return localStorage.getItem("hte-sound") === "1"; } catch (e) { return false; }
}
function hteSetSound(on) {
  try { localStorage.setItem("hte-sound", on ? "1" : "0"); } catch (e) {}
  if (!on && window.speechSynthesis) window.speechSynthesis.cancel();
  window.dispatchEvent(new CustomEvent("hte-sound-changed", { detail: { on } }));
}
function useSound() {
  const [on, setOnState] = useStateA(hteSoundOn());
  useEffectA(() => {
    const h = (e) => setOnState(e.detail.on);
    window.addEventListener("hte-sound-changed", h);
    return () => window.removeEventListener("hte-sound-changed", h);
  }, []);
  const setOn = (v) => { hteSetSound(v); if (v) hteResume(); };
  return { on, setOn };
}

/* ─── tone helpers ───────────────────────────────────────────────────────── */
function hteTone({ freq = 440, dur = 0.12, type = "sine", gain = 0.18, glideTo = null }) {
  if (!hteSoundOn()) return;
  const ac = hteAC(); if (!ac) return;
  hteResume();
  const t = ac.currentTime;
  const osc = ac.createOscillator();
  const g = ac.createGain();
  osc.type = type;
  osc.frequency.setValueAtTime(freq, t);
  if (glideTo) osc.frequency.exponentialRampToValueAtTime(Math.max(1, glideTo), t + dur);
  g.gain.setValueAtTime(0.0001, t);
  g.gain.exponentialRampToValueAtTime(gain, t + 0.012);
  g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
  osc.connect(g); g.connect(_master);
  osc.start(t); osc.stop(t + dur + 0.02);
}
function hteClick(strength = 1) {
  if (!hteSoundOn()) return;
  const ac = hteAC(); if (!ac) return;
  hteResume();
  const t = ac.currentTime;
  // short filtered noise burst → a "tick"
  const buf = ac.createBuffer(1, 256, ac.sampleRate);
  const d = buf.getChannelData(0);
  for (let i = 0; i < d.length; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / d.length);
  const src = ac.createBufferSource(); src.buffer = buf;
  const bp = ac.createBiquadFilter(); bp.type = "bandpass"; bp.frequency.value = 1400; bp.Q.value = 0.8;
  const g = ac.createGain(); g.gain.value = 0.22 * strength;
  src.connect(bp); bp.connect(g); g.connect(_master);
  src.start(t);
}

/* map a normalized 0..1 value to a pleasant pitch range (a pentatonic-ish sweep) */
function hteValToFreq(norm) {
  const lo = 220, hi = 1320; // A3 → E6-ish
  return lo * Math.pow(hi / lo, Math.max(0, Math.min(1, norm)));
}

/* ─── useToneFollow ──────────────────────────────────────────────────────────
   Sustains a tone whose pitch tracks `norm` (0..1) and gain tracks `level`
   (0..1). Use for "hear the current" — call setActive(true) while dragging. */
function useToneFollow() {
  const ref = useRefA({ osc: null, gain: null });
  const stop = () => {
    const ac = _ac; const s = ref.current;
    if (s.osc && ac) {
      try { s.gain.gain.cancelScheduledValues(ac.currentTime);
            s.gain.gain.setTargetAtTime(0.0001, ac.currentTime, 0.04);
            s.osc.stop(ac.currentTime + 0.2); } catch (e) {}
    }
    ref.current = { osc: null, gain: null };
  };
  const set = (norm, level = 0.5) => {
    if (!hteSoundOn()) { stop(); return; }
    const ac = hteAC(); if (!ac) return; hteResume();
    let s = ref.current;
    if (!s.osc) {
      const osc = ac.createOscillator(); const g = ac.createGain();
      osc.type = "triangle"; g.gain.value = 0.0001;
      osc.connect(g); g.connect(_master); osc.start();
      s = ref.current = { osc, gain: g };
    }
    s.osc.frequency.setTargetAtTime(hteValToFreq(norm), ac.currentTime, 0.03);
    s.gain.gain.setTargetAtTime(0.0001 + 0.16 * Math.max(0, Math.min(1, level)), ac.currentTime, 0.03);
  };
  useEffectA(() => stop, []);
  return { set, stop };
}

/* ─── useBlinkBeep ───────────────────────────────────────────────────────────
   Ticks (and pulses a callback) in time with `hz`, while `active`. This is the
   555 made audible: pass the computed blink rate. onTick(isOn) fires each edge. */
function useBlinkBeep(hz, active, onTick) {
  const cb = useRefA(onTick); cb.current = onTick;
  const soundRef = useRefA(hteSoundOn());
  useEffectA(() => {
    const h = (e) => { soundRef.current = e.detail.on; };
    window.addEventListener("hte-sound-changed", h);
    return () => window.removeEventListener("hte-sound-changed", h);
  }, []);
  useEffectA(() => {
    if (!active || !hz || hz <= 0) return;
    const halfMs = Math.max(40, 500 / hz);
    let on = false;
    const id = setInterval(() => {
      on = !on;
      if (cb.current) cb.current(on);
      if (on && soundRef.current) hteClick(1);
    }, halfMs);
    return () => clearInterval(id);
  }, [hz, active]);
}

/* ─── narration (speechSynthesis) ────────────────────────────────────────── */
let _voice = null;
function htePickVoice() {
  if (!window.speechSynthesis) return null;
  if (_voice) return _voice;
  const vs = window.speechSynthesis.getVoices();
  // prefer a natural en voice
  _voice = vs.find(v => /en[-_]?(GB|US)/i.test(v.lang) && /natural|google|samantha|daniel/i.test(v.name))
         || vs.find(v => /^en/i.test(v.lang)) || vs[0] || null;
  return _voice;
}
function hteSpeak(text, { onEnd } = {}) {
  if (!window.speechSynthesis || !text) { if (onEnd) onEnd(); return; }
  window.speechSynthesis.cancel();
  const u = new SpeechSynthesisUtterance(text);
  const v = htePickVoice(); if (v) u.voice = v;
  u.rate = 0.98; u.pitch = 1.0;
  if (onEnd) u.onend = onEnd;
  window.speechSynthesis.speak(u);
}
/* ─── read-as-I-scroll: a hands-free narrator ────────────────────────────────
   When armed, it watches the page's lesson beats / sections and reads the one
   currently centered in the viewport aloud, advancing automatically as you
   scroll. A floating control bar lets you arm/pause it. Only relevant when
   sound is on. Reads elements matching `.beat, .section, .cover-page, .story-panel`
   that carry visible heading + prose; we extract their text on the fly. */
function hteReadableText(el) {
  if (!el) return "";
  // pull the heading + lead paragraphs, skip slider/readout chrome
  const parts = [];
  const h = el.querySelector("h1, h2, h3");
  if (h) parts.push(hteNodeText0(h));
  el.querySelectorAll(":scope p, :scope .lede, :scope .marg").forEach((p) => {
    // skip tiny captions and mono blocks
    if (p.closest(".vis-readout, .slider, .pj-result, .quiz, .pp-item")) return;
    const t = hteNodeText0(p);
    if (t && t.length > 12) parts.push(t);
  });
  return parts.join(". ").replace(/\s+/g, " ").trim();
}
function hteNodeText0(node) {
  // DOM textContent but collapse whitespace and strip sub/sup oddities
  return (node.textContent || "").replace(/\s+/g, " ").trim();
}

function ReadAlongBar() {
  const { on } = useSound();
  const [armed, setArmed] = useStateA(false);
  const [label, setLabel] = useStateA("");
  const lastEl = useRefA(null);
  const armedRef = useRefA(false);
  armedRef.current = armed;

  // pick the beat/section whose center is nearest the viewport center
  useEffectA(() => {
    if (!on || !armed) return;
    let raf = null;
    const sel = ".beat, section.section, .cover-page, .story-panel";
    const pick = () => {
      const mid = window.innerHeight / 2;
      let best = null, bestD = Infinity;
      document.querySelectorAll(sel).forEach((el) => {
        const r = el.getBoundingClientRect();
        if (r.bottom < 40 || r.top > window.innerHeight - 40) return;
        const d = Math.abs((r.top + r.bottom) / 2 - mid);
        if (d < bestD) { bestD = d; best = el; }
      });
      if (best && best !== lastEl.current) {
        const text = hteReadableText(best);
        if (text) {
          lastEl.current = best;
          const lbl = best.getAttribute("data-screen-label")
                   || (best.querySelector("h1,h2,h3") ? hteNodeText0(best.querySelector("h1,h2,h3")) : "");
          setLabel(lbl);
          hteSpeak(text);
        }
      }
    };
    const onScroll = () => { if (raf) return; raf = requestAnimationFrame(() => { raf = null; pick(); }); };
    pick();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => { window.removeEventListener("scroll", onScroll); if (raf) cancelAnimationFrame(raf); };
  }, [on, armed]);

  // disarm + hush when sound turns off
  useEffectA(() => { if (!on && armed) { setArmed(false); hteStopSpeak(); } }, [on]);
  useEffectA(() => () => hteStopSpeak(), []);

  if (!on) return null;
  const toggle = () => {
    const next = !armed;
    setArmed(next);
    if (!next) { hteStopSpeak(); lastEl.current = null; setLabel(""); }
  };
  return (
    <div className={`readalong ${armed ? "on" : ""}`} role="region" aria-label="Read-along narrator">
      <button className="readalong-btn" onClick={toggle}
              aria-pressed={armed}
              title={armed ? "Stop reading as you scroll" : "Read each section aloud as you scroll"}>
        <span className="readalong-ico" aria-hidden="true">{armed ? "❙❙" : "►"}</span>
        {armed ? "Reading along" : "Read as I scroll"}
      </button>
      {armed && label && <span className="readalong-now">{label}</span>}
    </div>
  );
}

function hteStopSpeak() { if (window.speechSynthesis) window.speechSynthesis.cancel(); }

/* flatten React children / strings to a plain narratable string */
function hteNodeText(node) {
  if (node == null || node === false) return "";
  if (typeof node === "string" || typeof node === "number") return String(node);
  if (Array.isArray(node)) return node.map(hteNodeText).join(" ");
  if (node.props && node.props.children) return hteNodeText(node.props.children);
  return "";
}

/* "Listen" button — reads `text` aloud. Only visible when sound is on. */
function NarrateButton({ text, label = "Listen" }) {
  const { on } = useSound();
  const [speaking, setSpeaking] = useStateA(false);
  if (!on) return null;
  const toggle = () => {
    if (speaking) { hteStopSpeak(); setSpeaking(false); return; }
    setSpeaking(true);
    hteSpeak(typeof text === "function" ? text() : text, { onEnd: () => setSpeaking(false) });
  };
  return (
    <button className={`narrate-btn ${speaking ? "on" : ""}`} onClick={toggle}
            aria-label={speaking ? "Stop narration" : "Listen to this section"}>
      <span className="narrate-ico" aria-hidden="true">{speaking ? "■" : "►"}</span>
      {speaking ? "Stop" : label}
    </button>
  );
}

/* the fab toggle (added into the existing .fab-stack via GlossaryFab) */
function SoundToggleButton() {
  const { on, setOn } = useSound();
  return (
    <button className={`sound-fab ${on ? "on" : ""}`} onClick={() => setOn(!on)}
            aria-pressed={on} aria-label={on ? "Turn sound off" : "Turn sound on"}
            title={on ? "Sound on — click to mute" : "Sound off — click for audio & narration"}>
      <span className="gf-q" aria-hidden="true">{on ? "♪" : "♪"}</span>
      <span className="gf-label">{on ? "Sound on" : "Sound"}</span>
    </button>
  );
}

// warm up voices list (some browsers populate async)
if (window.speechSynthesis) {
  window.speechSynthesis.onvoiceschanged = () => { _voice = null; htePickVoice(); };
}

/* a small inline hint shown only when sound is on, inviting the user to listen */
function SonifyHint({ text }) {
  const { on } = useSound();
  if (!on) return null;
  return (
    <div className="sonify-hint">
      <span className="sonify-hint-ico" aria-hidden="true">♪</span>
      {text || "Sound's on — drag the sliders and listen: pitch rises with the current."}
    </div>
  );
}

Object.assign(window, {
  useSound, SoundToggleButton, NarrateButton, SonifyHint, ReadAlongBar,
  hteTone, hteClick, hteValToFreq, useToneFollow, useBlinkBeep,
  hteSpeak, hteStopSpeak, hteSoundOn, hteNodeText,
});
