///|
/// One fully expanded instruction from a VCDIFF instruction stream.
pub(all) struct TraceInstruction {
  window_index : Int
  ordinal : Int
  code_index : Int
  code_offset : Int
  code_slot : Int
  kind : InstructionKind
  size : Int
  mode : Int
  target_offset : Int
  data_offset : Int
  data_size : Int
  address_offset : Int
  address : Int
} derive(Eq, Debug)

///|
/// Expanded instruction trace for a complete delta file.
pub(all) struct DeltaTrace {
  file_size : Int
  window_count : Int
  target_size : Int
  instruction_count : Int
  add_count : Int
  run_count : Int
  copy_count : Int
  instructions : Array[TraceInstruction]
} derive(Eq, Debug)

///|
priv struct WindowTraceState {
  mut target_offset : Int
  mut ordinal : Int
  mut add_count : Int
  mut run_count : Int
  mut copy_count : Int
}

///|
fn WindowTraceState::new() -> WindowTraceState {
  { target_offset: 0, ordinal: 0, add_count: 0, run_count: 0, copy_count: 0 }
}

///|
fn trace_size(
  instruction : CodeInstruction,
  cursor : ByteCursor,
) -> Int raise VcdiffError {
  if instruction.size == 0 {
    read_varint(cursor)
  } else {
    instruction.size
  }
}

///|
fn trace_add(
  window_index : Int,
  code_index : Int,
  code_offset : Int,
  code_slot : Int,
  instruction : CodeInstruction,
  size : Int,
  state : WindowTraceState,
  data_cursor : ByteCursor,
) -> TraceInstruction raise VcdiffError {
  ignore(instruction)
  let data_offset = data_cursor.offset()
  ignore(data_cursor.read_exact(size))
  let traced = {
    window_index,
    ordinal: state.ordinal,
    code_index,
    code_offset,
    code_slot,
    kind: Add,
    size,
    mode: 0,
    target_offset: state.target_offset,
    data_offset,
    data_size: size,
    address_offset: -1,
    address: -1,
  }
  state.target_offset += size
  state.ordinal += 1
  state.add_count += 1
  traced
}

///|
fn trace_run(
  window_index : Int,
  code_index : Int,
  code_offset : Int,
  code_slot : Int,
  instruction : CodeInstruction,
  size : Int,
  state : WindowTraceState,
  data_cursor : ByteCursor,
) -> TraceInstruction raise VcdiffError {
  ignore(instruction)
  let data_offset = data_cursor.offset()
  ignore(data_cursor.read_byte())
  let traced = {
    window_index,
    ordinal: state.ordinal,
    code_index,
    code_offset,
    code_slot,
    kind: Run,
    size,
    mode: 0,
    target_offset: state.target_offset,
    data_offset,
    data_size: 1,
    address_offset: -1,
    address: -1,
  }
  state.target_offset += size
  state.ordinal += 1
  state.run_count += 1
  traced
}

///|
fn trace_copy(
  window_index : Int,
  code_index : Int,
  code_offset : Int,
  code_slot : Int,
  instruction : CodeInstruction,
  size : Int,
  source_size : Int,
  state : WindowTraceState,
  address_cursor : ByteCursor,
  cache : AddressCache,
) -> TraceInstruction raise VcdiffError {
  let address_offset = address_cursor.offset()
  let here = source_size + state.target_offset
  let address = cache.decode(address_cursor, here, instruction.mode)
  let traced = {
    window_index,
    ordinal: state.ordinal,
    code_index,
    code_offset,
    code_slot,
    kind: Copy,
    size,
    mode: instruction.mode,
    target_offset: state.target_offset,
    data_offset: -1,
    data_size: 0,
    address_offset,
    address,
  }
  state.target_offset += size
  state.ordinal += 1
  state.copy_count += 1
  traced
}

///|
fn trace_one_instruction(
  window_index : Int,
  window : ParsedWindow,
  code_index : Int,
  code_offset : Int,
  code_slot : Int,
  instruction : CodeInstruction,
  instruction_cursor : ByteCursor,
  data_cursor : ByteCursor,
  address_cursor : ByteCursor,
  cache : AddressCache,
  state : WindowTraceState,
) -> TraceInstruction? raise VcdiffError {
  match instruction.kind {
    Noop => None
    Add => {
      let size = trace_size(instruction, instruction_cursor)
      Some(
        trace_add(
          window_index, code_index, code_offset, code_slot, instruction, size, state,
          data_cursor,
        ),
      )
    }
    Run => {
      let size = trace_size(instruction, instruction_cursor)
      Some(
        trace_run(
          window_index, code_index, code_offset, code_slot, instruction, size, state,
          data_cursor,
        ),
      )
    }
    Copy => {
      let size = trace_size(instruction, instruction_cursor)
      Some(
        trace_copy(
          window_index,
          code_index,
          code_offset,
          code_slot,
          instruction,
          size,
          window.source_size,
          state,
          address_cursor,
          cache,
        ),
      )
    }
  }
}

///|
fn trace_window(
  window_index : Int,
  window : ParsedWindow,
  table : Array[CodeTableEntry],
  output : Array[TraceInstruction],
) -> WindowTraceState raise VcdiffError {
  let instruction_cursor = ByteCursor::with_base(
    window.instructions,
    window.instruction_offset,
  )
  let data_cursor = ByteCursor::with_base(window.data, window.data_offset)
  let address_cursor = ByteCursor::with_base(
    window.addresses,
    window.address_offset,
  )
  let cache = AddressCache::new()
  let state = WindowTraceState::new()
  while instruction_cursor.remaining() > 0 {
    let code_offset = instruction_cursor.offset()
    let code_index = instruction_cursor.read_byte().to_int()
    let entry = table[code_index]
    let first = trace_one_instruction(
      window_index,
      window,
      code_index,
      code_offset,
      0,
      entry.first,
      instruction_cursor,
      data_cursor,
      address_cursor,
      cache,
      state,
    )
    guard first is Some(item) else { continue }
    output.push(item)
    let second = trace_one_instruction(
      window_index,
      window,
      code_index,
      code_offset,
      1,
      entry.second,
      instruction_cursor,
      data_cursor,
      address_cursor,
      cache,
      state,
    )
    if second is Some(item) {
      output.push(item)
    }
  }
  if state.target_offset != window.target_size {
    raise LengthMismatch(
      offset=window.instruction_offset + window.instructions.length(),
      expected=window.target_size,
      actual=state.target_offset,
    )
  }
  if data_cursor.remaining() != 0 {
    raise LengthMismatch(
      offset=data_cursor.offset(),
      expected=window.data.length() - data_cursor.remaining(),
      actual=window.data.length(),
    )
  }
  if address_cursor.remaining() != 0 {
    raise LengthMismatch(
      offset=address_cursor.offset(),
      expected=window.addresses.length() - address_cursor.remaining(),
      actual=window.addresses.length(),
    )
  }
  state
}

///|
/// Expands every code byte into semantic ADD, RUN, and COPY operations.
///
/// COPY addresses are resolved through the standard caches. Source contents
/// are not needed because source segment sizes are carried by window headers.
pub fn trace(delta : Bytes) -> DeltaTrace raise VcdiffError {
  let parsed = parse_delta(delta)
  let table = default_code_table()
  let instructions : Array[TraceInstruction] = []
  let mut target_size = 0
  let mut add_count = 0
  let mut run_count = 0
  let mut copy_count = 0
  for window_index, window in parsed.windows {
    let state = trace_window(window_index, window, table, instructions)
    target_size = checked_summary_add(
      target_size,
      state.target_offset,
      window.offset,
    )
    add_count = checked_summary_add(add_count, state.add_count, window.offset)
    run_count = checked_summary_add(run_count, state.run_count, window.offset)
    copy_count = checked_summary_add(
      copy_count,
      state.copy_count,
      window.offset,
    )
  }
  {
    file_size: delta.length(),
    window_count: parsed.windows.length(),
    target_size,
    instruction_count: instructions.length(),
    add_count,
    run_count,
    copy_count,
    instructions,
  }
}

///|
/// Renders an expanded trace as one operation per line.
pub fn DeltaTrace::to_text(self : DeltaTrace) -> String {
  let builder = StringBuilder()
  builder <+
    "windows=\{self.window_count} target=\{self.target_size} operations=\{self.instruction_count}\n"
  for item in self.instructions {
    builder <+ "w\{item.window_index} #\{item.ordinal} @\{item.target_offset} "
    builder <+ "\{item.kind.name()} size=\{item.size}"
    if item.kind == Copy {
      builder <+ " mode=\{item.mode} address=\{item.address}"
    } else {
      builder <+ " data=\{item.data_offset}+\{item.data_size}"
    }
    builder <+ " code=\{item.code_index}:\{item.code_slot}\n"
  }
  builder.to_string()
}