///|
/// Does this object key match a `patternProperties` regex?
fn key_matches_pattern(ctx : Ctx, pattern : String, key : String) -> Bool {
  match ctx.regex(pattern) {
    Some(re) => re.match_(key) is Some(_)
    None => false
  }
}

///|
/// Object keywords: properties, patternProperties, additionalProperties,
/// required, minProperties, maxProperties, propertyNames,
/// dependentRequired, dependentSchemas.
fn kw_object(
  ctx : Ctx,
  obj : Map[String, Json],
  inst : Json,
  ip : Path,
  sp : Path,
) -> Unit {
  guard inst is Object(props) else { return }
  let prop_defs : Map[String, Json] = match obj.get("properties") {
    Some(Object(m)) => m
    _ => {}
  }
  let pattern_defs : Map[String, Json] = match obj.get("patternProperties") {
    Some(Object(m)) => m
    _ => {}
  }
  let required : Array[String] = match obj.get("required") {
    Some(Array(items)) => {
      let names = []
      for item in items {
        if item is String(s) {
          names.push(s)
        }
      }
      names
    }
    _ => []
  }
  for name in required {
    if props.get(name) is None {
      ctx.add_error(
        ip,
        sp.key("required"),
        "required",
        "required property \{name} is missing",
      )
    }
  }
  let count = props.length()
  match obj.get("maxProperties") {
    Some(Number(m, ..)) =>
      if count > m.to_int() {
        ctx.add_error(
          ip,
          sp.key("maxProperties"),
          "maxProperties",
          "object has \{count} properties, more than maxProperties \{m.to_int()}",
        )
      }
    _ => ()
  }
  match obj.get("minProperties") {
    Some(Number(m, ..)) =>
      if count < m.to_int() {
        ctx.add_error(
          ip,
          sp.key("minProperties"),
          "minProperties",
          "object has \{count} properties, fewer than minProperties \{m.to_int()}",
        )
      }
    _ => ()
  }
  let ip_str = render_path(ip)
  for name, sub in prop_defs {
    match props.get(name) {
      Some(v) => {
        validate_node(ctx, sub, v, ip.key(name), sp.key("properties").key(name))
        ctx.note_prop(ip_str, name)
      }
      None => ()
    }
  }
  // which instance keys were directly checked by properties/patternProperties
  let covered : Map[String, Bool] = {}
  for key, _v in props {
    if prop_defs.get(key) is Some(_) {
      covered.set(key, true)
    } else {
      for pattern, _sub in pattern_defs {
        if key_matches_pattern(ctx, pattern, key) {
          covered.set(key, true)
          break
        }
      }
    }
  }
  for pattern, sub in pattern_defs {
    for key, v in props {
      if key_matches_pattern(ctx, pattern, key) {
        validate_node(
          ctx,
          sub,
          v,
          ip.key(key),
          sp.key("patternProperties").key(pattern),
        )
        ctx.note_prop(ip_str, key)
      }
    }
  }
  match obj.get("additionalProperties") {
    Some(False) =>
      for key, _v in props {
        if covered.get(key) is None {
          ctx.add_error(
            ip.key(key),
            sp.key("additionalProperties"),
            "additionalProperties",
            "property \{key} is not allowed by properties/patternProperties and additionalProperties is false",
          )
        }
      }
    Some(ap_schema) =>
      for key, v in props {
        if covered.get(key) is None {
          validate_node(
            ctx,
            ap_schema,
            v,
            ip.key(key),
            sp.key("additionalProperties"),
          )
          ctx.note_prop(ip_str, key)
        }
      }
    None => ()
  }
  match obj.get("propertyNames") {
    Some(pn_schema) =>
      for key, _v in props {
        validate_node(
          ctx,
          pn_schema,
          Json::string(key),
          ip.key(key),
          sp.key("propertyNames"),
        )
      }
    None => ()
  }
  match obj.get("dependentRequired") {
    Some(Object(deps)) =>
      for key, dep_list in deps {
        if props.get(key) is Some(_) {
          if dep_list is Array(list) {
            for dep in list {
              if dep is String(d) {
                if props.get(d) is None {
                  ctx.add_error(
                    ip,
                    sp.key("dependentRequired").key(key),
                    "dependentRequired",
                    "property \{d} is required when \{key} is present",
                  )
                }
              }
            }
          }
        }
      }
    _ => ()
  }
  match obj.get("dependentSchemas") {
    Some(Object(deps)) =>
      for key, dep_schema in deps {
        if props.get(key) is Some(_) {
          validate_node(
            ctx,
            dep_schema,
            inst,
            ip,
            sp.key("dependentSchemas").key(key),
          )
        }
      }
    _ => ()
  }
}