///|
pub(all) struct DataObject {
  path : ObjectPath
  properties : Array[Property]
  mut kind : DataType?
  values : Array[Value]
} derive(Eq, Debug)

///|
pub(all) struct Document {
  objects : Array[DataObject]
} derive(Eq, Debug)

///|
pub fn Document::new() -> Document {
  { objects: [], }
}

///|
pub fn DataObject::new(
  path : ObjectPath,
  kind? : DataType,
  values~ : Array[Value] = [],
  properties~ : Array[Property] = [],
) -> DataObject {
  { path, kind, values, properties, }
}

///|
pub fn Document::find(self : Document, path : ObjectPath) -> DataObject? {
  for o in self.objects {
    if o.path == path {
      return Some(o)
    }
  }
  None
}

///|
pub fn DataObject::property(self : DataObject, name : String) -> Value? {
  for p in self.properties {
    if p.name == name {
      return Some(p.value)
    }
  }
  None
}

///|
fn set_property(obj : DataObject, p : Property) -> Unit {
  for i = 0; i < obj.properties.length(); i = i + 1 {
    if obj.properties[i].name == p.name {
      obj.properties[i] = p
      return
    }
  }
  obj.properties.push(p)
}

///|
fn Document::validate(self : Document, limits : Limits) -> Unit raise TdmsError {
  limits.validate()
  if self.objects.length() > limits.max_objects {
    raise Budget("objects")
  }
  let paths : Map[String, Bool] = Map([])
  let mut properties = 0
  let mut samples = 0
  for obj in self.objects {
    let path = obj.path.text()
    bounded_text(path, limits)
    demand(!paths.contains(path), "duplicate object path")
    paths[path] = true
    demand(
      obj.path.parts.length() == 2 || obj.kind is None,
      "only channels may have data types",
    )
    demand(
      obj.kind is Some(_) || obj.values.is_empty(),
      "untyped channel samples",
    )
    let names : Map[String, Bool] = Map([])
    for p in obj.properties {
      demand(
        !p.name.is_empty() && !names.contains(p.name),
        "empty or duplicate property name",
      )
      bounded_text(p.name, limits)
      bounded_value(p.value, limits)
      names[p.name] = true
      if p.name == "name" {
        raise Unsupported("TDMS name-based object renaming")
      }
      p.value.validate(p.kind)
      if properties >= limits.max_properties {
        raise Budget("properties")
      }
      properties += 1
    }
    if obj.values.length() > limits.max_samples - samples {
      raise Budget("samples")
    }
    samples += obj.values.length()
    if obj.kind is Some(kind) {
      for v in obj.values {
        v.validate(kind)
        bounded_value(v, limits)
      }
    }
  }
}

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

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

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

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

///|
fn bounded_text(text : String, limits : Limits) -> Unit raise TdmsError {
  if @utf8.encode(text).length() > limits.max_string_bytes {
    raise Budget("string bytes")
  }
}

///|
fn bounded_value(value : Value, limits : Limits) -> Unit raise TdmsError {
  if value is Textual(text) {
    bounded_text(text, limits)
  }
}