/* global React */

// ─── Audio bus: guarantees only one source plays at a time ────────────
// Any audio source (hero tiles, the example player, etc.) calls
// __audioBus.claim(id) when it starts; every subscriber stops itself
// when it sees a claim with a different id.
(function installAudioBus() {
  if (window.__audioBus) return;
  const listeners = new Set();
  window.__audioBus = {
    claim(id) { listeners.forEach((fn) => { try { fn(id); } catch (_) {} }); },
    subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); },
  };
})();

// Shared AudioContext, lazily created on first user gesture.
function getAC() {
  if (!window.__hero_ac) {
    const Ctx = window.AudioContext || window.webkitAudioContext;
    if (!Ctx) return null;
    window.__hero_ac = new Ctx();
  }
  if (window.__hero_ac.state === "suspended") window.__hero_ac.resume();
  return window.__hero_ac;
}

// Hero narration is a pre-rendered ElevenLabs MP3 (audio/hero-narr.mp3).
// Voice is wired through the shared AudioContext so the waveform can be
// driven by a real AnalyserNode reading the actual playback.
const NARR_SRC = "audio/hero-narr.mp3";

function NarrationTile() {
  const BARS = 28;
  const [playing, setPlaying] = React.useState(false);
  const [unsupported] = React.useState(() => {
    if (typeof window === "undefined") return true;
    if (!(window.AudioContext || window.webkitAudioContext)) return true;
    const probe = document.createElement("audio");
    return !probe.canPlayType || probe.canPlayType("audio/mpeg") === "";
  });
  const barsRef = React.useRef(Array.from({length: BARS}, (_, i) => 0.2 + ((i * 17) % 60) / 100));
  const [, force] = React.useReducer((x) => x + 1, 0);
  const rafRef = React.useRef(0);
  const audioRef = React.useRef(null);
  const nodesRef = React.useRef({ src: null, analyser: null, data: null });

  // Build the audio element + analyser graph once. MediaElementSource can
  // only be created once per HTMLAudioElement, so we cache it.
  function setupGraph() {
    if (nodesRef.current.analyser) return true;
    const ac = getAC();
    if (!ac) return false;
    const audio = audioRef.current;
    if (!audio) return false;
    try {
      const src = ac.createMediaElementSource(audio);
      const analyser = ac.createAnalyser();
      analyser.fftSize = 256;
      analyser.smoothingTimeConstant = 0.6;
      src.connect(analyser);
      analyser.connect(ac.destination);
      nodesRef.current = { src, analyser, data: new Uint8Array(analyser.frequencyBinCount) };
      return true;
    } catch (_) {
      return false;
    }
  }

  function stop() {
    const a = audioRef.current;
    if (a) { try { a.pause(); a.currentTime = 0; } catch (_) {} }
    cancelAnimationFrame(rafRef.current);
    setPlaying(false);
  }

  React.useEffect(() => {
    return window.__audioBus.subscribe((id) => {
      if (id !== "hero-narr" && playing) stop();
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [playing]);

  // Create the audio element once; clean up on unmount.
  React.useEffect(() => {
    const a = new Audio(NARR_SRC);
    a.preload = "auto";
    a.addEventListener("ended", stop);
    a.addEventListener("error", stop);
    audioRef.current = a;
    return () => {
      a.removeEventListener("ended", stop);
      a.removeEventListener("error", stop);
      try { a.pause(); } catch (_) {}
    };
  }, []);

  function play() {
    if (unsupported) return;
    window.__audioBus.claim("hero-narr");
    const ok = setupGraph();
    if (!ok) return;
    const a = audioRef.current;
    try { a.currentTime = 0; } catch (_) {}
    const p = a.play();
    if (p && p.catch) p.catch(() => stop());
    setPlaying(true);

    const tick = () => {
      const { analyser, data } = nodesRef.current;
      if (analyser) {
        analyser.getByteFrequencyData(data);
        // Voice energy lives in the lower-mid range; average the first ~48 bins.
        let sum = 0;
        const lim = Math.min(48, data.length);
        for (let i = 2; i < lim; i++) sum += data[i];
        const avg = sum / Math.max(1, lim - 2) / 255;
        // Lift quiet passages a little so bars never dead-stop mid-sentence.
        const lvl = Math.max(0.18, Math.min(1, avg * 1.7));
        for (let i = 0; i < BARS; i++) {
          // Bow-tie: tall at the edges, dip in the middle.
          const bowtie = 0.4 + 0.6 * Math.abs(Math.cos((i / (BARS - 1)) * Math.PI));
          const n = 0.85 + Math.random() * 0.25;
          const target = Math.min(1, lvl * bowtie * n);
          barsRef.current[i] = barsRef.current[i] * 0.55 + target * 0.45;
        }
        force();
      }
      rafRef.current = requestAnimationFrame(tick);
    };
    rafRef.current = requestAnimationFrame(tick);
  }

  React.useEffect(() => () => stop(), []);

  return (
    <button
      type="button"
      className={"tile tile-b hero-play" + (playing ? " playing" : "")}
      onClick={() => (playing ? stop() : play())}
      disabled={unsupported}
      aria-label={playing ? "Stop narration" : "Play narration"}
    >
      <span className="tile-label">Narration</span>
      <span className="tile-idx">02</span>
      <span className={"hero-play-badge" + (playing ? " on" : "")} aria-hidden="true">
        {playing ? <window.Ic.pause /> : <window.Ic.play />}
      </span>
      <div className="wave wave-live">
        {barsRef.current.map((h, i) => (
          <span
            key={i}
            style={{
              height: `${Math.max(8, h * 100)}%`,
              transition: playing ? "height 60ms linear" : "height 240ms",
            }}
          />
        ))}
      </div>
      {unsupported && <span className="hero-unsupp">Audio unavailable</span>}
    </button>
  );
}

function MusicTile() {
  const BARS = 40;
  const [playing, setPlaying] = React.useState(false);
  const [unsupported] = React.useState(() => !(window.AudioContext || window.webkitAudioContext));
  const barsRef = React.useRef(Array.from({length: BARS}, (_, i) => 0.25 + ((i * 11) % 50) / 100));
  const [, force] = React.useReducer((x) => x + 1, 0);
  const refs = React.useRef({ nodes: [], analyser: null, raf: 0, data: null, master: null });

  function stop() {
    const r = refs.current;
    cancelAnimationFrame(r.raf);
    r.nodes.forEach((n) => {
      try { n.stop && n.stop(); } catch (_) {}
      try { n.disconnect && n.disconnect(); } catch (_) {}
    });
    r.nodes = [];
    if (r.master) { try { r.master.disconnect(); } catch (_) {} r.master = null; }
    if (r.analyser) { try { r.analyser.disconnect(); } catch (_) {} r.analyser = null; }
    setPlaying(false);
  }

  React.useEffect(() => {
    return window.__audioBus.subscribe((id) => {
      if (id !== "hero-music" && playing) stop();
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [playing]);

  function play() {
    if (unsupported) return;
    window.__audioBus.claim("hero-music");
    const ac = getAC();
    if (!ac) return;
    const now = ac.currentTime;

    const master = ac.createGain();
    master.gain.value = 0;
    master.gain.linearRampToValueAtTime(0.5, now + 0.8);

    const lp = ac.createBiquadFilter();
    lp.type = "lowpass"; lp.frequency.value = 2200; lp.Q.value = 0.6;

    const comp = ac.createDynamicsCompressor();
    comp.threshold.value = -14; comp.knee.value = 16; comp.ratio.value = 3;
    comp.attack.value = 0.01; comp.release.value = 0.25;

    const analyser = ac.createAnalyser();
    analyser.fftSize = 256;
    const data = new Uint8Array(analyser.frequencyBinCount);

    master.connect(lp); lp.connect(comp); comp.connect(analyser); analyser.connect(ac.destination);

    // Pad — A3 C4 E4 G4, three detuned voices each.
    const chord = [220.00, 261.63, 329.63, 392.00];
    const pad = ac.createGain(); pad.gain.value = 0.55;
    pad.connect(master);
    chord.forEach((f, i) => {
      [-6, 0, 6].forEach((cents) => {
        const o = ac.createOscillator();
        o.type = i < 2 ? "sine" : "triangle";
        o.frequency.value = f * Math.pow(2, cents / 1200);
        const g = ac.createGain();
        g.gain.value = 0; g.gain.linearRampToValueAtTime(0.09, now + 1.2);
        const lfo = ac.createOscillator();
        lfo.frequency.value = 0.08 + Math.random() * 0.12;
        const lfoGain = ac.createGain(); lfoGain.gain.value = 0.03;
        lfo.connect(lfoGain); lfoGain.connect(g.gain);
        o.connect(g); g.connect(pad);
        o.start(now); lfo.start(now);
        refs.current.nodes.push(o, lfo);
      });
    });

    // Arpeggio — C5 E5 G5 B5 G5 E5, ~109 bpm.
    const arpNotes = [523.25, 659.25, 783.99, 987.77, 783.99, 659.25];
    const step = 0.55;
    const loopLen = arpNotes.length * step;
    const arpGain = ac.createGain(); arpGain.gain.value = 0.35; arpGain.connect(master);

    function schedulePluck(freq, t) {
      const o = ac.createOscillator();
      o.type = "triangle"; o.frequency.value = freq;
      const g = ac.createGain();
      g.gain.setValueAtTime(0, t);
      g.gain.linearRampToValueAtTime(0.18, t + 0.01);
      g.gain.exponentialRampToValueAtTime(0.001, t + 0.6);
      const hp = ac.createBiquadFilter();
      hp.type = "highpass"; hp.frequency.value = 400;
      o.connect(hp); hp.connect(g); g.connect(arpGain);
      o.start(t); o.stop(t + 0.7);
      refs.current.nodes.push(o);
    }
    function scheduleLoop(startT) {
      for (let loop = 0; loop < 4; loop++) {
        arpNotes.forEach((f, i) => schedulePluck(f, startT + loop * loopLen + i * step));
      }
    }
    scheduleLoop(now + 0.8);
    const restartT = setInterval(() => {
      if (!refs.current.analyser) { clearInterval(restartT); return; }
      scheduleLoop(ac.currentTime + 0.2);
    }, loopLen * 4 * 1000 - 800);
    refs.current.nodes.push({ stop() { clearInterval(restartT); } });

    refs.current.analyser = analyser;
    refs.current.data = data;
    refs.current.master = master;

    setPlaying(true);

    const tick = () => {
      analyser.getByteFrequencyData(data);
      const n = data.length;
      for (let i = 0; i < BARS; i++) {
        const idx = Math.floor(Math.pow(i / (BARS - 1), 1.35) * (n - 1));
        const v = data[idx] / 255;
        const shape = 0.85 + 0.15 * Math.sin((i / (BARS - 1)) * Math.PI);
        const target = Math.max(0.12, Math.min(1, v * 1.15 * shape));
        barsRef.current[i] = barsRef.current[i] * 0.5 + target * 0.5;
      }
      force();
      refs.current.raf = requestAnimationFrame(tick);
    };
    refs.current.raf = requestAnimationFrame(tick);
  }

  React.useEffect(() => () => stop(), []);

  return (
    <button
      type="button"
      className={"tile tile-e hero-play" + (playing ? " playing" : "")}
      onClick={() => (playing ? stop() : play())}
      disabled={unsupported}
      aria-label={playing ? "Stop music" : "Play music"}
    >
      <span className="tile-label">Music · Score</span>
      <span className="tile-idx">05</span>
      <span className={"hero-play-badge" + (playing ? " on" : "")} aria-hidden="true">
        {playing ? <window.Ic.pause /> : <window.Ic.play />}
      </span>
      <div className="wave wave-live wave-music">
        {barsRef.current.map((h, i) => (
          <span
            key={i}
            style={{
              height: `${Math.max(10, h * 100)}%`,
              transition: playing ? "height 60ms linear" : "height 240ms",
            }}
          />
        ))}
      </div>
      {unsupported && <span className="hero-unsupp">WebAudio unavailable</span>}
    </button>
  );
}

function Hero() {
  return (
    <section className="hero">
      <div className="wrap">
        <div className="hero-kicker">
          <span className="dot" /> Course guide · Faculty &amp; Students · v2.4
        </div>
        <h1>
          How to make a <em>digital</em> story —<br />
          from blank page to final cut.
        </h1>
        <p className="hero-sub">
          A deep-dive field guide for faculty designing digital storytelling assignments and the students who bring them to life. Twelve steps, four tool stacks, one scrubbable example, and a drag-and-drop storyboard you can steal.
        </p>
        <div className="hero-meta">
          <div><span>Reading</span> <span className="v">28 min</span></div>
          <div><span>Steps</span> <span className="v">12</span></div>
          <div><span>Templates</span> <span className="v">6 downloads</span></div>
          <div><span>Updated</span> <span className="v">Fall 2026</span></div>
        </div>

        <div className="hero-collage">
          <div className="tile tile-a">
            <span className="tile-label">Scene 03 · Interview</span>
            <span className="tile-idx">01</span>
            <div className="ph" style={{background: "linear-gradient(135deg, #7c9cff33, #c7a6ff33), radial-gradient(circle at 30% 40%, #ffffff22, transparent 40%)"}} />
          </div>
          <NarrationTile />
          <div className="tile tile-c">
            <span className="tile-label">B-roll · Campus</span>
            <span className="tile-idx">03</span>
            <div className="ph" />
          </div>
          <div className="tile tile-d" style={{background: "var(--panel-2)"}}>
            <span className="tile-label">Quote</span>
            <span className="tile-idx">04</span>
            <div style={{padding: "28px 16px 16px", height: "100%", display: "flex", alignItems: "center"}}>
              <span style={{fontFamily: "var(--serif)", fontSize: 18, lineHeight: 1.2, color: "var(--ink)"}}>
                "The first time I told it, I didn't know what it was about yet."
              </span>
            </div>
          </div>
          <MusicTile />
        </div>
        <p className="hero-tip">Tap the <b>Narration</b> or <b>Music</b> tile to hear a sample. Only one plays at a time.</p>
      </div>
    </section>
  );
}

window.Hero = Hero;
