///|
priv struct InstructionTotals {
  mut output_size : Int
  mut add_count : Int
  mut run_count : Int
  mut copy_count : Int
}

///|
fn InstructionTotals::new() -> InstructionTotals {
  { output_size: 0, add_count: 0, run_count: 0, copy_count: 0 }
}

///|
fn InstructionTotals::record(
  self : InstructionTotals,
  instruction : CodeInstruction,
  cursor : ByteCursor,
) -> Unit raise VcdiffError {
  if instruction.kind == Noop {
    return
  }
  let size = if instruction.size == 0 {
    read_varint(cursor)
  } else {
    instruction.size
  }
  if self.output_size > MAX_PORTABLE_INT - size {
    raise IntegerOverflow(offset=cursor.offset())
  }
  self.output_size += size
  match instruction.kind {
    Noop => ()
    Add => self.add_count += 1
    Run => self.run_count += 1
    Copy => self.copy_count += 1
  }
}

///|
fn inspect_instruction_stream(
  window : ParsedWindow,
  table : Array[CodeTableEntry],
) -> InstructionTotals raise VcdiffError {
  let cursor = ByteCursor::with_base(
    window.instructions,
    window.instruction_offset,
  )
  let totals = InstructionTotals::new()
  while cursor.remaining() > 0 {
    let code = cursor.read_byte().to_int()
    let entry = table[code]
    totals.record(entry.first, cursor)
    totals.record(entry.second, cursor)
  }
  if totals.output_size != window.target_size {
    raise LengthMismatch(
      offset=cursor.offset(),
      expected=window.target_size,
      actual=totals.output_size,
    )
  }
  totals
}

///|
fn window_source_kind(indicator : Int) -> WindowSourceKind {
  if indicator == VCD_SOURCE {
    SourceDictionary
  } else if indicator == VCD_TARGET {
    TargetDictionary
  } else {
    NoDictionary
  }
}

///|
fn make_window_summary(
  index : Int,
  window : ParsedWindow,
  totals : InstructionTotals,
) -> WindowSummary {
  {
    index,
    offset: window.offset,
    source_kind: window_source_kind(window.indicator),
    source_size: window.source_size,
    source_position: window.source_position,
    target_size: window.target_size,
    delta_size: window.delta_length,
    data_size: window.data.length(),
    instruction_size: window.instructions.length(),
    address_size: window.addresses.length(),
    add_count: totals.add_count,
    run_count: totals.run_count,
    copy_count: totals.copy_count,
  }
}

///|
fn checked_summary_add(
  current : Int,
  amount : Int,
  offset : Int,
) -> Int raise VcdiffError {
  if current > MAX_PORTABLE_INT - amount {
    raise IntegerOverflow(offset~)
  }
  current + amount
}

///|
/// Parses a delta into a stable file-level and per-window summary.
///
/// Inspection validates code bytes and separately encoded instruction sizes,
/// but does not require the external source file.
pub fn inspect(delta : Bytes) -> DeltaSummary raise VcdiffError {
  let parsed = parse_delta(delta)
  let table = default_code_table()
  let windows : Array[WindowSummary] = []
  let mut target_size = 0
  let mut data_size = 0
  let mut instruction_size = 0
  let mut address_size = 0
  let mut add_count = 0
  let mut run_count = 0
  let mut copy_count = 0
  let mut uses_source = false
  let mut uses_target = false
  for index, window in parsed.windows {
    let totals = inspect_instruction_stream(window, table)
    let summary = make_window_summary(index, window, totals)
    windows.push(summary)
    target_size = checked_summary_add(
      target_size,
      summary.target_size,
      window.offset,
    )
    data_size = checked_summary_add(data_size, summary.data_size, window.offset)
    instruction_size = checked_summary_add(
      instruction_size,
      summary.instruction_size,
      window.offset,
    )
    address_size = checked_summary_add(
      address_size,
      summary.address_size,
      window.offset,
    )
    add_count = checked_summary_add(add_count, summary.add_count, window.offset)
    run_count = checked_summary_add(run_count, summary.run_count, window.offset)
    copy_count = checked_summary_add(
      copy_count,
      summary.copy_count,
      window.offset,
    )
    if summary.source_kind == SourceDictionary {
      uses_source = true
    }
    if summary.source_kind == TargetDictionary {
      uses_target = true
    }
  }
  {
    file_size: delta.length(),
    header_size: parsed.header.length,
    window_count: windows.length(),
    target_size,
    data_size,
    instruction_size,
    address_size,
    add_count,
    run_count,
    copy_count,
    uses_source,
    uses_target,
    windows,
  }
}

///|
fn first_mismatch(expected : Bytes, actual : Bytes) -> Int {
  let shared = if expected.length() < actual.length() {
    expected.length()
  } else {
    actual.length()
  }
  for index = 0; index < shared; index = index + 1 {
    if expected[index] != actual[index] {
      return index
    }
  }
  if expected.length() != actual.length() {
    shared
  } else {
    -1
  }
}

///|
/// Decodes a delta with standard limits and compares it to expected bytes.
pub fn verify(
  source : Bytes,
  delta : Bytes,
  expected : Bytes,
) -> VerifyReport raise VcdiffError {
  let actual = decode(source, delta, default_decode_limits())
  let mismatch = first_mismatch(expected, actual)
  {
    matches: mismatch < 0,
    expected_size: expected.length(),
    actual_size: actual.length(),
    first_mismatch: mismatch,
  }
}