///|
/// Offset of the entry size field inside an HvLE entry header.
let log_entry_size_offset : Int32 = 0x04

///|
/// Offset of the sequence number inside an HvLE entry header.
let log_entry_sequence_offset : Int32 = 0x0C

///|
/// Offset of the hive bins data size inside an HvLE entry header.
let log_entry_bins_offset : Int32 = 0x10

///|
/// Offset of the dirty page count inside an HvLE entry header.
let log_entry_page_count_offset : Int32 = 0x14

///|
/// Size of an HvLE entry header, up to its page reference array.
let log_entry_header_size : Int32 = 0x28

///|
/// Alignment of every transaction log entry.
let log_entry_alignment : Int32 = 512

///|
/// Offset of the first entry, past the 512-byte compatibility header.
let log_first_entry_offset : Int32 = 512

///|
/// Largest number of dirty pages a single entry may describe.
let log_max_pages : Int32 = 65536

///|
/// Largest dirty page a single entry may describe.
let log_max_page_size : Int32 = 0x100000

///|
/// One dirty page reference inside a log entry.
pub(all) struct LogPageRef {
  offset : UInt32
  size : Int32
}

///|
/// One transaction log entry: dirty page references plus their bytes.
pub(all) struct LogEntry {
  sequence : UInt32
  bins_data_size : UInt32
  size : Int32
  pages : Array[LogPageRef]
  data : Bytes
}

///|
/// A parsed transaction log file.
pub(all) struct LogFile {
  primary_sequence : UInt32
  secondary_sequence : UInt32
  entries : Array[LogEntry]
}

///|
/// Outcome of replaying a log onto a hive.
pub(all) struct LogReplay {
  bytes : Bytes
  applied : Int32
  final_sequence : UInt32
}

///|
/// Number of entries in the log.
pub fn LogFile::entry_count(self : LogFile) -> Int32 {
  self.entries.length()
}

///|
/// Total number of dirty pages across every entry.
pub fn LogFile::page_count(self : LogFile) -> Int32 {
  let mut total = 0
  let mut index = 0
  while index < self.entries.length() {
    total = total + self.entries[index].pages.length()
    index = index + 1
  }
  total
}

///|
/// True when the four bytes at start are the HvLE entry signature.
fn log_signature_matches(bytes : Bytes, start : Int32) -> Bool {
  bytes[start].to_int() == 0x48 &&
  bytes[start + 1].to_int() == 0x76 &&
  bytes[start + 2].to_int() == 0x4C &&
  bytes[start + 3].to_int() == 0x45
}

///|
/// Parses the log entry whose header starts at position.
fn parse_log_entry(
  bytes : Bytes,
  position : Int32,
) -> Result[LogEntry, RegfError] {
  let size = bytes.length()
  if position + log_entry_header_size > size {
    return Err(BadLogEntry(position))
  }
  let entry_size = read_u32_le(bytes, position + log_entry_size_offset).reinterpret_as_int()
  if entry_size < log_entry_header_size || entry_size % log_entry_alignment != 0 {
    return Err(BadLogEntry(position))
  }
  if position + entry_size > size {
    return Err(BadLogEntry(position))
  }
  let count = read_u32_le(bytes, position + log_entry_page_count_offset).reinterpret_as_int()
  if count <= 0 || count > log_max_pages {
    return Err(BadLogEntry(position))
  }
  let refs_end = position + log_entry_header_size + count * 8
  if refs_end > position + entry_size {
    return Err(BadLogEntry(position))
  }
  let pages : Array[LogPageRef] = []
  let mut total = 0
  let mut index = 0
  while index < count {
    let ref_at = position + log_entry_header_size + index * 8
    let page_offset = read_u32_le(bytes, ref_at)
    let page_size = read_u32_le(bytes, ref_at + 4).reinterpret_as_int()
    if page_size <= 0 || page_size > log_max_page_size {
      return Err(BadLogEntry(position))
    }
    let reference : LogPageRef = { offset: page_offset, size: page_size, }
    pages.push(reference)
    total = total + page_size
    index = index + 1
  }
  if refs_end + total > position + entry_size {
    return Err(BadLogEntry(position))
  }
  let entry : LogEntry = {
    sequence: read_u32_le(bytes, position + log_entry_sequence_offset),
    bins_data_size: read_u32_le(bytes, position + log_entry_bins_offset),
    size: entry_size,
    pages,
    data: copy_range(bytes, refs_end, total),
  }
  Ok(entry)
}

///|
/// Parses a transaction log file.
///
/// Windows 8.1 and newer write a REGF base block with file type 6 followed by
/// HvLE entries on 512-byte boundaries; the parser only accepts that layout and
/// reports older formats as UnsupportedLogVersion instead of guessing.
pub fn parse_log(bytes : Bytes) -> Result[LogFile, RegfError] {
  let size = bytes.length()
  if size < base_block_size {
    return Err(TooShort(size))
  }
  if !signature_matches(bytes) {
    return Err(BadSignature(signature_hex(bytes)))
  }
  let raw_type = read_u32_le(bytes, 0x1C)
  if raw_type == 1 {
    return Err(UnsupportedLogVersion(raw_type))
  }
  if raw_type != 6 {
    return Err(NotALogFile(raw_type))
  }
  if size < log_first_entry_offset + log_entry_header_size {
    return Err(BadLogEntry(log_first_entry_offset))
  }
  let mut position = log_first_entry_offset
  let mut first = -1
  while first < 0 && position + log_entry_header_size <= size {
    if log_signature_matches(bytes, position) {
      first = position
    } else {
      position = position + log_entry_alignment
    }
  }
  let entries : Array[LogEntry] = []
  let mut cursor = first
  let mut running = true
  while running {
    if cursor < 0 || cursor + log_entry_header_size > size {
      running = false
    } else if !log_signature_matches(bytes, cursor) {
      running = false
    } else {
      let entry = match parse_log_entry(bytes, cursor) {
        Err(error) => return Err(error)
        Ok(parsed) => parsed
      }
      entries.push(entry)
      cursor = cursor + entry.size
    }
  }
  let log : LogFile = {
    primary_sequence: read_u32_le(bytes, 0x04),
    secondary_sequence: read_u32_le(bytes, 0x08),
    entries,
  }
  Ok(log)
}

///|
/// Replays a transaction log onto a copy of a hive.
///
/// Entries are applied in stored order and only when their sequence is newer
/// than the secondary sequence of the hive, so replaying a stale log leaves the
/// hive untouched. Dirty page offsets are relative to the hive bins data, and
/// the base block sequence numbers plus checksum are rewritten after the last
/// applied entry.
pub fn replay_log(hive : Bytes, log : Bytes) -> Result[LogReplay, RegfError] {
  let header = match parse_header(hive) {
    Err(error) => return Err(error)
    Ok(parsed) => parsed
  }
  let parsed_log = match parse_log(log) {
    Err(error) => return Err(error)
    Ok(parsed) => parsed
  }
  let out = copy_range(hive, 0, hive.length()).to_array()
  let size = out.length()
  let mut applied = 0
  let mut final_sequence = header.secondary_sequence
  let mut index = 0
  while index < parsed_log.entries.length() {
    let entry = parsed_log.entries[index]
    if entry.sequence > header.secondary_sequence {
      let mut source = 0
      let mut page_index = 0
      while page_index < entry.pages.length() {
        let page = entry.pages[page_index]
        let target = hive_bins_base + page.offset.reinterpret_as_int()
        if target < 0 || target + page.size > size {
          return Err(BadLogPage(page.offset.reinterpret_as_int()))
        }
        let mut copied = 0
        while copied < page.size {
          out[target + copied] = entry.data[source + copied]
          copied = copied + 1
        }
        source = source + page.size
        page_index = page_index + 1
      }
      applied = applied + 1
      final_sequence = entry.sequence
    }
    index = index + 1
  }
  if applied > 0 {
    w_i32(out, 0x04, final_sequence.reinterpret_as_int())
    w_i32(out, 0x08, final_sequence.reinterpret_as_int())
    let checksum = array_checksum(out)
    w_i32(out, 0x1FC, checksum.reinterpret_as_int())
  }
  let replay : LogReplay = {
    bytes: Bytes::from_array(out),
    applied,
    final_sequence,
  }
  Ok(replay)
}

///|
/// Replays a log and returns only the patched hive bytes.
pub fn apply_log(hive : Bytes, log : Bytes) -> Result[Bytes, RegfError] {
  match replay_log(hive, log) {
    Err(error) => Err(error)
    Ok(replay) => Ok(replay.bytes)
  }
}