// The offline tile bag — the shuffled draw pile for one local game. Structurally a port of
// backend/internal/engine/bag.go (fill from the variant's counts + blanks, draw from the end,
// return-and-reshuffle for an exchange), but shuffled with a small DETERMINISTIC in-house PRNG
// rather than Go's math/rand: a local game only needs to be reproducible from its own seed and
// sequence of operations (for replay from the stored journal), not bit-identical to a server
// game (docs plan). Blanks ride as BLANK_INDEX, matching lib/alphabet.ts (and the engine's
// blankTile = 0xff = 255).

import { RULESETS } from './ruleset';
import { BLANK_INDEX } from '../alphabet';
import type { Variant } from '../model';

// mulberry32 is a compact deterministic PRNG returning a float in [0, 1). Seeded from the game
// seed so the shuffle sequence — and thus the draws — replay identically for the same seed and
// the same sequence of returns.
function mulberry32(seed: number): () => number {
  let a = seed >>> 0;
  return () => {
    a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

/**
 * Bag is one local game's draw pile. Construct it with the variant and a numeric seed; draw()
 * and return() mutate it in place. It is reproducible: the same seed and the same sequence of
 * operations yield the same draws.
 */
export class Bag {
  private tiles: number[];
  private readonly rand: () => number;

  constructor(variant: Variant, seed: number) {
    const rs = RULESETS[variant];
    const tiles: number[] = [];
    for (let i = 0; i < rs.counts.length; i++) {
      for (let n = 0; n < rs.counts[i]; n++) tiles.push(i);
    }
    for (let n = 0; n < rs.blanks; n++) tiles.push(BLANK_INDEX);
    this.tiles = tiles;
    this.rand = mulberry32(seed);
    this.shuffle();
  }

  /** length is the number of tiles left in the bag. */
  get length(): number {
    return this.tiles.length;
  }

  /**
   * draw removes up to n tiles from the end of the bag and returns them. Drawing more than
   * remain returns all of them; drawing from an empty bag returns an empty array.
   */
  draw(n: number): number[] {
    const take = Math.min(n, this.tiles.length);
    return this.tiles.splice(this.tiles.length - take, take);
  }

  /** return puts tiles back into the bag and reshuffles, as when a player exchanges tiles. */
  return(tiles: readonly number[]): void {
    for (const t of tiles) this.tiles.push(t);
    this.shuffle();
  }

  // shuffle randomises the remaining tiles in place with the bag's own PRNG (Fisher–Yates).
  private shuffle(): void {
    for (let i = this.tiles.length - 1; i > 0; i--) {
      const j = Math.floor(this.rand() * (i + 1));
      const tmp = this.tiles[i];
      this.tiles[i] = this.tiles[j];
      this.tiles[j] = tmp;
    }
  }
}
