///|
fn dmx_pack(value : DMXValue) -> Json {
  let kind = dmx_types()[dmx_type(value)]
  let payload = match value {
    DElement(id) =>
      match id {
        Some(s) => Json::string(s)
        None => Json::null()
      }
    DInt(n) | DTime(n) => Json::number(n.to_double())
    DFloatBits(n) | DColor(n) => Json::string(n.to_string())
    DBool(b) => Json::boolean(b)
    DString(s) => Json::string(s)
    DBinary(bytes) =>
      kv3_pack(KV3Blob(bytes))
      |> json => {
        match json {
          Object(fields) => fields["value"]
          _ => Json::null()
        }
      }
    DVector(_, bits) => Json::array(bits.map(n => Json::string(n.to_string())))
    DArray(_, values) => Json::array(values.map(dmx_pack))
  }
  Json::object({
    "type": Json::string(
      kind + (if value is DArray(_, _) { "_array" } else { "" }),
    ),
    "value": payload,
  })
}

///|
pub fn dmx_to_json(document : DMXDocument) -> Json raise {
  dmx_validate(document, BinaryOptions::default())
  Json::object({
    "format_name": Json::string(document.format_name),
    "format_version": Json::number(document.format_version.to_double()),
    "root": Json::string(document.root),
    "elements": Json::array(
      document.elements.map(e => {
        Json::object({
          "id": Json::string(e.id),
          "name": Json::string(e.name),
          "type": Json::string(e.kind),
          "attributes": Json::array(
            e.attributes.map(a => {
              Json::object({
                "name": Json::string(a.name),
                "value": dmx_pack(a.value),
              })
            }),
          ),
        })
      }),
    ),
  })
}

///|
fn dmx_json_string(fields : Map[String, Json], key : String) -> String raise {
  guard fields.get(key) is Some(String(s)) else {
    raise InvalidDocument("Expected DMX JSON string: " + key)
  }
  s
}

///|
fn dmx_unpack(
  json : Json,
  in_array : Bool,
  budget : Ref[Int32],
) -> DMXValue raise {
  budget.val += 1
  if budget.val > 100000 {
    raise InvalidDocument("DMX JSON value budget exceeded")
  }
  guard json is Object(fields) &&
    fields.length() == 2 &&
    fields.get("value") is Some(payload) else {
    raise InvalidDocument("Invalid DMX typed JSON")
  }
  let kind = dmx_json_string(fields, "type")
  let array = kind.has_suffix("_array")
  let base = if array { kind[:kind.length() - 6].to_owned() } else { kind }
  let mut tag = 0
  let types = dmx_types()
  for i in 1.. dmx_unpack(v, true, budget)))
  }
  match (tag, payload) {
    (1, Null) => DElement(None)
    (1, String(id)) => DElement(Some(dmx_uuid(id)))
    (2 | 7, Number(n, ..)) if n >= -2147483648.0 &&
      n <= 2147483647.0 &&
      n == n.to_int().to_double() =>
      if tag == 2 {
        DInt(n.to_int())
      } else {
        DTime(n.to_int())
      }
    (3 | 8, String(bits)) => {
      let n = @strconv.parse_uint(bits[:])
      if tag == 3 {
        DFloatBits(n)
      } else {
        DColor(n)
      }
    }
    (4, True) => DBool(true)
    (4, False) => DBool(false)
    (5, String(s)) => DString(s)
    (6, String(hex)) => dmx_scalar_text(6, hex)
    (9..<=14, Array(bits)) =>
      DVector(
        tag,
        bits.map(v => {
          guard v is String(s) else {
            raise InvalidDocument("DMX vector bits must be decimal strings")
          }
          @strconv.parse_uint(s[:])
        }),
      )
    _ => raise InvalidDocument("DMX JSON payload does not match its type")
  }
}

///|
pub fn dmx_from_json(json : Json) -> DMXDocument raise {
  guard json is Object(fields) &&
    fields.length() == 4 &&
    fields.get("format_version") is Some(Number(version, ..)) &&
    version >= 0.0 &&
    version <= 2147483647.0 &&
    version == version.to_int().to_double() &&
    fields.get("elements") is Some(Array(elements)) &&
    elements.length() <= 100000 else {
    raise InvalidDocument("Invalid DMX JSON document")
  }
  let budget : Ref[Int32] = { val: 0, }
  let document = DMXDocument::{
    format_name: dmx_json_string(fields, "format_name"),
    format_version: version.to_int(),
    root: dmx_json_string(fields, "root"),
    elements: elements.map(element => {
      guard element is Object(row) &&
        row.length() == 4 &&
        row.get("attributes") is Some(Array(attributes)) &&
        attributes.length() <= 100000 else {
        raise InvalidDocument("Invalid DMX JSON element")
      }
      DMXElement::{
        id: dmx_json_string(row, "id"),
        name: dmx_json_string(row, "name"),
        kind: dmx_json_string(row, "type"),
        attributes: attributes.map(attribute => {
          guard attribute is Object(a) &&
            a.length() == 2 &&
            a.get("value") is Some(value) else {
            raise InvalidDocument("Invalid DMX JSON attribute")
          }
          DMXAttribute::{
            name: dmx_json_string(a, "name"),
            value: dmx_unpack(value, false, budget),
          }
        }),
      }
    }),
  }
  dmx_validate(document, BinaryOptions::default())
  document
}