// The noise the room makes.
//
// The room speaks in two voices. The cards and chips are *recorded* — real clay
// and real card stock, CC0 foley from Kenney's casino pack, sitting as small ogg
// files under /static/audio/casino. Synthesis never quite caught the grain of a
// chip landing on a chip, so for those we stopped trying. Everything melodic —
// the wins, the losses, the little ticks — is still *made*: an oscillator, a
// burst of filtered noise, an envelope, the same bargain the weather engine takes
// with its clouds. A win is four notes going up; a chip is a recording of a chip.
//
// A name is a recording if it appears in SAMPLES and synthesised if it appears in
// SOUNDS. The recordings guard against the one weakness of a sample — the same wav
// 300 times is a machine gun — by keeping several takes per sound and rotating
// through them, and by nudging every play a few percent off pitch. The synthesised
// half was always immune to that; it varies itself.
//
// Two rules hold the whole file up.
//
// **Nothing is built until it's asked for.** A browser will not let a page make a
// noise before the user has touched it, and a page that builds an AudioContext on
// load gets one in the "suspended" state and a warning in the console for its
// trouble. So the context is made on the first sound anybody actually asks for —
// which, in a casino, is a click on a chip.
//
// **Muted means silent, not quiet.** When the room is muted nothing is
// constructed, nothing is scheduled and no context is opened. Mute is the
// default-off switch for the entire file, checked before anything else happens.
//
// Exposed as window.PeteSFX. Nothing in here knows what blackjack is.
//
// If a recording hasn't finished decoding on the frame it's first needed, that one
// call falls through to the synthesised version — so the table is never silent
// while the ogg loads, and by the second card everything is real.
(function () {
  "use strict";

  var KEY = "pete.sfx.off";

  var ctx = null;
  var master = null;
  var noiseBuf = null;
  var muted = false;
  var listeners = [];

  try { muted = localStorage.getItem(KEY) === "1"; } catch (e) {}

  // ---- the instrument --------------------------------------------------------

  function boot() {
    if (ctx) return ctx;
    var AC = window.AudioContext || window.webkitAudioContext;
    if (!AC) return null;
    try {
      ctx = new AC();
    } catch (e) {
      return null;
    }
    master = ctx.createGain();
    master.gain.value = 0.45; // the room is furniture, not a nightclub
    master.connect(ctx.destination);
    return ctx;
  }

  // A second of white noise, made once and played at different speeds and through
  // different filters for the rest of the session. Card flicks, riffles, the
  // transient on a chip: all of them are this buffer wearing a different coat.
  function noise() {
    if (noiseBuf) return noiseBuf;
    var n = ctx.sampleRate;
    noiseBuf = ctx.createBuffer(1, n, n);
    var d = noiseBuf.getChannelData(0);
    for (var i = 0; i < n; i++) d[i] = Math.random() * 2 - 1;
    return noiseBuf;
  }

  // env is the shape of every sound in here: up fast, down slow, and never to
  // zero — an exponential ramp to actual zero is undefined, and a browser that
  // hits one either throws or clicks.
  function env(g, t0, attack, decay, peak) {
    g.gain.setValueAtTime(0.0001, t0);
    g.gain.exponentialRampToValueAtTime(Math.max(0.0001, peak), t0 + attack);
    g.gain.exponentialRampToValueAtTime(0.0001, t0 + attack + decay);
  }

  // tone is one note.
  function tone(t0, freq, o) {
    o = o || {};
    var osc = ctx.createOscillator();
    var g = ctx.createGain();
    osc.type = o.type || "sine";
    osc.frequency.setValueAtTime(freq, t0);
    if (o.to) osc.frequency.exponentialRampToValueAtTime(o.to, t0 + (o.glide || o.decay || 0.1));
    env(g, t0, o.attack || 0.004, o.decay || 0.12, o.gain == null ? 0.3 : o.gain);
    osc.connect(g).connect(master);
    osc.start(t0);
    osc.stop(t0 + (o.attack || 0.004) + (o.decay || 0.12) + 0.02);
  }

  // hiss is a burst of the noise buffer through a filter, which is what a card,
  // a riffle and the knock on a chip all are.
  function hiss(t0, o) {
    o = o || {};
    var src = ctx.createBufferSource();
    src.buffer = noise();
    src.playbackRate.value = o.rate || 1;

    var f = ctx.createBiquadFilter();
    f.type = o.filter || "bandpass";
    f.frequency.setValueAtTime(o.freq || 2000, t0);
    if (o.sweepTo) f.frequency.exponentialRampToValueAtTime(o.sweepTo, t0 + (o.decay || 0.1));
    f.Q.value = o.q == null ? 1.1 : o.q;

    var g = ctx.createGain();
    env(g, t0, o.attack || 0.003, o.decay || 0.09, o.gain == null ? 0.3 : o.gain);

    src.connect(f).connect(g).connect(master);
    src.start(t0);
    src.stop(t0 + (o.attack || 0.003) + (o.decay || 0.09) + 0.02);
  }

  // ---- the recordings --------------------------------------------------------
  //
  // The foley. Each name maps to a handful of takes; `v` (the index of the card or
  // chip in a run) picks which one and how far off pitch it lands, so a dealt hand
  // is four different cards rather than one card four times. `gain` is per-sound
  // and multiplies the master, exactly like the synthesised sounds' peak does.

  var SAMPLE_BASE = "/static/audio/casino/";
  var SAMPLES = {
    // A card thrown down onto the table. `vary` is the largest fraction a take is
    // ever pitched by, up or down — 0.20 means a play can land anywhere from a fifth
    // slower (deeper) to a fifth faster (brighter).
    card:    { files: ["cardPlace1", "cardPlace2", "cardPlace3", "cardPlace4"], gain: 0.6, vary: 0.20 },
    // A card slid into place — softer, longer than a throw.
    deal:    { files: ["cardSlide1", "cardSlide2", "cardSlide3", "cardSlide4", "cardSlide5", "cardSlide6"], gain: 0.5, vary: 0.20 },
    // A card flicked over.
    flip:    { files: ["cardFan1", "cardFan2"], gain: 0.5, vary: 0.18 },
    // The riffle — one take, so the pitch wobble is all that keeps two shuffles apart.
    shuffle: { files: ["cardShuffle"], gain: 0.6, vary: 0.10 },
    // A clay chip set down on a stack.
    chip:    { files: ["chipLay1", "chipLay2"], gain: 0.6, vary: 0.22 },
    // Chips gathered and slid away.
    sweep:   { files: ["chipsHandle2", "chipsHandle4", "chipsHandle6"], gain: 0.5, vary: 0.14 },
  };

  // Decoded buffers by filename. A value of null means "claimed, in flight or
  // decoding"; false means "we tried and it failed, never bother again"; an
  // AudioBuffer means ready.
  var buffers = {};
  var warmed = false;

  // A rotating cursor per sound, so that even a run of identical calls walks
  // through the takes rather than replaying the first one. Callers pass `v` to
  // separate simultaneous sounds (the cards of one deal), but many pass the same
  // constant every time — the cursor is what keeps a stack of chips from being the
  // same click over and over.
  var turn = {};

  // decode fetches a file and turns it into a buffer, exactly once per filename.
  // decodeAudioData exists in a modern promise flavour and an old callback one;
  // this handles both so the foley works on older Safari too.
  function decode(file) {
    if (file in buffers) return; // ready, in flight, or known-bad
    buffers[file] = null;
    fetch(SAMPLE_BASE + file + ".ogg")
      .then(function (r) { return r.ok ? r.arrayBuffer() : Promise.reject(); })
      .then(function (ab) {
        return new Promise(function (res, rej) {
          var p = ctx.decodeAudioData(ab, res, rej);
          if (p && p.then) p.then(res, rej);
        });
      })
      .then(function (buf) { buffers[file] = buf; })
      .catch(function () { buffers[file] = false; });
  }

  // warm pulls every take down once the context is awake, so that after the first
  // click the whole set is decoded and ready and nothing has to fall back.
  function warm() {
    if (warmed || !ctx) return;
    warmed = true;
    for (var name in SAMPLES) SAMPLES[name].files.forEach(decode);
  }

  // sample plays one recorded take. It returns false — rather than making a noise —
  // when the name isn't a recording or its buffer isn't decoded yet, which is the
  // caller's signal to reach for the synthesised version instead.
  function sample(name, t0, v) {
    var cfg = SAMPLES[name];
    if (!cfg) return false;
    // Advance the cursor, then offset it by v. The cursor guarantees consecutive
    // calls move to the next take; v spreads apart sounds fired together (one
    // deal's cards) so they don't all land on the same take at once.
    var k = (turn[name] = (turn[name] || 0) + 1) + Math.abs(Math.round(v));
    var buf = buffers[cfg.files[k % cfg.files.length]];
    if (!buf) return false; // not decoded yet, or failed: let synth cover it
    var src = ctx.createBufferSource();
    src.buffer = buf;
    // Pitch, walking with the same cursor across seven steps between -vary and
    // +vary, so even a single-take sound like the shuffle isn't identical twice in
    // a row. Seven steps rather than a couple means the runs don't read as a loop.
    if (cfg.vary) src.playbackRate.value = 1 + (((k % 7) - 3) / 3) * cfg.vary;
    var g = ctx.createGain();
    g.gain.value = cfg.gain == null ? 0.6 : cfg.gain;
    src.connect(g).connect(master);
    src.start(t0);
    return true;
  }

  // ---- the sounds ------------------------------------------------------------
  //
  // Each one takes the time it starts at, and a `v` — a small per-call variation,
  // usually the index of the card or chip in a run. Four cards dealt with the
  // identical sample four times is a machine gun; four cards each a semitone off
  // is a hand dealing.

  var SOUNDS = {
    // A card landing: mostly air, with a slap on the front of it.
    card: function (t, v) {
      hiss(t, { freq: 1800 + v * 140, q: 0.9, decay: 0.075, rate: 1 + v * 0.05, gain: 0.3 });
      hiss(t, { filter: "highpass", freq: 4200, decay: 0.02, gain: 0.16 });
    },

    // Dealing is the same card, lower and softer: it's being placed, not thrown.
    deal: function (t, v) {
      hiss(t, { freq: 1250 + v * 110, q: 1.1, decay: 0.085, rate: 0.9, gain: 0.24 });
    },

    // A card turning over — shorter, brighter, and it comes in two halves, which
    // is what a flip actually sounds like.
    flip: function (t) {
      hiss(t, { freq: 2600, q: 1.4, decay: 0.035, gain: 0.22 });
      hiss(t + 0.045, { freq: 1500, q: 1.0, decay: 0.06, gain: 0.24 });
    },

    // The riffle. One long sweep of noise with the filter climbing through it,
    // and a rattle laid over the top so it isn't just a shhh.
    shuffle: function (t) {
      hiss(t, { freq: 700, sweepTo: 3400, q: 0.7, attack: 0.02, decay: 0.34, gain: 0.2 });
      for (var i = 0; i < 9; i++) {
        hiss(t + 0.03 + i * 0.032, { freq: 2400 + i * 130, q: 2.2, decay: 0.02, gain: 0.09 });
      }
    },

    // A chip landing on a chip. Two detuned partials well above the staff, with a
    // knock underneath — the knock is most of what makes it read as *clay* rather
    // than as a bell.
    chip: function (t, v) {
      var f = 1750 + v * 55;
      tone(t, f, { type: "sine", decay: 0.07, gain: 0.16 });
      tone(t, f * 1.34, { type: "sine", decay: 0.05, gain: 0.1 });
      hiss(t, { filter: "lowpass", freq: 900, decay: 0.03, gain: 0.3, q: 0.5 });
    },

    // Chips sliding away across felt.
    sweep: function (t) {
      hiss(t, { freq: 1600, sweepTo: 500, q: 0.6, attack: 0.015, decay: 0.26, gain: 0.16 });
    },

    // Four notes up. This is the only sound in the room allowed to be pleased.
    win: function (t) {
      [523.25, 659.25, 783.99, 1046.5].forEach(function (f, i) {
        tone(t + i * 0.085, f, { type: "triangle", decay: 0.26, gain: 0.24 });
      });
    },

    // It rains money. A bright run of coins spilling out over the win fanfare —
    // ten metallic pings climbing a pentatonic scale, each with a tick on the
    // front of it so it lands as coin rather than as bell.
    jackpot: function (t) {
      var notes = [784, 988, 1175, 1319, 1568, 1760];
      for (var i = 0; i < 10; i++) {
        tone(t + i * 0.05, notes[i % notes.length], { type: "triangle", decay: 0.13, gain: 0.09 });
        hiss(t + i * 0.05, { freq: 5200, q: 3, decay: 0.02, gain: 0.05 });
      }
    },

    // Two notes down, and the second one is flat. Nobody needs telling twice.
    lose: function (t) {
      tone(t, 311.13, { type: "triangle", decay: 0.24, gain: 0.22 });
      tone(t + 0.15, 233.08, { type: "triangle", decay: 0.42, gain: 0.22 });
    },

    // Nothing happened and nothing was lost.
    push: function (t) {
      tone(t, 392, { type: "sine", decay: 0.2, gain: 0.16 });
    },

    // A button. Short enough that you feel it rather than hear it.
    blip: function (t) {
      tone(t, 880, { type: "sine", decay: 0.055, gain: 0.12 });
    },

    // UNO, called. A bright stab plus a zip upwards: it is a shout, and it is the
    // one thing on that table you want to feel good about pressing.
    uno: function (t) {
      [659.25, 830.61, 987.77].forEach(function (f) {
        tone(t, f, { type: "triangle", decay: 0.3, gain: 0.2 });
      });
      tone(t, 500, { type: "sawtooth", to: 1600, glide: 0.16, decay: 0.18, gain: 0.09 });
    },

    // Got them. A rising snap with a thump on the end — the sound of a finger
    // being pointed.
    catch: function (t) {
      tone(t, 300, { type: "square", to: 900, glide: 0.11, decay: 0.12, gain: 0.13 });
      hiss(t + 0.1, { filter: "lowpass", freq: 1100, decay: 0.1, gain: 0.34, q: 0.7 });
      tone(t + 0.1, 174.61, { type: "triangle", decay: 0.22, gain: 0.2 });
    },

    // Something bad landed on you: a stack, a +4, a catch you walked into.
    bad: function (t) {
      tone(t, 155.56, { type: "square", to: 98, glide: 0.18, decay: 0.22, gain: 0.14 });
      hiss(t, { filter: "lowpass", freq: 700, decay: 0.14, gain: 0.3, q: 0.6 });
    },

    // A clock you can hear. Used sparingly, and never in a run of more than a few.
    tick: function (t) {
      hiss(t, { freq: 3200, q: 3, decay: 0.02, gain: 0.14 });
    },

    // The mercy rule buries someone. Four ways to go, one sound each — uno.js
    // rolls which animation plays and asks for the sound that matches it.
    //
    // Rocks coming down: a low rumble with tumbling knocks over the top, and a
    // second slump as the pile settles.
    rockslide: function (t) {
      hiss(t, { filter: "lowpass", freq: 320, sweepTo: 140, decay: 0.5, gain: 0.34, q: 0.6 });
      for (var i = 0; i < 6; i++) {
        tone(t + i * 0.055, 128 - i * 9, { type: "square", decay: 0.1, gain: 0.09 });
      }
      hiss(t + 0.3, { filter: "lowpass", freq: 200, decay: 0.32, gain: 0.3, q: 0.5 });
    },

    // A headstone dropping in: one heavy stone thud that rings a little as it sets.
    tombstone: function (t) {
      tone(t, 92, { type: "sine", to: 58, glide: 0.16, decay: 0.42, gain: 0.32 });
      hiss(t, { filter: "lowpass", freq: 520, decay: 0.12, gain: 0.34, q: 0.6 });
      tone(t + 0.02, 184, { type: "triangle", decay: 0.5, gain: 0.08 });
    },

    // The Mega Man death: the pieces zip outward and the pitch falls away as they go.
    zap: function (t) {
      tone(t, 1250, { type: "sawtooth", to: 170, glide: 0.34, decay: 0.36, gain: 0.12 });
      for (var i = 0; i < 4; i++) {
        tone(t + i * 0.02, 940 - i * 130, { type: "square", decay: 0.06, gain: 0.06 });
      }
      hiss(t, { freq: 3000, sweepTo: 600, decay: 0.3, gain: 0.1, q: 0.8 });
    },

    // A coffin lid: two flat wooden knocks, the second lower and heavier.
    coffin: function (t) {
      tone(t, 150, { type: "triangle", decay: 0.14, gain: 0.26 });
      hiss(t, { filter: "lowpass", freq: 820, decay: 0.05, gain: 0.24, q: 0.7 });
      tone(t + 0.17, 108, { type: "triangle", decay: 0.22, gain: 0.28 });
      hiss(t + 0.17, { filter: "lowpass", freq: 600, decay: 0.06, gain: 0.24, q: 0.7 });
    },
  };

  // ---- the door --------------------------------------------------------------

  // A browser will not make a noise until the user has touched the page, and a
  // context built before that arrives suspended. This wakes it on the first real
  // gesture — after which play() can schedule freely.
  function wake() {
    if (ctx && ctx.state === "suspended") ctx.resume().catch(function () {});
    warm();
  }
  ["pointerdown", "keydown", "touchstart"].forEach(function (ev) {
    window.addEventListener(ev, wake, { passive: true });
  });

  // play makes a sound, or — if the room is muted — makes nothing at all, opens no
  // context and builds no graph.
  //
  // `v` is the variation: pass the index of the card or chip in a run and the
  // sound moves a little each time, which is the difference between a hand and a
  // machine. `delay` schedules it ahead in seconds, so a caller that knows a card
  // lands in 400ms can say so rather than sleeping.
  function play(name, opts) {
    if (muted) return;
    if (!SOUNDS[name] && !SAMPLES[name]) return; // known to neither voice
    if (!boot()) return;
    wake();
    if (ctx.state !== "running") return; // not yet touched: no sound, and no error
    var t0 = ctx.currentTime + ((opts && opts.delay) || 0);
    var v = (opts && opts.v) || 0;
    try {
      // A recording if we have one decoded; the synthesised take otherwise — both
      // for names that are only ever synthesised, and for the first call of a
      // recorded name while its ogg is still loading.
      if (sample(name, t0, v)) return;
      if (SOUNDS[name]) SOUNDS[name](t0, v);
    } catch (e) {
      /* a sound is never worth throwing over */
    }
  }

  function setMuted(on) {
    muted = !!on;
    try {
      if (muted) localStorage.setItem(KEY, "1");
      else localStorage.removeItem(KEY);
    } catch (e) {}
    if (window.PetePrefs) window.PetePrefs.push();
    listeners.forEach(function (fn) { fn(muted); });
    if (!muted) play("blip"); // so you know what you just turned back on
  }

  window.PeteSFX = {
    play: play,
    muted: function () { return muted; },
    toggle: function () { setMuted(!muted); return muted; },
    set: setMuted,
    // onChange lets the mute button re-label itself without owning the state.
    onChange: function (fn) { listeners.push(fn); fn(muted); },
  };
})();
