// MusicRoom - ReplayGain module
// Applies per-track loudness normalization using server-provided rsgain metadata.
// Gain is applied client-side via the shared Web Audio gain node (M.gainNode),
// so it only affects this listener's playback. Preferences are persisted
// per-account via /api/auth/me/preferences (localStorage fallback).

(function() {
  const M = window.MusicRoom;

  const LS_ENABLED = "blastoise_replaygain_enabled";
  const LS_PREAMP = "blastoise_replaygain_preamp";
  const PREAMP_MIN = -12;
  const PREAMP_MAX = 12;
  const PREAMP_STEP = 0.5;
  const MAX_LINEAR = 10; // +20 dB hard cap to avoid extreme boosts

  M.replayGain = {
    enabled: localStorage.getItem(LS_ENABLED) !== "false", // default true
    preampDb: clampPreamp(Number.parseFloat(localStorage.getItem(LS_PREAMP)) || 0),
  };

  M.currentTrack = null;

  function clampPreamp(v) {
    if (!Number.isFinite(v)) return 0;
    return Math.max(PREAMP_MIN, Math.min(PREAMP_MAX, Math.round(v / PREAMP_STEP) * PREAMP_STEP));
  }

  // linear gain factor for a track, honoring enable flag, preamp, and peak clipping
  function computeLinearGain(track) {
    if (!M.replayGain.enabled) return 1.0;
    const preamp = M.replayGain.preampDb || 0;
    let gainDb = preamp;
    const rgDb = track ? track.replayGainDb : null;
    if (Number.isFinite(rgDb)) gainDb += rgDb;
    let linear = Math.pow(10, gainDb / 20);
    const peak = track ? track.replayPeak : null;
    if (Number.isFinite(peak) && peak > 0 && peak * linear > 1) {
      linear = 1 / peak;
    }
    return Math.min(linear, MAX_LINEAR);
  }

  function setGain(node, linear) {
    const ctx = node.context;
    if (ctx.state === "running") {
      const t = ctx.currentTime;
      node.gain.cancelScheduledValues(t);
      node.gain.setTargetAtTime(linear, t, 0.02); // ~20ms ramp, avoids clicks
    } else {
      node.gain.value = linear; // context suspended; apply directly
    }
  }

  // Apply gain for the current (or given) track. Safe to call before the graph
  // exists — the value is re-applied when the graph initializes.
  M.applyReplayGain = function(track) {
    if (track) M.currentTrack = track;
    const t = M.currentTrack;
    const hasData = Number.isFinite(t && t.replayGainDb) || Number.isFinite(t && t.replayPeak);
    // Only build the audio graph when there's actually something to apply (or it
    // already exists), preserving default behavior for libraries without RG.
    if (M.replayGain.enabled && (hasData || M.gainNode || M.replayGain.preampDb)) {
      M.ensureAudioGraph && M.ensureAudioGraph();
    }
    const node = M.gainNode;
    if (!node) return;
    setGain(node, computeLinearGain(t));
  };

  M.setReplayGainEnabled = function(enabled) {
    M.replayGain.enabled = !!enabled;
    localStorage.setItem(LS_ENABLED, M.replayGain.enabled ? "true" : "false");
    M.applyReplayGain();
    M.updateReplayGainUI && M.updateReplayGainUI();
    M.saveReplayGainPrefs && M.saveReplayGainPrefs();
  };

  M.setReplayGainPreamp = function(db) {
    M.replayGain.preampDb = clampPreamp(db);
    localStorage.setItem(LS_PREAMP, String(M.replayGain.preampDb));
    M.applyReplayGain();
    M.updateReplayGainUI && M.updateReplayGainUI();
    M.saveReplayGainPrefs && M.saveReplayGainPrefs();
  };

  // Debounced persistence to the server (per-account). localStorage is the
  // immediate fallback for guests/offline.
  let saveTimer = null;
  M.saveReplayGainPrefs = function() {
    if (saveTimer) clearTimeout(saveTimer);
    saveTimer = setTimeout(() => {
      saveTimer = null;
      const body = {
        replaygain_enabled: M.replayGain.enabled ? "true" : "false",
        replaygain_preamp: String(M.replayGain.preampDb),
      };
      fetch("/api/auth/me/preferences", {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      }).catch(() => {});
    }, 400);
  };

  // Hydrate from the /api/auth/me preferences object (called by auth.js).
  M.loadReplayGainPrefs = function(prefs) {
    if (!prefs) return;
    if (prefs.replaygain_enabled != null) {
      M.replayGain.enabled = prefs.replaygain_enabled === "true";
      localStorage.setItem(LS_ENABLED, M.replayGain.enabled ? "true" : "false");
    }
    if (prefs.replaygain_preamp != null) {
      const v = Number.parseFloat(prefs.replaygain_preamp);
      if (Number.isFinite(v)) {
        M.replayGain.preampDb = clampPreamp(v);
        localStorage.setItem(LS_PREAMP, String(M.replayGain.preampDb));
      }
    }
    M.applyReplayGain();
    M.updateReplayGainUI && M.updateReplayGainUI();
  };

  // ---- UI ----
  function initReplayGainUI() {
    const btn = M.$("#btn-replaygain");
    const popover = M.$("#replaygain-popover");
    const enabledCheckbox = M.$("#rg-enabled");
    const preampSlider = M.$("#rg-preamp");
    const preampValue = M.$("#rg-preamp-value");
    if (!btn) return;

    function open() {
      if (!popover) return;
      popover.classList.remove("hidden");
      document.addEventListener("pointerdown", onOutside, true);
      document.addEventListener("keydown", onKey);
    }
    function close() {
      if (!popover) return;
      popover.classList.add("hidden");
      document.removeEventListener("pointerdown", onOutside, true);
      document.removeEventListener("keydown", onKey);
    }
    function toggle() {
      if (popover && popover.classList.contains("hidden")) open();
      else close();
    }
    function onOutside(e) {
      if (popover && !popover.contains(e.target) && e.target !== btn) close();
    }
    function onKey(e) {
      if (e.key === "Escape") close();
    }

    btn.onclick = (e) => {
      e.stopPropagation();
      toggle();
    };

    if (enabledCheckbox) {
      enabledCheckbox.onchange = () => M.setReplayGainEnabled(enabledCheckbox.checked);
    }
    if (preampSlider) {
      preampSlider.min = String(PREAMP_MIN);
      preampSlider.max = String(PREAMP_MAX);
      preampSlider.step = String(PREAMP_STEP);
      preampSlider.oninput = () => M.setReplayGainPreamp(Number.parseFloat(preampSlider.value));
    }

    M.updateReplayGainUI = function() {
      if (btn) btn.classList.toggle("active", M.replayGain.enabled);
      if (enabledCheckbox) enabledCheckbox.checked = M.replayGain.enabled;
      if (preampSlider) preampSlider.value = String(M.replayGain.preampDb);
      if (preampValue) {
        const db = M.replayGain.preampDb;
        preampValue.textContent = (db > 0 ? "+" : "") + (Number.isInteger(db) ? db : db.toFixed(1)) + " dB";
      }
    };

    M.updateReplayGainUI();
  }

  document.addEventListener("DOMContentLoaded", initReplayGainUI);
})();
