'use strict';
(function(global) {

  global.LabFX = global.LabFX || {};

  /* ── AudioContext lazy init ── */
  var _ctx = null;
  var _masterGain = null;
  var _enabled = (function() {
    var stored = localStorage.getItem('labfx-sound');
    return stored === null ? true : stored === 'true';
  })();

  function getCtx() {
    if (!_ctx) {
      _ctx = new (window.AudioContext || window.webkitAudioContext)();
      _masterGain = _ctx.createGain();
      _masterGain.gain.value = _enabled ? 1 : 0;
      _masterGain.connect(_ctx.destination);
    }
    if (_ctx.state === 'suspended') {
      _ctx.resume();
    }
    return _ctx;
  }

  function master() {
    getCtx();
    return _masterGain;
  }

  /* ── helper: connect chain to master ── */
  function chain(nodes) {
    /* nodes: array of AudioNode — each is connected to the next, last → master */
    for (var i = 0; i < nodes.length - 1; i++) {
      nodes[i].connect(nodes[i + 1]);
    }
    nodes[nodes.length - 1].connect(master());
  }

  /* ── helper: create gain ── */
  function mkGain(value) {
    var g = getCtx().createGain();
    g.gain.value = value;
    return g;
  }

  /* ── helper: create oscillator ── */
  function mkOsc(type, freq) {
    var o = getCtx().createOscillator();
    o.type = type;
    o.frequency.value = freq;
    return o;
  }

  /* ── helper: create biquad ── */
  function mkFilter(type, freq, Q) {
    var f = getCtx().createBiquadFilter();
    f.type = type;
    f.frequency.value = freq;
    if (Q != null) f.Q.value = Q;
    return f;
  }

  /* ── helper: noise buffer (white) ── */
  function makeNoise(color) {
    /* color: 'white' | 'pink' | 'brown' */
    var ac       = getCtx();
    var length   = ac.sampleRate * 2;
    var buffer   = ac.createBuffer(1, length, ac.sampleRate);
    var data     = buffer.getChannelData(0);
    var b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0;

    for (var i = 0; i < length; i++) {
      var white = Math.random() * 2 - 1;
      if (color === 'pink') {
        b0 = 0.99886 * b0 + white * 0.0555179;
        b1 = 0.99332 * b1 + white * 0.0750759;
        b2 = 0.96900 * b2 + white * 0.1538520;
        b3 = 0.86650 * b3 + white * 0.3104856;
        b4 = 0.55000 * b4 + white * 0.5329522;
        b5 = -0.7616 * b5 - white * 0.0168980;
        data[i] = (b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362) * 0.11;
        b6 = white * 0.115926;
      } else if (color === 'brown') {
        b0 = (b0 + 0.02 * white) / 1.02;
        data[i] = b0 * 3.5;
      } else {
        data[i] = white;
      }
    }
    return buffer;
  }

  /* ── noise source ── */
  function noiseSource(color) {
    var ac  = getCtx();
    var src = ac.createBufferSource();
    src.buffer = makeNoise(color || 'white');
    src.loop   = true;
    return src;
  }

  /* ─────────────────────────────────────────────
     SYNTH RECIPES
  ───────────────────────────────────────────── */
  var recipes = {

    click: function(opts) {
      /* short filtered noise burst, 30ms */
      var ac  = getCtx();
      var now = ac.currentTime;
      var vol = (opts.volume || 0.5) * 0.6;
      var src = noiseSource('white');
      var flt = mkFilter('bandpass', 1200 * (opts.pitch || 1), 8);
      var g   = mkGain(vol);
      g.gain.setValueAtTime(vol, now);
      g.gain.exponentialRampToValueAtTime(0.0001, now + 0.030);
      chain([src, flt, g]);
      src.start(now);
      src.stop(now + 0.035);
    },

    tick: function(opts) {
      /* Geiger tick: brown noise + very short envelope, 15ms */
      var ac  = getCtx();
      var now = ac.currentTime;
      var vol = (opts.volume || 0.5) * 0.4;
      var src = noiseSource('brown');
      var flt = mkFilter('highpass', 800, 1);
      var g   = mkGain(vol);
      g.gain.setValueAtTime(vol, now);
      g.gain.exponentialRampToValueAtTime(0.0001, now + 0.015);
      chain([src, flt, g]);
      src.start(now);
      src.stop(now + 0.02);
    },

    whoosh: function(opts) {
      /* descending swoosh: filtered noise sweep 800→200 Hz, 250ms */
      var ac  = getCtx();
      var now = ac.currentTime;
      var vol = (opts.volume || 0.5) * 0.7;
      var src = noiseSource('pink');
      var flt = mkFilter('bandpass', 800 * (opts.pitch || 1), 3);
      var g   = mkGain(vol);
      flt.frequency.setValueAtTime(800 * (opts.pitch || 1), now);
      flt.frequency.exponentialRampToValueAtTime(200 * (opts.pitch || 1), now + 0.25);
      g.gain.setValueAtTime(0.0001, now);
      g.gain.linearRampToValueAtTime(vol, now + 0.03);
      g.gain.exponentialRampToValueAtTime(0.0001, now + 0.25);
      chain([src, flt, g]);
      src.start(now);
      src.stop(now + 0.28);
    },

    chime: function(opts) {
      /* bright bell: additive sines 880+1320+1760 Hz, exp decay, 600ms */
      var ac    = getCtx();
      var now   = ac.currentTime;
      var vol   = (opts.volume || 0.5);
      var pitch = opts.pitch || 1;
      var freqs = [880, 1320, 1760];
      var pan   = opts.pan || 0;

      freqs.forEach(function(f, idx) {
        var osc = mkOsc('sine', f * pitch);
        var g   = mkGain(vol * (0.5 / (idx + 1)));
        var panner = ac.createStereoPanner ? ac.createStereoPanner() : null;
        g.gain.setValueAtTime(vol * (0.5 / (idx + 1)), now);
        g.gain.exponentialRampToValueAtTime(0.0001, now + 0.6);
        if (panner) {
          panner.pan.value = pan;
          chain([osc, g, panner]);
        } else {
          chain([osc, g]);
        }
        osc.start(now);
        osc.stop(now + 0.65);
      });
    },

    fizz: function(opts) {
      /* pink noise, 300ms, low-pass sweep */
      var ac  = getCtx();
      var now = ac.currentTime;
      var vol = (opts.volume || 0.5) * 0.6;
      var src = noiseSource('pink');
      var flt = mkFilter('lowpass', 3000 * (opts.pitch || 1), 1);
      var g   = mkGain(vol);
      flt.frequency.setValueAtTime(3000 * (opts.pitch || 1), now);
      flt.frequency.exponentialRampToValueAtTime(400 * (opts.pitch || 1), now + 0.3);
      g.gain.setValueAtTime(vol, now);
      g.gain.exponentialRampToValueAtTime(0.0001, now + 0.3);
      chain([src, flt, g]);
      src.start(now);
      src.stop(now + 0.35);
    },

    spark: function(opts) {
      /* white noise + triangle 2000Hz, 50ms */
      var ac  = getCtx();
      var now = ac.currentTime;
      var vol = (opts.volume || 0.5) * 0.7;
      var src = noiseSource('white');
      var osc = mkOsc('triangle', 2000 * (opts.pitch || 1));
      var gn  = mkGain(vol * 0.5);
      var go  = mkGain(vol * 0.5);
      var mix = mkGain(1);
      gn.gain.setValueAtTime(vol * 0.5, now);
      gn.gain.exponentialRampToValueAtTime(0.0001, now + 0.05);
      go.gain.setValueAtTime(vol * 0.5, now);
      go.gain.exponentialRampToValueAtTime(0.0001, now + 0.05);
      src.connect(gn); gn.connect(mix);
      osc.connect(go); go.connect(mix);
      mix.connect(master());
      src.start(now); osc.start(now);
      src.stop(now + 0.06); osc.stop(now + 0.06);
    },

    bounce: function(opts) {
      /* sine sweep 600→300 Hz, 100ms */
      var ac  = getCtx();
      var now = ac.currentTime;
      var vol = (opts.volume || 0.5) * 0.6;
      var osc = mkOsc('sine', 600 * (opts.pitch || 1));
      var g   = mkGain(vol);
      osc.frequency.setValueAtTime(600 * (opts.pitch || 1), now);
      osc.frequency.exponentialRampToValueAtTime(300 * (opts.pitch || 1), now + 0.1);
      g.gain.setValueAtTime(vol, now);
      g.gain.exponentialRampToValueAtTime(0.0001, now + 0.1);
      chain([osc, g]);
      osc.start(now);
      osc.stop(now + 0.12);
    },

    pour: function(opts) {
      /* filtered noise with random pitch wobble, 200ms+ */
      var ac    = getCtx();
      var now   = ac.currentTime;
      var vol   = (opts.volume || 0.5) * 0.5;
      var src   = noiseSource('pink');
      var flt   = mkFilter('bandpass', 400 * (opts.pitch || 1), 2);
      var g     = mkGain(vol);
      /* wobble frequency */
      var steps = 8;
      for (var i = 0; i <= steps; i++) {
        var t    = now + (i / steps) * 0.2;
        var freq = (350 + Math.random() * 150) * (opts.pitch || 1);
        flt.frequency.setValueAtTime(freq, t);
      }
      g.gain.setValueAtTime(0.0001, now);
      g.gain.linearRampToValueAtTime(vol, now + 0.04);
      g.gain.setValueAtTime(vol, now + 0.18);
      g.gain.exponentialRampToValueAtTime(0.0001, now + 0.22);
      chain([src, flt, g]);
      src.start(now);
      src.stop(now + 0.25);
    }
  };

  /* ─────────────────────────────────────────────
     DRONE — sustained sound (separate lifecycle)
  ───────────────────────────────────────────── */
  var _droneNodes = {};

  var droneRecipes = {
    drone: function(ac, vol) {
      var sine = mkOsc('sine', 80);
      var saw  = mkOsc('sawtooth', 160);
      var gs   = mkGain(vol * 0.5);
      var gw   = mkGain(vol * 0.25);
      var mix  = mkGain(0.05);
      sine.connect(gs); gs.connect(mix);
      saw.connect(gw);  gw.connect(mix);
      mix.connect(master());
      sine.start();
      saw.start();
      return { nodes: [sine, saw, gs, gw, mix] };
    }
  };

  /* ─────────────────────────────────────────────
     PUBLIC API
  ───────────────────────────────────────────── */
  global.LabFX.sound = {

    play: function(name, opts) {
      if (!_enabled) return;
      opts = opts || {};
      getCtx();
      var recipe = recipes[name];
      if (recipe) {
        try { recipe(opts); } catch(e) { /* silent */ }
      }
    },

    startDrone: function(name) {
      if (_droneNodes[name]) return _droneNodes[name]; /* already running */
      var recipe = droneRecipes[name];
      if (!recipe) return null;
      var ac     = getCtx();
      var vol    = 0.5;
      var handle = recipe(ac, vol);
      _droneNodes[name] = handle;

      handle.stop = function() {
        handle.nodes.forEach(function(n) {
          try { n.stop(); } catch(e) {}
          try { n.disconnect(); } catch(e) {}
        });
        delete _droneNodes[name];
      };

      return handle;
    },

    setEnabled: function(bool) {
      _enabled = !!bool;
      localStorage.setItem('labfx-sound', _enabled ? 'true' : 'false');
      if (_masterGain) {
        _masterGain.gain.cancelScheduledValues(0);
        _masterGain.gain.value = _enabled ? 1 : 0;
      }
    },

    isEnabled: function() {
      return _enabled;
    }
  };

})(window);
