///|
/// Signed current-minus-baseline aggregate statistics; never file ranges.
pub struct ModuleOpcodeDelta {
  file_bytes : Int
  code_bytes : Int
  non_code_bytes : Int
  code_header_bytes : Int
  count_encoding_bytes : Int
  body_prefix_bytes : Int
  locals_encoding_bytes : Int
  instruction_bytes : Int
  instruction_count : Int
  imported_function_count : Int
  local_function_count : Int
}

///|
/// status is shared_opcode, baseline_only or current_only. Missing-side counts
/// are zero; neither rows nor function_count deltas establish function identity.
pub struct ModuleOpcodeChange {
  mnemonic : String
  status : String
  baseline_count : Int
  current_count : Int
  count_delta : Int
  baseline_encoded_bytes : Int
  current_encoded_bytes : Int
  encoded_bytes_delta : Int
  baseline_function_count : Int
  current_function_count : Int
  function_count_delta : Int
}

///|
pub struct ModuleOpcodeComparison {
  baseline : ModuleOpcodeReport
  current : ModuleOpcodeReport
  delta : ModuleOpcodeDelta
  opcodes : Array[ModuleOpcodeChange]
  scope : String
}

///|
pub enum ModuleOpcodeComparisonError {
  BaselineModuleOpcodeError(ModuleOpcodeError)
  CurrentModuleOpcodeError(ModuleOpcodeError)
}

///|
pub fn describe_module_opcode_comparison_error(
  error : ModuleOpcodeComparisonError,
) -> String {
  match error {
    BaselineModuleOpcodeError(error) =>
      "baseline: " + describe_module_opcode_error(error)
    CurrentModuleOpcodeError(error) =>
      "current: " + describe_module_opcode_error(error)
  }
}

///|
fn module_opcode_code_bytes(report : ModuleOpcodeReport) -> Int {
  match report.code_section {
    Some(code) => code.total_size
    None => 0
  }
}

///|
fn module_opcode_change(
  mnemonic : String,
  before : ModuleOpcodeGroup?,
  after : ModuleOpcodeGroup?,
) -> ModuleOpcodeChange {
  let (bc, bb, bf) = match before {
    Some(g) => (g.count, g.encoded_bytes, g.function_count)
    None => (0, 0, 0)
  }
  let (cc, cb, cf) = match after {
    Some(g) => (g.count, g.encoded_bytes, g.function_count)
    None => (0, 0, 0)
  }
  {
    mnemonic,
    status: match (before, after) {
      (Some(_), Some(_)) => "shared_opcode"
      (Some(_), None) => "baseline_only"
      _ => "current_only"
    },
    baseline_count: bc,
    current_count: cc,
    count_delta: cc - bc,
    baseline_encoded_bytes: bb,
    current_encoded_bytes: cb,
    encoded_bytes_delta: cb - bb,
    baseline_function_count: bf,
    current_function_count: cf,
    function_count_delta: cf - bf,
  }
}

///|
/// Inspect each side exactly once. No per-function matching or re-decoding.
pub fn compare_module_opcodes(
  baseline_data : Bytes,
  current_data : Bytes,
) -> Result[ModuleOpcodeComparison, ModuleOpcodeComparisonError] {
  let before = match inspect_module_opcodes(baseline_data) {
    Ok(report) => report
    Err(error) => return Err(BaselineModuleOpcodeError(error))
  }
  let after = match inspect_module_opcodes(current_data) {
    Ok(report) => report
    Err(error) => return Err(CurrentModuleOpcodeError(error))
  }
  let previous : Map[String, ModuleOpcodeGroup] = Map([])
  let next : Map[String, ModuleOpcodeGroup] = Map([])
  for g in before.groups {
    previous.set(g.mnemonic, g)
  }
  for g in after.groups {
    next.set(g.mnemonic, g)
  }
  let rows : Array[ModuleOpcodeChange] = []
  for g in before.groups {
    rows.push(module_opcode_change(g.mnemonic, Some(g), next.get(g.mnemonic)))
  }
  for g in after.groups {
    if !previous.contains(g.mnemonic) {
      rows.push(module_opcode_change(g.mnemonic, None, Some(g)))
    }
  }
  // Differences of bounded nonnegative Int statistics cannot be Int::MIN.
  // Compare magnitudes individually, never subtract sorting keys.
  rows.sort_by(fn(a, b) {
    let bytes = b.encoded_bytes_delta.abs().compare(a.encoded_bytes_delta.abs())
    if bytes != 0 {
      bytes
    } else {
      let count = b.count_delta.abs().compare(a.count_delta.abs())
      if count != 0 {
        count
      } else {
        a.mnemonic.lexical_compare(b.mnemonic)
      }
    }
  })
  let bc = module_opcode_code_bytes(before)
  let cc = module_opcode_code_bytes(after)
  Ok({
    baseline: before,
    current: after,
    delta: {
      file_bytes: after.file_size - before.file_size,
      code_bytes: cc - bc,
      non_code_bytes: after.file_size - cc - (before.file_size - bc),
      code_header_bytes: after.code_header_bytes - before.code_header_bytes,
      count_encoding_bytes: after.count_encoding_bytes -
      before.count_encoding_bytes,
      body_prefix_bytes: after.body_prefix_bytes - before.body_prefix_bytes,
      locals_encoding_bytes: after.locals_encoding_bytes -
      before.locals_encoding_bytes,
      instruction_bytes: after.instruction_bytes - before.instruction_bytes,
      instruction_count: after.instruction_count - before.instruction_count,
      imported_function_count: after.imported_function_count -
      before.imported_function_count,
      local_function_count: after.local_function_count -
      before.local_function_count,
    },
    opcodes: rows,
    scope: "All local function instruction encoding aggregates; delta = current - baseline. No function matching or instruction alignment. Non-code bytes include the file header and every non-code encoding, not just metadata. Actual code header, count field, body prefixes, locals and instruction widths are separate. Initializer expressions, runtime frequency, retained and gzip are excluded. function_count counts distinct functions per mnemonic; do not sum these rows or deltas as a function total, or infer which functions were added/removed. Zero aggregate deltas do not prove equal bytes, constants, instruction order or semantics. File offsets remain side-specific; deltas are quantities, not ranges. Encoded changes do not predict safe removal, rebuilding or gzip savings. Top limits text opcode rows only; both original reports and JSON remain complete.",
  })
}