///|
/// DMX is a UUID-addressed graph, independently typed from KV1 and KV3.
/// Time uses 1/10000-second ticks; floating point components retain IEEE bits.
pub(all) enum DMXValue {
  DElement(String?)
  DInt(Int32)
  DFloatBits(UInt32)
  DBool(Bool)
  DString(String)
  DBinary(Bytes)
  DTime(Int32)
  DColor(UInt32)
  DVector(Int32, Array[UInt32])
  DArray(Int32, Array[DMXValue])
} derive(Eq, Debug)

///|
pub(all) struct DMXAttribute {
  name : String
  value : DMXValue
} derive(Eq, Debug)

///|
pub(all) struct DMXElement {
  id : String
  name : String
  kind : String
  attributes : Array[DMXAttribute]
} derive(Eq, Debug)

///|
pub(all) struct DMXDocument {
  format_name : String
  format_version : Int32
  root : String
  elements : Array[DMXElement]
} derive(Eq, Debug)

///|
pub extend DMXValue with Eq::{equal, not_equal}

///|
pub extend DMXValue with @debug.Debug::{to_repr}

///|
pub extend DMXAttribute with Eq::{equal, not_equal}

///|
pub extend DMXAttribute with @debug.Debug::{to_repr}

///|
pub extend DMXElement with Eq::{equal, not_equal}

///|
pub extend DMXElement with @debug.Debug::{to_repr}

///|
pub extend DMXDocument with Eq::{equal, not_equal}

///|
pub extend DMXDocument with @debug.Debug::{to_repr}

///|
fn dmx_uuid(text : String) -> String raise {
  kv3_bytes_guid(kv3_guid_bytes(text.to_lower()))
}

///|
fn dmx_types() -> Array[String] {
  [
    "", "element", "int", "float", "bool", "string", "binary", "time", "color", "vector2",
    "vector3", "vector4", "qangle", "quaternion", "vmatrix",
  ]
}

///|
fn dmx_type(value : DMXValue) -> Int32 {
  match value {
    DElement(_) => 1
    DInt(_) => 2
    DFloatBits(_) => 3
    DBool(_) => 4
    DString(_) => 5
    DBinary(_) => 6
    DTime(_) => 7
    DColor(_) => 8
    DVector(tag, _) | DArray(tag, _) => tag
  }
}

///|
fn dmx_string_check(
  text : String,
  options : BinaryOptions,
  total : Ref[Int32],
) -> Unit raise {
  let size = @utf8.encode(text[:]).length()
  if text.contains("\u0000") ||
    size > options.max_string_bytes ||
    size > options.max_bytes - total.val {
    raise InvalidBinary(0, "DMX string exceeds budget or contains NUL")
  }
  total.val += size
}

///|
fn dmx_value_check(
  value : DMXValue,
  options : BinaryOptions,
  total : Ref[Int32],
  nodes : Ref[Int32],
  array : Bool,
) -> Unit raise {
  nodes.val += 1
  if nodes.val > options.max_entries {
    raise InvalidBinary(0, "DMX attribute/value budget exceeded")
  }
  match value {
    DElement(Some(id)) =>
      if dmx_uuid(id) != id {
        raise InvalidBinary(0, "DMX UUIDs must be canonical lowercase")
      }
    DString(s) => dmx_string_check(s, options, total)
    DBinary(bytes) => {
      if bytes.length() > options.max_bytes - total.val {
        raise InvalidBinary(0, "DMX blob budget exceeded")
      }
      total.val += bytes.length()
    }
    DVector(tag, bits) => {
      if tag < 9 || tag > 14 || bits.length() != [2, 3, 4, 3, 4, 16][tag - 9] {
        raise InvalidBinary(0, "DMX vector type or dimension mismatch")
      }
      total.val += bits.length() * 4
    }
    DArray(tag, values) => {
      if array ||
        tag < 1 ||
        tag > 14 ||
        values.length() > options.max_entries - nodes.val {
        raise InvalidBinary(0, "Invalid DMX typed array")
      }
      for child in values {
        if child is DArray(_, _) || dmx_type(child) != tag {
          raise InvalidBinary(0, "DMX array element type mismatch")
        }
        dmx_value_check(child, options, total, nodes, true)
      }
    }
    _ => total.val += 4
  }
  if total.val > options.max_bytes {
    raise InvalidBinary(0, "DMX aggregate bytes exceeded")
  }
}

///|
fn dmx_validate(document : DMXDocument, options : BinaryOptions) -> Unit raise {
  check_binary_options(options)
  if document.format_name.is_empty() ||
    document.format_name.length() > 256 ||
    document.format_name
    .to_array()
    .any(c => {
      !((c >= 'a' && c <= 'z') ||
      (c >= 'A' && c <= 'Z') ||
      localization_digit(c) ||
      ['_', '-', '.'].contains(c))
    }) ||
    document.format_version < 0 ||
    document.elements.is_empty() ||
    document.elements.length() > options.max_entries {
    raise InvalidBinary(0, "Invalid DMX document metadata")
  }
  let ids : Map[String, Bool] = Map([])
  let total : Ref[Int32] = { val: 0, }
  let nodes : Ref[Int32] = { val: 0, }
  for element in document.elements {
    if dmx_uuid(element.id) != element.id ||
      ids.contains(element.id) ||
      element.kind.is_empty() {
      raise InvalidBinary(0, "Duplicate/invalid DMX element identity")
    }
    ids[element.id] = true
    dmx_string_check(element.name, options, total)
    dmx_string_check(element.kind, options, total)
    let names : Map[String, Bool] = Map([])
    for attribute in element.attributes {
      let name = attribute.name.to_lower()
      if name.is_empty() || name == "name" || names.contains(name) {
        raise InvalidBinary(0, "Duplicate/reserved DMX attribute name")
      }
      names[name] = true
      dmx_string_check(attribute.name, options, total)
      dmx_value_check(attribute.value, options, total, nodes, false)
    }
  }
  if !ids.contains(document.root) {
    raise InvalidBinary(0, "DMX root UUID is not defined")
  }
}

///|
pub fn DMXDocument::element(self : DMXDocument, id : String) -> DMXElement? {
  for element in self.elements {
    if element.id == id.to_lower() {
      return Some(element)
    }
  }
  None
}

///|
fn dmx_references(value : DMXValue, ids : Map[String, Bool]) -> Unit {
  match value {
    DElement(Some(id)) => ids[id] = true
    DArray(_, values) =>
      for child in values {
        dmx_references(child, ids)
      }
    _ => ()
  }
}

///|
pub fn DMXDocument::external_references(
  self : DMXDocument,
) -> Array[String] raise {
  dmx_validate(self, BinaryOptions::default())
  let ids : Map[String, Bool] = Map([])
  for element in self.elements {
    for attribute in element.attributes {
      dmx_references(attribute.value, ids)
    }
  }
  for element in self.elements {
    ids.remove(element.id) |> ignore
  }
  let result = ids.keys().collect()
  result.sort_by((a, b) => a.lexical_compare(b))
  result
}

///|
pub fn DMXDocument::with_attribute(
  self : DMXDocument,
  id : String,
  name : String,
  value : DMXValue,
) -> DMXDocument raise {
  dmx_validate(self, BinaryOptions::default())
  let id = dmx_uuid(id)
  if self.element(id) is None {
    raise InvalidEdit("DMX element UUID was not found")
  }
  let elements = self.elements.map(element => {
    if element.id != id {
      return element
    }
    let attributes = element.attributes.copy()
    let mut found = false
    for i in 0..