///|
/// Aggregate facts about one 256-entry instruction code table.
pub(all) struct CodeTableMetrics {
  entry_count : Int
  compound_count : Int
  variable_size_count : Int
  noop_count : Int
  add_count : Int
  run_count : Int
  copy_count : Int
  highest_copy_mode : Int
  serialized_size : Int
  checksum : Int
} derive(Eq, Debug)

///|
fn instruction_kind_byte(kind : InstructionKind) -> Byte {
  match kind {
    Noop => b'\x00'
    Add => b'\x01'
    Run => b'\x02'
    Copy => b'\x03'
  }
}

///|
fn instruction_kind_from_byte(
  byte : Byte,
  offset : Int,
) -> InstructionKind raise VcdiffError {
  match byte {
    b'\x00' => Noop
    b'\x01' => Add
    b'\x02' => Run
    b'\x03' => Copy
    _ =>
      raise InvalidInstruction(
        offset~,
        reason="code table contains an unknown instruction type",
      )
  }
}

///|
fn validate_code_instruction(
  instruction : CodeInstruction,
  mode_count : Int,
  index : Int,
  slot : Int,
) -> Unit raise VcdiffError {
  if instruction.size < 0 || instruction.size > 255 {
    raise InvalidInstruction(
      offset=index * 6 + slot,
      reason="code-table size is outside the byte range",
    )
  }
  match instruction.kind {
    Noop =>
      if instruction.size != 0 || instruction.mode != 0 {
        raise InvalidInstruction(
          offset=index * 6 + slot,
          reason="NOOP must have zero size and mode",
        )
      }
    Add | Run =>
      if instruction.mode != 0 {
        raise InvalidInstruction(
          offset=index * 6 + slot,
          reason="ADD and RUN must use address mode zero",
        )
      }
    Copy =>
      if instruction.mode < 0 || instruction.mode >= mode_count {
        raise InvalidInstruction(
          offset=index * 6 + slot,
          reason="COPY mode exceeds configured address caches",
        )
      }
  }
}

///|
fn validate_code_table(
  table : Array[CodeTableEntry],
  near_size : Int,
  same_size : Int,
) -> Unit raise VcdiffError {
  if table.length() != 256 {
    raise InvalidOption(
      option="code_table",
      reason="must contain exactly 256 entries",
    )
  }
  if near_size < 0 || same_size < 0 {
    raise InvalidOption(
      option="address_cache",
      reason="cache sizes must not be negative",
    )
  }
  if near_size > 253 || same_size > 253 - near_size {
    raise InvalidOption(
      option="address_cache",
      reason="cache sizes produce more than 255 address modes",
    )
  }
  let mode_count = 2 + near_size + same_size
  for index, entry in table {
    validate_code_instruction(entry.first, mode_count, index, 0)
    validate_code_instruction(entry.second, mode_count, index, 3)
  }
}

///|
fn write_instruction_field(
  output : @buffer.Buffer,
  table : Array[CodeTableEntry],
  selector : Int,
) -> Unit {
  for entry in table {
    let value = match selector {
      0 => instruction_kind_byte(entry.first.kind)
      1 => instruction_kind_byte(entry.second.kind)
      2 => entry.first.size.to_byte()
      3 => entry.second.size.to_byte()
      4 => entry.first.mode.to_byte()
      _ => entry.second.mode.to_byte()
    }
    output.write_byte(value)
  }
}

///|
/// Serializes a validated table using RFC 3284 section 7 field ordering.
pub fn encode_code_table_data(
  table : Array[CodeTableEntry],
  near_size : Int,
  same_size : Int,
) -> Bytes raise VcdiffError {
  validate_code_table(table, near_size, same_size)
  let output = @buffer.Buffer(size_hint=1536)
  for field = 0; field < 6; field = field + 1 {
    write_instruction_field(output, table, field)
  }
  output.to_bytes()
}

///|
fn table_byte(data : Bytes, field : Int, index : Int) -> Byte {
  data[field * 256 + index]
}

///|
/// Parses an uncompressed 1536-byte code-table representation.
///
/// This utility does not enable custom tables in the file decoder; it exists
/// for inspection, generation, and interoperability tooling.
pub fn decode_code_table_data(
  data : Bytes,
  near_size : Int,
  same_size : Int,
) -> Array[CodeTableEntry] raise VcdiffError {
  if data.length() != 1536 {
    raise LengthMismatch(offset=0, expected=1536, actual=data.length())
  }
  let table : Array[CodeTableEntry] = []
  for index = 0; index < 256; index = index + 1 {
    let first : CodeInstruction = {
      kind: instruction_kind_from_byte(table_byte(data, 0, index), index),
      size: table_byte(data, 2, index).to_int(),
      mode: table_byte(data, 4, index).to_int(),
    }
    let second : CodeInstruction = {
      kind: instruction_kind_from_byte(table_byte(data, 1, index), 256 + index),
      size: table_byte(data, 3, index).to_int(),
      mode: table_byte(data, 5, index).to_int(),
    }
    table.push({ first, second })
  }
  validate_code_table(table, near_size, same_size)
  table
}

///|
/// Returns the canonical serialized form of the RFC default table.
pub fn default_code_table_data() -> Bytes {
  encode_code_table_data(
    default_code_table(),
    DEFAULT_NEAR_CACHE_SIZE,
    DEFAULT_SAME_CACHE_SIZE,
  ) catch {
    _ => abort("internal default code table is invalid")
  }
}

///|
fn update_table_checksum(checksum : Int, instruction : CodeInstruction) -> Int {
  let kind = instruction_kind_byte(instruction.kind).to_int()
  let first = (checksum + kind + 1) % 65521
  let second = (first * 3 + instruction.size + 1) % 65521
  (second * 3 + instruction.mode + 1) % 65521
}

///|
fn count_instruction_kind(
  instruction : CodeInstruction,
  metrics : Array[Int],
) -> Unit {
  match instruction.kind {
    Noop => metrics[0] += 1
    Add => metrics[1] += 1
    Run => metrics[2] += 1
    Copy => metrics[3] += 1
  }
  if instruction.kind != Noop && instruction.size == 0 {
    metrics[4] += 1
  }
  if instruction.kind == Copy && instruction.mode > metrics[5] {
    metrics[5] = instruction.mode
  }
}

///|
/// Validates a table and returns stable aggregate metrics.
pub fn inspect_code_table(
  table : Array[CodeTableEntry],
  near_size : Int,
  same_size : Int,
) -> CodeTableMetrics raise VcdiffError {
  validate_code_table(table, near_size, same_size)
  let counts = Array::make(6, 0)
  let mut compound_count = 0
  let mut checksum = 1
  for entry in table {
    count_instruction_kind(entry.first, counts)
    count_instruction_kind(entry.second, counts)
    if entry.second.kind != Noop {
      compound_count += 1
    }
    checksum = update_table_checksum(checksum, entry.first)
    checksum = update_table_checksum(checksum, entry.second)
  }
  {
    entry_count: table.length(),
    compound_count,
    variable_size_count: counts[4],
    noop_count: counts[0],
    add_count: counts[1],
    run_count: counts[2],
    copy_count: counts[3],
    highest_copy_mode: counts[5],
    serialized_size: table.length() * 6,
    checksum,
  }
}