///|
priv struct WireInstruction {
  kind : InstructionKind
  size : Int
  mode : Int
}

///|
priv struct EncodedSections {
  data : Bytes
  instructions : Bytes
  addresses : Bytes
  output_size : Int
}

///|
fn make_wire_instruction(
  kind : InstructionKind,
  size : Int,
  mode : Int,
) -> WireInstruction {
  { kind, size, mode }
}

///|
fn write_encoded_address(
  section : @buffer.Buffer,
  encoded : EncodedAddress,
) -> Unit raise VcdiffError {
  if encoded.single_byte {
    if encoded.value < 0 || encoded.value > 255 {
      raise InvalidAddress(offset=-1, address=encoded.value, limit=255)
    }
    section.write_byte(encoded.value.to_byte())
  } else {
    write_varint(section, encoded.value)
  }
}

///|
fn prepare_wire_instructions(
  target : Bytes,
  plan : WindowPlan,
  data_section : @buffer.Buffer,
  address_section : @buffer.Buffer,
) -> Array[WireInstruction] raise VcdiffError {
  let wires : Array[WireInstruction] = []
  let cache = AddressCache::new()
  let mut produced = 0
  for instruction in plan.instructions {
    match instruction {
      PlannedAdd(target_offset~, size~) => {
        data_section.write_bytes(target[target_offset:target_offset + size])
        wires.push(make_wire_instruction(Add, size, 0))
        produced += size
      }
      PlannedRun(byte~, size~) => {
        data_section.write_byte(byte)
        wires.push(make_wire_instruction(Run, size, 0))
        produced += size
      }
      PlannedCopy(address~, size~) => {
        let here = plan.source_size + produced
        let encoded = cache.encode(address, here)
        write_encoded_address(address_section, encoded)
        wires.push(make_wire_instruction(Copy, size, encoded.mode))
        produced += size
      }
    }
  }
  if produced != plan.target_size {
    raise LengthMismatch(offset=-1, expected=plan.target_size, actual=produced)
  }
  wires
}

///|
fn code_matches_wire(
  code : CodeInstruction,
  wire : WireInstruction,
  require_fixed_size : Bool,
) -> Bool {
  if code.kind != wire.kind || code.mode != wire.mode {
    return false
  }
  if require_fixed_size {
    code.size > 0 && code.size == wire.size
  } else {
    code.size == wire.size || code.size == 0
  }
}

///|
fn find_pair_code(
  table : Array[CodeTableEntry],
  first : WireInstruction,
  second : WireInstruction,
) -> Int {
  for index, entry in table {
    if entry.second.kind != Noop &&
      code_matches_wire(entry.first, first, true) &&
      code_matches_wire(entry.second, second, true) {
      return index
    }
  }
  -1
}

///|
fn find_single_code(
  table : Array[CodeTableEntry],
  wire : WireInstruction,
) -> Int raise VcdiffError {
  let mut variable_size_code = -1
  for index, entry in table {
    if entry.second.kind == Noop &&
      entry.first.kind == wire.kind &&
      entry.first.mode == wire.mode {
      if entry.first.size == wire.size && wire.size > 0 {
        return index
      }
      if entry.first.size == 0 {
        variable_size_code = index
      }
    }
  }
  if variable_size_code < 0 {
    raise InvalidInstruction(
      offset=-1,
      reason="no default code-table entry can represent the instruction",
    )
  }
  variable_size_code
}

///|
fn write_code_size(
  section : @buffer.Buffer,
  code : CodeInstruction,
  wire : WireInstruction,
) -> Unit raise VcdiffError {
  if code.size == 0 {
    write_varint(section, wire.size)
  }
}

///|
fn encode_wire_instruction_section(
  wires : Array[WireInstruction],
) -> Bytes raise VcdiffError {
  let table = default_code_table()
  let section = @buffer.Buffer(size_hint=wires.length() * 2)
  let mut index = 0
  while index < wires.length() {
    if index + 1 < wires.length() {
      let pair_code = find_pair_code(table, wires[index], wires[index + 1])
      if pair_code >= 0 {
        section.write_byte(pair_code.to_byte())
        index += 2
        continue
      }
    }
    let single_code = find_single_code(table, wires[index])
    let entry = table[single_code]
    section.write_byte(single_code.to_byte())
    write_code_size(section, entry.first, wires[index])
    index += 1
  }
  section.to_bytes()
}

///|
fn encode_window_sections(
  target : Bytes,
  plan : WindowPlan,
) -> EncodedSections raise VcdiffError {
  let data_section = @buffer.Buffer()
  let address_section = @buffer.Buffer()
  let wires = prepare_wire_instructions(
    target, plan, data_section, address_section,
  )
  let instruction_section = encode_wire_instruction_section(wires)
  {
    data: data_section.to_bytes(),
    instructions: instruction_section,
    addresses: address_section.to_bytes(),
    output_size: plan.target_size,
  }
}