// Deterministic RNG. Every random draw in the game flows from a run seed via
// named streams so systems can't perturb each other's sequences.

export interface RngStream {
  next(): number;                 // [0, 1)
  int(min: number, max: number): number; // inclusive both ends
  pick<T>(arr: readonly T[]): T;
  chance(p: number): boolean;
  weighted<T>(entries: ReadonlyArray<readonly [T, number]>): T;
}

function mulberry32(seed: number): () => number {
  let a = seed >>> 0;
  return () => {
    a = (a + 0x6d2b79f5) >>> 0;
    let t = a;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

// FNV-1a over the stream name, mixed with the run seed.
function hashName(seed: number, name: string): number {
  let h = 0x811c9dc5 ^ seed;
  for (let i = 0; i < name.length; i++) {
    h ^= name.charCodeAt(i);
    h = Math.imul(h, 0x01000193);
  }
  return h >>> 0;
}

class Stream implements RngStream {
  private readonly rand: () => number;
  constructor(seed: number) {
    this.rand = mulberry32(seed);
  }
  next(): number {
    return this.rand();
  }
  int(min: number, max: number): number {
    return min + Math.floor(this.rand() * (max - min + 1));
  }
  pick<T>(arr: readonly T[]): T {
    if (arr.length === 0) throw new Error('pick from empty array');
    return arr[Math.floor(this.rand() * arr.length)] as T;
  }
  chance(p: number): boolean {
    return this.rand() < p;
  }
  weighted<T>(entries: ReadonlyArray<readonly [T, number]>): T {
    let total = 0;
    for (const [, w] of entries) total += w;
    if (total <= 0) throw new Error('weighted with non-positive total weight');
    let roll = this.rand() * total;
    for (const [value, w] of entries) {
      roll -= w;
      if (roll < 0) return value;
    }
    return entries[entries.length - 1]![0];
  }
}

export class SeededRNG {
  private readonly streams = new Map<string, Stream>();
  constructor(public readonly seed: number) {}

  /** Same (seed, name) always yields the same stream state history. */
  stream(name: string): RngStream {
    let s = this.streams.get(name);
    if (!s) {
      s = new Stream(hashName(this.seed, name));
      this.streams.set(name, s);
    }
    return s;
  }
}
