module PG
  # The Postgres numeric type has arbitrary precision, and can be NaN, "not a
  # number".
  #
  # The default version of `Numeric` in this driver only has `#to_f` which
  # provides approximate conversion. To get true arbitrary precision, there is
  # an optional extension `pg_ext/big_rational`, however LibGMP must be
  # installed.
  struct Numeric
    # :nodoc:
    enum Sign
      Pos =  0x0000
      Neg =  0x4000
      Nan = -0x4000
    end

    # size of digits array
    getter ndigits : Int16

    # location of decimal point in digits array
    # can be negative for small numbers such as 0.0000001
    getter weight : Int16

    # positive, negative, or nan
    getter sign : Sign

    # number of decimal point digits shown
    # 1.10 is and 1.100 would only differ here
    getter dscale : Int16

    # array of numbers from 0-10,000 representing the numeric
    # (not an array of individual digits!)
    getter digits : Array(Int16)

    def initialize(@ndigits : Int16, @weight : Int16, sign, @dscale : Int16, @digits : Array(Int16))
      @sign = Sign.from_value(sign)
    end

    # Returns `true` if the numeric is not a number.
    def nan?
      sign == Sign::Nan
    end

    # Returns `true` if the numeric is negative.
    def neg?
      sign == Sign::Neg
    end

    # The approximate representation of the numeric as a 64-bit float.
    #
    # Very small and very large values may be inaccurate and precision will be
    # lost. If you require full precision, require the optional big_rational
    # support and use #to_big_r
    # NaN returns `0.0`.
    def to_f : Float64
      to_f64
    end

    # ditto
    def to_f64 : Float64
      return Float64::NAN if nan?
      # simple numerator calculation:
      #   num = digits.reduce(0_f64) { |a, n| a*10_000_f64 + n }
      # however reverse and add the smaller digits first to try and not lose as much precision
      num = digits.reverse.each_with_index.reduce(0_f64) { |a, (n, idx)| a + (n * (10_000_f64**idx)) }
      den = 10_000_f64**(ndigits - 1 - weight)
      quot = num / den

      # this alternate implementation introduces more error in the tests
      # quot = digits.map_with_index { |n, idx| 10_000_f64**(weight - idx) * n }.reduce(0_f64) { |a, n| a + n }

      neg? ? -quot : quot
    end

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

    def to_s(io : IO)
      if ndigits == 0
        if nan?
          io << "NaN"
        else
          io << '0'
          if dscale > 0
            io << '.'
            dscale.times { io << '0' }
          end
        end

        return
      end

      io << '-' if neg?

      pos = 0

      if weight >= 0
        io << digits[0].to_s
        pos += 1
        (1..weight).each do |idx|
          pos += 1
          str = digits[idx]?.to_s
          (4 - str.size).times { io << '0' }
          io << str
        end
      end

      return if dscale <= 0

      io << '0' if weight < 0
      io << '.'

      count = 0
      (-1 - weight).times do
        io << "0000"
        count += 4
      end

      (pos...ndigits).each do |idx|
        str = digits[idx].to_s

        (4 - str.size).times do
          io << '0'
          count += 1
        end

        if idx == ndigits - 1
          remain = (dscale + str.size) % 4
          str = str[0...remain] unless remain == 0
        end
        io << str
        count += str.size
      end

      (dscale - count).times { io << '0' }
    end
  end
end
