// Take-and-remove field helpers -- what every generated decoder is built from.
//
// The discipline, ported from `slack/internal/jsonx` and unchanged because it
// is right: copy the object, TAKE the fields the type models -- which removes
// them -- and keep whatever is left in `extra`. Encoding writes the modelled
// fields back and then merges `extra`.
//
// What take-and-remove buys is the round-trip property. A decoder that instead
// listed its known keys and treated the rest as extra would silently drop any
// known key whose value had an unexpected type; here a field that fails to
// parse is simply never taken, so it stays in `extra` and comes back out
// unchanged. For atproto that is not a nicety -- Lexicons gain fields
// continuously, and a client that truncates records on read-modify-write
// destroys data it never knew about.
//
// The `take_*` family is for OPTIONAL fields and answers `None` for both
// "absent" and "present but not the right type". The `require_*` family is for
// fields the schema marks required and raises, with the path, for either.

///|
/// A working copy of an object's fields, for a decoder to consume.
pub fn fields_of(fields : Map[String, LexValue]) -> Map[String, LexValue] {
  let out : Map[String, LexValue] = Map([])
  for key, value in fields {
    out[key] = value
  }
  out
}

///|
/// The object's fields, or a decode error naming what arrived instead.
pub fn object_fields(
  value : LexValue,
  path? : String = "",
) -> Map[String, LexValue] raise DecodeError {
  guard value.as_object() is Some(fields) else {
    raise DecodeError(path~, reason="expected object, got \{value.kind()}")
  }
  fields_of(fields)
}

///|
pub fn take_lex(rest : Map[String, LexValue], key : String) -> LexValue? {
  match rest.get(key) {
    Some(value) => {
      rest.remove(key)
      Some(value)
    }
    None => None
  }
}

///|
pub fn take_string(rest : Map[String, LexValue], key : String) -> String? {
  match rest.get(key) {
    Some(Str(s)) => {
      rest.remove(key)
      Some(s)
    }
    _ => None
  }
}

///|
pub fn take_bool(rest : Map[String, LexValue], key : String) -> Bool? {
  match rest.get(key) {
    Some(Bool(b)) => {
      rest.remove(key)
      Some(b)
    }
    _ => None
  }
}

///|
pub fn take_int(rest : Map[String, LexValue], key : String) -> Int64? {
  match rest.get(key) {
    Some(Int(i)) => {
      rest.remove(key)
      Some(i)
    }
    _ => None
  }
}

///|
pub fn take_bytes(rest : Map[String, LexValue], key : String) -> Bytes? {
  match rest.get(key) {
    Some(Bytes(b)) => {
      rest.remove(key)
      Some(b)
    }
    _ => None
  }
}

///|
pub fn take_link(rest : Map[String, LexValue], key : String) -> @syntax.Cid? {
  match rest.get(key) {
    Some(Link(c)) => {
      rest.remove(key)
      Some(c)
    }
    _ => None
  }
}

///|
/// A string field whose Lexicon declares a `format`, parsed into the matching
/// type -- which is what stops a `Did` and a `Handle` from being the same thing
/// at 214 and 28 call sites respectively.
///
/// Absent gives `None`; present but malformed RAISES rather than answering
/// `None`. A format failure is a server sending something it should not, and
/// quietly dropping the field would turn that into a mysterious missing value.
pub fn[T] take_format(
  rest : Map[String, LexValue],
  key : String,
  path? : String = "",
  parse : (String) -> T raise @syntax.SyntaxError,
) -> T? raise DecodeError {
  guard take_string(rest, key) is Some(text) else { return None }
  Some(parse(text)) catch {
    e => raise DecodeError(path=field_path(path, key), reason=e.reason())
  }
}

///|
/// A nested object, decoded by `parse`. Absent gives `None`; present and
/// malformed raises, because a sub-object that fails to decode is a schema
/// mismatch rather than an absent value.
pub fn[T] take_object(
  rest : Map[String, LexValue],
  key : String,
  path? : String = "",
  parse : (LexValue, String) -> T raise DecodeError,
) -> T? raise DecodeError {
  guard take_lex(rest, key) is Some(value) else { return None }
  Some(parse(value, field_path(path, key)))
}

///|
/// An array, every element decoded by `parse`.
///
/// All-or-nothing on purpose: a half-decoded list would round-trip as a shorter
/// list, which is silent data loss.
pub fn[T] take_array(
  rest : Map[String, LexValue],
  key : String,
  path? : String = "",
  parse : (LexValue, String) -> T raise DecodeError,
) -> Array[T]? raise DecodeError {
  guard take_lex(rest, key) is Some(value) else { return None }
  let here = field_path(path, key)
  guard value.as_array() is Some(items) else {
    raise DecodeError(path=here, reason="expected array, got \{value.kind()}")
  }
  let out = Array::new(capacity=items.length())
  for i, item in items {
    out.push(parse(item, index_path(here, i)))
  }
  Some(out)
}

///|
pub fn take_string_array(
  rest : Map[String, LexValue],
  key : String,
  path? : String = "",
) -> Array[String]? raise DecodeError {
  take_array(rest, key, path~, (value, item_path) => {
    match value.as_string() {
      Some(s) => s
      None =>
        raise DecodeError(
          path=item_path,
          reason="expected string, got \{value.kind()}",
        )
    }
  })
}

///|
pub fn require_string(
  rest : Map[String, LexValue],
  key : String,
  path? : String = "",
) -> String raise DecodeError {
  match take_string(rest, key) {
    Some(s) => s
    None => raise missing(rest, key, path, "string")
  }
}

///|
pub fn require_int(
  rest : Map[String, LexValue],
  key : String,
  path? : String = "",
) -> Int64 raise DecodeError {
  match take_int(rest, key) {
    Some(i) => i
    None => raise missing(rest, key, path, "integer")
  }
}

///|
pub fn require_bool(
  rest : Map[String, LexValue],
  key : String,
  path? : String = "",
) -> Bool raise DecodeError {
  match take_bool(rest, key) {
    Some(b) => b
    None => raise missing(rest, key, path, "boolean")
  }
}

///|
pub fn require_bytes(
  rest : Map[String, LexValue],
  key : String,
  path? : String = "",
) -> Bytes raise DecodeError {
  match take_bytes(rest, key) {
    Some(b) => b
    None => raise missing(rest, key, path, "bytes")
  }
}

///|
pub fn require_link(
  rest : Map[String, LexValue],
  key : String,
  path? : String = "",
) -> @syntax.Cid raise DecodeError {
  match take_link(rest, key) {
    Some(c) => c
    None => raise missing(rest, key, path, "cid-link")
  }
}

///|
pub fn require_lex(
  rest : Map[String, LexValue],
  key : String,
  path? : String = "",
) -> LexValue raise DecodeError {
  match take_lex(rest, key) {
    Some(value) => value
    None => raise missing(rest, key, path, "value")
  }
}

///|
pub fn[T] require_format(
  rest : Map[String, LexValue],
  key : String,
  path? : String = "",
  parse : (String) -> T raise @syntax.SyntaxError,
) -> T raise DecodeError {
  match take_format(rest, key, path~, parse) {
    Some(value) => value
    None => raise missing(rest, key, path, "string")
  }
}

///|
pub fn[T] require_object(
  rest : Map[String, LexValue],
  key : String,
  path? : String = "",
  parse : (LexValue, String) -> T raise DecodeError,
) -> T raise DecodeError {
  match take_object(rest, key, path~, parse) {
    Some(value) => value
    None => raise missing(rest, key, path, "object")
  }
}

///|
pub fn[T] require_array(
  rest : Map[String, LexValue],
  key : String,
  path? : String = "",
  parse : (LexValue, String) -> T raise DecodeError,
) -> Array[T] raise DecodeError {
  match take_array(rest, key, path~, parse) {
    Some(value) => value
    None => raise missing(rest, key, path, "array")
  }
}

///|
/// Distinguishes "not there" from "there, wrong type" in the message. The field
/// is still in `rest` when the type was wrong, because `take_*` only removes
/// what it accepts -- which is what makes the distinction available here.
fn missing(
  rest : Map[String, LexValue],
  key : String,
  path : String,
  expected : String,
) -> DecodeError {
  let here = field_path(path, key)
  match rest.get(key) {
    Some(value) =>
      DecodeError(path=here, reason="expected \{expected}, got \{value.kind()}")
    None => DecodeError(path=here, reason="missing required field")
  }
}

// Encoding. `put_*` writes only what is present -- an absent field is OMITTED,
// never written as null. For a record that is not a style choice: an extra
// `"field": null` changes the DAG-CBOR bytes and therefore the record's CID.

///|
pub fn put_lex(
  out : Map[String, LexValue],
  key : String,
  value : LexValue?,
) -> Unit {
  if value is Some(v) {
    out[key] = v
  }
}

///|
pub fn put_string(
  out : Map[String, LexValue],
  key : String,
  value : String?,
) -> Unit {
  if value is Some(v) {
    out[key] = Str(v)
  }
}

///|
pub fn put_bool(
  out : Map[String, LexValue],
  key : String,
  value : Bool?,
) -> Unit {
  if value is Some(v) {
    out[key] = Bool(v)
  }
}

///|
pub fn put_int(
  out : Map[String, LexValue],
  key : String,
  value : Int64?,
) -> Unit {
  if value is Some(v) {
    out[key] = Int(v)
  }
}

///|
pub fn[T] put_object(
  out : Map[String, LexValue],
  key : String,
  value : T?,
  encode : (T) -> LexValue,
) -> Unit {
  if value is Some(v) {
    out[key] = encode(v)
  }
}

///|
pub fn[T] put_array(
  out : Map[String, LexValue],
  key : String,
  value : Array[T]?,
  encode : (T) -> LexValue,
) -> Unit {
  if value is Some(items) {
    out[key] = Arr(items.map(encode))
  }
}

///|
pub fn put_string_array(
  out : Map[String, LexValue],
  key : String,
  value : Array[String]?,
) -> Unit {
  put_array(out, key, value, s => Str(s))
}

///|
/// Writes back the fields the type did not model, and returns the finished
/// object. Modelled fields win a collision -- `extra` should not contain one,
/// and if it does the typed value is the one the caller set.
pub fn merge_extra(
  out : Map[String, LexValue],
  extra : Map[String, LexValue],
) -> LexValue {
  for key, value in extra {
    if !out.contains(key) {
      out[key] = value
    }
  }
  Obj(out)
}