///|
/// A type-checking function: given the checker doing the checking and an
/// instance, whether the instance is of the type.
pub type TypeCheckFn = (TypeChecker, Json) -> Bool raise

///|
/// A `type` property checker (`jsonschema.TypeChecker`).
///
/// A `TypeChecker` performs type checking for a validator, converting
/// between the defined JSON Schema types and JSON values.  It is immutable:
/// `redefine`, `redefine_many` and `remove` each return a new checker.
pub struct TypeChecker {
  priv type_checkers : Map[String, TypeCheckFn]
}

///|
/// `TypeChecker(type_checkers)`.
pub fn TypeChecker::new(
  type_checkers? : ArrayView[(String, TypeCheckFn)] = [],
) -> TypeChecker {
  let m = Map([])
  for pair in type_checkers {
    m[pair.0] = pair.1
  }
  { type_checkers: m, }
}

///|
/// Raised by `TypeChecker::is_type` / `remove` for unknown types
/// (`jsonschema.exceptions.UndefinedTypeCheck`).
pub suberror UndefinedTypeCheck {
  UndefinedTypeCheck(String)
}

///|
pub impl Show for UndefinedTypeCheck with fn output(self, logger) {
  let UndefinedTypeCheck(type_) = self
  logger.write_string(
    "Type \{@pycompat.repr_string(type_)} is unknown to this type checker",
  )
}

///|
/// `repr(checker)`: ``.
pub impl Show for TypeChecker with fn output(self, logger) {
  let names = self.type_checkers.keys().collect()
  names.sort_by(py_str_compare)
  logger.write_string(" @pycompat.repr_string(n)).join(", "))
  logger.write_string("}>")
}

///|
/// Two checkers are equal when they map the same types to the very same
/// functions.
pub impl Eq for TypeChecker with fn equal(self, other) {
  if self.type_checkers.length() != other.type_checkers.length() {
    return false
  }
  for k, f in self.type_checkers {
    match other.type_checkers.get(k) {
      Some(g) if physical_equal(f, g) => ()
      _ => return false
    }
  }
  true
}

///|
/// Check if the instance is of the appropriate type.
///
/// Raises `UndefinedTypeCheck` if `type_` is unknown to this object.
pub fn TypeChecker::is_type(
  self : TypeChecker,
  instance : Json,
  type_ : String,
) -> Bool raise {
  guard self.type_checkers.get(type_) is Some(f) else {
    raise UndefinedTypeCheck(type_)
  }
  f(self, instance)
}

///|
/// The names of the types known to this checker, in definition order.
pub fn TypeChecker::types(self : TypeChecker) -> Array[String] {
  self.type_checkers.keys().collect()
}

///|
/// Produce a new checker with the given type redefined.
pub fn TypeChecker::redefine(
  self : TypeChecker,
  type_ : String,
  f : TypeCheckFn,
) -> TypeChecker {
  self.redefine_many([(type_, f)])
}

///|
/// Produce a new checker with the given types redefined.
pub fn TypeChecker::redefine_many(
  self : TypeChecker,
  definitions : ArrayView[(String, TypeCheckFn)],
) -> TypeChecker {
  let m = self.type_checkers.copy()
  for pair in definitions {
    m[pair.0] = pair.1
  }
  { type_checkers: m, }
}

///|
/// Produce a new checker with the given types forgotten.
///
/// Raises `UndefinedTypeCheck` if any given type is unknown to this object.
pub fn TypeChecker::remove(
  self : TypeChecker,
  types : ArrayView[String],
) -> TypeChecker raise UndefinedTypeCheck {
  let m = self.type_checkers.copy()
  for t in types {
    if !m.contains(t) {
      raise UndefinedTypeCheck(t)
    }
    m.remove(t)
  }
  { type_checkers: m, }
}

///|
fn is_array(_ : TypeChecker, instance : Json) -> Bool {
  instance is Array(_)
}

///|
fn is_bool(_ : TypeChecker, instance : Json) -> Bool {
  instance is (True | False)
}

///|
/// Python `isinstance(instance, int)` excluding bools.
fn is_integer(_ : TypeChecker, instance : Json) -> Bool {
  @pycompat.is_int(instance)
}

///|
fn is_null(_ : TypeChecker, instance : Json) -> Bool {
  instance is Null
}

///|
/// Python `isinstance(instance, numbers.Number)` excluding bools.
fn is_number(_ : TypeChecker, instance : Json) -> Bool {
  instance is Number(_)
}

///|
fn is_object(_ : TypeChecker, instance : Json) -> Bool {
  instance is Object(_)
}

///|
fn is_string(_ : TypeChecker, instance : Json) -> Bool {
  instance is String(_)
}

///|
fn is_any(_ : TypeChecker, _ : Json) -> Bool {
  true
}

///|
/// draft6+ integers: Python ints, or floats for which `is_integer()` holds.
fn is_integer_or_integral_float(checker : TypeChecker, instance : Json) -> Bool {
  is_integer(checker, instance) ||
  (
    @pycompat.is_float(instance) &&
    @pycompat.is_whole(@pycompat.number_value(instance))
  )
}

///|
/// The type checker for draft 3.
///
/// Note: like upstream, `integer` means a Python `int`, so a JSON `1.0` is
/// not an integer under drafts 3 and 4 (see `loads` for how the distinction
/// is kept).
pub let draft3_type_checker : TypeChecker = TypeChecker::new(type_checkers=[
  ("any", (c, i) => is_any(c, i)),
  ("array", (c, i) => is_array(c, i)),
  ("boolean", (c, i) => is_bool(c, i)),
  ("integer", (c, i) => is_integer(c, i)),
  ("object", (c, i) => is_object(c, i)),
  ("null", (c, i) => is_null(c, i)),
  ("number", (c, i) => is_number(c, i)),
  ("string", (c, i) => is_string(c, i)),
])

///|
/// The type checker for draft 4.
pub let draft4_type_checker : TypeChecker = draft3_type_checker.remove(["any"]) catch {
  _ => panic()
}

///|
/// The type checker for draft 6.
pub let draft6_type_checker : TypeChecker = draft4_type_checker.redefine(
  "integer",
  (c, i) => is_integer_or_integral_float(c, i),
)

///|
/// The type checker for draft 7.
pub let draft7_type_checker : TypeChecker = draft6_type_checker

///|
/// The type checker for draft 2019-09.
pub let draft201909_type_checker : TypeChecker = draft7_type_checker

///|
/// The type checker for draft 2020-12.
pub let draft202012_type_checker : TypeChecker = draft201909_type_checker