/**
 * Exact rational arithmetic for genetics probabilities.
 *
 * Punnett distributions are unions of dyadic/rational fractions (1/4, 3/16, …).
 * Using exact fractions instead of floats keeps probabilities precise and lets
 * us assert e.g. "exactly 1/4 of young are lost" after renormalisation.
 */
export interface Fraction {
  readonly num: number
  readonly den: number
}

function gcd(a: number, b: number): number {
  a = Math.abs(a)
  b = Math.abs(b)
  while (b !== 0) {
    ;[a, b] = [b, a % b]
  }
  return a
}

/** Create a normalised (reduced, positive-denominator) fraction. */
export function frac(num: number, den: number): Fraction {
  if (den === 0) throw new Error('Fraction denominator must not be zero')
  if (den < 0) {
    num = -num
    den = -den
  }
  if (num === 0) return { num: 0, den: 1 }
  const g = gcd(num, den)
  return { num: num / g, den: den / g }
}

export const ZERO: Fraction = { num: 0, den: 1 }
export const ONE: Fraction = { num: 1, den: 1 }

export function add(a: Fraction, b: Fraction): Fraction {
  return frac(a.num * b.den + b.num * a.den, a.den * b.den)
}

export function multiply(a: Fraction, b: Fraction): Fraction {
  return frac(a.num * b.num, a.den * b.den)
}

export function divide(a: Fraction, b: Fraction): Fraction {
  if (b.num === 0) throw new Error('Cannot divide by zero fraction')
  return frac(a.num * b.den, a.den * b.num)
}

export function equals(a: Fraction, b: Fraction): boolean {
  return a.num * b.den === b.num * a.den
}

export function toNumber(f: Fraction): number {
  return f.num / f.den
}

/** Human-readable "3/16" (or "0" / "1"). */
export function toString(f: Fraction): string {
  if (f.num === 0) return '0'
  if (f.den === 1) return String(f.num)
  return `${f.num}/${f.den}`
}

/** "18.75 %" style label for display (rounded to `digits` decimals). */
export function toPercentString(f: Fraction, digits = 2): string {
  const pct = (f.num / f.den) * 100
  const rounded = Number(pct.toFixed(digits))
  return `${rounded} %`
}
