// Python semantics needed by the keyword implementations.

///|
/// Raised where upstream would crash with a Python-level error (e.g. a
/// `TypeError` from `len(True)` on a malformed schema).
pub suberror PythonError {
  PythonError(String)
} derive(Debug)

///|
pub impl Show for PythonError with fn output(self, logger) {
  let PythonError(msg) = self
  logger.write_string(msg)
}

///|
/// Python truthiness of the value a JSON document stands for.
fn truthy(j : Json) -> Bool {
  match j {
    Null | False => false
    True => true
    Number(d, ..) => d != 0.0
    String(s) => s.length() > 0
    Array(xs) => xs.length() > 0
    Object(m) => m.length() > 0
  }
}

///|
/// `len(x)`.
fn py_len(j : Json) -> Int raise PythonError {
  match j {
    String(s) => @unicodedata.py_len(s)
    Array(xs) => xs.length()
    Object(m) => m.length()
    _ =>
      raise PythonError(
        "TypeError: object of type \{type_name(j)} has no len()",
      )
  }
}

///|
/// Python's type name for the value a JSON document stands for.
fn type_name(j : Json) -> String {
  match j {
    Null => "NoneType"
    True | False => "bool"
    String(_) => "str"
    Array(_) => "list"
    Object(_) => "dict"
    Number(_) => if @pycompat.is_int(j) { "int" } else { "float" }
  }
}

///|
/// A Python `int` as JSON.
fn int_json(n : Int) -> Json {
  Json::number(n.to_double())
}

///|
/// Python's numeric comparison `a OP b` (`None` when either side is not a
/// number; comparisons involving NaN are false).
fn num_cmp(a : Json, b : Json) -> Int? raise PythonError {
  match (a, b) {
    (Number(_), Number(_)) => @pycompat.compare_numbers(a, b)
    (True | False, _) | (_, True | False) => {
      // bool is an int in Python
      let to_num = (j : Json) => {
        match j {
          True => int_json(1)
          False => int_json(0)
          _ => j
        }
      }
      num_cmp(to_num(a), to_num(b))
    }
    _ =>
      raise PythonError(
        "TypeError: '<' not supported between instances of '\{type_name(a)}' and '\{type_name(b)}'",
      )
  }
}

///|
fn py_lt(a : Json, b : Json) -> Bool raise PythonError {
  num_cmp(a, b) is Some(c) && c < 0
}

///|
fn py_le(a : Json, b : Json) -> Bool raise PythonError {
  num_cmp(a, b) is Some(c) && c <= 0
}

///|
fn py_gt(a : Json, b : Json) -> Bool raise PythonError {
  num_cmp(a, b) is Some(c) && c > 0
}

///|
fn py_ge(a : Json, b : Json) -> Bool raise PythonError {
  num_cmp(a, b) is Some(c) && c >= 0
}

///|
/// Python `str(x)` for a number (same as `repr`).
fn py_str(j : Json) -> String {
  match j {
    String(s) => s
    _ => @pycompat.repr(j)
  }
}

///|
/// Python's ordering of `str`s (by code point).
fn py_str_compare(a : String, b : String) -> Int {
  let x = @unicodedata.code_points(a)
  let y = @unicodedata.code_points(b)
  for i in 0..<@cmp.minimum(x.length(), y.length()) {
    if x[i] != y[i] {
      return x[i].compare(y[i])
    }
  }
  x.length().compare(y.length())
}

///|
/// `sorted(strings)`.
fn py_sorted(strings : Array[String]) -> Array[String] {
  let out = strings.copy()
  out.sort_by(py_str_compare)
  out
}

///|
/// `schema.get(key)` for object schemas.
fn get_key(schema : Json, key : String) -> Json? {
  match schema {
    Object(m) => m.get(key)
    _ => None
  }
}

///|
/// `key in schema`.
fn has_key(schema : Json, key : String) -> Bool {
  match schema {
    Object(m) => m.contains(key)
    Array(xs) => xs.contains(Json::string(key))
    _ => false
  }
}

///|
/// Iterating a container the way Python's `for x in container` does (dict
/// keys, list items).
fn py_iter(j : Json) -> Array[Json] raise PythonError {
  match j {
    Array(xs) => xs
    Object(m) => m.keys().map(k => Json::string(k)).collect()
    String(s) =>
      @unicodedata.code_points(s).map(c => {
        Json::string(@unicodedata.from_code_points([c]))
      })
    _ =>
      raise PythonError("TypeError: '\{type_name(j)}' object is not iterable")
  }
}