// A port of `jsonschema/_keywords.py`.

///|
fn err(message : String) -> ValidationError {
  ValidationError::new(message)
}

///|
fn r(j : Json) -> String {
  @pycompat.repr(j)
}

///|
fn object_items(j : Json) -> Array[(String, Json)] {
  match j {
    Object(m) => m.iter().collect()
    _ => []
  }
}

///|
fn kw_pattern_properties(
  validator : Validator,
  pattern_properties : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    if !validator.is_type(instance, "object") {
      return true
    }
    for entry in object_items(pattern_properties) {
      let (pattern, subschema) = entry
      for item in object_items(instance) {
        let (k, v) = item
        if @regex.search(pattern, k) {
          let ok = validator
            .descend(v, subschema, path=Key(k), schema_path=Key(pattern))
            .run(yield_)
          if !ok {
            return false
          }
        }
      }
    }
    true
  })
}

///|
fn kw_property_names(
  validator : Validator,
  property_names : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    if !validator.is_type(instance, "object") {
      return true
    }
    for item in object_items(instance) {
      if !validator.descend(Json::string(item.0), property_names).run(yield_) {
        return false
      }
    }
    true
  })
}

///|
fn kw_additional_properties(
  validator : Validator,
  additional : Json,
  instance : Json,
  schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    guard instance is Object(obj) && validator.is_type(instance, "object") else {
      return true
    }
    let extras = find_additional_properties(obj, schema)
    if validator.is_type(additional, "object") {
      for extra in extras {
        if !validator
          .descend(obj[extra], additional, path=Key(extra))
          .run(yield_) {
          return false
        }
      }
    } else if !truthy(additional) && extras.length() > 0 {
      if has_key(schema, "patternProperties") {
        let verb = if extras.length() == 1 { "does" } else { "do" }
        let joined = py_sorted(extras)
          .map(e => @pycompat.repr_string(e))
          .join(", ")
        let patterns = py_sorted(
            object_items(get_key(schema, "patternProperties").unwrap()).map(p => {
              p.0
            }),
          )
          .map(p => @pycompat.repr_string(p))
          .join(", ")
        return yield_(
          err("\{joined} \{verb} not match any of the regexes: \{patterns}"),
        )
      } else {
        let (extras_repr, verb) = extras_msg(
          py_sorted(extras).map(e => Json::string(e)),
        )
        return yield_(
          err(
            "Additional properties are not allowed (\{extras_repr} \{verb} unexpected)",
          ),
        )
      }
    }
    true
  })
}

///|
fn kw_items(
  validator : Validator,
  items : Json,
  instance : Json,
  schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    guard instance is Array(xs) && validator.is_type(instance, "array") else {
      return true
    }
    let prefix = match get_key(schema, "prefixItems") {
      Some(p) => py_len(p)
      None => 0
    }
    let total = xs.length()
    let extra = total - prefix
    if extra <= 0 {
      return true
    }
    if items is False {
      let rest = if extra != 1 {
        Json::array(xs[prefix:].to_owned())
      } else {
        xs[prefix]
      }
      let item = if prefix != 1 { "items" } else { "item" }
      return yield_(
        err(
          "Expected at most \{prefix} \{item} but found \{extra} extra: \{r(rest)}",
        ),
      )
    }
    for index in prefix.. Errors {
  Errors::new(fn(yield_) raise {
    if !equal(instance, const_) {
      return yield_(err("\{r(const_)} was expected"))
    }
    true
  })
}

///|
fn kw_contains(
  validator : Validator,
  contains : Json,
  instance : Json,
  schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    guard instance is Array(xs) && validator.is_type(instance, "array") else {
      return true
    }
    let mut matches = 0
    let min_contains = get_key(schema, "minContains").unwrap_or(int_json(1))
    let max_contains = get_key(schema, "maxContains").unwrap_or(
      int_json(xs.length()),
    )
    let contains_validator = validator.evolve(schema=contains)
    for each in xs {
      if contains_validator.is_valid(each) {
        matches += 1
        if py_gt(int_json(matches), max_contains) {
          return yield_(
            ValidationError::new(
              "Too many items match the given schema (expected at most \{py_str(max_contains)})",
              validator=Json::string("maxContains"),
              validator_value=max_contains,
            ),
          )
        }
      }
    }
    if py_lt(int_json(matches), min_contains) {
      if matches == 0 {
        return yield_(
          err("\{r(instance)} does not contain items matching the given schema"),
        )
      } else {
        return yield_(
          ValidationError::new(
            "Too few items match the given schema (expected at least \{py_str(min_contains)} but only \{matches} matched)",
            validator=Json::string("minContains"),
            validator_value=min_contains,
          ),
        )
      }
    }
    true
  })
}

///|
fn kw_exclusive_minimum(
  validator : Validator,
  minimum : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    if !validator.is_type(instance, "number") {
      return true
    }
    if py_le(instance, minimum) {
      return yield_(
        err(
          "\{r(instance)} is less than or equal to the minimum of \{r(minimum)}",
        ),
      )
    }
    true
  })
}

///|
fn kw_exclusive_maximum(
  validator : Validator,
  maximum : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    if !validator.is_type(instance, "number") {
      return true
    }
    if py_ge(instance, maximum) {
      return yield_(
        err(
          "\{r(instance)} is greater than or equal to the maximum of \{r(maximum)}",
        ),
      )
    }
    true
  })
}

///|
fn kw_minimum(
  validator : Validator,
  minimum : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    if !validator.is_type(instance, "number") {
      return true
    }
    if py_lt(instance, minimum) {
      return yield_(
        err("\{r(instance)} is less than the minimum of \{r(minimum)}"),
      )
    }
    true
  })
}

///|
fn kw_maximum(
  validator : Validator,
  maximum : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    if !validator.is_type(instance, "number") {
      return true
    }
    if py_gt(instance, maximum) {
      return yield_(
        err("\{r(instance)} is greater than the maximum of \{r(maximum)}"),
      )
    }
    true
  })
}

///|
fn kw_multiple_of(
  validator : Validator,
  db : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    if !validator.is_type(instance, "number") {
      return true
    }
    let failed = @pycompat.multiple_of_failed(instance, db) catch {
      e => raise PythonError("\{e}")
    }
    if failed {
      return yield_(err("\{r(instance)} is not a multiple of \{py_str(db)}"))
    }
    true
  })
}

///|
fn kw_min_items(
  validator : Validator,
  mi : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    if validator.is_type(instance, "array") &&
      py_lt(int_json(py_len(instance)), mi) {
      let message = if @pycompat.numbers_equal(mi, int_json(1)) {
        "should be non-empty"
      } else {
        "is too short"
      }
      return yield_(err("\{r(instance)} \{message}"))
    }
    true
  })
}

///|
fn kw_max_items(
  validator : Validator,
  mi : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    if validator.is_type(instance, "array") &&
      py_gt(int_json(py_len(instance)), mi) {
      let message = if @pycompat.numbers_equal(mi, int_json(0)) {
        "is expected to be empty"
      } else {
        "is too long"
      }
      return yield_(err("\{r(instance)} \{message}"))
    }
    true
  })
}

///|
fn kw_unique_items(
  validator : Validator,
  ui : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    if truthy(ui) &&
      validator.is_type(instance, "array") &&
      instance is Array(xs) &&
      !uniq(xs) {
      return yield_(err("\{r(instance)} has non-unique elements"))
    }
    true
  })
}

///|
fn kw_pattern(
  validator : Validator,
  patrn : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    if validator.is_type(instance, "string") && instance is String(s) {
      guard patrn is String(p) else {
        raise PythonError(
          "TypeError: first argument must be string or compiled pattern",
        )
      }
      if !@regex.search(p, s) {
        return yield_(err("\{r(instance)} does not match \{r(patrn)}"))
      }
    }
    true
  })
}

///|
fn kw_format(
  validator : Validator,
  format : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    guard validator.format_checker is Some(checker) && format is String(f) else {
      return true
    }
    checker.check(instance, f) catch {
      FormatError(message~, cause~) =>
        return yield_(ValidationError::new(message, cause?))
      e => raise e
    }
    true
  })
}

///|
fn kw_min_length(
  validator : Validator,
  ml : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    if validator.is_type(instance, "string") &&
      py_lt(int_json(py_len(instance)), ml) {
      let message = if @pycompat.numbers_equal(ml, int_json(1)) {
        "should be non-empty"
      } else {
        "is too short"
      }
      return yield_(err("\{r(instance)} \{message}"))
    }
    true
  })
}

///|
fn kw_max_length(
  validator : Validator,
  ml : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    if validator.is_type(instance, "string") &&
      py_gt(int_json(py_len(instance)), ml) {
      let message = if @pycompat.numbers_equal(ml, int_json(0)) {
        "is expected to be empty"
      } else {
        "is too long"
      }
      return yield_(err("\{r(instance)} \{message}"))
    }
    true
  })
}

///|
fn kw_dependent_required(
  validator : Validator,
  dependent_required : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    guard instance is Object(obj) && validator.is_type(instance, "object") else {
      return true
    }
    for entry in object_items(dependent_required) {
      let (property, dependency) = entry
      if !obj.contains(property) {
        continue
      }
      for each in py_iter(dependency) {
        if !has_key(instance, py_str(each)) || !(each is String(_)) {
          if !yield_(
              err(
                "\{r(each)} is a dependency of \{@pycompat.repr_string(property)}",
              ),
            ) {
            return false
          }
        }
      }
    }
    true
  })
}

///|
fn kw_dependent_schemas(
  validator : Validator,
  dependent_schemas : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    guard instance is Object(obj) && validator.is_type(instance, "object") else {
      return true
    }
    for entry in object_items(dependent_schemas) {
      let (property, dependency) = entry
      if !obj.contains(property) {
        continue
      }
      if !validator
        .descend(instance, dependency, schema_path=Key(property))
        .run(yield_) {
        return false
      }
    }
    true
  })
}

///|
fn kw_enum(
  _validator : Validator,
  enums : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    if py_iter(enums).iter().all(each => !equal(each, instance)) {
      return yield_(err("\{r(instance)} is not one of \{r(enums)}"))
    }
    true
  })
}

///|
fn kw_ref(
  validator : Validator,
  ref_ : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    guard ref_ is String(s) else {
      raise PythonError("TypeError: $ref must be a string")
    }
    validator.validate_reference(s, instance).run(yield_)
  })
}

///|
fn kw_dynamic_ref(
  validator : Validator,
  dynamic_ref : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    guard dynamic_ref is String(s) else {
      raise PythonError("TypeError: $dynamicRef must be a string")
    }
    validator.validate_reference(s, instance).run(yield_)
  })
}

///|
fn kw_type(
  validator : Validator,
  types : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    let types = ensure_list(types)
    for t in types {
      guard t is String(name) else {
        raise PythonError("TypeError: unhashable type: \{type_name(t)}")
      }
      if validator.is_type(instance, name) {
        return true
      }
    }
    let reprs = types.map(r).join(", ")
    yield_(err("\{r(instance)} is not of type \{reprs}"))
  })
}

///|
fn kw_properties(
  validator : Validator,
  properties : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    guard instance is Object(obj) && validator.is_type(instance, "object") else {
      return true
    }
    for entry in object_items(properties) {
      let (property, subschema) = entry
      if obj.get(property) is Some(value) {
        let ok = validator
          .descend(
            value,
            subschema,
            path=Key(property),
            schema_path=Key(property),
          )
          .run(yield_)
        if !ok {
          return false
        }
      }
    }
    true
  })
}

///|
fn kw_required(
  validator : Validator,
  required : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    guard instance is Object(obj) && validator.is_type(instance, "object") else {
      return true
    }
    for property in py_iter(required) {
      let present = match property {
        String(p) => obj.contains(p)
        _ => false
      }
      if !present {
        if !yield_(err("\{r(property)} is a required property")) {
          return false
        }
      }
    }
    true
  })
}

///|
fn kw_min_properties(
  validator : Validator,
  mp : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    if validator.is_type(instance, "object") &&
      py_lt(int_json(py_len(instance)), mp) {
      let message = if @pycompat.numbers_equal(mp, int_json(1)) {
        "should be non-empty"
      } else {
        "does not have enough properties"
      }
      return yield_(err("\{r(instance)} \{message}"))
    }
    true
  })
}

///|
fn kw_max_properties(
  validator : Validator,
  mp : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    if !validator.is_type(instance, "object") {
      return true
    }
    if validator.is_type(instance, "object") &&
      py_gt(int_json(py_len(instance)), mp) {
      let message = if @pycompat.numbers_equal(mp, int_json(0)) {
        "is expected to be empty"
      } else {
        "has too many properties"
      }
      return yield_(err("\{r(instance)} \{message}"))
    }
    true
  })
}

///|
fn kw_all_of(
  validator : Validator,
  all_of : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    for index, subschema in py_iter(all_of) {
      if !validator
        .descend(instance, subschema, schema_path=Index(index))
        .run(yield_) {
        return false
      }
    }
    true
  })
}

///|
fn kw_any_of(
  validator : Validator,
  any_of : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    let all_errors = []
    for index, subschema in py_iter(any_of) {
      let errs = validator
        .descend(instance, subschema, schema_path=Index(index))
        .to_array()
      if errs.is_empty() {
        return true
      }
      all_errors.append(errs)
    }
    yield_(
      ValidationError::new(
        "\{r(instance)} is not valid under any of the given schemas",
        context=all_errors,
      ),
    )
  })
}

///|
fn kw_one_of(
  validator : Validator,
  one_of : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    let subschemas = py_iter(one_of)
    let all_errors = []
    let mut first_valid : (Int, Json)? = None
    for index, subschema in subschemas {
      let errs = validator
        .descend(instance, subschema, schema_path=Index(index))
        .to_array()
      if errs.is_empty() {
        first_valid = Some((index, subschema))
        break
      }
      all_errors.append(errs)
    }
    guard first_valid is Some((first_index, first)) else {
      return yield_(
        ValidationError::new(
          "\{r(instance)} is not valid under any of the given schemas",
          context=all_errors,
        ),
      )
    }
    let more_valid = []
    for i in (first_index + 1).. 0 {
      more_valid.push(first)
      let reprs = more_valid.map(r).join(", ")
      return yield_(err("\{r(instance)} is valid under each of \{reprs}"))
    }
    true
  })
}

///|
fn kw_not(
  validator : Validator,
  not_schema : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    if validator.evolve(schema=not_schema).is_valid(instance) {
      return yield_(
        err("\{r(instance)} should not be valid under \{r(not_schema)}"),
      )
    }
    true
  })
}

///|
fn kw_if(
  validator : Validator,
  if_schema : Json,
  instance : Json,
  schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    if validator.evolve(schema=if_schema).is_valid(instance) {
      if get_key(schema, "then") is Some(then) {
        return validator
          .descend(instance, then, schema_path=Key("then"))
          .run(yield_)
      }
    } else if get_key(schema, "else") is Some(else_) {
      return validator
        .descend(instance, else_, schema_path=Key("else"))
        .run(yield_)
    }
    true
  })
}

///|
fn kw_unevaluated_items(
  validator : Validator,
  _unevaluated_items : Json,
  instance : Json,
  schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    guard instance is Array(xs) && validator.is_type(instance, "array") else {
      return true
    }
    let evaluated = find_evaluated_item_indexes_by_schema(
      validator, instance, schema,
    )
    let unevaluated = []
    for index, item in xs {
      if !evaluated.contains(index) {
        unevaluated.push(item)
      }
    }
    if unevaluated.length() > 0 {
      let (extras, verb) = extras_msg(unevaluated)
      return yield_(
        err("Unevaluated items are not allowed (\{extras} \{verb} unexpected)"),
      )
    }
    true
  })
}

///|
fn kw_unevaluated_properties(
  validator : Validator,
  unevaluated_properties : Json,
  instance : Json,
  schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    guard instance is Object(obj) && validator.is_type(instance, "object") else {
      return true
    }
    let evaluated_keys = find_evaluated_property_keys_by_schema(
      validator, instance, schema,
    )
    unevaluated_properties_errors(
      validator, unevaluated_properties, obj, evaluated_keys, yield_,
    )
  })
}

///|
/// The shared tail of the `unevaluatedProperties` implementations.
fn unevaluated_properties_errors(
  validator : Validator,
  unevaluated_properties : Json,
  obj : Map[String, Json],
  evaluated_keys : Array[String],
  yield_ : (ValidationError) -> Bool raise,
) -> Bool raise {
  let unevaluated_keys = []
  for property, value in obj {
    if !evaluated_keys.contains(property) {
      validator
      .descend(
        value,
        unevaluated_properties,
        path=Key(property),
        schema_path=Key(property),
      )
      .each(_ => unevaluated_keys.push(property))
    }
  }
  if unevaluated_keys.length() > 0 {
    if unevaluated_properties is False {
      let (extras, verb) = extras_msg(
        py_sorted(unevaluated_keys).map(k => Json::string(k)),
      )
      return yield_(
        err(
          "Unevaluated properties are not allowed (\{extras} \{verb} unexpected)",
        ),
      )
    } else {
      let (extras, verb) = extras_msg(
        unevaluated_keys.map(k => Json::string(k)),
      )
      return yield_(
        err(
          "Unevaluated properties are not valid under the given schema (\{extras} \{verb} unevaluated and invalid)",
        ),
      )
    }
  }
  true
}

///|
fn kw_prefix_items(
  validator : Validator,
  prefix_items : Json,
  instance : Json,
  _schema : Json,
) -> Errors {
  Errors::new(fn(yield_) raise {
    guard instance is Array(xs) && validator.is_type(instance, "array") else {
      return true
    }
    let subschemas = py_iter(prefix_items)
    for index, item in xs {
      if index >= subschemas.length() {
        break
      }
      let ok = validator
        .descend(
          item,
          subschemas[index],
          schema_path=Index(index),
          path=Index(index),
        )
        .run(yield_)
      if !ok {
        return false
      }
    }
    true
  })
}