///|
priv struct RawIndex {
  kind : DataType
  count : Int32
  size : Int32
}

///|
priv struct ReadState {
  doc : Document
  by_path : Map[String, Int32]
  cache : Array[RawIndex?]
  enabled : Array[Bool]
  endian : Array[Bool?]
  active : Array[Int32]
  mut property_records : Int32
  mut samples : Int32
}

///|
fn ReadState::new() -> ReadState {
  {
    doc: Document::new(),
    by_path: Map([]),
    cache: [],
    enabled: [],
    endian: [],
    active: [],
    property_records: 0,
    samples: 0,
  }
}

///|
fn ReadState::object(
  self : ReadState,
  path : ObjectPath,
  limits : Limits,
) -> Int32 raise TdmsError {
  let key = path.text()
  if self.by_path.get(key) is Some(i) {
    return i
  }
  if self.doc.objects.length() >= limits.max_objects {
    raise Budget("objects")
  }
  let i = self.doc.objects.length()
  self.doc.objects.push(DataObject::new(path))
  self.by_path[key] = i
  self.cache.push(None)
  self.enabled.push(false)
  self.endian.push(None)
  i
}

///|
fn read_index(r : Reader, length : UInt32) -> RawIndex raise TdmsError {
  if length == 0x1269U || length == 0x126aU {
    raise Unsupported("DAQmx raw index")
  }
  let kind = data_type(r.u32())
  demand(r.u32() == 1U, "TDMS array dimension must be one")
  let count = r.bounded(8, r.limits.max_samples, "chunk samples")
  let size = match kind.width() {
    Some(width) => {
      demand(length == 20U, "invalid fixed raw index length")
      checked_product(count, width, r.limits.max_bytes)
    }
    None => {
      // npTDMS 1.10.0 emits marker 20 for strings but includes the size field.
      demand(length == 28U || length == 20U, "invalid string raw index length")
      let n = r.bounded(8, r.limits.max_bytes, "string chunk bytes")
      let offsets = checked_product(count, 4, r.limits.max_bytes)
      demand(n >= offsets, "string chunk smaller than offsets")
      n
    }
  }
  { kind, count, size, }
}

///|
fn read_metadata(
  bytes : Bytes,
  seg : SegmentInfo,
  state : ReadState,
  limits : Limits,
) -> Unit raise TdmsError {
  if !flag(seg.toc, 2U) {
    return
  }
  let r = Reader::new(
    bytes,
    seg.offset + 28,
    seg.raw_offset,
    seg.big_endian,
    limits,
  )
  let count = r.bounded(4, limits.max_objects, "metadata objects")
  let new_list = flag(seg.toc, 4U)
  if new_list {
    state.active.clear()
  }
  let seen : Map[Int32, Bool] = Map([])
  for record = 0; record < count; record = record + 1 {
    let path = parse_path(r.string())
    let i = state.object(path, limits)
    demand(!seen.contains(i), "object repeated in segment metadata")
    seen[i] = true
    if new_list || !state.active.contains(i) {
      state.active.push(i)
    }
    let marker = r.u32()
    if marker == 0xffffffffU {
      state.enabled[i] = false
    } else {
      let idx = if marker == 0U {
        match state.cache[i] {
          Some(previous) => previous
          None => raise Invalid("raw index reuse without previous index")
        }
      } else {
        read_index(r, marker)
      }
      demand(path.parts.length() == 2, "raw data on non-channel object")
      let obj = state.doc.objects[i]
      demand(
        obj.kind is None || obj.kind == Some(idx.kind),
        "channel data type changed",
      )
      obj.kind = Some(idx.kind)
      state.cache[i] = Some(idx)
      state.enabled[i] = true
    }
    let n = r.bounded(4, limits.max_properties, "properties per object")
    if n > limits.max_properties - state.property_records {
      raise Budget("property records")
    }
    state.property_records += n
    let names : Map[String, Bool] = Map([])
    for prop = 0; prop < n; prop = prop + 1 {
      let p = read_property(r)
      demand(!names.contains(p.name), "duplicate property in metadata record")
      names[p.name] = true
      if p.name == "name" {
        raise Unsupported("TDMS name-based object renaming")
      }
      set_property(state.doc.objects[i], p)
    }
  }
  demand(r.pos == r.end, "unconsumed metadata bytes")
}