///|
/// Name of a JSON value's type as used by the `type` keyword.
fn json_type_name(v : Json) -> String {
  match v {
    Null => "null"
    True | False => "boolean"
    Number(_, ..) => "number"
    String(_) => "string"
    Array(_) => "array"
    Object(_) => "object"
  }
}

///|
/// `type: integer` accepts numbers with zero fractional part (1, 1.0, -2).
fn is_double_integer(n : Double) -> Bool {
  n.floor() == n
}

///|
fn type_allows(t : String, v : Json) -> Bool {
  match (t, v) {
    ("null", Null) => true
    ("boolean", True | False) => true
    ("number", Number(_, ..)) => true
    ("integer", Number(n, ..)) => is_double_integer(n)
    ("string", String(_)) => true
    ("array", Array(_)) => true
    ("object", Object(_)) => true
    _ => false
  }
}

///|
/// `type`, `enum`, `const`.
fn kw_type_enum_const(
  ctx : Ctx,
  obj : Map[String, Json],
  inst : Json,
  ip : Path,
  sp : Path,
) -> Unit {
  match obj.get("type") {
    Some(String(t)) =>
      if !type_allows(t, inst) {
        ctx.add_error(
          ip,
          sp.key("type"),
          "type",
          "expected \{t}, got \{json_type_name(inst)}",
        )
      }
    Some(Array(alts)) => {
      let mut ok = false
      for alt in alts {
        if alt is String(t) {
          if type_allows(t, inst) {
            ok = true
            break
          }
        }
      }
      if !ok {
        ctx.add_error(
          ip,
          sp.key("type"),
          "type",
          "value of type \{json_type_name(inst)} is not in the allowed type list",
        )
      }
    }
    _ => ()
  }
  match obj.get("enum") {
    Some(Array(items)) => {
      let mut found = false
      for item in items {
        if item == inst {
          found = true
          break
        }
      }
      if !found {
        ctx.add_error(
          ip,
          sp.key("enum"),
          "enum",
          "value is not one of the allowed enum values",
        )
      }
    }
    _ => ()
  }
  match obj.get("const") {
    Some(c) =>
      if c != inst {
        ctx.add_error(
          ip,
          sp.key("const"),
          "const",
          "value does not equal the const value",
        )
      }
    None => ()
  }
}

///|
/// Numeric keywords: multipleOf, maximum, exclusiveMaximum, minimum,
/// exclusiveMinimum.
fn kw_numeric(
  ctx : Ctx,
  obj : Map[String, Json],
  inst : Json,
  ip : Path,
  sp : Path,
) -> Unit {
  guard inst is Number(n, ..) else { return }
  match obj.get("maximum") {
    Some(Number(m, ..)) =>
      if n > m {
        ctx.add_error(
          ip,
          sp.key("maximum"),
          "maximum",
          "\{n} is greater than maximum \{m}",
        )
      }
    _ => ()
  }
  match obj.get("exclusiveMaximum") {
    Some(Number(m, ..)) =>
      if n >= m {
        ctx.add_error(
          ip,
          sp.key("exclusiveMaximum"),
          "exclusiveMaximum",
          "\{n} is not less than exclusiveMaximum \{m}",
        )
      }
    _ => ()
  }
  match obj.get("minimum") {
    Some(Number(m, ..)) =>
      if n < m {
        ctx.add_error(
          ip,
          sp.key("minimum"),
          "minimum",
          "\{n} is less than minimum \{m}",
        )
      }
    _ => ()
  }
  match obj.get("exclusiveMinimum") {
    Some(Number(m, ..)) =>
      if n <= m {
        ctx.add_error(
          ip,
          sp.key("exclusiveMinimum"),
          "exclusiveMinimum",
          "\{n} is not greater than exclusiveMinimum \{m}",
        )
      }
    _ => ()
  }
  match obj.get("multipleOf") {
    Some(Number(m, ..)) =>
      if m != 0.0 && !is_multiple(n, m) {
        ctx.add_error(
          ip,
          sp.key("multipleOf"),
          "multipleOf",
          "\{n} is not a multiple of \{m}",
        )
      }
    _ => ()
  }
}

///|
/// Tolerant divisibility check for floating-point numbers:
/// a quotient within 1e-9 of an integer counts as divisible.
/// (JSON Schema defines multiples mathematically; doubles carry
/// representation error, e.g. 0.0075 / 0.0001 = 74.99999999999986.)
fn is_multiple(n : Double, m : Double) -> Bool {
  let q = n / m
  (q - q.floor()).abs() < 0.000000001
}

///|
/// Number of Unicode code points (minLength / maxLength are defined
/// over code points, not UTF-16 units).
fn code_point_length(s : String) -> Int {
  let mut n = 0
  for _ in s {
    n = n + 1
  }
  n
}

///|
/// String keywords: maxLength, minLength, pattern.
fn kw_string(
  ctx : Ctx,
  obj : Map[String, Json],
  inst : Json,
  ip : Path,
  sp : Path,
) -> Unit {
  guard inst is String(s) else { return }
  let len = code_point_length(s)
  match obj.get("maxLength") {
    Some(Number(m, ..)) =>
      if len > m.to_int() {
        ctx.add_error(
          ip,
          sp.key("maxLength"),
          "maxLength",
          "string length \{len} exceeds maxLength \{m.to_int()}",
        )
      }
    _ => ()
  }
  match obj.get("minLength") {
    Some(Number(m, ..)) =>
      if len < m.to_int() {
        ctx.add_error(
          ip,
          sp.key("minLength"),
          "minLength",
          "string length \{len} is below minLength \{m.to_int()}",
        )
      }
    _ => ()
  }
  match obj.get("pattern") {
    Some(String(p)) =>
      match ctx.regex(p) {
        Some(re) =>
          if re.match_(s) is None {
            ctx.add_error(
              ip,
              sp.key("pattern"),
              "pattern",
              "string does not match pattern \{p}",
            )
          }
        None =>
          ctx.add_error(
            ip,
            sp.key("pattern"),
            "pattern",
            "invalid regular expression \{p}",
          )
      }
    _ => ()
  }
}