///|
pub(all) struct TraceIndex {
  header_offset : Int
  sample_offset : Int
  samples : Int
  interval_us : Double
} derive(ToJson)

///|
pub extend TraceIndex with ToJson::{to_json}

///|
pub struct Dataset {
  header : FileHeader
  priv raw : Bytes
  priv index : Array[TraceIndex]
  priv extensions : Array[Bytes]
}

///|
fn extended_text(data : Bytes, encoding : String) -> String raise {
  let text = decode_text(data, encoding)
  let compact = StringBuilder()
  for c in text {
    if !c.is_whitespace() {
      compact.write_char(c)
    }
  }
  let key = compact.to_string().to_upper()
  if key.contains("TRACEHEADERMAPPING") ||
    key.contains("TRACEHEADERLAYOUT") ||
    key.contains("SEG:USERDATA") {
    raise Failure("custom trace mapping or binary user stanza is unsupported")
  }
  text
}

///|
/// Scan fixed/variable traces with strict bounds; does not expand all samples.
/// Undefined extended-text counts stop only at the SEG EndText marker.
pub fn decode(
  data : Bytes,
  endian? : Endian,
  text_encoding? : String,
  legacy_revision_one? : Bool = false,
) -> Dataset raise {
  let h = match (endian, text_encoding) {
    (Some(e), Some(t)) =>
      parse_header(data, endian=e, text_encoding=t, legacy_revision_one~)
    (Some(e), None) => parse_header(data, endian=e, legacy_revision_one~)
    (None, Some(t)) => parse_header(data, text_encoding=t, legacy_revision_one~)
    (None, None) => parse_header(data, legacy_revision_one~)
  }
  let e = h.endian
  let extensions = []
  let mut pos = 3600
  let mut count = h.extended_count
  if h.first_trace != 0 {
    if h.first_trace < 3600 || (h.first_trace - 3600) % 3200 != 0 {
      raise Failure("unsupported first-trace layout")
    }
    count = (h.first_trace - 3600) / 3200
    if count > 1024 {
      raise Failure("too many extended text blocks")
    }
  }
  if count >= 0 {
    need(data, pos, count * 3200)
    for _ in 0..= 1024 {
        raise Failure("EndText not found in 1024 blocks")
      }
      need(data, pos, 3200)
      let block = slice(data, pos, 3200)
      let text = extended_text(block, h.text_encoding)
      extensions.push(block)
      pos += 3200
      // Standard EndText stanza permits spacing after the namespace colon.
      let compact = StringBuilder()
      for c in text {
        if !c.is_whitespace() {
          compact.write_char(c)
        }
      }
      if compact.to_string().to_upper().contains("((SEG:ENDTEXT))") {
        break
      }
    }
  }
  let width = sample_width(h.sample_code)
  let index = []
  let mut total = 0
  while pos < data.length() {
    need(data, pos, 240)
    let raw_count = uint_at(data, pos + 114, 2, e).to_int()
    let raw_dt = uint_at(data, pos + 116, 2, e).to_double()
    let n = if h.fixed_length { h.samples } else { raw_count }
    let dt = if h.fixed_length || raw_dt == 0.0 {
      h.interval_us
    } else {
      raw_dt
    }
    if n < 1 || n > 1000000 || dt <= 0.0 || !finite(dt) {
      raise Failure("invalid trace sample count/interval")
    }
    if h.fixed_length &&
      (
        (raw_count != 0 && n <= 65535 && raw_count != n) ||
        (raw_dt != 0.0 && raw_dt != dt)
      ) {
      raise Failure("fixed trace contradicts binary header")
    }
    need(data, pos + 240, n * width)
    index.push({
      header_offset: pos,
      sample_offset: pos + 240,
      samples: n,
      interval_us: dt,
    })
    total += n
    if index.length() > 1000000 || total > 16000000 {
      raise Failure("trace/sample resource limit exceeded")
    }
    pos += 240 + n * width
  }
  if index.is_empty() {
    raise Failure("no data traces")
  }
  if h.declared_traces != 0UL && h.declared_traces != index.length().to_uint64() {
    raise Failure("declared trace count mismatch")
  }
  { header: h, raw: data, index, extensions, }
}

///|
pub fn Dataset::trace_count(self : Dataset) -> Int {
  self.index.length()
}

///|
pub fn Dataset::trace_index(self : Dataset) -> Array[TraceIndex] {
  self.index.copy()
}

///|
pub fn Dataset::extended_headers(self : Dataset) -> Array[Bytes] {
  self.extensions.copy()
}

///|
/// Exact byte copy (Bytes is immutable); does not canonicalize unknown fields.
pub fn Dataset::encode(self : Dataset) -> Bytes {
  self.raw
}

///|
pub fn Dataset::trace(self : Dataset, index : Int) -> Trace raise {
  if index < 0 || index >= self.index.length() {
    raise Failure("trace index outside dataset")
  }
  let t = self.index[index]
  {
    head: slice(self.raw, t.header_offset, 240),
    payload: slice(
      self.raw,
      t.sample_offset,
      t.samples * sample_width(self.header.sample_code),
    ),
    count: t.samples,
    dt: t.interval_us,
    byte_order: self.header.endian,
    code: self.header.sample_code,
    revision: self.header.revision,
  }
}

///|
// Recompute structural fields only; retain all other raw header bytes. A change
// of byte order or revision needs explicit field semantics and is not guessed.
fn assemble(
  header : FileHeader,
  traces : Array[Trace],
  extensions : Array[Bytes],
) -> Dataset raise {
  if traces.is_empty() ||
    traces.length() > 1000000 ||
    extensions.length() > 1024 ||
    (header.revision == 0 && !extensions.is_empty()) {
    raise Failure("invalid assembly counts")
  }
  let width = sample_width(header.sample_code)
  let n = traces[0].count
  let dt = traces[0].dt
  let fixed = header.revision != 0 &&
    traces.all(t => t.count == n && t.dt == dt)
  let first = 3600 + extensions.length() * 3200
  let mut size = first
  let mut total = 0
  for t in traces {
    if t.byte_order != header.endian ||
      t.code != header.sample_code ||
      t.revision != header.revision {
      raise Failure("trace encoding/header mismatch")
    }
    if !fixed && (t.count > 65535 || t.dt > 65535.0 || t.dt != t.dt.trunc()) {
      raise Failure("variable trace needs unsupported wide extension")
    }
    size += 240 + t.count * width
    total += t.count
    if size > 268435456 || total > 16000000 {
      raise Failure("assembled file exceeds resource limit")
    }
  }
  let out = Array::make(size, b'\x00')
  copy_into(out, 0, header.raw)
  let e = header.endian
  put_uint(
    out,
    3216,
    2,
    if dt <= 65535.0 && dt == dt.trunc() {
      dt.to_uint64()
    } else {
      0UL
    },
    e,
  )
  put_uint(out, 3220, 2, if n <= 65535 { n.to_uint64() } else { 0UL }, e)
  if header.revision >= 1 {
    put_uint(out, 3502, 2, if fixed { 1UL } else { 0UL }, e)
    put_uint(out, 3504, 2, extensions.length().to_uint64(), e)
  }
  if header.revision == 2 {
    put_uint(out, 3268, 4, n.to_uint64(), e)
    put_uint(out, 3272, 8, dt.reinterpret_as_uint64(), e)
    put_uint(out, 3512, 8, traces.length().to_uint64(), e)
    put_uint(out, 3520, 8, first.to_uint64(), e)
  }
  let mut pos = 3600
  for block in extensions {
    ignore(extended_text(block, header.text_encoding))
    copy_into(out, pos, block)
    pos += 3200
  }
  for t in traces {
    copy_into(out, pos, t.head)
    copy_into(out, pos + 240, t.payload)
    pos += 240 + t.payload.length()
  }
  decode(
    Bytes::from_array(out),
    endian=e,
    text_encoding=header.text_encoding,
    legacy_revision_one=header.assumed_legacy_revision_one,
  )
}

///|
/// Assemble same-format traces, derive structural counts and validate by scan.
pub fn create(
  header : FileHeader,
  traces : Array[Trace],
  extended_headers? : Array[Bytes] = [],
) -> Dataset raise {
  assemble(header, traces, extended_headers)
}