///|
/// Human-readable file dump covering header, chunks and record summaries.
pub fn dump_file(
  file : EvtxFile,
  style : TimeStyle,
) -> Result[String, EvtxError] {
  let out = StringBuilder()
  out.write_string("EVTX file\n")
  out.write_string("  records: ")
  out.write_string(file.records.length().to_string())
  out.write_string("\n  chunks: ")
  out.write_string(file.chunks.length().to_string())
  out.write_string("\n  next_record_number: ")
  out.write_string(file.header.next_record_number.to_string())
  out.write_string("\n  version: ")
  out.write_string(file.header.major_version.to_string())
  out.write_string(".")
  out.write_string(file.header.minor_version.to_string())
  out.write_string("\n  flags: ")
  out.write_string(file.header.flags.to_string())
  out.write_string("\n  header_checksum_ok: ")
  out.write_string(file.header.checksum_ok.to_string())
  out.write_string("\n")
  for i = 0; i < file.chunks.length(); i = i + 1 {
    let chunk = file.chunks[i]
    out.write_string("CHUNK ")
    out.write_string(i.to_string())
    out.write_string(" offset=")
    out.write_string(chunk.offset.to_string())
    out.write_string(" records=")
    out.write_string(chunk.first_record.to_string())
    out.write_string("-")
    out.write_string(chunk.last_record.to_string())
    out.write_string(" header_ok=")
    out.write_string(chunk.header_ok.to_string())
    out.write_string(" data_ok=")
    out.write_string(chunk.data_ok.to_string())
    out.write_string("\n")
  }
  for i = 0; i < file.records.length(); i = i + 1 {
    let rec = file.records[i]
    let ev = match rec.event(style) {
      Ok(v) => v
      Err(e) => return Err(e)
    }
    out.write_string("RECORD ")
    out.write_string(rec.number.to_string())
    out.write_string(" size=")
    out.write_string(rec.size.to_string())
    out.write_string(" EventID=")
    out.write_string(ev.event_id.to_string())
    out.write_string(" Level=")
    out.write_string(ev.level.to_string())
    out.write_string(" Provider=")
    out.write_string(ev.provider)
    out.write_string(" Channel=")
    out.write_string(ev.channel)
    out.write_string(" Computer=")
    out.write_string(ev.computer)
    out.write_string(" Time=")
    out.write_string(ev.time_text)
    out.write_string("\n  Message=")
    out.write_string(ev.message)
    out.write_string("\n")
  }
  Ok(out.to_string())
}

///|
/// Count records by EventID / channel / level.
pub fn file_stats(
  file : EvtxFile,
  style : TimeStyle,
) -> Result[String, EvtxError] {
  let events = match file.events(style) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let ids : Array[(UInt, Int)] = []
  let channels : Array[(String, Int)] = []
  let levels : Array[(Int, Int)] = []
  for i = 0; i < events.length(); i = i + 1 {
    bump_uint(ids, events[i].event_id)
    bump_str(channels, events[i].channel)
    bump_int(levels, events[i].level)
  }
  let out = StringBuilder()
  out.write_string("records=")
  out.write_string(events.length().to_string())
  out.write_string("\n")
  for i = 0; i < ids.length(); i = i + 1 {
    let (k, n) = ids[i]
    out.write_string("event_id ")
    out.write_string(k.to_string())
    out.write_string("=")
    out.write_string(n.to_string())
    out.write_string("\n")
  }
  for i = 0; i < channels.length(); i = i + 1 {
    let (k, n) = channels[i]
    out.write_string("channel ")
    out.write_string(k)
    out.write_string("=")
    out.write_string(n.to_string())
    out.write_string("\n")
  }
  for i = 0; i < levels.length(); i = i + 1 {
    let (k, n) = levels[i]
    out.write_string("level ")
    out.write_string(k.to_string())
    out.write_string("=")
    out.write_string(n.to_string())
    out.write_string("\n")
  }
  Ok(out.to_string())
}

///|
fn bump_uint(rows : Array[(UInt, Int)], key : UInt) -> Unit {
  for i = 0; i < rows.length(); i = i + 1 {
    let (k, n) = rows[i]
    if k == key {
      rows[i] = (k, n + 1)
      return
    }
  }
  rows.push((key, 1))
}

///|
fn bump_int(rows : Array[(Int, Int)], key : Int) -> Unit {
  for i = 0; i < rows.length(); i = i + 1 {
    let (k, n) = rows[i]
    if k == key {
      rows[i] = (k, n + 1)
      return
    }
  }
  rows.push((key, 1))
}

///|
fn bump_str(rows : Array[(String, Int)], key : String) -> Unit {
  for i = 0; i < rows.length(); i = i + 1 {
    let (k, n) = rows[i]
    if k == key {
      rows[i] = (k, n + 1)
      return
    }
  }
  rows.push((key, 1))
}

///|
fn write_json_pair(
  out : StringBuilder,
  key : String,
  value : String,
  quoted : Bool,
) -> Unit {
  out.write_char('"')
  out.write_string(key)
  out.write_char('"')
  out.write_char(':')
  if quoted {
    out.write_char('"')
    out.write_string(value)
    out.write_char('"')
  } else {
    out.write_string(value)
  }
}

///|
/// JSON object for one extracted event.
pub fn Event::json(self : Event) -> String {
  let out = StringBuilder()
  out.write_char('{')
  write_json_pair(out, "record_id", self.record_id.to_string(), false)
  out.write_char(',')
  write_json_pair(out, "event_id", self.event_id.to_string(), false)
  out.write_char(',')
  write_json_pair(out, "level", self.level.to_string(), false)
  out.write_char(',')
  write_json_pair(out, "provider", json_escape(self.provider), true)
  out.write_char(',')
  write_json_pair(out, "channel", json_escape(self.channel), true)
  out.write_char(',')
  write_json_pair(out, "computer", json_escape(self.computer), true)
  out.write_char(',')
  write_json_pair(out, "time", json_escape(self.time_text), true)
  out.write_char(',')
  write_json_pair(out, "message", json_escape(self.message), true)
  out.write_char(',')
  out.write_char('"')
  out.write_string("data")
  out.write_string("\":[")
  for i = 0; i < self.data.length(); i = i + 1 {
    if i > 0 {
      out.write_char(',')
    }
    let (k, v) = self.data[i]
    out.write_char('{')
    write_json_pair(out, "name", json_escape(k), true)
    out.write_char(',')
    write_json_pair(out, "value", json_escape(v), true)
    out.write_char('}')
  }
  out.write_string("]}")
  out.to_string()
}

///|
pub fn events_json(events : Array[Event]) -> String {
  let out = StringBuilder()
  out.write_char('[')
  for i = 0; i < events.length(); i = i + 1 {
    if i > 0 {
      out.write_char(',')
    }
    out.write_string(events[i].json())
  }
  out.write_char(']')
  out.to_string()
}

///|
/// Hex dump of a record body, 16 bytes per row.
pub fn hexdump_range(
  data : Bytes,
  start : Int,
  end : Int,
) -> Result[String, EvtxError] {
  let _ = match need(data, start, end - start, "hexdump") {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let out = StringBuilder()
  let mut i = start
  while i < end {
    out.write_string(hex_byte((i >> 8) & 255))
    out.write_string(hex_byte(i & 255))
    out.write_string(": ")
    let mut j = 0
    while j < 16 && i + j < end {
      out.write_string(hex_byte(data[i + j].to_int()))
      out.write_char(' ')
      j = j + 1
    }
    out.write_char('\n')
    i = i + 16
  }
  Ok(out.to_string())
}

///|
pub fn EvtxRecord::hexdump(
  self : EvtxRecord,
  file : EvtxFile,
) -> Result[String, EvtxError] {
  hexdump_range(
    file.data,
    self.offset,
    self.offset + self.size.reinterpret_as_int(),
  )
}

///|
/// True when the log was opened dirty (flag bit 0).
pub fn EvtxFile::is_dirty(self : EvtxFile) -> Bool {
  (self.header.flags & 1U) == 1U
}

///|
/// True when the log reported a full-file write failure (flag bit 1).
pub fn EvtxFile::is_full(self : EvtxFile) -> Bool {
  (self.header.flags & 2U) == 2U
}

///|
pub fn EvtxFile::xml_records(
  self : EvtxFile,
  style : TimeStyle,
) -> Result[Array[String], EvtxError] {
  let out : Array[String] = []
  for i = 0; i < self.records.length(); i = i + 1 {
    let xml = match self.records[i].xml(style) {
      Ok(v) => v
      Err(e) => return Err(e)
    }
    out.push(xml)
  }
  Ok(out)
}