///|
#cfg(target="native")
priv suberror QuicPacketProtectionError {
  QuicPacketNumberTooLarge
  QuicPacketNumberLengthInvalid
  QuicProtectedPayloadTooShort
  QuicRetryIntegrityFailed
} derive(Debug, ToJson)

///|
#cfg(target="native")
#warnings("-unused_field")
priv struct QuicProtectedPacket {
  packet : Bytes
  header_length : Int
  packet_number_offset : Int
  packet_number_length : Int
}

///|
#cfg(target="native")
#warnings("-unused_field")
priv struct QuicInitialPacket {
  destination_connection_id : Bytes
  source_connection_id : Bytes
  token : Bytes
  packet_number : Int
  payload : Bytes
}

///|
#cfg(target="native")
#warnings("-unused_field")
priv struct QuicRetryPacket {
  destination_connection_id : Bytes
  source_connection_id : Bytes
  token : Bytes
  integrity_tag : Bytes
}

///|
#cfg(target="native")
#warnings("-unused_field")
priv struct QuicLongProtectedPacket {
  packet_type : Int
  destination_connection_id : Bytes
  source_connection_id : Bytes
  packet_number : Int
  payload : Bytes
}

///|
#cfg(target="native")
#warnings("-unused_field")
priv struct QuicShortPacket {
  destination_connection_id : Bytes
  packet_number : Int
  payload : Bytes
}

///|
#cfg(target="native")
fn quic_copy_slice(data : Bytes, start : Int, end : Int) -> Bytes {
  let out = @buffer.new()
  out.write_bytes(data[start:end])
  out.contents()
}

///|
#cfg(target="native")
fn quic_packet_number_len(packet_number : Int) -> Int raise {
  guard packet_number >= 0 else { raise QuicPacketNumberTooLarge }
  if packet_number <= 0xff {
    1
  } else if packet_number <= 0xffff {
    2
  } else if packet_number <= 0xffffff {
    3
  } else if packet_number <= 0x3fffffff {
    4
  } else {
    raise QuicPacketNumberTooLarge
  }
}

///|
#cfg(target="native")
fn quic_write_packet_number(
  out : @buffer.Buffer,
  packet_number : Int,
  len : Int,
) -> Unit raise {
  guard len >= 1 && len <= 4 else { raise QuicPacketNumberLengthInvalid }
  for i in 0..> shift) & 0xff).to_byte())
  }
}

///|
#cfg(target="native")
fn quic_read_packet_number(data : Bytes, offset : Int, len : Int) -> Int raise {
  guard len >= 1 && len <= 4 else { raise QuicPacketNumberLengthInvalid }
  guard offset + len <= data.length() else { raise QuicPacketTooShort }
  let mut packet_number = 0
  for i in 0.. Bytes raise {
  guard packet_number >= 0 else { raise QuicPacketNumberTooLarge }
  guard iv.length() == 12 else { raise AesBadNonceLength }
  let nonce = FixedArray::make(12, b'\x00')
  for i in 0..<12 {
    nonce[i] = iv[i]
  }
  for i in 0..<8 {
    let shift = (7 - i) * 8
    nonce[4 + i] = nonce[4 + i] ^ ((packet_number >> shift) & 0xff).to_byte()
  }
  nonce.unsafe_reinterpret_as_bytes()
}

///|
#cfg(target="native")
fn quic_build_initial_header(
  destination_connection_id : Bytes,
  source_connection_id : Bytes,
  token : Bytes,
  packet_number : Int,
  protected_payload_length : Int,
) -> (Bytes, Int, Int) raise {
  let packet_number_length = quic_packet_number_len(packet_number)
  let out = @buffer.new()
  out.write_byte((0xc0 | ((packet_number_length - 1) & 0x03)).to_byte())
  out.write_byte(b'\x00')
  out.write_byte(b'\x00')
  out.write_byte(b'\x00')
  out.write_byte(b'\x01')
  out.write_byte(destination_connection_id.length().to_byte())
  out.write_bytes(destination_connection_id)
  out.write_byte(source_connection_id.length().to_byte())
  out.write_bytes(source_connection_id)
  quic_write_varint(out, token.length())
  out.write_bytes(token)
  quic_write_varint(out, packet_number_length + protected_payload_length)
  let packet_number_offset = out.length()
  quic_write_packet_number(out, packet_number, packet_number_length)
  (out.contents(), packet_number_offset, packet_number_length)
}

///|
#cfg(target="native")
fn quic_build_handshake_header(
  destination_connection_id : Bytes,
  source_connection_id : Bytes,
  packet_number : Int,
  protected_payload_length : Int,
) -> (Bytes, Int, Int) raise {
  let packet_number_length = quic_packet_number_len(packet_number)
  let out = @buffer.new()
  out.write_byte((0xe0 | ((packet_number_length - 1) & 0x03)).to_byte())
  out.write_byte(b'\x00')
  out.write_byte(b'\x00')
  out.write_byte(b'\x00')
  out.write_byte(b'\x01')
  out.write_byte(destination_connection_id.length().to_byte())
  out.write_bytes(destination_connection_id)
  out.write_byte(source_connection_id.length().to_byte())
  out.write_bytes(source_connection_id)
  quic_write_varint(out, packet_number_length + protected_payload_length)
  let packet_number_offset = out.length()
  quic_write_packet_number(out, packet_number, packet_number_length)
  (out.contents(), packet_number_offset, packet_number_length)
}

///|
#cfg(target="native")
fn quic_apply_initial_header_protection(
  packet : Bytes,
  header_protection_key : Bytes,
  packet_number_offset : Int,
  packet_number_length : Int,
) -> Bytes raise {
  let sample_offset = packet_number_offset + 4
  guard sample_offset + 16 <= packet.length() else {
    raise QuicProtectedPayloadTooShort
  }
  let mask = aes128_encrypt_block(
    header_protection_key,
    quic_copy_slice(packet, sample_offset, sample_offset + 16),
  )
  let out = FixedArray::make(packet.length(), b'\x00')
  for i in 0.. (Bytes, Int) raise {
  let sample_offset = packet_number_offset + 4
  guard sample_offset + 16 <= packet.length() else {
    raise QuicProtectedPayloadTooShort
  }
  let mask = aes128_encrypt_block(
    header_protection_key,
    quic_copy_slice(packet, sample_offset, sample_offset + 16),
  )
  let packet_number_length = ((packet[0] ^ (mask[0] & b'\x0f')).to_int() & 0x03) +
    1
  let out = FixedArray::make(packet.length(), b'\x00')
  for i in 0.. Bytes raise {
  let sample_offset = packet_number_offset + 4
  guard sample_offset + 16 <= packet.length() else {
    raise QuicProtectedPayloadTooShort
  }
  let mask = aes128_encrypt_block(
    header_protection_key,
    quic_copy_slice(packet, sample_offset, sample_offset + 16),
  )
  let out = FixedArray::make(packet.length(), b'\x00')
  for i in 0.. (Bytes, Int) raise {
  let sample_offset = packet_number_offset + 4
  guard sample_offset + 16 <= packet.length() else {
    raise QuicProtectedPayloadTooShort
  }
  let mask = aes128_encrypt_block(
    header_protection_key,
    quic_copy_slice(packet, sample_offset, sample_offset + 16),
  )
  let packet_number_length = ((packet[0] ^ (mask[0] & b'\x1f')).to_int() & 0x03) +
    1
  let out = FixedArray::make(packet.length(), b'\x00')
  for i in 0.. QuicProtectedPacket raise {
  quic_build_initial_packet(
    destination_connection_id,
    source_connection_id,
    token,
    packet_number,
    payload,
    quic_initial_packet_keys(destination_connection_id, QuicClientInitial),
  )
}

///|
#cfg(target="native")
#warnings("-unused_value")
fn quic_build_retry_packet(
  original_destination_connection_id : Bytes,
  destination_connection_id : Bytes,
  source_connection_id : Bytes,
  token : Bytes,
) -> Bytes raise {
  let out = @buffer.new()
  out.write_byte(b'\xf0')
  out.write_bytes(b"\x00\x00\x00\x01")
  out.write_byte(destination_connection_id.length().to_byte())
  out.write_bytes(destination_connection_id)
  out.write_byte(source_connection_id.length().to_byte())
  out.write_bytes(source_connection_id)
  out.write_bytes(token)
  out.write_bytes(
    quic_retry_integrity_tag(original_destination_connection_id, out.contents()),
  )
  out.contents()
}

///|
#cfg(target="native")
fn quic_retry_integrity_tag(
  original_destination_connection_id : Bytes,
  retry_without_tag : Bytes,
) -> Bytes raise {
  let pseudo = @buffer.new()
  pseudo.write_byte(original_destination_connection_id.length().to_byte())
  pseudo.write_bytes(original_destination_connection_id)
  pseudo.write_bytes(retry_without_tag)
  let result = aes128_gcm_encrypt(
    bytes_from_hex("be0c690b9f66575a1d766b54e368c84e"),
    bytes_from_hex("461599d35d632bf2239825bb"),
    b"",
    pseudo.contents(),
  )
  result.tag
}

///|
#cfg(target="native")
fn quic_parse_retry_packet(
  packet : Bytes,
  original_destination_connection_id? : Bytes = b"",
) -> QuicRetryPacket raise {
  let long_header = quic_parse_long_header(packet)
  guard long_header.packet_type == 3 else { raise QuicPacketTooShort }
  guard packet.length() >= long_header.payload_offset + 16 else {
    raise QuicPacketTooShort
  }
  if original_destination_connection_id.length() > 0 {
    let expected = quic_retry_integrity_tag(
      original_destination_connection_id,
      quic_copy_slice(packet, 0, packet.length() - 16),
    )
    let actual = quic_copy_slice(packet, packet.length() - 16, packet.length())
    guard tls13_bytes_equal(expected, actual) else {
      raise QuicRetryIntegrityFailed
    }
  }
  {
    destination_connection_id: long_header.destination_connection_id,
    source_connection_id: long_header.source_connection_id,
    token: quic_copy_slice(
      packet,
      long_header.payload_offset,
      packet.length() - 16,
    ),
    integrity_tag: quic_copy_slice(
      packet,
      packet.length() - 16,
      packet.length(),
    ),
  }
}

///|
#cfg(target="native")
fn quic_build_initial_packet(
  destination_connection_id : Bytes,
  source_connection_id : Bytes,
  token : Bytes,
  packet_number : Int,
  payload : Bytes,
  keys : QuicPacketKeys,
) -> QuicProtectedPacket raise {
  let protected_payload_length = payload.length() + 16
  let (header, packet_number_offset, packet_number_length) = quic_build_initial_header(
    destination_connection_id, source_connection_id, token, packet_number, protected_payload_length,
  )
  let nonce = quic_initial_nonce(keys.iv, packet_number)
  let result = aes128_gcm_encrypt(keys.key, nonce, payload, header)
  let unprotected = @buffer.new()
  unprotected.write_bytes(header)
  unprotected.write_bytes(result.ciphertext)
  unprotected.write_bytes(result.tag)
  let packet = quic_apply_initial_header_protection(
    unprotected.contents(),
    keys.hp,
    packet_number_offset,
    packet_number_length,
  )
  {
    packet,
    header_length: header.length(),
    packet_number_offset,
    packet_number_length,
  }
}

///|
#cfg(target="native")
fn quic_protect_long_packet(
  header : Bytes,
  packet_number_offset : Int,
  packet_number_length : Int,
  packet_number : Int,
  payload : Bytes,
  keys : QuicPacketKeys,
) -> QuicProtectedPacket raise {
  let nonce = quic_initial_nonce(keys.iv, packet_number)
  let result = aes128_gcm_encrypt(keys.key, nonce, payload, header)
  let unprotected = @buffer.new()
  unprotected.write_bytes(header)
  unprotected.write_bytes(result.ciphertext)
  unprotected.write_bytes(result.tag)
  let packet = quic_apply_initial_header_protection(
    unprotected.contents(),
    keys.hp,
    packet_number_offset,
    packet_number_length,
  )
  {
    packet,
    header_length: header.length(),
    packet_number_offset,
    packet_number_length,
  }
}

///|
#cfg(target="native")
#warnings("-unused_value")
fn quic_build_handshake_packet(
  destination_connection_id : Bytes,
  source_connection_id : Bytes,
  packet_number : Int,
  payload : Bytes,
  keys : QuicPacketKeys,
) -> QuicProtectedPacket raise {
  let protected_payload_length = payload.length() + 16
  let (header, packet_number_offset, packet_number_length) = quic_build_handshake_header(
    destination_connection_id, source_connection_id, packet_number, protected_payload_length,
  )
  quic_protect_long_packet(
    header, packet_number_offset, packet_number_length, packet_number, payload, keys,
  )
}

///|
#cfg(target="native")
#warnings("-unused_value")
fn quic_build_server_initial_packet(
  original_destination_connection_id : Bytes,
  destination_connection_id : Bytes,
  source_connection_id : Bytes,
  packet_number : Int,
  payload : Bytes,
) -> QuicProtectedPacket raise {
  quic_build_initial_packet(
    destination_connection_id,
    source_connection_id,
    b"",
    packet_number,
    payload,
    quic_initial_packet_keys(
      original_destination_connection_id,
      QuicServerInitial,
    ),
  )
}

///|
#cfg(target="native")
#warnings("-unused_value")
fn quic_build_client_initial_datagram(
  destination_connection_id : Bytes,
  source_connection_id : Bytes,
  packet_number : Int,
  crypto_data : Bytes,
  token? : Bytes = b"",
) -> QuicProtectedPacket raise {
  let crypto_frame = quic_encode_crypto_frame(0, crypto_data)
  let mut payload = crypto_frame
  let mut packet = quic_build_client_initial_packet(
    destination_connection_id, source_connection_id, token, packet_number, payload,
  )
  while packet.packet.length() < 1200 {
    let padded = @buffer.new()
    padded.write_bytes(payload)
    padded.write_bytes(quic_encode_padding(1200 - packet.packet.length()))
    payload = padded.contents()
    packet = quic_build_client_initial_packet(
      destination_connection_id, source_connection_id, token, packet_number, payload,
    )
  }
  packet
}

///|
#cfg(target="native")
fn quic_initial_packet_number_offset(packet : Bytes) -> (Int, Int, Int) raise {
  let long_header = quic_parse_long_header(packet)
  guard long_header.packet_type == 0 else { raise QuicPacketTooShort }
  let (token_length, token_start) = quic_read_varint(
    packet,
    long_header.payload_offset,
  )
  let length_offset = token_start + token_length
  let (payload_length, packet_number_offset) = quic_read_varint(
    packet, length_offset,
  )
  (payload_length, packet_number_offset, token_start)
}

///|
#cfg(target="native")
fn quic_handshake_packet_number_offset(packet : Bytes) -> (Int, Int) raise {
  let long_header = quic_parse_long_header(packet)
  guard long_header.packet_type == 2 else { raise QuicPacketTooShort }
  let (payload_length, packet_number_offset) = quic_read_varint(
    packet,
    long_header.payload_offset,
  )
  (payload_length, packet_number_offset)
}

///|
#cfg(target="native")
#warnings("-unused_value")
fn quic_open_client_initial_packet(
  packet : Bytes,
  original_destination_connection_id : Bytes,
) -> QuicInitialPacket raise {
  quic_open_initial_packet(
    packet,
    quic_initial_packet_keys(
      original_destination_connection_id,
      QuicClientInitial,
    ),
  )
}

///|
#cfg(target="native")
#warnings("-unused_value")
fn quic_open_server_initial_packet(
  packet : Bytes,
  original_destination_connection_id : Bytes,
) -> QuicInitialPacket raise {
  quic_open_initial_packet(
    packet,
    quic_initial_packet_keys(
      original_destination_connection_id,
      QuicServerInitial,
    ),
  )
}

///|
#cfg(target="native")
fn quic_open_initial_packet(
  packet : Bytes,
  keys : QuicPacketKeys,
) -> QuicInitialPacket raise {
  let (_, packet_number_offset, _) = quic_initial_packet_number_offset(packet)
  let (unprotected, packet_number_length) = quic_remove_initial_header_protection(
    packet,
    keys.hp,
    packet_number_offset,
  )
  let long_header = quic_parse_long_header(unprotected)
  let (token_length, token_payload_start) = quic_read_varint(
    unprotected,
    long_header.payload_offset,
  )
  let token = @buffer.new()
  token.write_bytes(
    unprotected[token_payload_start:token_payload_start + token_length],
  )
  let (payload_length, packet_number_offset) = quic_read_varint(
    unprotected,
    token_payload_start + token_length,
  )
  let packet_number = quic_read_packet_number(
    unprotected, packet_number_offset, packet_number_length,
  )
  let encrypted_offset = packet_number_offset + packet_number_length
  let encrypted_length = payload_length - packet_number_length
  guard encrypted_length >= 16 else { raise QuicProtectedPayloadTooShort }
  guard encrypted_offset + encrypted_length <= unprotected.length() else {
    raise QuicPacketTooShort
  }
  let ciphertext_length = encrypted_length - 16
  let ciphertext = quic_copy_slice(
    unprotected,
    encrypted_offset,
    encrypted_offset + ciphertext_length,
  )
  let tag = quic_copy_slice(
    unprotected,
    encrypted_offset + ciphertext_length,
    encrypted_offset + encrypted_length,
  )
  let nonce = quic_initial_nonce(keys.iv, packet_number)
  let plaintext = aes128_gcm_decrypt(
    keys.key,
    nonce,
    ciphertext,
    quic_copy_slice(unprotected, 0, encrypted_offset),
    tag,
  )
  {
    destination_connection_id: long_header.destination_connection_id,
    source_connection_id: long_header.source_connection_id,
    token: token.contents(),
    packet_number,
    payload: plaintext,
  }
}

///|
#cfg(target="native")
#warnings("-unused_value")
fn quic_open_handshake_packet(
  packet : Bytes,
  keys : QuicPacketKeys,
) -> QuicLongProtectedPacket raise {
  let (_, packet_number_offset) = quic_handshake_packet_number_offset(packet)
  let (unprotected, packet_number_length) = quic_remove_initial_header_protection(
    packet,
    keys.hp,
    packet_number_offset,
  )
  let long_header = quic_parse_long_header(unprotected)
  let (payload_length, packet_number_offset) = quic_read_varint(
    unprotected,
    long_header.payload_offset,
  )
  let packet_number = quic_read_packet_number(
    unprotected, packet_number_offset, packet_number_length,
  )
  let encrypted_offset = packet_number_offset + packet_number_length
  let encrypted_length = payload_length - packet_number_length
  guard encrypted_length >= 16 else { raise QuicProtectedPayloadTooShort }
  guard encrypted_offset + encrypted_length <= unprotected.length() else {
    raise QuicPacketTooShort
  }
  let ciphertext_length = encrypted_length - 16
  let ciphertext = quic_copy_slice(
    unprotected,
    encrypted_offset,
    encrypted_offset + ciphertext_length,
  )
  let tag = quic_copy_slice(
    unprotected,
    encrypted_offset + ciphertext_length,
    encrypted_offset + encrypted_length,
  )
  let nonce = quic_initial_nonce(keys.iv, packet_number)
  let plaintext = aes128_gcm_decrypt(
    keys.key,
    nonce,
    ciphertext,
    quic_copy_slice(unprotected, 0, encrypted_offset),
    tag,
  )
  {
    packet_type: long_header.packet_type,
    destination_connection_id: long_header.destination_connection_id,
    source_connection_id: long_header.source_connection_id,
    packet_number,
    payload: plaintext,
  }
}

///|
#cfg(target="native")
#warnings("-unused_value")
fn quic_build_short_packet(
  destination_connection_id : Bytes,
  packet_number : Int,
  payload : Bytes,
  keys : QuicPacketKeys,
) -> QuicProtectedPacket raise {
  let packet_number_length = quic_packet_number_len(packet_number)
  let header = @buffer.new()
  header.write_byte((0x40 | ((packet_number_length - 1) & 0x03)).to_byte())
  header.write_bytes(destination_connection_id)
  let packet_number_offset = header.length()
  quic_write_packet_number(header, packet_number, packet_number_length)
  let header = header.contents()
  let nonce = quic_initial_nonce(keys.iv, packet_number)
  let result = aes128_gcm_encrypt(keys.key, nonce, payload, header)
  let unprotected = @buffer.new()
  unprotected.write_bytes(header)
  unprotected.write_bytes(result.ciphertext)
  unprotected.write_bytes(result.tag)
  let packet = quic_apply_short_header_protection(
    unprotected.contents(),
    keys.hp,
    packet_number_offset,
    packet_number_length,
  )
  {
    packet,
    header_length: header.length(),
    packet_number_offset,
    packet_number_length,
  }
}

///|
#cfg(target="native")
#warnings("-unused_value")
fn quic_open_short_packet(
  packet : Bytes,
  destination_connection_id_length : Int,
  keys : QuicPacketKeys,
) -> QuicShortPacket raise {
  guard packet.length() > 1 + destination_connection_id_length else {
    raise QuicPacketTooShort
  }
  let packet_number_offset = 1 + destination_connection_id_length
  let (unprotected, packet_number_length) = quic_remove_short_header_protection(
    packet,
    keys.hp,
    packet_number_offset,
  )
  let packet_number = quic_read_packet_number(
    unprotected, packet_number_offset, packet_number_length,
  )
  let encrypted_offset = packet_number_offset + packet_number_length
  let encrypted_length = unprotected.length() - encrypted_offset
  guard encrypted_length >= 16 else { raise QuicProtectedPayloadTooShort }
  let ciphertext_length = encrypted_length - 16
  let ciphertext = quic_copy_slice(
    unprotected,
    encrypted_offset,
    encrypted_offset + ciphertext_length,
  )
  let tag = quic_copy_slice(
    unprotected,
    encrypted_offset + ciphertext_length,
    unprotected.length(),
  )
  let nonce = quic_initial_nonce(keys.iv, packet_number)
  let plaintext = aes128_gcm_decrypt(
    keys.key,
    nonce,
    ciphertext,
    quic_copy_slice(unprotected, 0, encrypted_offset),
    tag,
  )
  {
    destination_connection_id: quic_copy_slice(
      unprotected,
      1,
      1 + destination_connection_id_length,
    ),
    packet_number,
    payload: plaintext,
  }
}