// Matchers for numbers.

///|
/// A number type with a zero. Implement this trait to use `to_be_positive`,
/// `to_be_negative` and `to_be_zero` on your own number type.
pub(open) trait Number: Compare {
  fn zero() -> Self
}

///|
pub impl Number for Int with fn zero() {
  0
}

///|
pub impl Number for Int16 with fn zero() {
  0
}

///|
pub impl Number for Int64 with fn zero() {
  0
}

///|
pub impl Number for UInt with fn zero() {
  0
}

///|
pub impl Number for UInt16 with fn zero() {
  0
}

///|
pub impl Number for UInt64 with fn zero() {
  0
}

///|
pub impl Number for Double with fn zero() {
  0.0
}

///|
pub impl Number for Float with fn zero() {
  0.0
}

///|
/// Assert the actual number is greater than zero.
#callsite(autofill(loc))
pub fn[T : Number + @debug.Debug] Expectation::to_be_positive(
  self : Expectation[T],
  loc~ : SourceLoc,
) -> Unit raise Error {
  self.assert_that(
    self.actual > (Number::zero() : T),
    "to_be_positive",
    expected=() => "> \{show((Number::zero() : T))}",
    received=() => show(self.actual),
    loc~,
  )
}

///|
/// Assert the actual number is less than zero.
#callsite(autofill(loc))
pub fn[T : Number + @debug.Debug] Expectation::to_be_negative(
  self : Expectation[T],
  loc~ : SourceLoc,
) -> Unit raise Error {
  self.assert_that(
    self.actual < (Number::zero() : T),
    "to_be_negative",
    expected=() => "< \{show((Number::zero() : T))}",
    received=() => show(self.actual),
    loc~,
  )
}

///|
/// Assert the actual number is zero. For floating-point numbers, `-0.0` is
/// zero too.
#callsite(autofill(loc))
pub fn[T : Number + @debug.Debug] Expectation::to_be_zero(
  self : Expectation[T],
  loc~ : SourceLoc,
) -> Unit raise Error {
  self.assert_that(
    self.actual == (Number::zero() : T),
    "to_be_zero",
    expected=() => show((Number::zero() : T)),
    received=() => show(self.actual),
    loc~,
  )
}

///|
/// Assert the actual value is between `low` and `high`. Both bounds are
/// inclusive unless you set `low_inclusive` or `high_inclusive` to `false`.
#callsite(autofill(loc))
pub fn[T : Compare + @debug.Debug] Expectation::to_be_between(
  self : Expectation[T],
  low : T,
  high : T,
  low_inclusive? : Bool = true,
  high_inclusive? : Bool = true,
  loc~ : SourceLoc,
) -> Unit raise Error {
  let above_low = if low_inclusive {
    self.actual >= low
  } else {
    self.actual > low
  }
  let below_high = if high_inclusive {
    self.actual <= high
  } else {
    self.actual < high
  }
  self.assert_that(
    above_low && below_high,
    "to_be_between",
    args="low, high",
    expected=() => {
      let bounds = match (low_inclusive, high_inclusive) {
        (true, true) => "inclusive"
        (false, false) => "exclusive"
        _ => {
          let describe = (value, inclusive) => {
            "\{show(value)} \{if inclusive { "inclusive" } else { "exclusive" }}"
          }
          "\{describe(low, low_inclusive)}, \{describe(high, high_inclusive)}"
        }
      }
      "between \{show(low)} and \{show(high)} (\{bounds})"
    },
    received=() => show(self.actual),
    loc~,
  )
}

///|
/// A floating-point type: `Double` or `Float`.
pub trait FloatingPoint {
  /// The value as a `Double`.
  fn as_double(Self) -> Double
  /// The number of representable values between two values.
  fn ulp_distance(Self, Self) -> UInt64
  /// The value as text, with a decimal point.
  fn describe(Self) -> String
}

///|
pub impl FloatingPoint for Double with fn as_double(self) {
  self
}

///|
pub impl FloatingPoint for Double with fn ulp_distance(self, other) {
  // Map the bits to integers that have the same order as the values.
  let ordered = (x : Double) => {
    let bits = x.reinterpret_as_int64()
    if bits < 0L {
      -9223372036854775808L - bits
    } else {
      bits
    }
  }
  let a = ordered(self)
  let b = ordered(other)
  // The difference always fits in `UInt64`, so wrapping subtraction is exact.
  if a >= b {
    a.reinterpret_as_uint64() - b.reinterpret_as_uint64()
  } else {
    b.reinterpret_as_uint64() - a.reinterpret_as_uint64()
  }
}

///|
pub impl FloatingPoint for Double with fn describe(self) {
  show_double(self)
}

///|
pub impl FloatingPoint for Float with fn as_double(self) {
  self.to_double()
}

///|
pub impl FloatingPoint for Float with fn ulp_distance(self, other) {
  let ordered = (x : Float) => {
    let bits = x.reinterpret_as_int()
    if bits < 0 {
      (-2147483648 - bits).to_int64()
    } else {
      bits.to_int64()
    }
  }
  (ordered(self) - ordered(other)).abs().reinterpret_as_uint64()
}

///|
pub impl FloatingPoint for Float with fn describe(self) {
  let text = self.to_string()
  if self.is_nan() || self.is_inf() || text.contains(".") || text.contains("e") {
    text
  } else {
    "\{text}.0"
  }
}

///|
/// Assert the actual value is NaN.
#callsite(autofill(loc))
pub fn[F : FloatingPoint] Expectation::to_be_nan(
  self : Expectation[F],
  loc~ : SourceLoc,
) -> Unit raise Error {
  self.assert_that(
    self.actual.as_double().is_nan(),
    "to_be_nan",
    expected=() => "NaN",
    received=() => self.actual.describe(),
    loc~,
  )
}

///|
/// Assert the actual value is finite: not NaN and not infinite.
#callsite(autofill(loc))
pub fn[F : FloatingPoint] Expectation::to_be_finite(
  self : Expectation[F],
  loc~ : SourceLoc,
) -> Unit raise Error {
  let value = self.actual.as_double()
  self.assert_that(
    !value.is_nan() && !value.is_inf(),
    "to_be_finite",
    expected=() => "a finite number",
    received=() => self.actual.describe(),
    loc~,
  )
}

///|
/// Assert the actual value is close to `expected`, within an absolute,
/// relative or ULP tolerance. NaN is never close to any value.
///
/// Give at most one tolerance:
///
/// - `tolerance`: the largest absolute difference. This is the default, with
///   `1.0e-9`.
/// - `relative`: the largest difference as a fraction of the larger of the
///   two magnitudes, for example `0.01` for 1%.
/// - `ulps`: the largest number of representable values between the two
///   values (units in the last place).
#callsite(autofill(loc))
pub fn[F : FloatingPoint] Expectation::to_be_close_to(
  self : Expectation[F],
  expected : F,
  tolerance? : Double,
  relative? : Double,
  ulps? : Int,
  loc~ : SourceLoc,
) -> Unit raise Error {
  let given = [
    if tolerance is Some(_) {
      Some("tolerance")
    } else {
      None
    },
    if relative is Some(_) {
      Some("relative")
    } else {
      None
    },
    if ulps is Some(_) {
      Some("ulps")
    } else {
      None
    },
  ].filter_map(x => x)
  // The absolute tolerance is the default, so the headline names it when no
  // tolerance is given.
  let args = if given.is_empty() {
    "expected, tolerance"
  } else {
    ["expected", ..given].join(", ")
  }
  if given.length() > 1 {
    self.report(
      "to_be_close_to",
      args,
      [("Error", "give only one of tolerance, relative and ulps")],
      loc,
    )
  }
  let actual = self.actual.as_double()
  let target = expected.as_double()
  let difference = (actual - target).abs()
  let nan = actual.is_nan() || target.is_nan()
  let magnitude = if actual.abs() > target.abs() {
    actual.abs()
  } else {
    target.abs()
  }
  let relative_difference = if magnitude == 0.0 {
    0.0
  } else {
    difference / magnitude
  }
  let distance = self.actual.ulp_distance(expected)
  let (pass, description, details) = match (relative, ulps) {
    (Some(fraction), _) =>
      (
        !nan && relative_difference <= fraction,
        "within relative tolerance \{show_double(fraction)} of \{expected.describe()}",
        [
          ("Difference", show_double(difference)),
          ("Relative difference", show_double(relative_difference)),
        ],
      )
    (_, Some(count)) =>
      (
        !nan && count >= 0 && distance <= count.to_uint64(),
        "within \{count} ULPs of \{expected.describe()}",
        [
          ("Difference", show_double(difference)),
          ("ULP distance", distance.to_string()),
        ],
      )
    _ => {
      let limit = tolerance.unwrap_or(1.0e-9)
      (
        difference <= limit,
        "within \{show_double(limit)} of \{expected.describe()}",
        [("Difference", show_double(difference))],
      )
    }
  }
  self.assert_that(
    pass,
    "to_be_close_to",
    args~,
    expected=() => description,
    received=() => self.actual.describe(),
    details=() => details,
    loc~,
  )
}