# backtick_javascript: true
# use_strict: true

require 'corelib/numeric'

class ::Number < ::Numeric
  ::Opal.bridge(`Number`, self)
  `Opal.prop(self.$$prototype, '$$is_number', true)`
  `self.$$is_number_class = true`
  `var number_id_map = new Map()`

  class << self
    def allocate
      ::Kernel.raise ::TypeError, "allocator undefined for #{name}"
    end

    undef :new
  end

  def coerce(other)
    %x{
      if (other === nil) {
        #{::Kernel.raise ::TypeError, "can't convert #{other.class} into Float"};
      }
      else if (other.$$is_string) {
        return [#{::Kernel.Float(other)}, self];
      }
      else if (#{other.respond_to?(:to_f)}) {
        return [#{::Opal.coerce_to!(other, ::Float, :to_f)}, self];
      }
      else if (other.$$is_number) {
        return [other, self];
      }
      else {
        #{::Kernel.raise ::TypeError, "can't convert #{other.class} into Float"};
      }
    }
  end

  def __id__
    %x{
      // Binary-safe integers
      if (self|0 === self) {
        return (self * 2) + 1;
      }
      else {
        if (number_id_map.has(self)) {
          return number_id_map.get(self);
        }
        var id = Opal.uid();
        number_id_map.set(self, id);
        return id;
      }
    }
  end

  def hash
    %x{
      // Binary-safe integers
      if (self|0 === self) {
        return #{__id__}
      }
      else {
        return self.toString().$hash();
      }
    }
  end

  def +(other)
    %x{
      if (other.$$is_number) {
        return self + other;
      }
      else {
        return #{__coerced__ :+, other};
      }
    }
  end

  def -(other)
    %x{
      if (other.$$is_number) {
        return self - other;
      }
      else {
        return #{__coerced__ :-, other};
      }
    }
  end

  def *(other)
    %x{
      if (other.$$is_number) {
        return self * other;
      }
      else {
        return #{__coerced__ :*, other};
      }
    }
  end

  def /(other)
    %x{
      if (other.$$is_number) {
        return self / other;
      }
      else {
        return #{__coerced__ :/, other};
      }
    }
  end

  def %(other)
    %x{
      if (other.$$is_number) {
        if (other == -Infinity) {
          return other;
        }
        else if (other == 0) {
          #{::Kernel.raise ::ZeroDivisionError, 'divided by 0'};
        }
        else if (other < 0 || self < 0) {
          return (self % other + other) % other;
        }
        else {
          return self % other;
        }
      }
      else {
        return #{__coerced__ :%, other};
      }
    }
  end

  def &(other)
    %x{
      if (other.$$is_number) {
        return self & other;
      }
      else {
        return #{__coerced__ :&, other};
      }
    }
  end

  def |(other)
    %x{
      if (other.$$is_number) {
        return self | other;
      }
      else {
        return #{__coerced__ :|, other};
      }
    }
  end

  def ^(other)
    %x{
      if (other.$$is_number) {
        return self ^ other;
      }
      else {
        return #{__coerced__ :^, other};
      }
    }
  end

  def <(other)
    %x{
      if (other.$$is_number) {
        return self < other;
      }
      else {
        return #{__coerced__ :<, other};
      }
    }
  end

  def <=(other)
    %x{
      if (other.$$is_number) {
        return self <= other;
      }
      else {
        return #{__coerced__ :<=, other};
      }
    }
  end

  def >(other)
    %x{
      if (other.$$is_number) {
        return self > other;
      }
      else {
        return #{__coerced__ :>, other};
      }
    }
  end

  def >=(other)
    %x{
      if (other.$$is_number) {
        return self >= other;
      }
      else {
        return #{__coerced__ :>=, other};
      }
    }
  end

  # Compute the result of the spaceship operator inside its own function so it
  # can be optimized despite a try/finally construct.
  %x{
    var spaceship_operator = function(self, other) {
      if (other.$$is_number) {
        if (isNaN(self) || isNaN(other)) {
          return nil;
        }

        if (self > other) {
          return 1;
        } else if (self < other) {
          return -1;
        } else {
          return 0;
        }
      }
      else {
        return #{__coerced__ :<=>, `other`};
      }
    }
  }

  def <=>(other)
    `spaceship_operator(self, other)`
  rescue ::ArgumentError
    nil
  end

  def <<(count)
    count = ::Opal.coerce_to! count, ::Integer, :to_int

    `#{count} > 0 ? self << #{count} : self >> -#{count}`
  end

  def >>(count)
    count = ::Opal.coerce_to! count, ::Integer, :to_int

    `#{count} > 0 ? self >> #{count} : self << -#{count}`
  end

  def [](bit)
    bit = ::Opal.coerce_to! bit, ::Integer, :to_int

    %x{
      if (#{bit} < 0) {
        return 0;
      }
      if (#{bit} >= 32) {
        return #{ self } < 0 ? 1 : 0;
      }
      return (self >> #{bit}) & 1;
    }
  end

  def +@
    `+self`
  end

  def -@
    `-self`
  end

  def ~
    `~self`
  end

  def **(other)
    if ::Integer === other
      if !(::Integer === self) || other > 0
        `Math.pow(self, other)`
      else
        ::Rational.new(self, 1)**other
      end
    elsif self < 0 && (::Float === other || ::Rational === other)
      ::Complex.new(self, 0)**other.to_f
    elsif `other.$$is_number != null`
      `Math.pow(self, other)`
    else
      __coerced__ :**, other
    end
  end

  def ==(other)
    %x{
      if (other.$$is_number) {
        return self.valueOf() === other.valueOf();
      }
      else if (#{other.respond_to? :==}) {
        return #{other == self};
      }
      else {
        return false;
      }
    }
  end

  alias === ==

  def abs
    `Math.abs(self)`
  end

  def abs2
    `Math.abs(self * self)`
  end

  def allbits?(mask)
    mask = ::Opal.coerce_to! mask, ::Integer, :to_int
    `(self & mask) == mask`
  end

  def anybits?(mask)
    mask = ::Opal.coerce_to! mask, ::Integer, :to_int
    `(self & mask) !== 0`
  end

  def angle
    return self if nan?

    %x{
      if (self == 0) {
        if (1 / self > 0) {
          return 0;
        }
        else {
          return Math.PI;
        }
      }
      else if (self < 0) {
        return Math.PI;
      }
      else {
        return 0;
      }
    }
  end

  def bit_length
    unless ::Integer === self
      ::Kernel.raise ::NoMethodError.new("undefined method `bit_length` for #{self}:Float", 'bit_length')
    end

    %x{
      if (self === 0 || self === -1) {
        return 0;
      }

      var result = 0,
          value  = self < 0 ? ~self : self;

      while (value != 0) {
        result   += 1;
        value  >>>= 1;
      }

      return result;
    }
  end

  def ceil(ndigits = 0)
    %x{
      var f = #{to_f};

      if (f % 1 === 0 && ndigits >= 0) {
        return f;
      }

      var factor = Math.pow(10, ndigits),
          result = Math.ceil(f * factor) / factor;

      if (f % 1 === 0) {
        result = Math.round(result);
      }

      return result;
    }
  end

  def chr(encoding = undefined)
    `Opal.str(String.fromCodePoint(self), encoding || "BINARY")`
  end

  def denominator
    if nan? || infinite?
      1
    else
      super
    end
  end

  def downto(stop, &block)
    unless block_given?
      return enum_for(:downto, stop) do
        ::Kernel.raise ::ArgumentError, "comparison of #{self.class} with #{stop.class} failed" unless ::Numeric === stop
        stop > self ? 0 : self - stop + 1
      end
    end

    %x{
      if (!stop.$$is_number) {
        #{::Kernel.raise ::ArgumentError, "comparison of #{self.class} with #{stop.class} failed"}
      }
      for (var i = self; i >= stop; i--) {
        block(i);
      }
    }

    self
  end

  def equal?(other)
    self == other || `isNaN(self) && isNaN(other)`
  end

  def even?
    `self % 2 === 0`
  end

  def floor(ndigits = 0)
    %x{
      var f = #{to_f};

      if (f % 1 === 0 && ndigits >= 0) {
        return f;
      }

      var factor = Math.pow(10, ndigits),
          result = Math.floor(f * factor) / factor;

      if (f % 1 === 0) {
        result = Math.round(result);
      }

      return result;
    }
  end

  def gcd(other)
    unless ::Integer === other
      ::Kernel.raise ::TypeError, 'not an integer'
    end

    %x{
      var min = Math.abs(self),
          max = Math.abs(other);

      while (min > 0) {
        var tmp = min;

        min = max % min;
        max = tmp;
      }

      return max;
    }
  end

  def gcdlcm(other)
    [gcd(other), lcm(other)]
  end

  def integer?
    `self % 1 === 0`
  end

  def is_a?(klass)
    return true if klass == ::Integer && ::Integer === self
    return true if klass == ::Integer && ::Integer === self
    return true if klass == ::Float && ::Float === self

    super
  end

  def instance_of?(klass)
    return true if klass == ::Integer && ::Integer === self
    return true if klass == ::Integer && ::Integer === self
    return true if klass == ::Float && ::Float === self

    super
  end

  def lcm(other)
    unless ::Integer === other
      ::Kernel.raise ::TypeError, 'not an integer'
    end

    %x{
      if (self == 0 || other == 0) {
        return 0;
      }
      else {
        return Math.abs(self * other / #{gcd(other)});
      }
    }
  end

  def next
    `self + 1`
  end

  def nobits?(mask)
    mask = ::Opal.coerce_to! mask, ::Integer, :to_int
    `(self & mask) == 0`
  end

  def nonzero?
    `self == 0 ? nil : self`
  end

  def numerator
    if nan? || infinite?
      self
    else
      super
    end
  end

  def odd?
    `self % 2 !== 0`
  end

  def ord
    self
  end

  def pow(b, m = undefined)
    %x{
      if (self == 0) {
        #{::Kernel.raise ::ZeroDivisionError, 'divided by 0'}
      }

      if (m === undefined) {
        return #{self**b};
      } else {
        if (!(#{::Integer === b})) {
          #{::Kernel.raise ::TypeError, 'Integer#pow() 2nd argument not allowed unless a 1st argument is integer'}
        }

        if (b < 0) {
          #{::Kernel.raise ::TypeError, 'Integer#pow() 1st argument cannot be negative when 2nd argument specified'}
        }

        if (!(#{::Integer === m})) {
          #{::Kernel.raise ::TypeError, 'Integer#pow() 2nd argument not allowed unless all arguments are integers'}
        }

        if (m === 0) {
          #{::Kernel.raise ::ZeroDivisionError, 'divided by 0'}
        }

        return #{(self**b) % m}
      }
    }
  end

  def pred
    `self - 1`
  end

  def quo(other)
    if ::Integer === self
      super
    else
      self / other
    end
  end

  def rationalize(eps = undefined)
    %x{
      if (arguments.length > 1) {
        #{::Kernel.raise ::ArgumentError, "wrong number of arguments (#{`arguments.length`} for 0..1)"};
      }
    }

    if ::Integer === self
      ::Rational.new(self, 1)
    elsif infinite?
      ::Kernel.raise ::FloatDomainError, 'Infinity'
    elsif nan?
      ::Kernel.raise ::FloatDomainError, 'NaN'
    elsif `eps == null`
      f, n  = ::Math.frexp self
      f     = ::Math.ldexp(f, ::Float::MANT_DIG).to_i
      n    -= ::Float::MANT_DIG

      ::Rational.new(2 * f, 1 << (1 - n)).rationalize(::Rational.new(1, 1 << (1 - n)))
    else
      to_r.rationalize(eps)
    end
  end

  def remainder(y)
    self - y * (self / y).truncate
  end

  def round(ndigits = undefined)
    if ::Integer === self
      if `ndigits == null`
        return self
      end

      if ::Float === ndigits && ndigits.infinite?
        ::Kernel.raise ::RangeError, 'Infinity'
      end

      ndigits = ::Opal.coerce_to!(ndigits, ::Integer, :to_int)

      if ndigits < ::Integer::MIN
        ::Kernel.raise ::RangeError, 'out of bounds'
      end

      if `ndigits >= 0`
        return self
      end

      ndigits = -ndigits

      %x{
        if (0.415241 * ndigits - 0.125 > #{size}) {
          return 0;
        }

        var f = Math.pow(10, ndigits),
            x = Math.floor((Math.abs(self) + f / 2) / f) * f;

        return self < 0 ? -x : x;
      }
    else
      if nan? && `ndigits == null`
        ::Kernel.raise ::FloatDomainError, 'NaN'
      end

      ndigits = ::Opal.coerce_to!(`ndigits || 0`, ::Integer, :to_int)

      if ndigits <= 0
        if nan?
          ::Kernel.raise ::RangeError, 'NaN'
        elsif infinite?
          ::Kernel.raise ::FloatDomainError, 'Infinity'
        end
      elsif ndigits == 0
        return `Math.round(self)`
      elsif nan? || infinite?
        return self
      end

      _, exp = ::Math.frexp(self)

      if ndigits >= (::Float::DIG + 2) - (exp > 0 ? exp / 4 : exp / 3 - 1)
        return self
      end

      if ndigits < -(exp > 0 ? exp / 3 + 1 : exp / 4)
        return 0
      end

      `Math.round(self * Math.pow(10, ndigits)) / Math.pow(10, ndigits)`
    end
  end

  def times(&block)
    return enum_for(:times) { self } unless block

    %x{
      for (var i = 0; i < self; i++) {
        block(i);
      }
    }

    self
  end

  def to_f
    self
  end

  def to_i
    `self < 0 ? Math.ceil(self) : Math.floor(self)`
  end

  def to_r
    if ::Integer === self
      ::Rational.new(self, 1)
    else
      f, e  = ::Math.frexp(self)
      f     = ::Math.ldexp(f, ::Float::MANT_DIG).to_i
      e    -= ::Float::MANT_DIG

      (f * (::Float::RADIX**e)).to_r
    end
  end

  def to_s(base = 10)
    base = ::Opal.coerce_to! base, ::Integer, :to_int

    if base < 2 || base > 36
      ::Kernel.raise ::ArgumentError, "invalid radix #{base}"
    end

    # Don't lose the negative zero
    if self == 0 && `1/self === -Infinity`
      return '-0.0'
    end

    `self.toString(base)`
  end

  def truncate(ndigits = 0)
    %x{
      var f = #{to_f};

      if (f % 1 === 0 && ndigits >= 0) {
        return f;
      }

      var factor = Math.pow(10, ndigits),
          result = parseInt(f * factor, 10) / factor;

      if (f % 1 === 0) {
        result = Math.round(result);
      }

      return result;
    }
  end

  def digits(base = 10)
    if self < 0
      ::Kernel.raise ::Math::DomainError, 'out of domain'
    end

    base = ::Opal.coerce_to! base, ::Integer, :to_int

    if base < 2
      ::Kernel.raise ::ArgumentError, "invalid radix #{base}"
    end

    %x{
      if (self != parseInt(self)) #{::Kernel.raise ::NoMethodError, "undefined method `digits' for #{inspect}"}

      var value = self, result = [];

      if (self == 0) {
        return [0];
      }

      while (value != 0) {
        result.push(value % base);
        value = parseInt(value / base, 10);
      }

      return result;
    }
  end

  def divmod(other)
    if nan? || other.nan?
      ::Kernel.raise ::FloatDomainError, 'NaN'
    elsif infinite?
      ::Kernel.raise ::FloatDomainError, 'Infinity'
    else
      super
    end
  end

  def upto(stop, &block)
    unless block_given?
      return enum_for(:upto, stop) do
        ::Kernel.raise ::ArgumentError, "comparison of #{self.class} with #{stop.class} failed" unless ::Numeric === stop
        stop < self ? 0 : stop - self + 1
      end
    end

    %x{
      if (!stop.$$is_number) {
        #{::Kernel.raise ::ArgumentError, "comparison of #{self.class} with #{stop.class} failed"}
      }
      for (var i = self; i <= stop; i++) {
        block(i);
      }
    }

    self
  end

  def zero?
    `self == 0`
  end

  # Since bitwise operations are 32 bit, declare it to be so.
  def size
    4
  end

  def nan?
    `isNaN(self)`
  end

  def finite?
    `self != Infinity && self != -Infinity && !isNaN(self)`
  end

  def infinite?
    %x{
      if (self == Infinity) {
        return +1;
      }
      else if (self == -Infinity) {
        return -1;
      }
      else {
        return nil;
      }
    }
  end

  def positive?
    `self != 0 && (self == Infinity || 1 / self > 0)`
  end

  def negative?
    `self == -Infinity || 1 / self < 0`
  end

  %x{
    function numberToUint8Array(num) {
      var uint8array = new Uint8Array(8);
      new DataView(uint8array.buffer).setFloat64(0, num, true);
      return uint8array;
    }

    function uint8ArrayToNumber(arr) {
      return new DataView(arr.buffer).getFloat64(0, true);
    }

    function incrementNumberBit(num) {
      var arr = numberToUint8Array(num);
      for (var i = 0; i < arr.length; i++) {
        if (arr[i] === 0xff) {
          arr[i] = 0;
        } else {
          arr[i]++;
          break;
        }
      }
      return uint8ArrayToNumber(arr);
    }

    function decrementNumberBit(num) {
      var arr = numberToUint8Array(num);
      for (var i = 0; i < arr.length; i++) {
        if (arr[i] === 0) {
          arr[i] = 0xff;
        } else {
          arr[i]--;
          break;
        }
      }
      return uint8ArrayToNumber(arr);
    }
  }

  def next_float
    return ::Float::INFINITY if self == ::Float::INFINITY
    return ::Float::NAN if nan?

    if self >= 0
      # Math.abs() is needed to handle -0.0
      `incrementNumberBit(Math.abs(self))`
    else
      `decrementNumberBit(self)`
    end
  end

  def prev_float
    return -::Float::INFINITY if self == -::Float::INFINITY
    return ::Float::NAN if nan?

    if self > 0
      `decrementNumberBit(self)`
    else
      `-incrementNumberBit(Math.abs(self))`
    end
  end

  alias arg angle
  alias eql? ==
  alias fdiv /
  alias inspect to_s
  alias kind_of? is_a?
  alias magnitude abs
  alias modulo %
  alias object_id __id__
  alias phase angle
  alias succ next
  alias to_int to_i
end

::Fixnum = ::Number

class ::Integer < ::Numeric
  `self.$$is_number_class = true`
  `self.$$is_integer_class = true`

  class << self
    def allocate
      ::Kernel.raise ::TypeError, "allocator undefined for #{name}"
    end

    undef :new

    def sqrt(n)
      n = ::Opal.coerce_to!(n, ::Integer, :to_int)
      %x{
        if (n < 0) {
          #{::Kernel.raise ::Math::DomainError, 'Numerical argument is out of domain - "isqrt"'}
        }

        return parseInt(Math.sqrt(n), 10);
      }
    end

    def try_convert(object)
      Opal.coerce_to?(object, self, :to_int)
    end
  end

  self::MAX = `Math.pow(2, 30) - 1`
  self::MIN = `-Math.pow(2, 30)`
end

class ::Float < ::Numeric
  `self.$$is_number_class = true`

  class << self
    def allocate
      ::Kernel.raise ::TypeError, "allocator undefined for #{name}"
    end

    undef :new

    def ===(other)
      `!!other.$$is_number`
    end
  end

  self::INFINITY = `Infinity`
  self::MAX      = `Number.MAX_VALUE`
  self::MIN      = `Number.MIN_VALUE`
  self::NAN      = `NaN`

  self::DIG      = 15
  self::MANT_DIG = 53
  self::RADIX    = 2

  self::EPSILON = `Number.EPSILON || 2.2204460492503130808472633361816E-16`
end
