///|
/// Helpers for hand-rolled JSON codecs; the wire format uses camelCase keys
/// that differ from MoonBit field names.
fn put_str(obj : Map[String, Json], key : String, value : String) -> Unit {
  obj[key] = Json::string(value)
}

///|
fn put_opt_str(obj : Map[String, Json], key : String, value : String?) -> Unit {
  if value is Some(v) {
    obj[key] = Json::string(v)
  }
}

///|
fn put_opt_bool(obj : Map[String, Json], key : String, value : Bool?) -> Unit {
  if value is Some(v) {
    obj[key] = Json::boolean(v)
  }
}

///|
fn put_opt_int(obj : Map[String, Json], key : String, value : Int32?) -> Unit {
  if value is Some(v) {
    obj[key] = Json::number(v.to_double())
  }
}

///|
fn put_opt_double(
  obj : Map[String, Json],
  key : String,
  value : Double,
) -> Unit {
  obj[key] = Json::number(value)
}

///|
fn put_opt_json(obj : Map[String, Json], key : String, value : Json?) -> Unit {
  if value is Some(v) {
    obj[key] = v
  }
}

///|
fn put_opt_array(
  obj : Map[String, Json],
  key : String,
  value : Array[Json]?,
) -> Unit {
  if value is Some(v) {
    obj[key] = Json::array(v)
  }
}

///|
fn put_opt_map(
  obj : Map[String, Json],
  key : String,
  value : Map[String, Json]?,
) -> Unit {
  if value is Some(v) {
    obj[key] = Json(v)
  }
}

///|
fn decode_error(
  path : @json.JsonPath,
  message : String,
) -> @json.JsonDecodeError {
  @json.JsonDecodeError((path, message))
}

///|
fn expect_obj(
  json : Json,
  path : @json.JsonPath,
  context : String,
) -> Map[String, Json] raise @json.JsonDecodeError {
  guard json is Object(obj) else {
    raise decode_error(path, "\{context}: expected object")
  }
  obj
}

///|
fn req_str(
  obj : Map[String, Json],
  path : @json.JsonPath,
  context : String,
  key : String,
) -> String raise @json.JsonDecodeError {
  guard obj.get(key) is Some(String(v)) else {
    raise decode_error(path, "\{context}: missing string field \{key}")
  }
  v
}

///|
fn opt_str(
  obj : Map[String, Json],
  path : @json.JsonPath,
  key : String,
) -> String? raise @json.JsonDecodeError {
  match obj.get(key) {
    Some(String(v)) => Some(v)
    Some(Null) | None => None
    Some(_) => raise decode_error(path, "field \{key}: expected string")
  }
}

///|
fn opt_bool(
  obj : Map[String, Json],
  path : @json.JsonPath,
  key : String,
) -> Bool? raise @json.JsonDecodeError {
  match obj.get(key) {
    Some(True) => Some(true)
    Some(False) => Some(false)
    Some(Null) | None => None
    Some(_) => raise decode_error(path, "field \{key}: expected bool")
  }
}

///|
fn opt_int(
  obj : Map[String, Json],
  path : @json.JsonPath,
  key : String,
) -> Int32? raise @json.JsonDecodeError {
  match obj.get(key) {
    Some(Number(v, ..)) => Some(v.to_int())
    Some(Null) | None => None
    Some(_) => raise decode_error(path, "field \{key}: expected number")
  }
}

///|
fn opt_double(
  obj : Map[String, Json],
  path : @json.JsonPath,
  key : String,
) -> Double? raise @json.JsonDecodeError {
  match obj.get(key) {
    Some(Number(v, ..)) => Some(v)
    Some(Null) | None => None
    Some(_) => raise decode_error(path, "field \{key}: expected number")
  }
}

///|
fn opt_obj(
  obj : Map[String, Json],
  path : @json.JsonPath,
  key : String,
) -> Map[String, Json]? raise @json.JsonDecodeError {
  match obj.get(key) {
    Some(Object(v)) => Some(v)
    Some(Null) | None => None
    Some(_) => raise decode_error(path, "field \{key}: expected object")
  }
}

///|
fn opt_array(
  obj : Map[String, Json],
  path : @json.JsonPath,
  key : String,
) -> Array[Json]? raise @json.JsonDecodeError {
  match obj.get(key) {
    Some(Array(v)) => Some(v)
    Some(Null) | None => None
    Some(_) => raise decode_error(path, "field \{key}: expected array")
  }
}

///|
fn req_array(
  obj : Map[String, Json],
  path : @json.JsonPath,
  context : String,
  key : String,
) -> Array[Json] raise @json.JsonDecodeError {
  guard opt_array(obj, path, key) is Some(v) else {
    raise decode_error(path, "\{context}: missing array field \{key}")
  }
  v
}

///|
fn opt_str_array(
  obj : Map[String, Json],
  path : @json.JsonPath,
  key : String,
) -> Array[String]? raise @json.JsonDecodeError {
  match opt_array(obj, path, key) {
    Some(values) =>
      Some(
        values.filter_map(fn(v) { if v is String(s) { Some(s) } else { None } }),
      )
    None => None
  }
}

///|
fn progress_token_of(meta : Map[String, Json]?) -> String? {
  guard meta is Some(meta) else { return None }
  match meta.get("progressToken") {
    Some(String(token)) => Some(token)
    _ => None
  }
}

///|
fn put_opt_str_array(
  obj : Map[String, Json],
  key : String,
  value : Array[String]?,
) -> Unit {
  if value is Some(v) {
    obj[key] = Json::array(v.map(Json::string))
  }
}