# helpers: type_error
# backtick_javascript: true
# use_strict: true

module ::Math
  self::E  = `Math.E`
  self::PI = `Math.PI`

  self::DomainError = ::Class.new(::StandardError)

  def self.checked(method, *args)
    %x{
      if (isNaN(args[0]) || (args.length == 2 && isNaN(args[1]))) {
        return NaN;
      }

      var result = Math[method].apply(null, args);

      if (isNaN(result)) {
        #{::Kernel.raise DomainError, "Numerical argument is out of domain - \"#{method}\""};
      }

      return result;
    }
  end

  def self.float!(value)
    ::Kernel.Float(value)
  rescue ::ArgumentError
    ::Kernel.raise `$type_error(value, #{::Float})`
  end

  def self.integer!(value)
    ::Kernel.Integer(value)
  rescue ::ArgumentError
    ::Kernel.raise `$type_error(value, #{::Integer})`
  end

  module_function

  unless defined?(`Math.erf`)
    %x{
      Opal.prop(Math, 'erf', function(x) {
        var A1 =  0.254829592,
            A2 = -0.284496736,
            A3 =  1.421413741,
            A4 = -1.453152027,
            A5 =  1.061405429,
            P  =  0.3275911;

        var sign = 1;

        if (x < 0) {
            sign = -1;
        }

        x = Math.abs(x);

        var t = 1.0 / (1.0 + P * x);
        var y = 1.0 - (((((A5 * t + A4) * t) + A3) * t + A2) * t + A1) * t * Math.exp(-x * x);

        return sign * y;
      });
    }
  end

  unless defined?(`Math.erfc`)
    %x{
      Opal.prop(Math, 'erfc', function(x) {
        var z = Math.abs(x),
            t = 1.0 / (0.5 * z + 1.0);

        var A1 = t * 0.17087277 + -0.82215223,
            A2 = t * A1 + 1.48851587,
            A3 = t * A2 + -1.13520398,
            A4 = t * A3 + 0.27886807,
            A5 = t * A4 + -0.18628806,
            A6 = t * A5 + 0.09678418,
            A7 = t * A6 + 0.37409196,
            A8 = t * A7 + 1.00002368,
            A9 = t * A8,
            A10 = -z * z - 1.26551223 + A9;

        var a = t * Math.exp(A10);

        if (x < 0.0) {
          return 2.0 - a;
        }
        else {
          return a;
        }
      });
    }
  end

  # Single argument equivalent functions
  %i[
    acos acosh asin asinh atan atanh cbrt
    cos cosh erf erfc exp sin sinh sqrt tanh
  ].each do |method|
    define_method method do |x|
      ::Math.checked method, ::Math.float!(x)
    end
  end

  def atan2(y, x)
    ::Math.checked :atan2, ::Math.float!(y), ::Math.float!(x)
  end

  def hypot(x, y)
    ::Math.checked :hypot, ::Math.float!(x), ::Math.float!(y)
  end

  def frexp(x)
    x = Math.float!(x)

    %x{
      if (isNaN(x)) {
        return [NaN, 0];
      }

      var ex   = Math.floor(Math.log(Math.abs(x)) / Math.log(2)) + 1,
          frac = x / Math.pow(2, ex);

      return [frac, ex];
    }
  end

  def gamma(n)
    n = Math.float!(n)

    %x{
      var i, t, x, value, result, twoN, threeN, fourN, fiveN;

      var G = 4.7421875;

      /* eslint no-loss-of-precision: "warn" */
      var P = [
         0.99999999999999709182,
         57.156235665862923517,
        -59.597960355475491248,
         14.136097974741747174,
        -0.49191381609762019978,
         0.33994649984811888699e-4,
         0.46523628927048575665e-4,
        -0.98374475304879564677e-4,
         0.15808870322491248884e-3,
        -0.21026444172410488319e-3,
         0.21743961811521264320e-3,
        -0.16431810653676389022e-3,
         0.84418223983852743293e-4,
        -0.26190838401581408670e-4,
         0.36899182659531622704e-5
      ];

      if (isNaN(n)) {
        return NaN;
      }

      if (n === 0 && 1 / n < 0) {
        return -Infinity;
      }

      if (n === -1 || n === -Infinity) {
        #{::Kernel.raise DomainError, 'Numerical argument is out of domain - "gamma"'};
      }

      if (#{Integer === n}) {
        if (n <= 0) {
          return isFinite(n) ? Infinity : NaN;
        }

        if (n > 171) {
          return Infinity;
        }

        value  = n - 2;
        result = n - 1;

        while (value > 1) {
          result *= value;
          value--;
        }

        if (result == 0) {
          result = 1;
        }

        return result;
      }

      if (n < 0.5) {
        return Math.PI / (Math.sin(Math.PI * n) * #{::Math.gamma(1 - n)});
      }

      if (n >= 171.35) {
        return Infinity;
      }

      if (n > 85.0) {
        twoN   = n * n;
        threeN = twoN * n;
        fourN  = threeN * n;
        fiveN  = fourN * n;

        return Math.sqrt(2 * Math.PI / n) * Math.pow((n / Math.E), n) *
          (1 + 1 / (12 * n) + 1 / (288 * twoN) - 139 / (51840 * threeN) -
          571 / (2488320 * fourN) + 163879 / (209018880 * fiveN) +
          5246819 / (75246796800 * fiveN * n));
      }

      n -= 1;
      x  = P[0];

      for (i = 1; i < P.length; ++i) {
        x += P[i] / (n + i);
      }

      t = n + G + 0.5;

      return Math.sqrt(2 * Math.PI) * Math.pow(t, n + 0.5) * Math.exp(-t) * x;
    }
  end

  def ldexp(mantissa, exponent)
    mantissa = Math.float!(mantissa)
    exponent = Math.integer!(exponent)

    %x{
      if (isNaN(exponent)) {
        #{::Kernel.raise ::RangeError, 'float NaN out of range of integer'};
      }

      return mantissa * Math.pow(2, exponent);
    }
  end

  def lgamma(n)
    %x{
      if (n == -1) {
        return [Infinity, 1];
      }
      else {
        return [Math.log(Math.abs(#{::Math.gamma(n)})), #{::Math.gamma(n)} < 0 ? -1 : 1];
      }
    }
  end

  def log(x, base = undefined)
    if ::String === x
      ::Kernel.raise `$type_error(x, #{::Float})`
    end

    if `base == null`
      ::Math.checked :log, ::Math.float!(x)
    else
      if ::String === base
        ::Kernel.raise `$type_error(base, #{::Float})`
      end

      ::Math.checked(:log, ::Math.float!(x)) / ::Math.checked(:log, ::Math.float!(base))
    end
  end

  def log10(x)
    if ::String === x
      ::Kernel.raise `$type_error(x, #{::Float})`
    end

    ::Math.checked :log10, ::Math.float!(x)
  end

  def log2(x)
    if ::String === x
      ::Kernel.raise `$type_error(x, #{::Float})`
    end

    ::Math.checked :log2, ::Math.float!(x)
  end

  def tan(x)
    x = ::Math.float!(x)

    if x.infinite?
      return ::Float::NAN
    end

    ::Math.checked :tan, ::Math.float!(x)
  end
end
