/**
 * Portable randomness and portable dates.
 *
 * A port of `envs/alert_triage/alert_triage/rng.py`, line for line. Nothing
 * in the generator may touch a language built-in for either: a built-in RNG
 * picks a different alert for the same seed on the two sides, and `Date` is
 * timezone-sensitive, so CI in UTC passes and a visitor elsewhere sees a
 * different fire date for the same `?seed=`.
 *
 * Pure and dependency-free. Runs on the main thread and under `node --test`.
 */

/**
 * FNV-1a, 32-bit, over the UTF-8 bytes. Identical to wordle's.
 *
 * `Math.imul` is what makes the 32-bit multiply exact; a plain `*` overflows
 * into a double and silently diverges from Python after the first few bytes.
 */
export function fnv1a32(text: string): number {
  let h = 0x811c9dc5;
  for (const byte of new TextEncoder().encode(text)) {
    h ^= byte;
    h = Math.imul(h, 0x01000193) >>> 0;
  }
  return h >>> 0;
}

/**
 * Python's floor division. `Math.floor(a / b)` is right for the positive
 * integers this generator deals in, but the subtraction form is exact for
 * every integer a double can hold and matches `//` on negatives too.
 */
export function floorDiv(a: number, b: number): number {
  return (a - pyMod(a, b)) / b;
}

/** Python's `%`: the result takes the sign of the divisor. */
export function pyMod(a: number, b: number): number {
  const r = a % b;
  return r !== 0 && r < 0 !== b < 0 ? r + b : r;
}

/**
 * Marsaglia xorshift32. Seven lines, and Python has the same seven.
 *
 * `below(n)` is a plain modulo. It is biased for large `n`, which does not
 * matter here (every `n` is tiny) and keeps the port a one-liner.
 */
export class XorShift32 {
  state: number;

  constructor(seed: number) {
    const s = seed >>> 0;
    this.state = s === 0 ? 0x9e3779b9 : s;
  }

  next(): number {
    let x = this.state;
    x = (x ^ (x << 13)) >>> 0;
    x = (x ^ (x >>> 17)) >>> 0;
    x = (x ^ (x << 5)) >>> 0;
    this.state = x;
    return x;
  }

  below(n: number): number {
    return this.next() % n;
  }

  /** Inclusive on both ends. */
  between(lo: number, hi: number): number {
    return lo + this.below(hi - lo + 1);
  }

  chance(percent: number): boolean {
    return this.below(100) < percent;
  }

  pick<T>(items: readonly T[]): T {
    return items[this.below(items.length)]!;
  }

  /** k distinct items, Fisher-Yates over a copy, first k taken. */
  sample<T>(items: readonly T[], k: number): T[] {
    const pool = [...items];
    const out: T[] = [];
    const n = Math.min(k, pool.length);
    for (let i = 0; i < n; i += 1) {
      const j = i + this.below(pool.length - i);
      const tmp = pool[i]!;
      pool[i] = pool[j]!;
      pool[j] = tmp;
      out.push(pool[i]!);
    }
    return out;
  }

  hex4(): string {
    return this.below(65536).toString(16).padStart(4, '0');
  }
}

/* ------------------------------------------------------------------ dates */
// Howard Hinnant's civil-from-days / days-from-civil. Integer arithmetic only.
// Day 0 is 1970-01-01.

export function daysFromCivil(y: number, m: number, d: number): number {
  y -= m <= 2 ? 1 : 0;
  const era = floorDiv(y >= 0 ? y : y - 399, 400);
  const yoe = y - era * 400;
  const mp = pyMod(m + 9, 12);
  const doy = floorDiv(153 * mp + 2, 5) + d - 1;
  const doe = yoe * 365 + floorDiv(yoe, 4) - floorDiv(yoe, 100) + doy;
  return era * 146097 + doe - 719468;
}

export function civilFromDays(z: number): [number, number, number] {
  z += 719468;
  const era = floorDiv(z >= 0 ? z : z - 146096, 146097);
  const doe = z - era * 146097;
  const yoe = floorDiv(doe - floorDiv(doe, 1460) + floorDiv(doe, 36524) - floorDiv(doe, 146096), 365);
  const y = yoe + era * 400;
  const doy = doe - (365 * yoe + floorDiv(yoe, 4) - floorDiv(yoe, 100));
  const mp = floorDiv(5 * doy + 2, 153);
  const d = doy - floorDiv(153 * mp + 2, 5) + 1;
  const m = mp < 10 ? mp + 3 : mp - 9;
  return [y + (m <= 2 ? 1 : 0), m, d];
}

export function daysInMonth(y: number, m: number): number {
  if (m === 12) return daysFromCivil(y + 1, 1, 1) - daysFromCivil(y, 12, 1);
  return daysFromCivil(y, m + 1, 1) - daysFromCivil(y, m, 1);
}

const pad = (n: number, width: number) => String(n).padStart(width, '0');

export function isoDate(day: number): string {
  const [y, m, d] = civilFromDays(day);
  return `${pad(y, 4)}-${pad(m, 2)}-${pad(d, 2)}`;
}

export function monthKey(y: number, m: number): string {
  return `${pad(y, 4)}-${pad(m, 2)}`;
}

export function monthOfDay(day: number): string {
  const [y, m] = civilFromDays(day);
  return monthKey(y, m);
}

export function addMonths(y: number, m: number, n: number): [number, number] {
  const index = y * 12 + (m - 1) + n;
  return [floorDiv(index, 12), pyMod(index, 12) + 1];
}
