// Frequency visualizer for the Frequency page.
//
// Six presets, each anchored to a teaching:
//   • Heart      1 Hz     — the rhythm of the body
//   • Schumann   7.83 Hz  — Earth's resonant cavity
//   • 432 Hz                — natural musical tuning
//   • 528 Hz                — solfeggio "love" / DNA-repair frequency
//   • 741 Hz                — solfeggio "intuition" frequency
//   • 963 Hz                — solfeggio crown / pineal activation
//
// Visuals: scrolling sine wave whose cycle density and scroll rate scale
// logically with the chosen frequency (compressed at audio rates so the eye
// can read it). Color shifts with band — purple for sub-audible, gold/pink
// for solfeggio.
//
// Audio: optional, OFF by default. Web Audio API oscillator (pure sine),
// soft attack/release to avoid clicks, conservative gain (-22 dB) so it
// never startles. Only the audible presets enable the sound button.

(function () {
  const root = document.getElementById("freqviz");
  if (!root) return;

  const canvas    = root.querySelector(".fv-wave");
  const ctx       = canvas.getContext("2d");
  const hzEl      = root.querySelector(".fv-hz");
  const labelEl   = root.querySelector(".fv-label");
  const presetEls = root.querySelectorAll(".fv-preset[data-hz]");
  const audioBtn  = root.querySelector(".fv-audio");

  const PRESETS = {
    heart:    { hz: 1,     audible: false, key: "page.frequency.fv_heart",    fallback: "Heart" },
    schumann: { hz: 7.83,  audible: false, key: "page.frequency.fv_schumann", fallback: "Schumann · Earth" },
    "432":    { hz: 432,   audible: true,  key: "page.frequency.fv_432",      fallback: "432 Hz · Natural" },
    "528":    { hz: 528,   audible: true,  key: "page.frequency.fv_528",      fallback: "528 Hz · Love" },
    "741":    { hz: 741,   audible: true,  key: "page.frequency.fv_741",      fallback: "741 Hz · Intuition" },
    "963":    { hz: 963,   audible: true,  key: "page.frequency.fv_963",      fallback: "963 Hz · Crown" },
  };

  let current = "schumann";   // sensible default — the Earth's heartbeat
  let phase = 0;
  let lastTs = null;
  let rafId = null;

  // Web Audio (created lazily on first user click).
  let audioCtx = null;
  let oscNode = null;
  let gainNode = null;
  let audioOn = false;

  function t(key, fallback) {
    return (window.i18n && window.i18n.t) ? window.i18n.t(key, fallback) : fallback;
  }

  // ----- Geometry / visual mapping ---------------------------------------
  function sizeCanvas() {
    const dpr = window.devicePixelRatio || 1;
    const rect = canvas.getBoundingClientRect();
    canvas.width  = Math.round(rect.width  * dpr);
    canvas.height = Math.round(rect.height * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }

  // How many cycles fit across the canvas at frequency f. We use sqrt so
  // 432 vs 528 vs 963 stay visually distinct without 432 Hz becoming
  // pixel mush.
  function visualCycles(hz) {
    return Math.max(1, Math.sqrt(hz) * 1.15);
  }
  // How fast the phase advances (in cycles per real second). Same scaling
  // so high frequencies scroll faster — but not jet-engine fast.
  function visualSpeed(hz) {
    return Math.sqrt(hz) * 0.32;
  }

  function strokeFor(hz) {
    if (hz < 4)   return { mid: "rgba(140, 110, 220, 0.95)", end: "rgba(120, 90, 200, 1)" };  // deep purple
    if (hz < 30)  return { mid: "rgba(183, 104, 255, 0.95)", end: "rgba(220, 160, 255, 1)" }; // brain/Schumann purple
    if (hz < 500) return { mid: "rgba(255, 200, 120, 0.95)", end: "rgba(255, 158, 199, 1)" }; // gold→pink (432, 528)
    return { mid: "rgba(255, 230, 180, 0.95)", end: "rgba(255, 200, 230, 1)" };               // crown-band
  }

  // ----- Draw loop --------------------------------------------------------
  function loop(ts) {
    if (lastTs == null) lastTs = ts;
    const dt = (ts - lastTs) / 1000;
    lastTs = ts;
    const hz = PRESETS[current].hz;
    phase += 2 * Math.PI * visualSpeed(hz) * dt;
    render(hz);
    rafId = requestAnimationFrame(loop);
  }

  function render(hz) {
    const w = canvas.width  / (window.devicePixelRatio || 1);
    const h = canvas.height / (window.devicePixelRatio || 1);
    ctx.clearRect(0, 0, w, h);

    // Baseline.
    ctx.strokeStyle = "rgba(255, 255, 255, 0.06)";
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(0, h / 2);
    ctx.lineTo(w, h / 2);
    ctx.stroke();

    // Wave.
    const cycles = visualCycles(hz);
    const amp    = h * 0.36;
    const grad   = ctx.createLinearGradient(0, 0, w, 0);
    const colors = strokeFor(hz);
    grad.addColorStop(0,    "rgba(255, 255, 255, 0)");
    grad.addColorStop(0.12, colors.mid);
    grad.addColorStop(0.88, colors.mid);
    grad.addColorStop(1,    colors.end);
    ctx.strokeStyle = grad;
    ctx.lineWidth = 2.2;
    ctx.lineJoin = "round";
    ctx.lineCap  = "round";
    ctx.beginPath();
    // Sample 2 px wide — sharp enough, cheap.
    const samples = Math.max(120, Math.floor(w / 2));
    for (let i = 0; i <= samples; i++) {
      const x = (i / samples) * w;
      const u = i / samples;              // 0 → 1 across canvas
      const y = h / 2 - Math.sin(phase + 2 * Math.PI * cycles * u) * amp;
      if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
    }
    ctx.stroke();

    // Soft glow underglow — same path, thicker + lower alpha.
    ctx.globalAlpha = 0.35;
    ctx.strokeStyle = colors.mid;
    ctx.lineWidth = 6;
    ctx.beginPath();
    for (let i = 0; i <= samples; i++) {
      const x = (i / samples) * w;
      const u = i / samples;
      const y = h / 2 - Math.sin(phase + 2 * Math.PI * cycles * u) * amp;
      if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
    }
    ctx.stroke();
    ctx.globalAlpha = 1;
  }

  function start() {
    if (rafId) return;
    sizeCanvas();
    lastTs = null;
    rafId = requestAnimationFrame(loop);
  }
  function stop() {
    if (rafId) cancelAnimationFrame(rafId);
    rafId = null;
  }

  // ----- Audio ------------------------------------------------------------
  function ensureAudio() {
    if (audioCtx) return;
    const Ctx = window.AudioContext || window.webkitAudioContext;
    if (!Ctx) return;
    audioCtx = new Ctx();
    gainNode = audioCtx.createGain();
    gainNode.gain.value = 0;
    gainNode.connect(audioCtx.destination);
  }

  function startTone(hz) {
    ensureAudio();
    if (!audioCtx) return;
    if (oscNode) { try { oscNode.stop(); } catch (_) {} oscNode = null; }
    oscNode = audioCtx.createOscillator();
    oscNode.type = "sine";
    oscNode.frequency.value = hz;
    oscNode.connect(gainNode);
    oscNode.start();
    // Soft attack: ramp to gentle volume over 200 ms.
    const now = audioCtx.currentTime;
    gainNode.gain.cancelScheduledValues(now);
    gainNode.gain.setValueAtTime(0, now);
    gainNode.gain.linearRampToValueAtTime(0.08, now + 0.2);
  }

  function stopTone() {
    if (!audioCtx || !oscNode) return;
    const now = audioCtx.currentTime;
    gainNode.gain.cancelScheduledValues(now);
    gainNode.gain.setValueAtTime(gainNode.gain.value, now);
    gainNode.gain.linearRampToValueAtTime(0, now + 0.15);
    const n = oscNode;
    setTimeout(() => { try { n.stop(); } catch (_) {} }, 200);
    oscNode = null;
  }

  function setAudio(on) {
    audioOn = on;
    audioBtn.setAttribute("aria-pressed", on ? "true" : "false");
    audioBtn.textContent = on
      ? t("page.frequency.fv_sound_on", "Sound on")
      : t("page.frequency.fv_sound_off", "Sound off");
    audioBtn.classList.toggle("on", on);
    if (on && PRESETS[current].audible) startTone(PRESETS[current].hz);
    else stopTone();
  }

  // ----- UI sync ----------------------------------------------------------
  function applyCurrent() {
    const p = PRESETS[current];
    hzEl.textContent = p.hz;
    labelEl.textContent = t(p.key, p.fallback);
    presetEls.forEach((b) => b.classList.toggle("active", b.dataset.preset === current));
    audioBtn.disabled = !p.audible;
    audioBtn.classList.toggle("disabled", !p.audible);
    if (audioOn) {
      if (p.audible) startTone(p.hz); else stopTone();
    }
  }

  // ----- Wiring -----------------------------------------------------------
  presetEls.forEach((b) => b.addEventListener("click", () => {
    current = b.dataset.preset;
    applyCurrent();
  }));
  audioBtn.addEventListener("click", () => {
    if (audioBtn.disabled) return;
    setAudio(!audioOn);
    // Resume context — required by browsers after first user gesture.
    if (audioCtx && audioCtx.state === "suspended") audioCtx.resume();
  });
  window.addEventListener("resize", sizeCanvas);
  window.addEventListener("langchange", () => {
    audioBtn.textContent = audioOn
      ? t("page.frequency.fv_sound_on", "Sound on")
      : t("page.frequency.fv_sound_off", "Sound off");
    applyCurrent();
  });

  // Pause animation + sound when off-screen / tab hidden.
  const freqView = document.querySelector('.view[data-view="frequency"]');
  function isActive() { return freqView && freqView.classList.contains("active"); }
  if (freqView && "MutationObserver" in window) {
    new MutationObserver(() => {
      if (isActive()) start();
      else { stop(); if (audioOn) setAudio(false); }
    }).observe(freqView, { attributes: true, attributeFilter: ["class"] });
    if (isActive()) start();
  } else {
    start();
  }
  document.addEventListener("visibilitychange", () => {
    if (document.hidden) { stop(); if (audioOn) setAudio(false); }
    else if (isActive()) start();
  });

  applyCurrent();
})();
