// Client-side port of player/alteration.py + player/audio.py (the alteration
// engine). Replaces the live POST /api/alteration in the static build — pure math,
// unity calibration (the client never sends a calibration, so DEFAULT_CALIBRATION
// = all gains 1.0 always applies). UMD so the browser gets `HEFAlteration` and
// `node --test` can require() it. Keep IN SYNC with the Python — the numeric
// contract is guarded by tests/test_alteration_js_parity.py.
(function (root, factory) {
  const api = factory();
  if (typeof module !== "undefined" && module.exports) module.exports = api;
  else root.HEFAlteration = api;
})(typeof self !== "undefined" ? self : this, function () {
  "use strict";
  const KNOB_MAX = 4;                                   // player/alteration.py:25
  const clamp = (x, lo, hi) => Math.max(lo, Math.min(hi, x));

  // Mirror of player/alteration.py::plan_alteration with DEFAULT_CALIBRATION (unity).
  function plan(c) {
    const tone = clamp((c.light - c.dark) / KNOB_MAX, -1, 1);
    const overlayIntensity = clamp(c.left / KNOB_MAX, 0, 1);
    const dreamIntensity = clamp(c.right / KNOB_MAX, 0, 1);
    const affectIntensity = clamp(c.right / KNOB_MAX, 0, 1);   // affect uses overlay_gain too
    const is_identity = tone === 0 && c.left === 0 && c.right === 0;
    return {
      grade: { tone },
      overlay: { level: c.left, intensity: overlayIntensity },
      affect: { strength: c.right, intensity: affectIntensity },
      dream: { strength: c.right, intensity: dreamIntensity },
      is_identity,
    };
  }

  // Mirror of player/audio.py::resolve_audio. `scaleAudio` is the current scale's
  // `audio` filename (relative to <mediaBase>audio/); off → silence.
  function renderAudio(source, scaleAudio, mediaBase) {
    if (source === "off") return { source: "off", url: null, altitude_coupled: false };
    const url = scaleAudio ? mediaBase + "audio/" + scaleAudio : null;
    return { source: "soundtrack", url, altitude_coupled: true };
  }

  // The full /api/alteration response shape, computed locally.
  function alteration(controls, scaleAudio, mediaBase) {
    return {
      plan: plan(controls),
      render: {
        video: { shown: controls.visual === "on" },   // player/audio.py::resolve_visual
        audio: renderAudio(controls.audio, scaleAudio, mediaBase),
      },
    };
  }

  return { plan, renderAudio, alteration, KNOB_MAX };
});
