require "big"

# Rational numbers are represented as the quotient of arbitrarily large
# numerators and denominators. Rationals are canonicalized such that the
# denominator and the numerator have no common factors, and that the
# denominator is positive. Zero has the unique representation 0/1.
#
# NOTE: To use `BigRational`, you must explicitly import it with `require "big"`
#
# ```
# require "big"
#
# r = BigRational.new(7.to_big_i, 3.to_big_i)
# r.to_s # => "7/3"
#
# r = BigRational.new(3, -9)
# r.to_s # => "-1/3"
# ```
#
# It is implemented under the hood with [GMP](https://gmplib.org/).
struct BigRational < Number
  include Comparable(BigRational)
  include Comparable(Int)
  include Comparable(Float)

  # Creates a new `BigRational`.
  #
  # If *denominator* is 0, this will raise an exception.
  def initialize(numerator : Int, denominator : Int)
    check_division_by_zero denominator

    numerator = BigInt.new(numerator) unless numerator.is_a?(BigInt)
    denominator = BigInt.new(denominator) unless denominator.is_a?(BigInt)

    LibGMP.mpq_init(out @mpq)
    LibGMP.mpq_set_num(mpq, numerator.to_unsafe)
    LibGMP.mpq_set_den(mpq, denominator.to_unsafe)
    LibGMP.mpq_canonicalize(mpq)
  end

  # Creates a new `BigRational` with *num* as the numerator and 1 for denominator.
  def initialize(num : Int)
    initialize(num, 1)
  end

  # Creates an exact representation of float as rational.
  #
  # Raises `ArgumentError` if *num* is not finite.
  def self.new(num : Float::Primitive) : self
    raise ArgumentError.new "Can only construct from a finite number" unless num.finite?
    new { |mpq| LibGMP.mpq_set_d(mpq, num) }
  end

  # Creates an exact representation of float as rational.
  def self.new(num : BigFloat) : self
    new { |mpq| LibGMP.mpq_set_f(mpq, num) }
  end

  # Creates a `BigRational` from the given *num*.
  def self.new(num : BigRational) : self
    num
  end

  # :ditto:
  def self.new(num : BigDecimal) : self
    num.to_big_r
  end

  # :nodoc:
  def initialize(@mpq : LibGMP::MPQ)
  end

  # :nodoc:
  def self.new(&)
    LibGMP.mpq_init(out mpq)
    yield pointerof(mpq)
    new(mpq)
  end

  def numerator : BigInt
    # Returns `LibGMP.mpq_numref(self)`, whose C macro expansion effectively
    # produces a raw member access. This is only as safe as copying `BigInt`s by
    # value, as both involve copying `LibGMP::MPZ` around which has reference
    # semantics, and `BigInt`s cannot be safely mutated in-place this way; see
    # #9825 for details. Ditto for `#denominator`.
    BigInt.new(@mpq._mp_num)
  end

  def denominator : BigInt
    BigInt.new(@mpq._mp_den)
  end

  def <=>(other : BigRational) : Int32
    LibGMP.mpq_cmp(mpq, other)
  end

  def <=>(other : Float::Primitive) : Int32?
    self <=> BigRational.new(other) unless other.nan?
  end

  def <=>(other : BigFloat) : Int32
    self <=> other.to_big_r
  end

  def <=>(other : Int) : Int32
    Int.primitive_si_ui_check(other) do |si, ui, big_i|
      {
        si:    LibGMP.mpq_cmp_si(self, {{ si }}, 1),
        ui:    LibGMP.mpq_cmp_ui(self, {{ ui }}, 1),
        big_i: self <=> {{ big_i }},
      }
    end
  end

  def <=>(other : BigInt) : Int32
    LibGMP.mpq_cmp_z(self, other)
  end

  def ==(other : BigRational) : Bool
    LibGMP.mpq_equal(self, other) != 0
  end

  def +(other : BigRational) : BigRational
    BigRational.new { |mpq| LibGMP.mpq_add(mpq, self, other) }
  end

  def +(other : Int) : BigRational
    self + other.to_big_r
  end

  def -(other : BigRational) : BigRational
    BigRational.new { |mpq| LibGMP.mpq_sub(mpq, self, other) }
  end

  def -(other : Int) : BigRational
    self - other.to_big_r
  end

  def *(other : BigRational) : BigRational
    BigRational.new { |mpq| LibGMP.mpq_mul(mpq, self, other) }
  end

  def *(other : Int) : BigRational
    self * other.to_big_r
  end

  def /(other : BigRational) : BigRational
    check_division_by_zero other
    BigRational.new { |mpq| LibGMP.mpq_div(mpq, self, other) }
  end

  Number.expand_div [BigInt, BigFloat, BigDecimal], BigRational

  def //(other : BigRational) : BigRational
    check_division_by_zero other
    BigRational.new((numerator * other.denominator) // (denominator * other.numerator))
  end

  def //(other : Int) : BigRational
    check_division_by_zero other
    BigRational.new(numerator // (denominator * other))
  end

  def %(other : BigRational) : BigRational
    check_division_by_zero other
    BigRational.new(
      (numerator * other.denominator) % (denominator * other.numerator),
      denominator * other.denominator,
    )
  end

  def %(other : Int) : BigRational
    check_division_by_zero other
    BigRational.new(numerator % (denominator * other), denominator)
  end

  def tdiv(other : BigRational) : BigRational
    check_division_by_zero other
    BigRational.new((numerator * other.denominator).tdiv(denominator * other.numerator))
  end

  def tdiv(other : Int) : BigRational
    check_division_by_zero other
    BigRational.new(numerator.tdiv(denominator * other))
  end

  def remainder(other : BigRational) : BigRational
    check_division_by_zero other
    BigRational.new(
      (numerator * other.denominator).remainder(denominator * other.numerator),
      denominator * other.denominator,
    )
  end

  def remainder(other : Int) : BigRational
    check_division_by_zero other
    BigRational.new(numerator.remainder(denominator * other), denominator)
  end

  def ceil : BigRational
    BigRational.new(-(-numerator // denominator))
  end

  def floor : BigRational
    BigRational.new(numerator // denominator)
  end

  def trunc : BigRational
    BigRational.new(numerator.tdiv(denominator))
  end

  def round_away : BigRational
    rem2 = numerator.remainder(denominator).abs * 2
    x = BigRational.new(numerator.tdiv(denominator))
    x += sign if rem2 >= denominator
    x
  end

  def round_even : BigRational
    rem2 = numerator.remainder(denominator).abs * 2
    x = BigRational.new(numerator.tdiv(denominator))
    x += sign if rem2 > denominator || (rem2 == denominator && x.numerator.odd?)
    x
  end

  # :inherit:
  def integer? : Bool
    # since all `BigRational`s are canonicalized, the denominator must be
    # positive and coprime with the numerator
    denominator == 1
  end

  # Divides the rational by (2 ** *other*)
  #
  # ```
  # require "big"
  #
  # BigRational.new(2, 3) >> 2 # => 1/6
  # ```
  def >>(other : Int) : BigRational
    BigRational.new { |mpq| LibGMP.mpq_div_2exp(mpq, self, other) }
  end

  # Multiplies the rational by (2 ** *other*)
  #
  # ```
  # require "big"
  #
  # BigRational.new(2, 3) << 2 # => 8/3
  # ```
  def <<(other : Int) : BigRational
    BigRational.new { |mpq| LibGMP.mpq_mul_2exp(mpq, self, other) }
  end

  def - : BigRational
    BigRational.new { |mpq| LibGMP.mpq_neg(mpq, self) }
  end

  # Raises the rational to the *other*th power
  #
  # This will raise `DivisionByZeroError` if rational is 0 and *other* is negative.
  #
  # ```
  # require "big"
  #
  # BigRational.new(2, 3) ** 2  # => 4/9
  # BigRational.new(2, 3) ** -1 # => 3/2
  # ```
  def **(other : Int) : BigRational
    if other < 0
      return (self ** other.abs).inv
    end
    BigRational.new(numerator ** other, denominator ** other)
  end

  # Returns a new `BigRational` as 1/r.
  #
  # This will raise an exception if rational is 0.
  def inv : BigRational
    check_division_by_zero self
    BigRational.new { |mpq| LibGMP.mpq_inv(mpq, self) }
  end

  def abs : BigRational
    BigRational.new { |mpq| LibGMP.mpq_abs(mpq, self) }
  end

  # Returns the `Float64` representing this rational.
  def to_f : Float64
    to_f64
  end

  def to_f32 : Float32
    to_f64.to_f32
  end

  def to_f64 : Float64
    LibGMP.mpq_get_d(mpq)
  end

  def to_f32! : Float32
    to_f64.to_f32!
  end

  def to_f64! : Float64
    to_f64
  end

  def to_f! : Float64
    to_f64!
  end

  def to_i : Int32
    to_i32
  end

  delegate to_i8, to_i16, to_i32, to_i64, to_u8, to_u16, to_u32, to_u64, to: to_f64

  # Returns `self`.
  #
  # ```
  # require "big"
  #
  # BigRational.new(4, 5).to_big_r # => 4/5
  # ```
  def to_big_r : BigRational
    self
  end

  def to_big_f : BigFloat
    BigFloat.new { |mpf| LibGMP.mpf_set_q(mpf, mpq) }
  end

  def to_big_i : BigInt
    BigInt.new { |mpz| LibGMP.set_q(mpz, mpq) }
  end

  # Returns the string representing this rational.
  #
  # Optionally takes a radix base (2 through 36).
  #
  # ```
  # require "big"
  #
  # r = BigRational.new(8243243, 562828882)
  # r.to_s     # => "8243243/562828882"
  # r.to_s(16) # => "7dc82b/218c1652"
  # r.to_s(36) # => "4woiz/9b3djm"
  # ```
  def to_s(base : Int = 10) : String
    String.new(to_cstr(base))
  end

  def to_s(io : IO, base : Int = 10) : Nil
    str = to_cstr(base)
    io.write_string Slice.new(str, LibC.strlen(str))
  end

  def inspect : String
    to_s
  end

  def inspect(io : IO) : Nil
    to_s io
  end

  # :inherit:
  def format(io : IO, separator = '.', delimiter = ',', decimal_places : Int? = nil, *, group : Int = 3, only_significant : Bool = false) : Nil
    numerator.format(io, separator, delimiter, decimal_places, group: group, only_significant: only_significant)
    io << '/'
    denominator.format(io, separator, delimiter, decimal_places, group: group, only_significant: only_significant)
  end

  def clone : BigRational
    self
  end

  private def mpq
    pointerof(@mpq)
  end

  def to_unsafe : Pointer(LibGMP::MPQ)
    mpq
  end

  private def to_cstr(base = 10)
    raise "Invalid base #{base}" unless 2 <= base <= 36
    LibGMP.mpq_get_str(nil, base, mpq)
  end

  private def check_division_by_zero(value)
    raise DivisionByZeroError.new if value == 0
  end
end

struct Int
  include Comparable(BigRational)

  # Returns a `BigRational` representing this integer.
  # ```
  # require "big"
  #
  # 123.to_big_r
  # ```
  def to_big_r : BigRational
    BigRational.new(self, 1)
  end

  def <=>(other : BigRational) : Int32
    -(other <=> self)
  end

  def +(other : BigRational) : BigRational
    other + self
  end

  def -(other : BigRational) : BigRational
    self.to_big_r - other
  end

  def /(other : BigRational)
    self.to_big_r / other
  end

  def *(other : BigRational) : BigRational
    other * self
  end
end

struct Float
  include Comparable(BigRational)

  # Returns a `BigRational` representing this float.
  # ```
  # require "big"
  #
  # 123.0.to_big_r
  # ```
  def to_big_r : BigRational
    BigRational.new(self)
  end

  def <=>(other : BigRational)
    cmp = other <=> self
    -cmp if cmp
  end
end

struct BigFloat
  def <=>(other : BigRational)
    -(other <=> self)
  end
end

module Math
  # Calculates the square root of *value*.
  #
  # ```
  # require "big"
  #
  # Math.sqrt(1_000_000_000_000.to_big_r * 1_000_000_000_000.to_big_r) # => 1000000000000.0
  # ```
  def sqrt(value : BigRational) : BigFloat
    sqrt(value.to_big_f)
  end
end

# :nodoc:
struct Crystal::Hasher
  def self.reduce_num(value : BigRational) : UInt64
    inverse = BigInt.new do |mpz|
      if LibGMP.invert(mpz, value.denominator, HASH_MODULUS_INT_P) == 0
        # inverse doesn't exist, i.e. denominator is a multiple of HASH_MODULUS
        return value >= 0 ? HASH_INF_PLUS : HASH_INF_MINUS
      end
    end
    UInt64.mulmod(reduce_num(value.numerator.abs), inverse.to_u64!, HASH_MODULUS) &* value.sign
  end
end
