# helpers: truthy
# backtick_javascript: true

module ::Comparable
  %x{
    function normalize(what) {
      if (Opal.is_a(what, Opal.Integer)) { return what; }

      if (#{`what` > 0}) { return 1; }
      if (#{`what` < 0}) { return -1; }
      return 0;
    }

    function fail_comparison(lhs, rhs) {
      var class_name;
      #{
        case `rhs`
        when nil, true, false, ::Integer, ::Float
          `class_name = rhs.$inspect()`
        else
          `class_name = rhs.$$class`
        end
      }
      #{::Kernel.raise ::ArgumentError, "comparison of #{`lhs`.class} with #{`class_name`} failed"}
    }

    function cmp_or_fail(lhs, rhs) {
      var cmp = #{`lhs` <=> `rhs`};
      if (!$truthy(cmp)) fail_comparison(lhs, rhs);
      return normalize(cmp);
    }
  }

  def ==(other)
    return true if equal?(other)

    %x{
      if (self["$<=>"] == Opal.Kernel["$<=>"]) {
        return false;
      }

      // check for infinite recursion
      if (self.$$comparable) {
        self.$$comparable = false;
        return false;
      }
    }

    return false unless cmp = (self <=> other)

    `normalize(cmp) == 0`
  end

  def >(other)
    `cmp_or_fail(self, other) > 0`
  end

  def >=(other)
    `cmp_or_fail(self, other) >= 0`
  end

  def <(other)
    `cmp_or_fail(self, other) < 0`
  end

  def <=(other)
    `cmp_or_fail(self, other) <= 0`
  end

  def between?(min, max)
    return false if self < min
    return false if self > max
    true
  end

  def clamp(min, max = nil)
    %x{
      var c, excl;

      if (max === nil) {
        // We are dealing with a new Ruby 2.7 behaviour that we are able to
        // provide a single Range argument instead of 2 Comparables.

        if (!Opal.is_a(min, Opal.Range)) {
          #{::Kernel.raise ::TypeError, "wrong argument type #{min.class} (expected Range)"}
        }

        excl = min.excl;
        max = min.end;
        min = min.begin;

        if (max !== nil && excl) {
          #{::Kernel.raise ::ArgumentError, 'cannot clamp with an exclusive range'}
        }
      }

      if (min !== nil && max !== nil && cmp_or_fail(min, max) > 0) {
        #{::Kernel.raise ::ArgumentError, 'min argument must be smaller than max argument'}
      }

      if (min !== nil) {
        c = cmp_or_fail(self, min);

        if (c == 0) return self;
        if (c < 0) return min;
      }

      if (max !== nil) {
        c = cmp_or_fail(self, max);

        if (c > 0) return max;
      }

      return self;
    }
  end
end
