const re = (() => {
  const inner = '[0-9]+(?:[.][0-9]*)?'
  const nonNegativeDecimal = `(${inner})`
  const decimal = `(-?${inner})`
  const ws = '\\s*'

  const make = (parts: string[]) => new RegExp(['^', ...parts, '$'].join(ws))

  return [
    make([decimal]),
    make([decimal, '/', nonNegativeDecimal]),
    make([decimal, nonNegativeDecimal, '/', nonNegativeDecimal]),
  ] as const
})()

export class BigRational {
  #numerator: bigint
  #denominator: bigint

  // Creates a new BigRational without simplifying the fraction or checking that
  // the denominator is positive.
  private constructor(numerator: bigint, denominator: bigint) {
    this.#numerator = numerator
    this.#denominator = denominator
  }

  static from(numerator: bigint, denominator: bigint): BigRational {
    if (denominator === 0n) throw new Error('denominator cannot be zero')

    // Fast case.
    if (denominator === 1n) return new BigRational(numerator, 1n)

    // The denominator must always be positive.
    if (denominator < 0) {
      numerator = -numerator
      denominator = -denominator
    }

    const gcd_ = gcd(abs(numerator), denominator)

    return new BigRational(numerator / gcd_, denominator / gcd_)
  }

  static fromString(string: string): BigRational {
    const match0 = string.match(re[0])
    if (match0) {
      return parseOne(match0[1]!)
    }
    const match1 = string.match(re[1])
    if (match1) {
      return parseOne(match1[1]!).divide(parseOne(match1[2]!))
    }
    const match2 = string.match(re[2])
    if (match2) {
      const whole = parseOne(match2[1]!)
      const fraction = parseOne(match2[2]!).divide(parseOne(match2[3]!))
      return whole.#numerator >= 0n
        ? whole.add(fraction)
        : whole.subtract(fraction)
    }
    throw new Error(`Invalid BigRational: ${string}`)
  }

  numerator(): bigint {
    return this.#numerator
  }

  denominator(): bigint {
    return this.#denominator
  }

  add(other: BigRational): BigRational {
    return BigRational.from(
      this.#numerator * other.#denominator +
        other.#numerator * this.#denominator,
      this.#denominator * other.#denominator,
    )
  }

  subtract(other: BigRational): BigRational {
    return BigRational.from(
      this.#numerator * other.#denominator -
        other.#numerator * this.#denominator,
      this.#denominator * other.#denominator,
    )
  }

  multiply(other: BigRational): BigRational {
    return BigRational.from(
      this.#numerator * other.#numerator,
      this.#denominator * other.#denominator,
    )
  }

  divide(other: BigRational): BigRational {
    return BigRational.from(
      this.#numerator * other.#denominator,
      this.#denominator * other.#numerator,
    )
  }

  compare(other: BigRational): number {
    const a = this.#numerator * other.#denominator
    const b = other.#numerator * this.#denominator
    return a === b ? 0 : a > b ? 1 : -1
  }

  power(n: bigint): BigRational {
    return BigRational.from(this.#numerator ** n, this.#denominator ** n)
  }

  modulo(other: BigRational): BigRational {
    return this.subtract(
      other.multiply(new BigRational(this.divide(other).floor(), 1n)),
    )
  }

  // Round towards negative infinity.
  floor(): bigint {
    return (
      (this.#numerator >= 0
        ? this.#numerator
        : this.#numerator - this.#denominator + 1n) / this.#denominator
    )
  }

  // Round towards positive infinity.
  ceil(): bigint {
    return (
      (this.#numerator >= 0
        ? this.#numerator + this.#denominator - 1n
        : this.#numerator) / this.#denominator
    )
  }

  // Round towards zero.
  truncate(): bigint {
    return this.#numerator / this.#denominator
  }

  negate(): BigRational {
    return new BigRational(-this.#numerator, this.#denominator)
  }

  inverse(): BigRational {
    if (this.#numerator === 0n) throw new Error('cannot take inverse of zero')

    return this.#numerator < 0n
      ? new BigRational(-this.#denominator, -this.#numerator)
      : new BigRational(this.#denominator, this.#numerator)
  }

  abs(): BigRational {
    return this.#numerator < 0n ? this.negate() : this
  }

  toNumberApproximate() {
    const whole = this.#numerator / this.#denominator
    const numerator = this.#numerator % this.#denominator
    return Number(whole) + Number(numerator) / Number(this.#denominator)
  }

  toFractionString() {
    return this.#denominator === 1n
      ? `${this.#numerator}`
      : `${this.#numerator}/${this.#denominator}`
  }

  toMixedString() {
    const whole = this.#numerator / this.#denominator
    let numerator = this.#numerator % this.#denominator
    if (whole < 0) numerator = -numerator
    return numerator === 0n
      ? `${whole}`
      : whole === 0n
      ? `${numerator}/${this.#denominator}`
      : `${whole} ${numerator}/${this.#denominator}`
  }

  toString() {
    return this.toFractionString()
  }
}

const abs = (a: bigint): bigint => (a >= 0n ? a : -a)

const gcd = (a: bigint, b: bigint): bigint => {
  while (b !== 0n) {
    const t = b
    b = a % b
    a = t
  }
  return a
}

const parseOne = (string: string) => {
  const indexOfDot = string.indexOf('.')
  if (indexOfDot === -1) {
    return BigRational.from(BigInt(string), 1n)
  }
  const before = string.slice(0, indexOfDot)
  const after = string.slice(indexOfDot + 1)
  return BigRational.from(BigInt(before + after), 10n ** BigInt(after.length))
}
