///|
fn kv3_binary_pack(value : KV3BinaryValue, depth : Int32) -> Json raise {
  if depth > 128 {
    raise InvalidBinary(0, "Binary KV3 JSON exceeds depth")
  }
  match value {
    BNull => Json::object({ "type": Json::string("null") })
    BBool(b) =>
      Json::object({ "type": Json::string("bool"), "value": Json::boolean(b) })
    BNumber(tag, bits) =>
      Json::object({
        "type": Json::string("number"),
        "storage": Json::number(tag.to_double()),
        "bits": Json::string(bits.to_string()),
      })
    BString(s) =>
      Json::object({ "type": Json::string("string"), "value": Json::string(s) })
    BBlob(bytes) => {
      let hex = "0123456789abcdef".to_array()
      let out = StringBuilder()
      for byte in bytes {
        let n = byte.to_int()
        out.write_char(hex[n / 16])
        out.write_char(hex[n % 16])
      }
      Json::object({
        "type": Json::string("blob"),
        "value": Json::string(out.to_string()),
      })
    }
    BArray(values) =>
      Json::object({
        "type": Json::string("array"),
        "value": Json::array(values.map(v => kv3_binary_pack(v, depth + 1))),
      })
    BObject(entries) =>
      Json::object({
        "type": Json::string("object"),
        "value": Json::array(
          entries.map(pair => {
            let (key, value) = pair
            Json::object({
              "key": Json::string(key),
              "value": kv3_binary_pack(value, depth + 1),
            })
          }),
        ),
      })
    BFlag(flag, value) =>
      Json::object({
        "type": Json::string("flag"),
        "flag": Json::string(flag),
        "value": kv3_binary_pack(value, depth + 1),
      })
  }
}

///|
pub fn kv3_binary_to_json(document : KV3BinaryDocument) -> Json raise {
  document.document() |> ignore
  Json::object({
    "version": Json::number(document.version.to_double()),
    "compression": Json::number(document.compression.to_double()),
    "format_id": Json::string(document.format_id),
    "root": kv3_binary_pack(document.root, 0),
  })
}

///|
fn kv3_binary_unpack(
  json : Json,
  depth : Int32,
  budget : Ref[Int32],
) -> KV3BinaryValue raise {
  budget.val += 1
  if depth > 128 || budget.val > 100000 {
    raise InvalidBinary(0, "Binary KV3 JSON exceeds budget")
  }
  guard json is Object(fields) && fields.get("type") is Some(String(kind)) else {
    raise InvalidBinary(0, "Expected typed binary KV3 JSON")
  }
  match kind {
    "null" if fields.length() == 1 => BNull
    "bool" if fields.length() == 2 && fields.get("value") is Some(True) =>
      BBool(true)
    "bool" if fields.length() == 2 && fields.get("value") is Some(False) =>
      BBool(false)
    "number" if fields.length() == 3 &&
      fields.get("storage") is Some(Number(n, ..)) &&
      n >= 0.0 &&
      n <= 27.0 &&
      n == n.to_int().to_double() &&
      fields.get("bits") is Some(String(bits)) =>
      BNumber(n.to_int(), @strconv.parse_uint64(bits[:]))
    "string" if fields.length() == 2 && fields.get("value") is Some(String(s)) =>
      BString(s)
    "blob" if fields.length() == 2 && fields.get("value") is Some(String(s)) => {
      let chars = s.to_array()
      if chars.length() % 2 != 0 || chars.length() > 33554432 {
        raise InvalidBinary(0, "Invalid blob hex length")
      }
      let out = @buffer.Buffer()
      for i in 0..<(chars.length() / 2) {
        guard kv3_hex(chars[i * 2]) is Some(a) &&
          kv3_hex(chars[i * 2 + 1]) is Some(b) else {
          raise InvalidBinary(0, "Invalid blob hexadecimal")
        }
        out.write_byte((a * 16 + b).to_byte())
      }
      BBlob(out.to_bytes())
    }
    "array" if fields.length() == 2 &&
      fields.get("value") is Some(Array(values)) =>
      BArray(values.map(v => kv3_binary_unpack(v, depth + 1, budget)))
    "object" if fields.length() == 2 &&
      fields.get("value") is Some(Array(entries)) =>
      BObject(
        entries.map(entry => {
          guard entry is Object(row) &&
            row.length() == 2 &&
            row.get("key") is Some(String(key)) &&
            row.get("value") is Some(value) else {
            raise InvalidBinary(0, "Invalid binary KV3 object member")
          }
          (key, kv3_binary_unpack(value, depth + 1, budget))
        }),
      )
    "flag" if fields.length() == 3 &&
      fields.get("flag") is Some(String(flag)) &&
      fields.get("value") is Some(value) =>
      BFlag(flag, kv3_binary_unpack(value, depth + 1, budget))
    _ => raise InvalidBinary(0, "Invalid binary KV3 JSON fields")
  }
}

///|
pub fn kv3_binary_from_json(json : Json) -> KV3BinaryDocument raise {
  guard json is Object(fields) &&
    fields.length() == 4 &&
    fields.get("version") is Some(Number(v, ..)) &&
    fields.get("compression") is Some(Number(c, ..)) &&
    fields.get("format_id") is Some(String(format_id)) &&
    fields.get("root") is Some(root) &&
    v >= 0.0 &&
    v <= 5.0 &&
    v == v.to_int().to_double() &&
    c >= 0.0 &&
    c <= 3.0 &&
    c == c.to_int().to_double() &&
    (c <= 2.0 || v == 0.0) else {
    raise InvalidBinary(0, "Invalid binary KV3 document JSON")
  }
  let result = KV3BinaryDocument::{
    version: v.to_int(),
    compression: c.to_int(),
    format_id,
    root: kv3_binary_unpack(root, 0, { val: 0, }),
  }
  result.document() |> ignore
  result
}