// Python equality / ordering semantics for JSON values.

///|
/// `_is_actual_number`: a number that is not a bool (always true for
/// `Json::Number`, since JSON booleans are a separate case).
fn is_actual_number(x : Json) -> Bool {
  x is Number(_, ..)
}

///|
/// Python's `bool`-as-`int` view used by `==` between bools and numbers.
fn bool_as_number(x : Json) -> Double? {
  match x {
    True => Some(1.0)
    False => Some(0.0)
    Number(d, ..) => Some(d)
    _ => None
  }
}

///|
/// `PyObject_RichCompareBool(x, y, Py_EQ)`: identical objects are equal
/// (this only matters for NaN).  Used for container elements and `in`.
fn py_eq_bool(x : Json, y : Json) -> Bool {
  physical_equal(x, y) || py_eq(x, y)
}

///|
/// Python's `x == y` for JSON values: `1 == 1.0`, and (nested) `True == 1`,
/// `False == 0`; dicts compare regardless of key order.  Container elements
/// are compared with the identity shortcut (`py_eq_bool`), so a list holding
/// the very same NaN object equals itself, while a bare NaN never does.
fn py_eq(x : Json, y : Json) -> Bool {
  match (x, y) {
    (Null, Null) => true
    (True, True) | (False, False) => true
    (True, False) | (False, True) => false
    (True | False | Number(_, ..), True | False | Number(_, ..)) =>
      bool_as_number(x).unwrap() == bool_as_number(y).unwrap()
    (String(a), String(b)) => a == b
    (Array(a), Array(b)) => {
      if a.length() != b.length() {
        return false
      }
      for i in 0.. {
      if a.length() != b.length() {
        return false
      }
      for k, v in a {
        match b.get(k) {
          Some(w) => if !py_eq_bool(v, w) { return false }
          None => return false
        }
      }
      true
    }
    _ => false
  }
}

///|
/// `_is_special_number_case`: comparing 0 or 1 with a bool.
fn is_special_number_case(x : Json, y : Json) -> Bool {
  if is_actual_number(x) &&
    (py_eq(x, Json::number(0.0)) || py_eq(x, Json::number(1.0))) {
    y is (True | False)
  } else if is_actual_number(y) &&
    (py_eq(y, Json::number(0.0)) || py_eq(y, Json::number(1.0))) {
    x is (True | False)
  } else {
    false
  }
}

///|
/// `_equals`: Python `==`, except that 0/1 never equal false/true.
fn jmespath_equals(x : Json, y : Json) -> Bool {
  if is_special_number_case(x, y) {
    false
  } else {
    py_eq(x, y)
  }
}

///|
/// `_is_comparable`: numbers and strings can be ordered.
fn is_comparable(x : Json) -> Bool {
  x is (Number(_, ..) | String(_))
}

///|
/// Compares two strings by code point, like Python (core's `String`
/// comparison orders by length first).
fn py_str_compare(a : String, b : String) -> Int {
  let ia = a.iter()
  let ib = b.iter()
  while true {
    match (ia.next(), ib.next()) {
      (None, None) => return 0
      (None, Some(_)) => return -1
      (Some(_), None) => return 1
      (Some(x), Some(y)) =>
        if x != y {
          return if x.to_int() < y.to_int() { -1 } else { 1 }
        }
    }
  }
  0
}

///|
/// Python's type name of a value, as seen by `type(value).__name__`.
fn py_type_name(x : Json) -> String {
  match x {
    Null => "NoneType"
    True | False => "bool"
    Number(d, repr~) => if number_is_float(d, repr) { "float" } else { "int" }
    String(_) => "str"
    Array(_) => "list"
    Object(_) => "dict"
  }
}

///|
/// Python's ordering operators (`<`, `<=`, `>`, `>=`) between numbers or
/// between strings.  Any other combination raises Python's `TypeError`.
fn py_order(op : String, x : Json, y : Json) -> Bool raise JMESPathError {
  let cmp = match (x, y) {
    (Number(a, ..), Number(b, ..)) =>
      // NaN compares false with everything.
      if a.is_nan() || b.is_nan() {
        return false
      } else if a < b {
        -1
      } else if a > b {
        1
      } else {
        0
      }
    (String(a), String(b)) => py_str_compare(a, b)
    _ =>
      raise TypeError(
        "'\{op}' not supported between instances of '\{py_type_name(x)}' and '\{py_type_name(y)}'",
      )
  }
  match op {
    "<" => cmp < 0
    "<=" => cmp <= 0
    ">" => cmp > 0
    ">=" => cmp >= 0
    _ => abort("unknown operator \{op}")
  }
}

///|
/// `_is_false`: JMESPath falsiness (`''`, `[]`, `{}`, `None`, `False`).
fn is_false(value : Json) -> Bool {
  match value {
    String(s) => s == ""
    Array(a) => a.is_empty()
    Object(m) => m.is_empty()
    Null | False => true
    True | Number(_, ..) => false
  }
}

///|
/// `_is_true`
fn is_true(value : Json) -> Bool {
  !is_false(value)
}

///|
/// The code points of a string.  Python strings are code point sequences;
/// a lone surrogate is a code point of its own and never matches half of a
/// surrogate pair.
fn code_points(s : String) -> Array[Char] {
  s.to_array()
}

///|
fn chars_match_at(hay : Array[Char], needle : Array[Char], at : Int) -> Bool {
  for i, c in needle {
    if hay[at + i] != c {
      return false
    }
  }
  true
}

///|
/// Python's `needle in hay` for strings.
fn py_str_contains(hay : String, needle : String) -> Bool {
  let h = code_points(hay)
  let n = code_points(needle)
  for at in 0..<=(h.length() - n.length()) {
    if chars_match_at(h, n, at) {
      return true
    }
  }
  false
}

///|
/// Python's `s.startswith(prefix)`.
fn py_str_startswith(s : String, prefix : String) -> Bool {
  let h = code_points(s)
  let n = code_points(prefix)
  n.length() <= h.length() && chars_match_at(h, n, 0)
}

///|
/// Python's `s.endswith(suffix)`.
fn py_str_endswith(s : String, suffix : String) -> Bool {
  let h = code_points(s)
  let n = code_points(suffix)
  n.length() <= h.length() && chars_match_at(h, n, h.length() - n.length())
}