///|
/// Aggregated semantic instruction usage for one window or complete delta.
pub(all) struct InstructionStatistics {
  instruction_count : Int
  add_count : Int
  run_count : Int
  copy_count : Int
  add_bytes : Int
  run_bytes : Int
  copy_bytes : Int
  literal_bytes : Int
  maximum_instruction_size : Int
  tiny_instructions : Int
  small_instructions : Int
  medium_instructions : Int
  large_instructions : Int
  copy_mode_counts : Array[Int]
} derive(Eq, Debug)

///|
/// Statistics for one target window, including its encoded section costs.
pub(all) struct WindowStatistics {
  window_index : Int
  target_size : Int
  delta_size : Int
  data_size : Int
  instruction_size : Int
  address_size : Int
  operations : InstructionStatistics
} derive(Eq, Debug)

///|
/// Complete statistics report suitable for benchmark and tuning tools.
pub(all) struct DeltaStatistics {
  file_size : Int
  target_size : Int
  window_count : Int
  header_bytes : Int
  window_header_bytes : Int
  data_bytes : Int
  instruction_bytes : Int
  address_bytes : Int
  operations : InstructionStatistics
  windows : Array[WindowStatistics]
} derive(Eq, Debug)

///|
/// Stable comparison between two valid delta documents.
pub(all) struct DeltaComparison {
  left_size : Int
  right_size : Int
  size_difference : Int
  left_instructions : Int
  right_instructions : Int
  instruction_difference : Int
  same_target_size : Bool
  same_window_count : Bool
  same_instruction_mix : Bool
} derive(Eq, Debug)

///|
priv struct StatisticsAccumulator {
  mut instruction_count : Int
  mut add_count : Int
  mut run_count : Int
  mut copy_count : Int
  mut add_bytes : Int
  mut run_bytes : Int
  mut copy_bytes : Int
  mut maximum_instruction_size : Int
  mut tiny_instructions : Int
  mut small_instructions : Int
  mut medium_instructions : Int
  mut large_instructions : Int
  copy_mode_counts : Array[Int]
}

///|
fn StatisticsAccumulator::new() -> StatisticsAccumulator {
  {
    instruction_count: 0,
    add_count: 0,
    run_count: 0,
    copy_count: 0,
    add_bytes: 0,
    run_bytes: 0,
    copy_bytes: 0,
    maximum_instruction_size: 0,
    tiny_instructions: 0,
    small_instructions: 0,
    medium_instructions: 0,
    large_instructions: 0,
    copy_mode_counts: Array::make(9, 0),
  }
}

///|
fn StatisticsAccumulator::record_size(
  self : StatisticsAccumulator,
  size : Int,
) -> Unit {
  self.maximum_instruction_size = Int::max(self.maximum_instruction_size, size)
  if size <= 4 {
    self.tiny_instructions += 1
  } else if size <= 16 {
    self.small_instructions += 1
  } else if size <= 64 {
    self.medium_instructions += 1
  } else {
    self.large_instructions += 1
  }
}

///|
fn StatisticsAccumulator::record(
  self : StatisticsAccumulator,
  item : TraceInstruction,
) -> Unit {
  self.instruction_count += 1
  self.record_size(item.size)
  match item.kind {
    Add => {
      self.add_count += 1
      self.add_bytes += item.size
    }
    Run => {
      self.run_count += 1
      self.run_bytes += item.size
    }
    Copy => {
      self.copy_count += 1
      self.copy_bytes += item.size
      if item.mode >= 0 && item.mode < self.copy_mode_counts.length() {
        self.copy_mode_counts[item.mode] += 1
      }
    }
    Noop => ()
  }
}

///|
fn StatisticsAccumulator::finish(
  self : StatisticsAccumulator,
) -> InstructionStatistics {
  {
    instruction_count: self.instruction_count,
    add_count: self.add_count,
    run_count: self.run_count,
    copy_count: self.copy_count,
    add_bytes: self.add_bytes,
    run_bytes: self.run_bytes,
    copy_bytes: self.copy_bytes,
    literal_bytes: self.add_bytes + self.run_count,
    maximum_instruction_size: self.maximum_instruction_size,
    tiny_instructions: self.tiny_instructions,
    small_instructions: self.small_instructions,
    medium_instructions: self.medium_instructions,
    large_instructions: self.large_instructions,
    copy_mode_counts: self.copy_mode_counts,
  }
}

///|
fn collect_operations(
  instructions : ArrayView[TraceInstruction],
) -> InstructionStatistics {
  let accumulator = StatisticsAccumulator::new()
  for item in instructions {
    accumulator.record(item)
  }
  accumulator.finish()
}

///|
fn window_instruction_view(
  trace_report : DeltaTrace,
  window_index : Int,
) -> Array[TraceInstruction] {
  let selected : Array[TraceInstruction] = []
  for item in trace_report.instructions {
    if item.window_index == window_index {
      selected.push(item)
    }
  }
  selected
}

///|
fn calculate_window_header_bytes(summary : DeltaSummary) -> Int {
  let mut total = 0
  for window in summary.windows {
    total += window.delta_size -
      window.data_size -
      window.instruction_size -
      window.address_size
  }
  total
}

///|
/// Computes semantic and encoded-size statistics for a valid delta.
pub fn statistics(delta : Bytes) -> DeltaStatistics raise VcdiffError {
  let summary = inspect(delta)
  let traced = trace(delta)
  let windows : Array[WindowStatistics] = []
  for window in summary.windows {
    let selected = window_instruction_view(traced, window.index)
    windows.push({
      window_index: window.index,
      target_size: window.target_size,
      delta_size: window.delta_size,
      data_size: window.data_size,
      instruction_size: window.instruction_size,
      address_size: window.address_size,
      operations: collect_operations(selected),
    })
  }
  {
    file_size: summary.file_size,
    target_size: summary.target_size,
    window_count: summary.window_count,
    header_bytes: summary.header_size,
    window_header_bytes: calculate_window_header_bytes(summary),
    data_bytes: summary.data_size,
    instruction_bytes: summary.instruction_size,
    address_bytes: summary.address_size,
    operations: collect_operations(traced.instructions),
    windows,
  }
}

///|
/// Compares structural cost without requiring source or target contents.
pub fn compare_deltas(
  left : Bytes,
  right : Bytes,
) -> DeltaComparison raise VcdiffError {
  let left_report = statistics(left)
  let right_report = statistics(right)
  let left_ops = left_report.operations
  let right_ops = right_report.operations
  {
    left_size: left_report.file_size,
    right_size: right_report.file_size,
    size_difference: right_report.file_size - left_report.file_size,
    left_instructions: left_ops.instruction_count,
    right_instructions: right_ops.instruction_count,
    instruction_difference: right_ops.instruction_count -
    left_ops.instruction_count,
    same_target_size: left_report.target_size == right_report.target_size,
    same_window_count: left_report.window_count == right_report.window_count,
    same_instruction_mix: left_ops.add_count == right_ops.add_count &&
    left_ops.run_count == right_ops.run_count &&
    left_ops.copy_count == right_ops.copy_count,
  }
}

///|
/// Encoded bytes per thousand target bytes, or zero for an empty target.
pub fn DeltaStatistics::encoded_per_mille(self : DeltaStatistics) -> Int {
  if self.target_size == 0 {
    0
  } else {
    self.file_size * 1000 / self.target_size
  }
}

///|
/// Percentage of target bytes reconstructed by COPY, rounded down.
pub fn InstructionStatistics::copy_percentage(
  self : InstructionStatistics,
) -> Int {
  let total = self.add_bytes + self.run_bytes + self.copy_bytes
  if total == 0 {
    0
  } else {
    self.copy_bytes * 100 / total
  }
}

///|
/// Percentage of target bytes reconstructed by RUN, rounded down.
pub fn InstructionStatistics::run_percentage(
  self : InstructionStatistics,
) -> Int {
  let total = self.add_bytes + self.run_bytes + self.copy_bytes
  if total == 0 {
    0
  } else {
    self.run_bytes * 100 / total
  }
}

///|
/// Renders stable multi-line statistics for command-line inspection.
pub fn DeltaStatistics::to_text(self : DeltaStatistics) -> String {
  let builder = StringBuilder()
  let operations = self.operations
  builder <+ "MoonVCDIFF instruction statistics\n"
  builder <+ "file size: \{self.file_size}\n"
  builder <+ "target size: \{self.target_size}\n"
  builder <+ "encoded per mille: \{self.encoded_per_mille()}\n"
  builder <+ "windows: \{self.window_count}\n"
  builder <+ "header bytes: \{self.header_bytes + self.window_header_bytes}\n"
  builder <+ "data bytes: \{self.data_bytes}\n"
  builder <+ "instruction bytes: \{self.instruction_bytes}\n"
  builder <+ "address bytes: \{self.address_bytes}\n"
  builder <+ "operations: \{operations.instruction_count}\n"
  builder <+
    "ADD: \{operations.add_count} operations, \{operations.add_bytes} bytes\n"
  builder <+
    "RUN: \{operations.run_count} operations, \{operations.run_bytes} bytes\n"
  builder <+
    "COPY: \{operations.copy_count} operations, \{operations.copy_bytes} bytes\n"
  builder <+ "copy coverage: \{operations.copy_percentage()}%\n"
  builder <+ "run coverage: \{operations.run_percentage()}%\n"
  builder <+ "maximum instruction: \{operations.maximum_instruction_size}\n"
  builder.to_string()
}

///|
/// Renders a stable comparison for regression reports.
pub fn DeltaComparison::to_text(self : DeltaComparison) -> String {
  let builder = StringBuilder()
  builder <+
    "delta size: \{self.left_size} -> \{self.right_size} (\{self.size_difference})\n"
  builder <+
    "instructions: \{self.left_instructions} -> \{self.right_instructions} (\{self.instruction_difference})\n"
  builder <+ "same target size: \{self.same_target_size}\n"
  builder <+ "same windows: \{self.same_window_count}\n"
  builder <+ "same instruction mix: \{self.same_instruction_mix}\n"
  builder.to_string()
}