///|
/// EVTX file header (4096-byte prefix, checksum over the first 0x78 bytes).
pub(all) struct FileHeader {
  oldest_chunk : UInt64
  current_chunk_number : UInt64
  next_record_number : UInt64
  header_size : UInt
  minor_version : Int
  major_version : Int
  header_chunk_size : Int
  chunk_count : Int
  flags : UInt
  checksum : UInt
  checksum_ok : Bool
} derive(Debug)

///|
/// One 64 KiB chunk header.
pub(all) struct ChunkHeader {
  offset : Int
  first_record : UInt64
  last_record : UInt64
  log_first_record : UInt64
  log_last_record : UInt64
  header_size : UInt
  last_record_offset : UInt
  next_record_offset : UInt
  data_checksum : UInt
  header_checksum : UInt
  header_ok : Bool
  data_ok : Bool
} derive(Debug)

///|
/// Parsed event record.
pub(all) struct EvtxRecord {
  number : UInt64
  timestamp : UInt64
  size : UInt
  chunk_offset : Int
  offset : Int
  root : ParsedRoot
} derive(Debug)

///|
/// In-memory EVTX file.
pub(all) struct EvtxFile {
  data : Bytes
  header : FileHeader
  chunks : Array[ChunkHeader]
  records : Array[EvtxRecord]
}

///|
fn parse_file_header(data : Bytes) -> Result[FileHeader, EvtxError] {
  let _ = match need(data, 0, 0x80, "file-header") {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let _magic = match ascii_magic(data, 0, 8) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  if data[0].to_int() != 69 ||
    data[1].to_int() != 108 ||
    data[2].to_int() != 102 ||
    data[3].to_int() != 70 ||
    data[4].to_int() != 105 ||
    data[5].to_int() != 108 ||
    data[6].to_int() != 101 ||
    data[7].to_int() != 0 {
    return Err(InvalidMagic("file", hex_encode(bytes_slice(data, 0, 8))))
  }
  let (oldest, _) = match read_u64(data, 8) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let (current, _) = match read_u64(data, 16) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let (next_record, _) = match read_u64(data, 24) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let (header_size, _) = match read_u32(data, 32) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let (minor, _) = match read_u16(data, 36) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let (major, _) = match read_u16(data, 38) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let (header_chunk, _) = match read_u16(data, 40) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let (chunk_count, _) = match read_u16(data, 42) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let (flags, _) = match read_u32(data, 0x78) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let (checksum, _) = match read_u32(data, 0x7C) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let calc = match crc32_slice(data, 0, 0x78) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  Ok(FileHeader::{
    oldest_chunk: oldest,
    current_chunk_number: current,
    next_record_number: next_record,
    header_size,
    minor_version: minor,
    major_version: major,
    header_chunk_size: header_chunk,
    chunk_count,
    flags,
    checksum,
    checksum_ok: calc == checksum,
  })
}

///|
fn parse_chunk_header(
  data : Bytes,
  offset : Int,
) -> Result[ChunkHeader, EvtxError] {
  let _ = match need(data, offset, 0x200, "chunk-header") {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let _magic = match ascii_magic(data, offset, 8) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  if data[offset].to_int() != 69 ||
    data[offset + 1].to_int() != 108 ||
    data[offset + 2].to_int() != 102 ||
    data[offset + 3].to_int() != 67 ||
    data[offset + 4].to_int() != 104 ||
    data[offset + 5].to_int() != 110 ||
    data[offset + 6].to_int() != 107 ||
    data[offset + 7].to_int() != 0 {
    return Err(
      InvalidMagic("chunk", hex_encode(bytes_slice(data, offset, offset + 8))),
    )
  }
  let (first_record, _) = match read_u64(data, offset + 8) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let (last_record, _) = match read_u64(data, offset + 16) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let (log_first, _) = match read_u64(data, offset + 24) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let (log_last, _) = match read_u64(data, offset + 32) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let (header_size, _) = match read_u32(data, offset + 40) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let (last_off, _) = match read_u32(data, offset + 44) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let (next_off, _) = match read_u32(data, offset + 48) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let (data_cs, _) = match read_u32(data, offset + 52) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let (header_cs, _) = match read_u32(data, offset + 0x7C) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let header_calc = match
    crc32_two_slices(data, offset, offset + 0x78, offset + 0x80, offset + 0x200) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let data_end = offset + next_off.reinterpret_as_int()
  if data_end < offset + 0x200 || data_end > data.length() {
    return Err(InvalidChunk("next_record_offset out of range"))
  }
  let data_calc = match crc32_slice(data, offset + 0x200, data_end) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  Ok(ChunkHeader::{
    offset,
    first_record,
    last_record,
    log_first_record: log_first,
    log_last_record: log_last,
    header_size,
    last_record_offset: last_off,
    next_record_offset: next_off,
    data_checksum: data_cs,
    header_checksum: header_cs,
    header_ok: header_calc == header_cs,
    data_ok: data_calc == data_cs,
  })
}

///|
fn preload_chunk_names(
  data : Bytes,
  chunk_offset : Int,
  names : Array[(Int, String)],
) -> Result[Unit, EvtxError] {
  for i = 0; i < 64; i = i + 1 {
    let (ofs, _) = match read_u32(data, chunk_offset + 0x80 + i * 4) {
      Ok(v) => v
      Err(e) => return Err(e)
    }
    let mut cur = ofs.reinterpret_as_int()
    let mut hops = 0
    while cur > 0 && hops < 64 {
      let name = match load_name(data, chunk_offset, cur, names) {
        Ok(v) => v
        Err(e) => return Err(e)
      }
      if name.length() < 0 {
        return Err(InvalidName("negative name"))
      }
      let (next, _) = match read_u32(data, chunk_offset + cur) {
        Ok(v) => v
        Err(e) => return Err(e)
      }
      cur = next.reinterpret_as_int()
      hops = hops + 1
    }
  }
  Ok(())
}

///|
fn parse_record(
  data : Bytes,
  offset : Int,
  chunk_offset : Int,
  names : Array[(Int, String)],
  style : TimeStyle,
) -> Result[EvtxRecord, EvtxError] {
  let _ = match need(data, offset, 24, "record-header") {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let (magic, _) = match read_u32(data, offset) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  if magic != 0x00002A2AU {
    return Err(
      InvalidRecord(
        "magic \{hex_encode(bytes_slice(data, offset, offset + 4))}",
      ),
    )
  }
  let (size, _) = match read_u32(data, offset + 4) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let (number, _) = match read_u64(data, offset + 8) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let (timestamp, _) = match read_u64(data, offset + 16) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let n = size.reinterpret_as_int()
  if n < 28 || n > 0x10000 {
    return Err(InvalidRecord("size \{n}"))
  }
  let _ = match need(data, offset, n, "record") {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let (size2, _) = match read_u32(data, offset + n - 4) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  if size2 != size {
    return Err(InvalidRecord("trailing size mismatch"))
  }
  let (root, _end) = match
    parse_root(data, offset + 24, chunk_offset, names, style) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  Ok(EvtxRecord::{ number, timestamp, size, chunk_offset, offset, root, })
}

///|
fn parse_chunk_records(
  data : Bytes,
  chunk : ChunkHeader,
  style : TimeStyle,
) -> Result[Array[EvtxRecord], EvtxError] {
  let names : Array[(Int, String)] = []
  let _ = match preload_chunk_names(data, chunk.offset, names) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  let records : Array[EvtxRecord] = []
  let mut ofs = chunk.offset + 0x200
  let end = chunk.offset + chunk.next_record_offset.reinterpret_as_int()
  while ofs + 8 <= end {
    let (magic, _) = match read_u32(data, ofs) {
      Ok(v) => v
      Err(e) => return Err(e)
    }
    if magic != 0x00002A2AU {
      break
    }
    let rec = match parse_record(data, ofs, chunk.offset, names, style) {
      Ok(v) => v
      Err(e) => return Err(e)
    }
    records.push(rec)
    ofs = ofs + rec.size.reinterpret_as_int()
  }
  Ok(records)
}

///|
/// Parse an EVTX buffer. Checksums are recorded rather than required.
pub fn parse_evtx(data : Bytes) -> Result[EvtxFile, EvtxError] {
  parse_evtx_styled(data, PythonEvtx)
}

///|
/// Parse with an explicit FILETIME rendering style used while walking BINXML values.
pub fn parse_evtx_styled(
  data : Bytes,
  style : TimeStyle,
) -> Result[EvtxFile, EvtxError] {
  let header = match parse_file_header(data) {
    Ok(v) => v
    Err(e) => return Err(e)
  }
  if header.header_chunk_size != 0x1000 {
    return Err(InvalidHeader("header_chunk_size \{header.header_chunk_size}"))
  }
  let chunks : Array[ChunkHeader] = []
  let records : Array[EvtxRecord] = []
  let mut i = 0
  while i < header.chunk_count {
    let offset = header.header_chunk_size + i * 0x10000
    if offset + 0x10000 > data.length() {
      return Err(Truncated("chunk", offset, 0x10000))
    }
    let chunk = match parse_chunk_header(data, offset) {
      Ok(v) => v
      Err(e) => return Err(e)
    }
    let recs = match parse_chunk_records(data, chunk, style) {
      Ok(v) => v
      Err(e) => return Err(e)
    }
    chunks.push(chunk)
    for r = 0; r < recs.length(); r = r + 1 {
      records.push(recs[r])
    }
    i = i + 1
  }
  Ok(EvtxFile::{ data, header, chunks, records, })
}

///|
/// Fail if header/chunk checksums do not match python-evtx.
pub fn EvtxFile::verify(self : EvtxFile) -> Result[Unit, EvtxError] {
  if !self.header.checksum_ok {
    return Err(
      InvalidChecksum(
        "file header",
        self.header.checksum,
        match crc32_slice(self.data, 0, 0x78) {
          Ok(v) => v
          Err(e) => return Err(e)
        },
      ),
    )
  }
  if self.header.major_version != 3 || self.header.minor_version != 1 {
    return Err(
      InvalidHeader(
        "version \{self.header.major_version}.\{self.header.minor_version}",
      ),
    )
  }
  for i = 0; i < self.chunks.length(); i = i + 1 {
    let chunk = self.chunks[i]
    if !chunk.header_ok {
      return Err(InvalidChecksum("chunk header", chunk.header_checksum, 0U))
    }
    if !chunk.data_ok {
      return Err(InvalidChecksum("chunk data", chunk.data_checksum, 0U))
    }
  }
  Ok(())
}

///|
pub fn EvtxFile::record_count(self : EvtxFile) -> Int {
  self.records.length()
}

///|
pub fn EvtxRecord::xml(
  self : EvtxRecord,
  style : TimeStyle,
) -> Result[String, EvtxError] {
  render_root(self.root, style)
}

///|
pub extend FileHeader with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend ChunkHeader with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend EvtxRecord with @moonbitlang/core/debug.Debug::{to_repr}