// The blackjack table.
//
// The browser holds no game. It sends intents — deal, hit, stand, double — and
// the server answers with the cards you're allowed to see plus the *script* of
// how they got there: one event per card off the shoe, in the order the shoe
// gave them up. This file's job is to play that script back at a human speed
// rather than snapping the finished hand into place.
//
// Which is also why the hole card works the way it does: the server sends a
// "dealer_hole" event with no card attached, because while you are still acting
// it hasn't told anyone what that card is. It arrives with the reveal.
//
// The money is animated on the same principle. Every number the server sends is
// also a movement: a stake is chips leaving your pile and landing on the spot, a
// payout is chips coming out of the house's rack, a loss is your stack being
// taken away. Nothing about the money changes on this table without something
// crossing the felt to make it change — including the count in the chip bar,
// which is deliberately not updated until the chips that justify it have landed.
(function () {
  "use strict";

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

  var FX = window.PeteFX;

  var dealerEl  = root.querySelector("[data-dealer]");
  var playerEl  = root.querySelector("[data-player]");
  var dTotalEl  = root.querySelector("[data-dealer-total]");
  var pTotalEl  = root.querySelector("[data-player-total]");
  var dLabelEl  = root.querySelector("[data-dealer-label]");
  var verdictEl = root.querySelector("[data-verdict]");
  var betting   = root.querySelector("[data-betting]");
  var actions   = root.querySelector("[data-actions]");
  var betAmount = root.querySelector("[data-bet-amount]");
  var dealBtn   = root.querySelector("[data-deal]");
  var msgEl     = root.querySelector("[data-table-msg]");

  // The three places a chip can be: your pile (in the bar above), the spot in
  // front of you, and the house's rack on the felt.
  var purseEl = document.querySelector("[data-chips]");
  var spotEl  = root.querySelector("[data-spot]");
  var stackEl = root.querySelector("[data-stack]");
  var spotTotalEl = root.querySelector("[data-spot-total]");
  var houseEl = root.querySelector("[data-house]");

  // The spot owns the chips on the felt and the number under them — see PeteFX.
  // Nothing is bet until a chip is on it, so `bet` starts at nothing rather than
  // at a default stake nobody put down.
  var spot = FX.spot({ spot: spotEl, stack: stackEl, total: spotTotalEl });

  var bet = 0;       // what you're building between hands
  var busy = false;  // a request is in flight, or cards are still landing
  var hand = null;   // the hand as the server last described it

  var DEAL_MS = 380;  // one card's flight, and the gap before the next
  var FLIP_MS = 450;
  var BEAT_MS = 600;  // the dealer thinking before they draw out

  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) {
    if (!text) { msgEl.classList.add("hidden"); return; }
    msgEl.textContent = text;
    msgEl.classList.remove("hidden");
    msgEl.style.color = tone === "bad" ? "#cc3d4a" : "";
  }

  // ---- drawing --------------------------------------------------------------
  //
  // The deck itself — the faces, the pips, the flip — is PeteCards, shared with
  // every other table in the room. Here a card is always dealt out of the shoe
  // and always lands with a degree or two of tilt on it, which are this table's
  // two opinions about a card and the only ones it has.

  var CARDS = window.PeteCards;

  function cardEl(face) { return CARDS.el(face); }
  var turnOver = CARDS.turnOver;

  // ---- the money on the felt -------------------------------------------------

  // stake moves chips from your pile onto the spot: the bet you build before a
  // deal, and the second bet a double puts down beside it.
  function stake(amount, from) {
    return spot.pour(from || purseEl, amount);
  }

  // settleChips is what the felt does about the outcome, after the cards have
  // finished telling you what it is. It reads the same two numbers the ledger
  // moved: `bet` (already off your pile since the deal) and `payout` (what comes
  // back — stake plus winnings less rake, or nothing at all).
  function settleChips(final) {
    var payout = final.payout || 0;
    var back = payout - final.bet; // what the house is adding, if anything

    if (payout <= 0) {
      // The house takes it. The stack goes to the rack and doesn't come back.
      return spot.sweep(houseEl, final.bet, { gap: 45, lift: 0.6, fade: true });
    }

    // The house pays first, into the spot beside your stake, so you watch the
    // winnings arrive on top of the bet that earned them.
    return spot
      .pour(houseEl, back, { gap: 60 })
      .then(function () { return wait(back > 0 ? 380 : 200); })
      // Paid, then swept up: the whole lot comes back to your pile, and only then
      // does the number in the bar move.
      .then(function () { return spot.sweep(purseEl, payout, { gap: 40, lift: 0.8 }); });
  }

  function totals(v) {
    if (v.total) {
      pTotalEl.textContent = v.total + (v.soft ? " (soft)" : "");
      pTotalEl.classList.remove("hidden");
    } else {
      pTotalEl.classList.add("hidden");
    }
    // While the hole card is down, the dealer's total is only what's showing —
    // so say so, rather than printing a number that quietly means something else.
    if (v.dealer && v.dealer.length) {
      dTotalEl.textContent = v.hole ? v.dealer_total + " showing" : String(v.dealer_total);
      dTotalEl.classList.remove("hidden");
    } else {
      dTotalEl.classList.add("hidden");
    }
  }

  // paint puts a hand on the felt with no animation. This is the resume path:
  // you reloaded, or Pete restarted, and your cards are simply there — including
  // the stake, which is still on the spot because the server still has it.
  function paint(v) {
    dealerEl.innerHTML = "";
    playerEl.innerHTML = "";
    if (!v) { setPhase(null); spot.render(0); return; }

    v.player.forEach(function (c) { playerEl.appendChild(cardEl(c)); });
    v.dealer.forEach(function (c) { dealerEl.appendChild(cardEl(c)); });
    if (v.hole) dealerEl.appendChild(cardEl(null));
    spot.render(v.phase === "done" ? 0 : v.bet);
    totals(v);
    setPhase(v);
  }

  var VERDICTS = {
    blackjack:   "Blackjack! 🎉",
    win:         "You win!",
    dealer_bust: "Dealer busts. You win!",
    lose:        "Dealer takes it.",
    bust:        "Bust.",
    push:        "Push — your bet comes back.",
  };

  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");
    playerEl.dataset.won = v.net > 0 ? "1" : v.net < 0 ? "-1" : "0";

    // The one thing in this room that gets confetti. A natural is rare, it pays
    // 3:2, and if everything celebrated then nothing would.
    if (v.outcome === "blackjack") FX.burst(verdictEl, { count: 34 });
  }

  // setPhase swaps the controls: bet between hands, act during one.
  function setPhase(v) {
    hand = v;
    var live = !!v && v.phase === "player";
    betting.classList.toggle("hidden", live);
    actions.classList.toggle("hidden", !live);

    if (live) {
      var dbl = actions.querySelector('[data-move="double"]');
      if (dbl) dbl.disabled = !v.can_double;
    }
    if (!v || v.phase !== "player") verdictEl.classList.toggle("hidden", !(v && v.outcome));
  }

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

  // play walks the server's events, one card at a time. It is deliberately the
  // only thing that renders during a hand: the final state is applied at the end,
  // so what you watch and what the server says can't disagree halfway through.
  //
  // `money` is the one exception, and it's a deliberate one. On a hand that is
  // still running, the chip bar is right immediately — your stake left your pile
  // when you pressed Deal, and it's sitting on the spot where you can see it. On
  // a hand that *settles*, the bar is left alone until the chips have physically
  // come home, because a counter that pays you before the dealer has turned over
  // is a counter that has told you the ending.
  function play(view, money) {
    var events = view.events || [];
    var final = view.hand;
    var settles = !!final && final.phase === "done";
    var hole = null;   // the face-down card element, once one has been dealt
    var chain = Promise.resolve();
    var drew = false;  // has the dealer drawn since the reveal?

    if (!settles) money();

    // Whatever the server says the stake is, that's what has to be on the spot.
    // Two things get here: a double, which puts a second bet down beside the
    // first, and a deal whose bet was typed rather than stacked (you kept last
    // hand's number and just pressed Deal). Either way the chips go down before
    // the card they're buying does.
    if (final && final.bet > spot.amount) {
      var extra = final.bet - spot.amount;
      chain = chain.then(function () { return stake(extra); });
    }

    events.forEach(function (e) {
      chain = chain.then(function () {
        switch (e.kind) {
          case "deal":
            dealerEl.innerHTML = "";
            playerEl.innerHTML = "";
            playerEl.dataset.won = "0";
            verdictEl.classList.add("hidden");
            return;

          case "player_card":
            playerEl.appendChild(cardEl(e.card));
            return wait(DEAL_MS);

          case "dealer_card":
            // The dealer takes a moment before the first card they draw out.
            // Card, card, card with no breath in between is a machine dealing;
            // the pause is the only thing on this table that plays as suspense.
            var beat = drew ? Promise.resolve() : think();
            drew = true;
            return beat.then(function () {
              dealerEl.appendChild(cardEl(e.card));
              return wait(DEAL_MS);
            });

          case "dealer_hole":
            hole = cardEl(null);
            dealerEl.appendChild(hole);
            return wait(DEAL_MS);

          case "reveal":
            // The hole card turns over. Its face is in the final hand — this is
            // the first moment the server has been willing to say what it was.
            if (!hole) hole = dealerEl.querySelector('.pete-card[data-face="down"]');
            if (hole && final && final.dealer && final.dealer[1]) {
              turnOver(hole, final.dealer[1]);
            }
            return wait(FLIP_MS);

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

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

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

      // The hand is over: nothing is on offer while the money is moving. Hit and
      // Stand go now, and Deal comes back at the far end.
      actions.classList.add("hidden");
      verdict(final);
      // The chips move, and the bar catches up with them when they arrive. The
      // betting controls come back last, once the felt is clear: offering Deal
      // over a table that is still being paid out invites a click the table then
      // has to refuse.
      return settleChips(final)
        .then(money)
        .then(function () { return standing(final.bet); })
        .then(function () { setPhase(final); });
    });
  }

  // standing leaves your bet up for the next hand, the way you would at a table:
  // the stake that just settled goes straight back on the spot. It costs nothing
  // — chips on the spot are a proposal until you press Deal — and it's what keeps
  // the number in the panel honest, because otherwise a settled hand leaves
  // "your bet: 300" printed over an empty spot.
  function standing(amount) {
    var money = window.PeteGames.view();
    if (!amount || !money || money.chips < amount) {
      bet = 0;
      showBet();
      return;
    }
    bet = amount;
    showBet();
    return stake(amount);
  }

  // think is the dealer's beat: a pause with something to look at, so it reads as
  // deliberation rather than as the page having hung.
  function think() {
    if (reduced || !dLabelEl) return wait(0);
    dLabelEl.classList.add("pete-dealer-think");
    return wait(BEAT_MS).then(function () {
      dLabelEl.classList.remove("pete-dealer-think");
    });
  }

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

  function send(path, body) {
    if (busy) return;
    busy = true;
    say("");
    return window.PeteGames.post(path, body)
      .then(function (view) {
        // play() decides *when* the money lands; see the note on it.
        return play(view, function () { window.PeteGames.apply(view); });
      })
      .catch(function (err) {
        say(err.message, "bad");
        // Whatever we thought was on the felt, the server is the authority on it.
        return window.PeteGames.refresh().then(function (v) {
          if (v && !v.hand) spot.render(0);
        });
      })
      .then(function () { busy = false; });
  }

  // ---- betting --------------------------------------------------------------
  //
  // A bet is built by putting chips on the spot, one at a time, and it is those
  // chips the deal then rides on — the number under the pile is a readout of the
  // pile, not the other way round.

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

  // Scoped to buttons: the bare [data-chip] spans in the corner are the house's
  // rack, and the house 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();

      // The chip you clicked is the chip that flies: same colour, same size, off
      // the button and onto the felt. The pile only grows once it gets there —
      // but the spot's total moves now, so a Deal pressed mid-flight still knows
      // the chip is on its way and doesn't put a second one down.
      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 || !spot.amount) { bet = 0; showBet(); return; }
      spot.sweep(purseEl, null, { gap: 40, lift: 0.7 });
      bet = 0;
      showBet();
    });
  }

  if (dealBtn) {
    dealBtn.addEventListener("click", function () {
      if (bet <= 0) { say("Put something on it first.", "bad"); return; }
      send("/api/games/blackjack/deal", { bet: bet });
    });
  }

  root.querySelectorAll("[data-move]").forEach(function (btn) {
    btn.addEventListener("click", function () {
      send("/api/games/blackjack/move", { move: btn.dataset.move });
    });
  });

  document.addEventListener("keydown", function (e) {
    if (e.metaKey || e.ctrlKey || e.altKey) return;
    if (/^(input|textarea|select)$/i.test((e.target.tagName || ""))) return;
    if (!hand || hand.phase !== "player" || busy) return;

    var move = { h: "hit", s: "stand", d: "double" }[e.key.toLowerCase()];
    if (!move) return;
    if (move === "double" && !hand.can_double) return;
    e.preventDefault();
    send("/api/games/blackjack/move", { move: move });
  });

  // The money bar owns the first fetch; the table picks up whatever it found,
  // including a hand left sitting on the felt by a reload or a redeploy.
  var resumed = false;
  window.PeteGames.onUpdate(function (v) {
    if (!resumed) {
      resumed = true;
      if (v.hand) paint(v.hand);
      if (v.hand && v.hand.phase === "done") verdict(v.hand);
    }
    showBet();
  });
})();
