///|
fn schema_error(
  code : DiagnosticCode,
  message : String,
  path : JsonPointer,
) -> SchemaError {
  SchemaDiagnostic(Diagnostic::new(code, message, schema_path=path))
}

///|
fn expect_object(
  value : Json,
  path : JsonPointer,
) -> Result[Map[String, Json], SchemaError] {
  match value {
    Object(object) => Ok(object)
    _ =>
      Err(
        schema_error(ExpectedObject, "JTD schema must be a JSON object", path),
      )
  }
}

///|
fn optional_boolean_member(
  object : Map[String, Json],
  name : String,
  default_value : Bool,
  path : JsonPointer,
) -> Result[Bool, SchemaError] {
  match object.get(name) {
    None => Ok(default_value)
    Some(True) => Ok(true)
    Some(False) => Ok(false)
    Some(_) =>
      Err(
        schema_error(
          InvalidMemberType,
          "member '" + name + "' must be a boolean",
          path.property(name),
        ),
      )
  }
}

///|
fn optional_metadata(
  object : Map[String, Json],
  path : JsonPointer,
) -> Result[Map[String, Json], SchemaError] {
  match object.get("metadata") {
    None => Ok(Map([]))
    Some(Object(metadata)) => Ok(metadata)
    Some(_) =>
      Err(
        schema_error(
          InvalidMemberType,
          "member 'metadata' must be an object",
          path.property("metadata"),
        ),
      )
  }
}

///|
fn required_string_member(
  object : Map[String, Json],
  name : String,
  path : JsonPointer,
) -> Result[String, SchemaError] {
  match object.get(name) {
    Some(String(value)) => Ok(value)
    Some(_) =>
      Err(
        schema_error(
          InvalidMemberType,
          "member '" + name + "' must be a string",
          path.property(name),
        ),
      )
    None =>
      Err(
        schema_error(
          InvalidMemberType,
          "missing required member '" + name + "'",
          path.property(name),
        ),
      )
  }
}

///|
fn form_keyword_count(object : Map[String, Json]) -> Int {
  let mut count = 0
  if object.contains("ref") {
    count += 1
  }
  if object.contains("type") {
    count += 1
  }
  if object.contains("enum") {
    count += 1
  }
  if object.contains("elements") {
    count += 1
  }
  if object.contains("properties") ||
    object.contains("optionalProperties") ||
    object.contains("additionalProperties") {
    count += 1
  }
  if object.contains("values") {
    count += 1
  }
  if object.contains("discriminator") || object.contains("mapping") {
    count += 1
  }
  count
}

///|
fn is_shared_keyword(name : String, root : Bool) -> Bool {
  name == "nullable" || name == "metadata" || (root && name == "definitions")
}

///|
fn is_form_keyword(form : SchemaForm, name : String) -> Bool {
  match form {
    EmptyForm => false
    RefForm(_) => name == "ref"
    TypeForm(_) => name == "type"
    EnumForm(_) => name == "enum"
    ElementsForm(_) => name == "elements"
    PropertiesForm(_, _, _) =>
      name == "properties" ||
      name == "optionalProperties" ||
      name == "additionalProperties"
    ValuesForm(_) => name == "values"
    DiscriminatorForm(_, _) => name == "discriminator" || name == "mapping"
  }
}

///|
fn check_schema_keywords(
  object : Map[String, Json],
  form : SchemaForm,
  path : JsonPointer,
  root : Bool,
) -> Result[Unit, SchemaError] {
  for name, _ in object {
    if !is_shared_keyword(name, root) && !is_form_keyword(form, name) {
      return Err(
        schema_error(
          UnknownKeyword,
          "keyword '" + name + "' is not allowed in this JTD schema form",
          path.property(name),
        ),
      )
    }
  }
  Ok(())
}

///|
fn parse_string_array(
  value : Json,
  path : JsonPointer,
) -> Result[Array[String], SchemaError] {
  guard value is Array(values) else {
    return Err(
      schema_error(InvalidMemberType, "expected an array of strings", path),
    )
  }
  let output : Array[String] = []
  for index, item in values {
    match item {
      String(text) => output.push(text)
      _ =>
        return Err(
          schema_error(
            InvalidMemberType,
            "enum value must be a string",
            path.index(index),
          ),
        )
    }
  }
  Ok(output)
}

///|
fn parse_schema_map(
  value : Json,
  path : JsonPointer,
  root : Bool,
) -> Result[Map[String, Schema], SchemaError] {
  guard value is Object(object) else {
    return Err(
      schema_error(InvalidMemberType, "expected an object of schemas", path),
    )
  }
  let output : Map[String, Schema] = Map([])
  for name, node in object {
    match parse_schema_node(node, path.property(name), root) {
      Ok(schema) => output.set(name, schema)
      Err(error) => return Err(error)
    }
  }
  Ok(output)
}

///|
fn parse_properties_form(
  object : Map[String, Json],
  path : JsonPointer,
) -> Result[SchemaForm, SchemaError] {
  let required = match object.get("properties") {
    None => Map([])
    Some(value) =>
      match parse_schema_map(value, path.property("properties"), false) {
        Ok(schemas) => schemas
        Err(error) => return Err(error)
      }
  }
  let optional = match object.get("optionalProperties") {
    None => Map([])
    Some(value) =>
      match
        parse_schema_map(value, path.property("optionalProperties"), false) {
        Ok(schemas) => schemas
        Err(error) => return Err(error)
      }
  }
  if !object.contains("properties") && !object.contains("optionalProperties") {
    return Err(
      schema_error(
        InvalidMemberType,
        "properties form requires properties or optionalProperties",
        path,
      ),
    )
  }
  let additional = match
    optional_boolean_member(object, "additionalProperties", false, path) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  Ok(PropertiesForm(required, optional, additional))
}

///|
fn parse_discriminator_form(
  object : Map[String, Json],
  path : JsonPointer,
) -> Result[SchemaForm, SchemaError] {
  let tag = match required_string_member(object, "discriminator", path) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  guard object.get("mapping") is Some(Object(mapping_nodes)) else {
    return Err(
      schema_error(
        InvalidMemberType,
        "discriminator member 'mapping' must be an object",
        path.property("mapping"),
      ),
    )
  }
  let mapping : Map[String, Schema] = Map([])
  for name, node in mapping_nodes {
    let branch_path = path.property("mapping").property(name)
    let branch = match parse_schema_node(node, branch_path, false) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    if branch.form_name() != "properties" {
      return Err(
        schema_error(
          InvalidDiscriminatorMapping,
          "each discriminator mapping value must use the properties form",
          branch_path,
        ),
      )
    }
    mapping.set(name, branch)
  }
  Ok(DiscriminatorForm(tag, mapping))
}

///|
fn parse_form(
  object : Map[String, Json],
  path : JsonPointer,
) -> Result[SchemaForm, SchemaError] {
  let forms = form_keyword_count(object)
  if forms > 1 {
    return Err(
      schema_error(
        MixedSchemaForms,
        "a JTD schema must use exactly one of the eight forms",
        path,
      ),
    )
  }
  if forms == 0 {
    return Ok(EmptyForm)
  }
  if object.contains("ref") {
    return match required_string_member(object, "ref", path) {
      Ok(name) => Ok(RefForm(name))
      Err(error) => Err(error)
    }
  }
  if object.contains("type") {
    let name = match required_string_member(object, "type", path) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    return match jtd_type_from_name(name) {
      Some(kind) => Ok(TypeForm(kind))
      None =>
        Err(
          schema_error(
            InvalidTypeName,
            "unknown JTD type '" + name + "'",
            path.property("type"),
          ),
        )
    }
  }
  if object.contains("enum") {
    guard object.get("enum") is Some(value) else { abort("unreachable") }
    return match parse_string_array(value, path.property("enum")) {
      Ok(values) => Ok(EnumForm(values))
      Err(error) => Err(error)
    }
  }
  if object.contains("elements") {
    guard object.get("elements") is Some(value) else { abort("unreachable") }
    return match parse_schema_node(value, path.property("elements"), false) {
      Ok(schema) => Ok(ElementsForm(schema))
      Err(error) => Err(error)
    }
  }
  if object.contains("properties") || object.contains("optionalProperties") {
    return parse_properties_form(object, path)
  }
  if object.contains("values") {
    guard object.get("values") is Some(value) else { abort("unreachable") }
    return match parse_schema_node(value, path.property("values"), false) {
      Ok(schema) => Ok(ValuesForm(schema))
      Err(error) => Err(error)
    }
  }
  parse_discriminator_form(object, path)
}

///|
fn parse_schema_node(
  value : Json,
  path : JsonPointer,
  root : Bool,
) -> Result[Schema, SchemaError] {
  let object = match expect_object(value, path) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  if !root && object.contains("definitions") {
    return Err(
      schema_error(
        NestedDefinitions,
        "definitions may appear only on the root schema",
        path.property("definitions"),
      ),
    )
  }
  let nullable = match
    optional_boolean_member(object, "nullable", false, path) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let metadata = match optional_metadata(object, path) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let form = match parse_form(object, path) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  match check_schema_keywords(object, form, path, root) {
    Ok(_) =>
      Ok(Schema::new(form).with_nullable(nullable).with_metadata(metadata))
    Err(error) => Err(error)
  }
}

///|
fn parse_root(value : Json) -> Result[SchemaDocument, SchemaError] {
  let root_path = JsonPointer::root()
  let object = match expect_object(value, root_path) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let root = match parse_schema_node(value, root_path, true) {
    Ok(schema) => schema
    Err(error) => return Err(error)
  }
  let definitions = match object.get("definitions") {
    None => Map([])
    Some(value) =>
      match parse_schema_map(value, root_path.property("definitions"), false) {
        Ok(schemas) => schemas
        Err(error) => return Err(error)
      }
  }
  Ok(SchemaDocument::new(root, definitions~))
}

///|
/// Parse a JSON value as a complete RFC 8927 schema document.
pub fn parse_schema_json(value : Json) -> Result[SchemaDocument, SchemaError] {
  parse_root(value)
}

///|
/// Parse UTF-8 JSON text as a complete RFC 8927 schema document.
pub fn parse_schema(text : StringView) -> Result[SchemaDocument, SchemaError] {
  let json = @json.parse(text) catch {
    error =>
      return Err(
        schema_error(
          InvalidJson,
          "invalid JSON: " + error.to_string(),
          JsonPointer::root(),
        ),
      )
  }
  parse_root(json)
}