// The moving parts of the room.
//
// A casino is mostly one gesture repeated: something of value is picked up here
// and put down there, in front of you, slowly enough that you can object. So the
// only real idea in this file is fly() — take an element's place on screen, take
// another's, and move a chip between them on an arc that a hand would make.
//
// Everything flies in a fixed overlay pinned to the viewport, not inside the
// felt, because a chip's journey starts on a button *outside* the felt and any
// container in between would clip it halfway. Coordinates come from
// getBoundingClientRect, so the overlay needs no knowledge of the layout at all
// and nothing has to be positioned relative to anything else.
//
// Exposed as window.PeteFX. Nothing in here knows what blackjack is.
(function () {
  "use strict";

  var reduced =
    window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  // The room's noise, if it's loaded. Everything that flies through this file
  // makes a sound when it lands, which is how every table got sound at once
  // without any of them being told about it: a chip is a chip wherever it's
  // thrown from. A game that wants something more specific says so per call.
  function sfx(name, opts) {
    if (window.PeteSFX) window.PeteSFX.play(name, opts);
  }

  var layer = null;
  function stage() {
    if (!layer) {
      layer = document.createElement("div");
      layer.className = "pete-fly-layer";
      layer.setAttribute("aria-hidden", "true");
      document.body.appendChild(layer);
    }
    return layer;
  }

  // Where a thing is, in viewport coordinates: the middle of it.
  function centre(target) {
    if (!target) return { x: 0, y: 0 };
    if (typeof target.x === "number") return target;
    var r = target.getBoundingClientRect();
    return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
  }

  // DENOMS, biggest first — the order you break an amount down in.
  var DENOMS = [500, 100, 25, 5];

  // chipsFor turns an amount into the chips a dealer would actually push across:
  // as few as possible, biggest first. Capped, because a €10,000 bet is 20 purple
  // chips and nobody wants to watch 20 of anything fly. Past the cap the stack
  // just gets shorter than the number under it, which is what a real tall stack
  // looks like from across a table anyway.
  function chipsFor(amount, cap) {
    var out = [];
    var left = Math.max(0, Math.round(amount || 0));
    for (var i = 0; i < DENOMS.length && left > 0; i++) {
      while (left >= DENOMS[i]) {
        out.push(DENOMS[i]);
        left -= DENOMS[i];
      }
    }
    if (left > 0) out.push(5); // a remainder below the smallest chip still shows up
    var max = cap || 12;
    return out.length > max ? out.slice(0, max) : out;
  }

  function disc(denom) {
    var el = document.createElement("div");
    el.className = "pete-disc";
    el.dataset.chip = String(denom);
    return el;
  }

  // A deterministic wobble per index, so a stack looks hand-placed but doesn't
  // reshuffle itself every time the page repaints.
  function jitter(i, spread) {
    var n = Math.sin((i + 1) * 12.9898) * 43758.5453;
    return ((n - Math.floor(n)) * 2 - 1) * (spread || 1);
  }

  // flyNode moves *anything* from one place to another and resolves when it
  // lands: a chip, a playing card, a card face down.
  //
  // The arc is the point. A straight line between two rects reads as a UI
  // transition; something that rises, travels and drops reads as a throw. WAAPI
  // does this in one keyframe list because it can interpolate a midpoint — a CSS
  // transition can't, which is why this isn't a class toggle.
  //
  // The node is yours: build it, hand it over, and it is gone when the promise
  // resolves. Nothing in here knows or cares what it was.
  function flyNode(node, from, to, opts) {
    opts = opts || {};
    var a = centre(from);
    var b = centre(to);
    node.classList.add("pete-fly");
    stage().appendChild(node);

    var dur = opts.duration || 420;
    if (reduced) dur = 1;

    // How high it goes: further throws arc higher, and a lob can be forced.
    var dist = Math.hypot(b.x - a.x, b.y - a.y);
    var lift = Math.min(120, 28 + dist * 0.18) * (opts.lift == null ? 1 : opts.lift);
    var midX = (a.x + b.x) / 2;
    var midY = (a.y + b.y) / 2 - lift;
    var spin = opts.spin == null ? jitter(opts.index || 0, 90) : opts.spin;
    var s0 = opts.fromScale == null ? 0.85 : opts.fromScale;
    var s1 = opts.toScale == null ? 1 : opts.toScale;
    // Bigger at the top of the arc than at either end — that swell is what sells
    // the thing as coming towards you. Taken off the larger end, so a throw that
    // starts small and lands full size still peaks above where it lands, rather
    // than averaging itself back down to nothing.
    var sMid = Math.max(s0, s1) * 1.12;

    var wait = reduced ? 0 : opts.delay || 0;

    // It makes a noise when it lands, and the noise is scheduled on the audio
    // clock for the moment it lands — not fired by a timer that goes off when the
    // animation ends. A timer that drifts thirty milliseconds is a card you hear
    // before you see, and that is worse than silence.
    if (opts.sound) {
      sfx(opts.sound, { delay: (wait + dur) / 1000, v: opts.index || 0 });
    }

    var anim = node.animate(
      [
        { transform: t(a.x, a.y, s0, opts.fromSpin || 0), opacity: 1, offset: 0 },
        { transform: t(midX, midY, sMid, spin * 0.6), opacity: 1, offset: 0.5 },
        { transform: t(b.x, b.y, s1, spin), opacity: opts.fade ? 0 : 1, offset: 1 },
      ],
      { duration: dur, easing: "cubic-bezier(0.33, 0, 0.25, 1)", delay: wait, fill: "both" }
    );

    return anim.finished
      .catch(function () {})
      .then(function () {
        node.remove();
      });
  }

  // fly throws one chip. It is flyNode with a chip in it, which is what it always
  // was — UNO wanted the same throw with a card in it, so the throw moved out.
  function fly(from, to, opts) {
    opts = opts || {};
    // A chip clinks. Every table in the room bets, pays and sweeps through this
    // one function, so saying it once here is every table having chip sounds.
    if (opts.sound === undefined) opts = Object.assign({ sound: "chip" }, opts);
    return flyNode(disc(opts.denom || 25), from, to, opts);
  }

  function t(x, y, scale, rot) {
    return "translate(" + x + "px," + y + "px) scale(" + scale + ") rotate(" + rot + "deg)";
  }

  // flyMany throws a whole bet across, one chip after another rather than all at
  // once, because a pile of chips arriving as a single object is a progress bar.
  // Resolves when the last one lands.
  function flyMany(from, to, denoms, opts) {
    opts = opts || {};
    var gap = reduced ? 0 : opts.gap == null ? 70 : opts.gap;
    var each = denoms.map(function (d, i) {
      return fly(from, to, {
        denom: d,
        index: i,
        delay: (opts.delay || 0) + i * gap,
        duration: opts.duration,
        lift: opts.lift,
        fade: opts.fade,
        spin: opts.spin,
      });
    });
    if (opts.onLand && !reduced) {
      denoms.forEach(function (d, i) {
        setTimeout(opts.onLand, (opts.delay || 0) + i * gap + (opts.duration || 420), d, i);
      });
    } else if (opts.onLand) {
      denoms.forEach(function (d, i) { opts.onLand(d, i); });
    }
    return Promise.all(each);
  }

  // A bet spot: the pile of chips sitting on it, and the number printed under
  // the pile.
  //
  // The rule the whole room is built on lives in here, which is why it's one
  // object and not two variables on a table: **the number is a readout of the
  // pile, never the other way round.** There is no way to change one without the
  // other, so a settled game can't leave "your bet: 300" printed over an empty
  // circle, and a payout can't be counted before the chips that justify it have
  // landed.
  //
  // els: {spot, stack, total}. Blackjack's spot holds the stake; solitaire's
  // holds what you've banked, which grows a card at a time. Same object.
  function spot(els) {
    var api = {
      // What the pile is holding. Written by render, and readable by a table that
      // needs to know whether a chip is already on its way down.
      amount: 0,

      render: function (n) {
        api.amount = n || 0;
        els.stack.innerHTML = "";
        if (els.spot) els.spot.dataset.live = api.amount > 0 ? "1" : "0";
        if (!api.amount) {
          if (els.total) els.total.classList.add("hidden");
          return;
        }
        chipsFor(api.amount).forEach(function (d, i) {
          var c = disc(d);
          c.style.setProperty("--i", i);
          c.style.setProperty("--spin", jitter(i, 12).toFixed(1) + "deg");
          c.style.animationDelay = (reduced ? 0 : i * 40) + "ms";
          els.stack.appendChild(c);
        });
        if (els.total) {
          els.total.textContent = api.amount.toLocaleString();
          els.total.classList.remove("hidden");
        }
      },

      // pour throws a run of chips onto the spot and grows the pile as each one
      // lands — by the value of the chip that landed, so the total under the pile
      // counts up the way the chips do. The last chip carries the remainder,
      // because chipsFor caps how many chips it will make you watch and the pile
      // still has to end on the real number.
      pour: function (from, amount, opts) {
        if (amount <= 0) return Promise.resolve();
        var base = api.amount;
        var chips = chipsFor(amount, 8);
        var run = 0;
        return flyMany(from, els.spot, chips, Object.assign({
          onLand: function (d, i) {
            run += d;
            api.render(base + (i === chips.length - 1 ? amount : run));
          },
        }, opts || {}));
      },

      // sweep sends chips off the spot to somewhere else — your pile, or the
      // house's rack. The spot is emptied *now* rather than when they land, so
      // nothing that is already in the air can be bet a second time.
      sweep: function (to, amount, opts) {
        var n = amount == null ? api.amount : amount;
        var left = api.amount - n;
        if (n <= 0) return Promise.resolve();
        sfx("sweep"); // the pile leaving the felt, under the chips landing
        var chain = flyMany(els.spot, to, chipsFor(n, 8), Object.assign({ gap: 40, lift: 0.8 }, opts || {}));
        api.render(left > 0 ? left : 0);
        return chain;
      },
    };
    return api;
  }

  // burst: confetti out of a point. Saved for the things worth celebrating — so
  // it is also, everywhere in the room, the sound of winning.
  function burst(target, opts) {
    sfx("win"); // before the reduced-motion bail: no confetti still deserves the fanfare
    if (reduced) return Promise.resolve();
    opts = opts || {};
    var c = centre(target);
    var n = opts.count || 26;
    var colours = opts.colours || ["#f2b53d", "#4caf7d", "#5aa9e6", "#b079d6", "#ffffff", "#cc3d4a"];
    var done = [];

    for (var i = 0; i < n; i++) {
      var bit = document.createElement("div");
      bit.className = "pete-spark";
      bit.style.background = colours[i % colours.length];
      stage().appendChild(bit);

      // Fired into a cone that mostly goes up, then let gravity have it.
      var angle = (-Math.PI / 2) + jitter(i, 1) * (opts.spread || 1.15);
      var speed = 120 + Math.abs(jitter(i + 7, 1)) * 190;
      var vx = Math.cos(angle) * speed;
      var vy = Math.sin(angle) * speed;
      var dropTo = c.y + 220 + jitter(i + 3, 60);

      done.push(
        bit
          .animate(
            [
              { transform: t(c.x, c.y, 0.6, 0), opacity: 1, offset: 0 },
              {
                transform: t(c.x + vx * 0.45, c.y + vy * 0.45, 1, jitter(i, 180)),
                opacity: 1,
                offset: 0.4,
              },
              // Held at full colour most of the way down: a piece of confetti that
              // starts fading the moment it's thrown reads as smoke.
              {
                transform: t(c.x + vx * 0.75, c.y + vy * 0.3 + 110, 0.95, jitter(i, 380)),
                opacity: 1,
                offset: 0.75,
              },
              {
                transform: t(c.x + vx * 0.9, dropTo, 0.9, jitter(i, 540)),
                opacity: 0,
                offset: 1,
              },
            ],
            {
              duration: 900 + Math.abs(jitter(i + 11, 1)) * 500,
              easing: "cubic-bezier(0.18, 0.7, 0.4, 1)",
              fill: "both",
            }
          )
          .finished.catch(function () {})
          .then(
            (function (b) {
              return function () { b.remove(); };
            })(bit)
          )
      );
    }
    return Promise.all(done);
  }

  // moneyRain: it rains money. The big finish on a win — bills and coins tumbling
  // the whole height of the window, swaying and spinning as they fall. Louder than
  // confetti, and it carries its own sound (a coin cascade) so it can stand in for
  // the plain win fanfare wherever a table decides a win is worth the theatre.
  //
  // Like burst, the sound comes before the reduced-motion bail: no rain still
  // deserves to be heard.
  function moneyRain(opts) {
    opts = opts || {};
    if (opts.sound !== false) sfx(opts.sound || "jackpot");
    if (reduced) return Promise.resolve();

    var n = opts.count || 22;
    var glyphs = opts.glyphs || ["💶", "💰", "🪙", "💵", "💴", "🤑"];
    var vw = window.innerWidth || document.documentElement.clientWidth;
    var vh = window.innerHeight || document.documentElement.clientHeight;
    var done = [];

    for (var i = 0; i < n; i++) {
      var bit = document.createElement("div");
      bit.className = "pete-money";
      bit.textContent = glyphs[i % glyphs.length];
      stage().appendChild(bit);

      // Spread across the width in even lanes, each nudged off its lane so the
      // curtain doesn't read as a grid. It sways sideways and spins on the way down.
      var x = ((i + 0.5) / n) * vw + jitter(i, vw / n / 2);
      var drift = jitter(i + 5, 80);
      var startY = -50 - Math.abs(jitter(i + 2, 180));
      var spin = jitter(i, 300);
      var scale = 0.85 + Math.abs(jitter(i + 3, 0.55));

      done.push(
        bit
          .animate(
            [
              { transform: t(x, startY, scale, 0), opacity: 0, offset: 0 },
              { transform: t(x + drift, startY + 40, scale, spin * 0.2), opacity: 1, offset: 0.1 },
              { transform: t(x - drift, vh * 0.55, scale, spin * 0.6), opacity: 1, offset: 0.6 },
              { transform: t(x + drift * 0.5, vh + 60, scale, spin), opacity: 1, offset: 1 },
            ],
            {
              duration: 1600 + Math.abs(jitter(i + 9, 1)) * 900,
              easing: "cubic-bezier(0.35, 0.15, 0.55, 1)",
              fill: "both",
              delay: Math.abs(jitter(i, 320)),
            }
          )
          .finished.catch(function () {})
          .then(
            (function (b) {
              return function () { b.remove(); };
            })(bit)
          )
      );
    }
    return Promise.all(done);
  }

  // count rolls a number to a new value instead of swapping it. A chip count that
  // jumps is a variable; one that climbs is a payout.
  function count(el, to, opts) {
    if (!el) return;
    opts = opts || {};
    var from = parseInt(String(el.textContent).replace(/[^0-9-]/g, ""), 10);
    if (isNaN(from) || reduced || from === to) {
      el.textContent = (to || 0).toLocaleString();
      return;
    }
    var dur = opts.duration || 520;
    var t0 = null;
    if (el._petePop) el._petePop.cancel();
    el._petePop = el.animate(
      [{ transform: "scale(1)" }, { transform: "scale(1.12)" }, { transform: "scale(1)" }],
      { duration: dur, easing: "ease-out" }
    );

    function step(ts) {
      if (t0 === null) t0 = ts;
      var p = Math.min(1, (ts - t0) / dur);
      var eased = 1 - Math.pow(1 - p, 3);
      el.textContent = Math.round(from + (to - from) * eased).toLocaleString();
      if (p < 1) requestAnimationFrame(step);
      else el.textContent = (to || 0).toLocaleString();
    }
    requestAnimationFrame(step);
  }

  window.PeteFX = {
    reduced: reduced,
    sfx: sfx, // so a table can make a noise without reaching past this file
    chipsFor: chipsFor,
    disc: disc,
    jitter: jitter,
    fly: fly,
    flyNode: flyNode,
    flyMany: flyMany,
    spot: spot,
    burst: burst,
    moneyRain: moneyRain,
    count: count,
    centre: centre,
  };
})();
