// The trivia table.
//
// Same bargain as every other table in the room: the browser holds no game. It
// sends an answer, and the server says how it went. The four buttons arrive
// without any mark on which of them is right — that index is in the engine
// state, on the server — and the reveal only comes back in the event that
// decides the question, by which point knowing it is worth nothing.
//
// The countdown here is decoration, and it is important to be clear about that.
// Nothing it does scores anything: the server timed the answer the moment it
// arrived, against the clock it started when it served the question. Stopping
// this bar, or reloading to restart it, changes nothing at all. What the bar
// owes the player is *honesty* — so it is seeded from the seconds the server
// says are left, not from when the browser got round to painting.
(function () {
  "use strict";

  var root = document.querySelector("[data-trivia]");
  if (!root) return;

  var FX = window.PeteFX;

  var questionEl  = root.querySelector("[data-question]");
  var categoryEl  = root.querySelector("[data-category]");
  var answersEl   = root.querySelector("[data-answers]");
  var clockEl     = root.querySelector("[data-clock]");
  var clockFillEl = root.querySelector("[data-clock-fill]");
  var countdownEl = root.querySelector("[data-countdown]");
  var ladderEl    = root.querySelector("[data-ladder]");
  var multEl      = root.querySelector("[data-multiple]");
  var meterEl     = root.querySelector("[data-meter]");
  var standsEl    = root.querySelector("[data-stands]");
  var standsLbl   = root.querySelector("[data-stands-label]");
  var rungEl      = root.querySelector("[data-rung]");
  var verdictEl   = root.querySelector("[data-verdict]");

  var betting   = root.querySelector("[data-betting]");
  var playing   = root.querySelector("[data-playing]");
  var walkBtn   = root.querySelector("[data-walk]");
  var walkAmtEl = root.querySelector("[data-walk-amount]");
  var betAmount = root.querySelector("[data-bet-amount]");
  var startBtn  = root.querySelector("[data-start]");
  var msgEl     = root.querySelector("[data-table-msg]");
  var gameMsgEl = root.querySelector("[data-game-msg]");

  var purseEl = document.querySelector("[data-chips]");
  var spotEl  = root.querySelector("[data-spot]");
  var houseEl = root.querySelector("[data-house]");

  // The bet spot, and the rule that comes with it: the number under the pile is
  // a readout of the pile, never the other way round.
  var spot = FX.spot({
    spot:  spotEl,
    stack: root.querySelector("[data-stack]"),
    total: root.querySelector("[data-spot-total]"),
  });

  var bet = 0;      // what you're building between games
  var busy = false;
  var game = null;  // the round as the server last described it
  var tier = "medium";

  var reduced = FX.reduced;
  function pace(ms) { return reduced ? 0 : ms; }
  function wait(ms) { return new Promise(function (r) { setTimeout(r, pace(ms)); }); }

  function say(text, tone, where) {
    var el = where || msgEl;
    if (!el) return;
    if (!text) { el.classList.add("hidden"); return; }
    el.textContent = text;
    el.classList.remove("hidden");
    el.style.color = tone === "bad" ? "#cc3d4a" : "";
  }

  // ---- the clock -------------------------------------------------------------

  var raf = null;
  var clockDeadline = 0; // performance.now() ms at which this question dies
  var clockLimit = 1;
  var timedOut = false;

  // HOT is when the bar stops being a progress meter and starts being a warning.
  var HOT = 5;

  // GRACE is how long past its own zero the browser waits before reporting the
  // timeout. It cannot be negative: the browser's countdown starts when the
  // response *arrives*, so it is already behind the server's by the latency of
  // that response, and it therefore always reaches zero after the server has.
  // The grace is only there so that "always" survives a rounding error.
  var GRACE = 400;

  function stopClock() {
    if (raf) cancelAnimationFrame(raf);
    raf = null;
  }

  function startClock(left, limit) {
    stopClock();
    timedOut = false;
    clockLimit = limit > 0 ? limit : 1;
    clockDeadline = performance.now() + left * 1000;
    tick();
  }

  function tick() {
    var left = (clockDeadline - performance.now()) / 1000;
    if (left < 0) left = 0;

    // A transform, not a width: the browser can run this one without laying the
    // page out again on every frame.
    clockFillEl.style.transform = "scaleX(" + (left / clockLimit).toFixed(4) + ")";
    countdownEl.textContent = left.toFixed(1) + "s";
    clockEl.dataset.hot = left <= HOT && left > 0 ? "1" : "0";

    if (left <= 0) {
      countdownEl.textContent = "0.0s";
      clockEl.dataset.hot = "0";
      timeUp();
      return;
    }
    raf = requestAnimationFrame(tick);
  }

  // timeUp reports the clock running out. It is not the browser *deciding* the
  // question — it is the browser telling the server the player never answered,
  // and the server (which has been holding the real clock all along) agreeing.
  function timeUp() {
    stopClock();
    if (timedOut || !game || game.phase !== "playing") return;
    // A move is already in flight. Come back rather than give up: this fires when
    // the server has rejected our last timeout report (its clock hadn't run out
    // yet) and the refresh has re-armed a countdown that is already at zero. Give
    // up here and the clock sits frozen at 0.0s and the question never resolves.
    if (busy) { setTimeout(timeUp, 250); return; }
    timedOut = true;
    lockAnswers();
    setTimeout(function () {
      // -1 is "no answer". The engine checks the clock before it checks the
      // choice, so this resolves as the timeout it is rather than a bad move.
      send("/api/games/trivia/answer", { choice: -1 }, gameMsgEl);
    }, GRACE);
  }

  function clearClock() {
    stopClock();
    clockFillEl.style.transform = "scaleX(0)";
    countdownEl.textContent = "";
    clockEl.dataset.hot = "0";
  }

  // ---- the question ----------------------------------------------------------

  var KEYS = ["1", "2", "3", "4"];

  function renderQuestion(v) {
    answersEl.innerHTML = "";
    if (!v || v.phase !== "playing" || !v.answers) {
      questionEl.textContent = "";
      categoryEl.textContent = "";
      return;
    }
    categoryEl.textContent = v.category || "";
    questionEl.textContent = v.question || "";

    v.answers.forEach(function (text, i) {
      var b = document.createElement("button");
      b.type = "button";
      b.className = "pete-answer";
      b.dataset.at = String(i);

      var key = document.createElement("span");
      key.className = "pete-answer-key";
      key.textContent = KEYS[i] || "";
      key.setAttribute("aria-hidden", "true");

      var label = document.createElement("span");
      label.textContent = text;

      b.appendChild(key);
      b.appendChild(label);
      b.addEventListener("click", function () { answer(i); });
      answersEl.appendChild(b);
    });
  }

  function lockAnswers() {
    answersEl.querySelectorAll(".pete-answer").forEach(function (b) { b.disabled = true; });
  }

  // reveal marks the board once the server has decided. Nothing in here is known
  // until it comes back: the right answer arrives in the event, not in the view.
  function reveal(choice, correct) {
    answersEl.querySelectorAll(".pete-answer").forEach(function (b) {
      var i = parseInt(b.dataset.at, 10);
      b.disabled = true;
      if (i === choice && i === correct) b.dataset.state = "right";
      else if (i === choice) b.dataset.state = "wrong";
      else if (i === correct) b.dataset.state = "missed";
      else b.dataset.state = "dim";
    });
  }

  // ---- the meters ------------------------------------------------------------

  function renderLadder(v) {
    ladderEl.innerHTML = "";
    var rungs = (v && v.rungs) || 12;
    var done = (v && v.rung) || 0;
    for (var i = 0; i < rungs; i++) {
      var pip = document.createElement("span");
      pip.className = "pete-rung";
      pip.dataset.on = i < done ? "1" : "0";
      ladderEl.appendChild(pip);
    }
  }

  function renderMeter(v) {
    if (!v) {
      multEl.textContent = "—";
      standsEl.textContent = "—";
      standsLbl.textContent = "if you walk";
      meterEl.dataset.cold = "1";
      rungEl.textContent = "";
      return;
    }
    multEl.textContent = v.multiple.toFixed(2) + "×";
    standsEl.textContent = (v.stands || 0).toLocaleString();
    meterEl.dataset.cold = v.rung === 0 ? "1" : "0";

    if (v.phase === "done") {
      standsLbl.textContent = v.net > 0 ? "banked" : "gone";
      rungEl.textContent = "";
      return;
    }
    standsLbl.textContent = v.can_walk ? "if you walk" : "answer one to unlock";
    rungEl.textContent = "Question " + (v.rung + 1) + " of " + v.rungs;
  }

  // knock rolls the multiple up to its new value rather than swapping it, so a
  // right answer reads as the total *growing* — which is the thing you're
  // deciding whether to risk.
  function climb(v) {
    var from = parseFloat(multEl.textContent) || 1;
    var to = v.multiple;
    if (reduced) { renderMeter(v); return; }
    var t0 = performance.now();
    meterEl.dataset.hit = "0";

    (function step(now) {
      var p = Math.min(1, (now - t0) / 420);
      var eased = 1 - Math.pow(1 - p, 3);
      multEl.textContent = (from + (to - from) * eased).toFixed(2) + "×";
      if (p < 1) requestAnimationFrame(step);
      else renderMeter(v);
    })(t0);
  }

  // ---- the money -------------------------------------------------------------

  function settleChips(final) {
    var payout = final.payout || 0;
    var back = payout - final.bet;

    if (payout <= 0) {
      var chain = spot.sweep(houseEl, final.bet, { gap: 45, lift: 0.6, fade: true });
      return chain;
    }
    return spot
      .pour(houseEl, back, { gap: 60 })
      .then(function () { return wait(back > 0 ? 380 : 200); })
      .then(function () { return spot.sweep(purseEl, payout, { gap: 40, lift: 0.8 }); });
  }

  // standing puts the stake back on the spot for the next ladder, the way every
  // other table in the room leaves your bet up.
  function standing(amount) {
    var money = window.PeteGames.view();
    if (!amount || !money || money.chips < amount) {
      bet = 0;
      showBet();
      return;
    }
    bet = amount;
    showBet();
    // pour grows the pile from whatever is on the spot, and settle has just swept
    // it clean — so it must not be told the chips are already there. Setting the
    // amount first counted the standing bet twice, and the spot printed 400 under
    // a 200 stake.
    return spot.pour(purseEl, amount);
  }

  // ---- phases ----------------------------------------------------------------

  var VERDICTS = {
    walked:  "Banked it.",
    cleared: "Cleared the board! 🎉",
    wrong:   "Wrong.",
    timeout: "Out of time.",
  };

  function verdict(v) {
    var text = VERDICTS[v.outcome] || "";
    if (!text) { verdictEl.classList.add("hidden"); return; }
    if (v.net > 0) text += "  +" + v.net.toLocaleString();
    else if (v.net < 0) text += "  " + v.net.toLocaleString();
    verdictEl.textContent = text;
    verdictEl.classList.remove("hidden");

    // Clearing all twelve keeps the confetti on top; any win at all makes it rain.
    if (v.outcome === "cleared") { FX.burst(verdictEl, { count: 34 }); FX.moneyRain({ count: 30 }); }
    else if (v.net > 0) FX.moneyRain();
  }

  function setPhase(v) {
    game = v;
    var live = !!v && v.phase === "playing";
    betting.classList.toggle("hidden", live);
    playing.classList.toggle("hidden", !live);
    if (walkBtn) {
      walkBtn.disabled = !live || !v.can_walk;
      walkAmtEl.textContent = (v && v.can_walk ? v.stands : 0).toLocaleString();
    }
    if (!v || !v.outcome) verdictEl.classList.add("hidden");
  }

  // paint puts a round up with no animation: the resume path, after a reload or a
  // redeploy. The clock picks up exactly where the server says it is — which is
  // the whole point of it being the server's clock.
  function paint(v) {
    if (!v) {
      clearClock();
      renderQuestion(null);
      renderLadder(null);
      renderMeter(null);
      spot.render(0);
      setPhase(null);
      return;
    }
    renderQuestion(v);
    renderLadder(v);
    renderMeter(v);
    spot.render(v.phase === "done" ? 0 : v.bet);
    setPhase(v);

    if (v.phase === "playing") startClock(v.left, v.limit);
    else clearClock();
  }

  // ---- the script ------------------------------------------------------------

  // play walks the server's events. Same rule as the other tables: on a live
  // round the money is already right (your stake left your pile when you pressed
  // Play, and it's on the spot), but on a settling one the chip bar is held back
  // until the chips have physically come home. A counter that pays you before
  // the reveal is a counter that has told you the ending.
  function play(view, money) {
    var events = view.triv_events || [];
    var final = view.trivia;
    var settles = !!final && final.phase === "done";
    var chain = Promise.resolve();

    if (!settles) money();

    stopClock();

    events.forEach(function (e) {
      chain = chain.then(function () {
        switch (e.kind) {
          case "ask":
            // A fresh question. Everything about the last one goes.
            verdictEl.classList.add("hidden");
            renderQuestion(final);
            renderLadder(final);
            if (final && final.phase === "playing") startClock(final.left, final.limit);
            FX.sfx("blip");
            return;

          case "right":
            reveal(e.choice, e.correct);
            // Three notes up, one per rung climbed — the ladder, in sound.
            [0, 0.09, 0.18].forEach(function (d, i) {
              FX.sfx("tick", { delay: d, v: i });
            });
            FX.sfx("win", { delay: 0.1 });
            if (final) {
              // The rung lighting and the multiple climbing are one event,
              // because they are one event: this is what the answer was worth.
              climb({ multiple: e.multiple, rung: final.rung, rungs: final.rungs,
                      stands: final.stands, can_walk: true, phase: "playing" });
              renderLadder(final);
            }
            return wait(900);

          case "wrong":
            reveal(e.choice, e.correct);
            FX.sfx("bad");
            return wait(1100);

          case "timeout":
            // The clock beat you to it, which is a different kind of bad.
            reveal(-1, e.correct);
            FX.sfx("lose");
            return wait(1100);

          case "settle":
            return;
        }
      });
    });

    return chain.then(function () {
      if (!final) { paint(null); money(); return; }

      if (!settles) {
        renderMeter(final);
        setPhase(final);
        return;
      }

      // Over: the clock stops, the money moves, and only then does the bar catch up.
      clearClock();
      playing.classList.add("hidden");
      renderMeter(final);
      renderLadder(final);
      verdict(final);
      return settleChips(final)
        .then(money)
        .then(function () { return standing(final.bet); })
        .then(function () { setPhase(final); });
    });
  }

  // ---- talking to the table ---------------------------------------------------

  function send(path, body, where) {
    if (busy) return;
    busy = true;
    say("", null, where);
    return window.PeteGames.post(path, body)
      .then(function (view) {
        return play(view, function () { window.PeteGames.apply(view); });
      })
      .catch(function (err) {
        say(err.message, "bad", where);
        return window.PeteGames.refresh().then(function (v) {
          if (v && v.trivia) paint(v.trivia);
          else { paint(null); spot.render(0); }
        });
      })
      .then(function () { busy = false; });
  }

  function answer(i) {
    if (busy || timedOut || !game || game.phase !== "playing") return;
    stopClock();
    lockAnswers();
    send("/api/games/trivia/answer", { choice: i }, gameMsgEl);
  }

  if (walkBtn) {
    walkBtn.addEventListener("click", function () {
      if (busy || !game || game.phase !== "playing" || !game.can_walk) return;
      stopClock();
      lockAnswers();
      send("/api/games/trivia/answer", { walk: true }, gameMsgEl);
    });
  }

  // 1–4 answers the question. The key is printed on the button it answers.
  document.addEventListener("keydown", function (e) {
    if (e.metaKey || e.ctrlKey || e.altKey) return;
    if (/^(input|textarea|select)$/i.test(e.target.tagName || "")) return;
    if (!game || game.phase !== "playing" || busy) return;
    var i = KEYS.indexOf(e.key);
    if (i === -1) return;
    var btn = answersEl.querySelector('.pete-answer[data-at="' + i + '"]');
    if (!btn || btn.disabled) return;
    e.preventDefault();
    answer(i);
  });

  // ---- betting ----------------------------------------------------------------

  function showBet() {
    betAmount.textContent = bet.toLocaleString();
    var money = window.PeteGames.view();
    if (startBtn) startBtn.disabled = bet <= 0 || !tier || !money || money.chips < bet;
  }

  function pickTier(slug) {
    tier = slug;
    root.querySelectorAll("[data-tier]").forEach(function (b) {
      b.dataset.on = b.dataset.tier === slug ? "1" : "0";
    });
    showBet();
  }

  root.querySelectorAll("[data-tier]").forEach(function (b) {
    b.addEventListener("click", function () {
      if (busy) return;
      pickTier(b.dataset.tier);
    });
  });

  // The chip you click is the chip that flies. Scoped to buttons: the bare
  // [data-chip] spans in the corner are the house's rack, and it is not betting.
  root.querySelectorAll("button[data-chip]").forEach(function (btn) {
    btn.addEventListener("click", function () {
      if (busy) return;
      var d = parseInt(btn.dataset.chip, 10);
      var money = window.PeteGames.view();
      if (money && bet + d > money.chips) {
        say("You haven't got that many chips.", "bad");
        return;
      }
      bet += d;
      showBet();

      var target = bet;
      spot.amount = bet;
      FX.fly(btn, spotEl, { denom: d }).then(function () {
        if (bet >= target) spot.render(target); // unless Clear got there first
      });
    });
  });

  var clearBtn = root.querySelector("[data-bet-clear]");
  if (clearBtn) {
    clearBtn.addEventListener("click", function () {
      if (busy) return;
      if (spot.amount) spot.sweep(purseEl, null, { gap: 40, lift: 0.7 });
      bet = 0;
      showBet();
    });
  }

  if (startBtn) {
    startBtn.addEventListener("click", function () {
      if (busy) return;
      if (!tier) { say("Pick a difficulty first.", "bad"); return; }
      if (bet <= 0) { say("Put something on it first.", "bad"); return; }
      // The stake stays on the spot for the whole ladder: it is what's at risk,
      // and it is riding on every question until you take it back or lose it.
      send("/api/games/trivia/start", { bet: bet, tier: tier });
    });
  }

  pickTier(tier);

  var resumed = false;
  window.PeteGames.onUpdate(function (v) {
    if (!resumed) {
      resumed = true;
      if (v.trivia) {
        paint(v.trivia);
        if (v.trivia.phase === "done") verdict(v.trivia);
      } else {
        paint(null);
      }
    }
    showBet();
  });
})();
