/**
 * Component 7 — Seeded RNG.
 *
 * Every random decision in the game derives from one stored seed, so any game is exactly
 * replayable. See architecture/components.md §2 A.7.
 *
 * IMPORTANT: never call Math.random() anywhere in the engine. A single ambient random call
 * silently breaks replay, and the failure is invisible until someone tries to reproduce a bug.
 */

/** An RNG is a value, not a global. Thread it through explicitly. */
export type Rng = {
  /** Raw 32-bit unsigned draw. */
  next(): number;
  /** Uniform integer in [0, bound). Throws if bound < 1. */
  nextInt(bound: number): number;
  /** A single twelve-sided die roll, 1..12 (§4.3, §4.4, §7). */
  d12(): number;
  /** Fisher-Yates. Returns a new array; does not mutate the input. */
  shuffle<T>(items: readonly T[]): T[];
  /** The current internal state, so a game can be snapshotted mid-play. */
  getState(): number;
};

/**
 * mulberry32 — small, fast, and good enough for a board game. Chosen because its entire state
 * is one 32-bit integer, which makes snapshotting and restoring an in-progress game trivial.
 */
export function createRng(seed: number): Rng {
  let state = seed >>> 0;

  const next = (): number => {
    state = (state + 0x6d2b79f5) >>> 0;
    let t = state;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return (t ^ (t >>> 14)) >>> 0;
  };

  const nextInt = (bound: number): number => {
    if (!Number.isInteger(bound) || bound < 1) {
      throw new Error(`nextInt bound must be a positive integer, got ${bound}`);
    }
    // Rejection sampling, so the distribution stays uniform rather than modulo-biased.
    const limit = Math.floor(0x100000000 / bound) * bound;
    let draw = next();
    while (draw >= limit) draw = next();
    return draw % bound;
  };

  return {
    next,
    nextInt,
    d12: () => nextInt(12) + 1,
    shuffle: <T,>(items: readonly T[]): T[] => {
      const out = items.slice();
      for (let i = out.length - 1; i > 0; i--) {
        const j = nextInt(i + 1);
        const a = out[i]!;
        const b = out[j]!;
        out[i] = b;
        out[j] = a;
      }
      return out;
    },
    getState: () => state,
  };
}

/** Restore an RNG mid-stream, for rebuilding a game from a snapshot. */
export function restoreRng(state: number): Rng {
  // mulberry32 advances from its state before drawing, and createRng seeds state directly,
  // so restoring is just seeding with the saved state.
  return createRng(state);
}
