// Drunk-typo text renderer for DialogueBox.
//
// Pure and deterministic: same (text, drunkenness, seed) always yields the same
// mangled string, so a patron's slurred answer is reproducible from the run seed
// like everything else. No Math.random — callers pass a SeededRNG stream.

export interface RngLike {
  /** 0..1, like SeededRNG streams. */
  next(): number;
}

/** Adjacent-key swaps — a drunk thumb misses sideways, not randomly. */
const NEIGHBOURS: Record<string, string> = {
  a: 's', b: 'v', c: 'x', d: 'f', e: 'w', f: 'g', g: 'h', h: 'j', i: 'o',
  j: 'k', k: 'l', l: 'k', m: 'n', n: 'm', o: 'p', p: 'o', q: 'w', r: 't',
  s: 'a', t: 'r', u: 'i', v: 'b', w: 'e', x: 'z', y: 'u', z: 'x',
};

/** Vowels get stretched when someone is really going for it. */
const VOWELS = 'aeiou';

/**
 * Clamp to 0..1, treating a non-finite input as 0.
 *
 * A NaN reaching `text.y` makes Phaser silently render nothing, so it must never
 * leave this module. Callers guard their own frame deltas — this is the backstop,
 * not the fix.
 */
function clamp01(n: number): number {
  return Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : 0;
}

export interface TypoOptions {
  /** 0..1 — 0 leaves text untouched, 1 is barely language. */
  drunkenness: number;
  rng: RngLike;
}

/**
 * Mangle a line in proportion to how gone the speaker is.
 *
 * Four escalating effects, each gated on its own probability so a tipsy speaker
 * gets the occasional doubled letter and a maggot one gets all of it at once:
 *  - adjacent-key substitution
 *  - doubled letters
 *  - dropped letters
 *  - stretched vowels ("yeahhh")
 */
export function drunkify(text: string, { drunkenness, rng }: TypoOptions): string {
  const d = clamp01(drunkenness);
  if (d <= 0) return text;

  // Below ~0.25 people type fine. Above that it ramps quickly.
  const intensity = Math.max(0, (d - 0.25) / 0.75);
  if (intensity <= 0) return text;

  const pSub = intensity * 0.14;
  const pDouble = intensity * 0.09;
  const pDrop = intensity * 0.07;
  const pStretch = intensity * 0.12;

  let out = '';
  for (const ch of text) {
    const lower = ch.toLowerCase();
    const isLetter = lower >= 'a' && lower <= 'z';
    if (!isLetter) {
      out += ch;
      continue;
    }

    if (rng.next() < pDrop) continue;

    let emitted = ch;
    if (rng.next() < pSub) {
      const swap = NEIGHBOURS[lower];
      if (swap !== undefined) emitted = ch === lower ? swap : swap.toUpperCase();
    }

    out += emitted;

    if (VOWELS.includes(lower) && rng.next() < pStretch) {
      const extra = 1 + Math.floor(rng.next() * 3);
      out += emitted.repeat(extra);
    } else if (rng.next() < pDouble) {
      out += emitted;
    }
  }
  return out;
}

/**
 * How many characters of `text` are visible after `elapsedMs` of typing.
 * Drunk speakers type slower and in lurches — the stagger is deterministic in
 * the character index so it does not shimmer between frames.
 */
export function typedLength(text: string, elapsedMs: number, charMs: number, drunkenness = 0): number {
  if (charMs <= 0) return text.length;
  const slow = 1 + clamp01(drunkenness) * 1.2;
  const raw = Math.floor(elapsedMs / (charMs * slow));
  if (Number.isNaN(raw)) return 0;
  return Math.max(0, Math.min(text.length, raw));
}

/** Per-character vertical wobble (px) for drunk render mode. Deterministic. */
export function wobbleAt(index: number, drunkenness: number, phase: number): number {
  const amp = clamp01(drunkenness) * 1.6;
  if (amp <= 0) return 0;
  const y = Math.sin(phase + index * 0.9) * amp;
  return Number.isNaN(y) ? 0 : y;
}
