///|
fn append_bytes(output : Array[Byte], bytes : Bytes) -> Unit {
  for byte in bytes {
    output.push(byte)
  }
}

///|
fn append_run(output : Array[Byte], byte : Byte, count : Int) -> Unit {
  for i = 0; i < count; i = i + 1 {
    output.push(byte)
  }
}

///|
fn checked_target_growth(
  window : ParsedWindow,
  current : Int,
  amount : Int,
) -> Unit raise VcdiffError {
  if amount < 0 || current > window.target_size - amount {
    raise LengthMismatch(
      offset=window.instruction_offset,
      expected=window.target_size,
      actual=current + amount,
    )
  }
}

///|
fn resolve_instruction_size(
  instruction : CodeInstruction,
  instruction_cursor : ByteCursor,
) -> Int raise VcdiffError {
  match instruction.kind {
    Noop => 0
    _ =>
      if instruction.size == 0 {
        read_varint(instruction_cursor)
      } else {
        instruction.size
      }
  }
}

///|
fn copy_from_dictionary(
  window : ParsedWindow,
  dictionary : Bytes,
  target : Array[Byte],
  address : Int,
  size : Int,
) -> Unit raise VcdiffError {
  checked_target_growth(window, target.length(), size)
  let source_size = dictionary.length()
  if address < source_size {
    if size > source_size - address {
      raise InvalidAddress(
        offset=window.address_offset,
        address~,
        limit=source_size - size,
      )
    }
    for index = 0; index < size; index = index + 1 {
      target.push(dictionary[address + index])
    }
  } else {
    let target_address = address - source_size
    if target_address < 0 || target_address >= target.length() {
      raise InvalidAddress(
        offset=window.address_offset,
        address~,
        limit=source_size + target.length() - 1,
      )
    }
    for index = 0; index < size; index = index + 1 {
      let read_index = target_address + index
      if read_index >= target.length() {
        raise InvalidAddress(
          offset=window.address_offset,
          address=address + index,
          limit=source_size + target.length() - 1,
        )
      }
      target.push(target[read_index])
    }
  }
}

///|
fn execute_instruction(
  window : ParsedWindow,
  instruction : CodeInstruction,
  instruction_cursor : ByteCursor,
  data_cursor : ByteCursor,
  address_cursor : ByteCursor,
  address_cache : AddressCache,
  dictionary : Bytes,
  target : Array[Byte],
) -> Bool raise VcdiffError {
  match instruction.kind {
    Noop => false
    Copy => {
      let size = resolve_instruction_size(instruction, instruction_cursor)
      let here = dictionary.length() + target.length()
      let address = address_cache.decode(address_cursor, here, instruction.mode)
      copy_from_dictionary(window, dictionary, target, address, size)
      true
    }
    Add => {
      let size = resolve_instruction_size(instruction, instruction_cursor)
      checked_target_growth(window, target.length(), size)
      append_bytes(target, data_cursor.read_exact(size))
      true
    }
    Run => {
      let size = resolve_instruction_size(instruction, instruction_cursor)
      checked_target_growth(window, target.length(), size)
      let byte = data_cursor.read_byte()
      append_run(target, byte, size)
      true
    }
  }
}

///|
fn decode_window(
  window : ParsedWindow,
  dictionary : Bytes,
  limits : DecodeLimits,
  table : Array[CodeTableEntry],
) -> Bytes raise VcdiffError {
  if window.target_size > limits.max_window_size {
    raise ResourceLimit(
      offset=window.offset,
      resource="window_size",
      limit=limits.max_window_size,
    )
  }
  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 address_cache = AddressCache::new()
  let target : Array[Byte] = []
  let mut instruction_count = 0
  while instruction_cursor.remaining() > 0 {
    let code_offset = instruction_cursor.offset()
    let code = instruction_cursor.read_byte().to_int()
    let entry = table[code]
    if execute_instruction(
        window,
        entry.first,
        instruction_cursor,
        data_cursor,
        address_cursor,
        address_cache,
        dictionary,
        target,
      ) {
      instruction_count += 1
    }
    if execute_instruction(
        window,
        entry.second,
        instruction_cursor,
        data_cursor,
        address_cursor,
        address_cache,
        dictionary,
        target,
      ) {
      instruction_count += 1
    }
    if instruction_count > limits.max_instructions_per_window {
      raise ResourceLimit(
        offset=code_offset,
        resource="instructions_per_window",
        limit=limits.max_instructions_per_window,
      )
    }
  }
  if target.length() != window.target_size {
    raise LengthMismatch(
      offset=window.instruction_offset + window.instructions.length(),
      expected=window.target_size,
      actual=target.length(),
    )
  }
  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(),
    )
  }
  Bytes::from_array(target)
}

///|
fn checked_dictionary_slice(
  bytes : Bytes,
  position : Int,
  size : Int,
  offset : Int,
) -> Bytes raise VcdiffError {
  if size > bytes.length() || position > bytes.length() - size {
    raise InvalidWindow(
      offset~,
      reason="source segment is outside the referenced file",
    )
  }
  bytes[position:position + size].to_owned()
}

///|
fn select_dictionary(
  source : Bytes,
  decoded_target : Array[Byte],
  window : ParsedWindow,
) -> Bytes raise VcdiffError {
  if window.indicator == 0 {
    return b""
  }
  if window.indicator == VCD_SOURCE {
    if source.length() == 0 && window.source_size > 0 {
      raise MissingSource(offset=window.offset)
    }
    return checked_dictionary_slice(
      source,
      window.source_position,
      window.source_size,
      window.offset,
    )
  }
  let previous_target = Bytes::from_array(decoded_target)
  checked_dictionary_slice(
    previous_target,
    window.source_position,
    window.source_size,
    window.offset,
  )
}

///|
/// Decodes an RFC 3284 delta using explicit resource limits.
///
/// ADD, RUN, and COPY instructions use the RFC default code table. COPY
/// addresses may use SELF, HERE, NEAR, or SAME modes.
pub fn decode(
  source : Bytes,
  delta : Bytes,
  limits : DecodeLimits,
) -> Bytes raise VcdiffError {
  let parsed = parse_delta(delta)
  if parsed.windows.length() > limits.max_windows {
    raise ResourceLimit(
      offset=parsed.header.length,
      resource="windows",
      limit=limits.max_windows,
    )
  }
  let output : Array[Byte] = []
  let table = default_code_table()
  for window in parsed.windows {
    let dictionary = select_dictionary(source, output, window)
    let bytes = decode_window(window, dictionary, limits, table)
    if output.length() > limits.max_output_size - bytes.length() {
      raise ResourceLimit(
        offset=window.offset,
        resource="output_size",
        limit=limits.max_output_size,
      )
    }
    append_bytes(output, bytes)
  }
  Bytes::from_array(output)
}