///|
priv suberror DecodeError {
  DecodeError(@exact.Diagnostic)
} derive(Debug)

///|
fn object(
  value : @exact.Value,
  path : Array[String],
) -> Array[(String, @exact.Value)] raise DecodeError {
  match value.kind() {
    @exact.Object(fields) => fields
    _ =>
      raise DecodeError(
        @exact.Diagnostic::new(@exact.WrongType, "Expected an object", path~),
      )
  }
}

///|
fn field(
  value : @exact.Value,
  name : String,
  path : Array[String],
) -> @exact.Value raise DecodeError {
  match value.get(name) {
    Some(v) => v
    None =>
      raise DecodeError(
        @exact.Diagnostic::new(
          @exact.MissingField,
          "Missing required field: " + name,
          path~,
        ),
      )
  }
}

///|
fn text(
  value : @exact.Value,
  path : Array[String],
  nonempty~ : Bool = false,
) -> String raise DecodeError {
  match value.text() {
    Some(v) if !nonempty || !v.is_empty() => v
    _ =>
      raise DecodeError(
        @exact.Diagnostic::new(
          @exact.WrongType,
          "Expected a string" +
          (if nonempty { " with at least one character" } else { "" }),
          path~,
        ),
      )
  }
}

///|
fn array(
  value : @exact.Value,
  path : Array[String],
) -> Array[@exact.Value] raise DecodeError {
  match value.kind() {
    @exact.Array(values) => values
    _ =>
      raise DecodeError(
        @exact.Diagnostic::new(@exact.WrongType, "Expected an array", path~),
      )
  }
}

///|
fn child(path : Array[String], part : String) -> Array[String] {
  let out = path.copy()
  out.push(part)
  out
}

///|
fn reference(
  value : @exact.Value,
  path : Array[String],
  limits~ : @exact.Limits = @exact.Limits::new(),
) -> @reference.Reference? raise DecodeError {
  object(value, path) |> ignore
  match value.get("$ref") {
    None => None
    Some(raw) =>
      match
        @reference.Reference::parse(text(raw, child(path, "$ref")), limits~) {
        Ok(ref_) => Some(ref_)
        Err(d) =>
          raise DecodeError(
            @exact.Diagnostic::new(
              d.code(),
              d.message(),
              path=child(path, "$ref"),
            ),
          )
      }
  }
}

///|
fn descriptor(
  value : @exact.Value,
  path : Array[String],
  limits~ : @exact.Limits = @exact.Limits::new(),
) -> InlineOrReference[ContentDescriptor] raise DecodeError {
  if reference(value, path, limits~) is Some(ref_) {
    return Reference(ref_)
  }
  let name = text(
    field(value, "name", path),
    child(path, "name"),
    nonempty=true,
  )
  check_strings(value, path, ["summary", "description"])
  check_bools(value, path, ["deprecated"])
  let schema = match
    @schema.Schema::from_json(field(value, "schema", path), limits~) {
    Ok(schema) => schema
    Err(d) => {
      let schema_path = child(path, "schema")
      for part in d.path() {
        schema_path.push(part)
      }
      raise DecodeError(
        @exact.Diagnostic::new(d.code(), d.message(), path=schema_path),
      )
    }
  }
  let required = match value.get("required") {
    None => None
    Some(v) =>
      match v.kind() {
        @exact.Boolean(b) => Some(b)
        _ =>
          raise DecodeError(
            @exact.Diagnostic::new(
              @exact.WrongType,
              "Expected a required boolean",
              path=child(path, "required"),
            ),
          )
      }
  }
  Inline({ name, schema, required, value, })
}

///|
fn method_(
  value : @exact.Value,
  path : Array[String],
  limits~ : @exact.Limits = @exact.Limits::new(),
) -> InlineOrReference[Method] raise DecodeError {
  if reference(value, path, limits~) is Some(ref_) {
    return Reference(ref_)
  }
  let raw_name = text(
    field(value, "name", path),
    child(path, "name"),
    nonempty=true,
  )
  let name = MethodName::parse(raw_name).unwrap()
  let raw_params = array(field(value, "params", path), child(path, "params"))
  let params = raw_params.mapi((i, v) => {
    descriptor(v, child(child(path, "params"), i.to_string()), limits~)
  })
  let result = match value.get("result") {
    None => None
    Some(v) => Some(descriptor(v, child(path, "result"), limits~))
  }
  let param_structure = match value.get("paramStructure") {
    None => None
    Some(v) =>
      match v.text() {
        Some("by-name") => Some(ByName)
        Some("by-position") => Some(ByPosition)
        Some("either") => Some(Either)
        _ =>
          raise DecodeError(
            @exact.Diagnostic::new(
              @exact.WrongType,
              "Expected by-name, by-position or either",
              path=child(path, "paramStructure"),
            ),
          )
      }
  }
  check_strings(value, path, ["summary", "description"])
  check_bools(value, path, ["deprecated"])
  if value.get("externalDocs") is Some(v) {
    documentation_(v, child(path, "externalDocs"), limits) |> ignore
  }
  optional_array_(value, "servers", path, server_, limits~) |> ignore
  optional_array_(value, "tags", path, tag_ref, limits~) |> ignore
  optional_array_(value, "errors", path, error_ref, limits~) |> ignore
  optional_array_(value, "links", path, link_ref, limits~) |> ignore
  optional_array_(value, "examples", path, pairing_ref, limits~) |> ignore
  Inline({ name, params, result, param_structure, value, })
}

///|
pub fn Document::from_json(
  value : @exact.Value,
  limits~ : @exact.Limits = @exact.Limits::new(),
) -> Result[Document, Array[@exact.Diagnostic]] {
  match @exact.write(value, limits~) {
    Err(d) => return Err([d])
    Ok(_) => ()
  }
  try {
    object(value, []) |> ignore
    let version = match
      DocumentVersion::parse(text(field(value, "openrpc", []), ["openrpc"])) {
      Ok(v) => v
      Err(d) => raise DecodeError(d)
    }
    let raw_info = field(value, "info", [])
    let info = info_(raw_info, ["info"], limits)
    let methods = array(field(value, "methods", []), ["methods"]).mapi((i, v) => {
      method_(v, ["methods", i.to_string()], limits~)
    })
    let schema_uri = match value.get("$schema") {
      Some(v) => Some(uri_(v, ["$schema"], limits))
      None => None
    }
    let servers = optional_array_(value, "servers", [], server_, limits~)
    let components = match value.get("components") {
      Some(v) => Some(components_(v, ["components"], limits))
      None => None
    }
    let external_docs = match value.get("externalDocs") {
      Some(v) => Some(documentation_(v, ["externalDocs"], limits))
      None => None
    }
    Ok({
      version,
      info,
      methods,
      value,
      schema_uri,
      servers,
      components,
      external_docs,
    })
  } catch {
    DecodeError(d) => Err([d])
  }
}

///|
pub fn lower(cst : @exact.CstDocument) -> ReadResult {
  match cst.lower() {
    Err(diagnostics) => { cst: Some(cst), document: None, diagnostics, }
    Ok(value) =>
      match Document::from_json(value, limits=cst.limits()) {
        Ok(document) =>
          { cst: Some(cst), document: Some(document), diagnostics: [], }
        Err(diagnostics) =>
          {
            cst: Some(cst),
            document: None,
            diagnostics: diagnostics.map(d => {
              @exact.Diagnostic::new(
                d.code(),
                d.message(),
                path=d.path(),
                span=cst.span_at(d.path()),
                related=d.related(),
              )
            }),
          }
      }
  }
}

///|
pub fn read(
  source : String,
  source_id~ : String? = None,
  limits~ : @exact.Limits = @exact.Limits::new(),
) -> ReadResult {
  match @exact.parse(source, source_id~, limits~) {
    Ok(cst) => lower(cst)
    Err(d) => { cst: None, document: None, diagnostics: [d], }
  }
}

///|
/// Replace one existing value in a parsed document and read the result again.
/// Diagnostics use the new source map; node handles from `cst` become stale.
pub fn replace_value(
  cst : @exact.CstDocument,
  node : @exact.NodeId,
  value : @exact.Value,
  limits? : @exact.Limits,
) -> Result[ReadResult, @exact.Diagnostic] {
  match cst.replace_value(node, value, limits?) {
    Ok(edited) => Ok(lower(edited))
    Err(d) => Err(d)
  }
}