///|
#cfg(target="native")
priv suberror QuicFrameError {
  QuicFrameTruncated
  QuicFrameUnsupported
  QuicCryptoOutOfOrder
} derive(Debug, ToJson)

///|
#cfg(target="native")
let quic_frame_padding : Int = 0x00

///|
#cfg(target="native")
let quic_frame_ping : Int = 0x01

///|
#cfg(target="native")
let quic_frame_ack : Int = 0x02

///|
#cfg(target="native")
let quic_frame_crypto : Int = 0x06

///|
#cfg(target="native")
let quic_frame_stream_base : Int = 0x08

///|
#cfg(target="native")
#warnings("-unused_field")
priv struct QuicCryptoFrame {
  offset : Int
  data : Bytes
}

///|
#cfg(target="native")
#warnings("-unused_field")
priv struct QuicAckFrame {
  largest_acknowledged : Int
  ack_delay : Int
  first_ack_range : Int
}

///|
#cfg(target="native")
#warnings("-unused_field")
priv struct QuicStreamFrame {
  stream_id : Int
  offset : Int
  fin : Bool
  data : Bytes
}

///|
#cfg(target="native")
#warnings("-unused_value")
fn quic_encode_padding(length : Int) -> Bytes raise {
  guard length >= 0 else { raise QuicFrameUnsupported }
  let out = @buffer.new()
  for _ in 0.. Bytes {
  let out = @buffer.new()
  out.write_byte(quic_frame_ping.to_byte())
  out.contents()
}

///|
#cfg(target="native")
#warnings("-unused_value")
fn quic_encode_ack(
  largest_acknowledged : Int,
  ack_delay : Int,
  first_ack_range : Int,
) -> Bytes raise {
  let out = @buffer.new()
  quic_write_varint(out, quic_frame_ack)
  quic_write_varint(out, largest_acknowledged)
  quic_write_varint(out, ack_delay)
  quic_write_varint(out, 0)
  quic_write_varint(out, first_ack_range)
  out.contents()
}

///|
#cfg(target="native")
#warnings("-unused_value")
fn quic_decode_ack(data : Bytes, offset : Int) -> (QuicAckFrame, Int) raise {
  let (frame_type, pos) = quic_read_varint(data, offset)
  guard frame_type == quic_frame_ack else { raise QuicFrameUnsupported }
  let (largest_acknowledged, pos) = quic_read_varint(data, pos)
  let (ack_delay, pos) = quic_read_varint(data, pos)
  let (range_count, pos) = quic_read_varint(data, pos)
  guard range_count == 0 else { raise QuicFrameUnsupported }
  let (first_ack_range, pos) = quic_read_varint(data, pos)
  ({ largest_acknowledged, ack_delay, first_ack_range }, pos)
}

///|
#cfg(target="native")
#warnings("-unused_value")
fn quic_encode_crypto_frame(offset : Int, data : Bytes) -> Bytes raise {
  let out = @buffer.new()
  quic_write_varint(out, quic_frame_crypto)
  quic_write_varint(out, offset)
  quic_write_varint(out, data.length())
  out.write_bytes(data)
  out.contents()
}

///|
#cfg(target="native")
#warnings("-unused_value")
fn quic_decode_crypto_frame(
  data : Bytes,
  offset : Int,
) -> (QuicCryptoFrame, Int) raise {
  let (frame_type, pos) = quic_read_varint(data, offset)
  guard frame_type == quic_frame_crypto else { raise QuicFrameUnsupported }
  let (crypto_offset, pos) = quic_read_varint(data, pos)
  let (len, pos) = quic_read_varint(data, pos)
  guard pos + len <= data.length() else { raise QuicFrameTruncated }
  (
    { offset: crypto_offset, data: quic_copy_slice(data, pos, pos + len) },
    pos + len,
  )
}

///|
#cfg(target="native")
#warnings("-unused_value")
fn quic_encode_stream_frame(
  stream_id : Int,
  offset : Int,
  fin : Bool,
  data : Bytes,
) -> Bytes raise {
  let out = @buffer.new()
  let mut frame_type = quic_frame_stream_base | 0x02
  if offset > 0 {
    frame_type = frame_type | 0x04
  }
  if fin {
    frame_type = frame_type | 0x01
  }
  quic_write_varint(out, frame_type)
  quic_write_varint(out, stream_id)
  if offset > 0 {
    quic_write_varint(out, offset)
  }
  quic_write_varint(out, data.length())
  out.write_bytes(data)
  out.contents()
}

///|
#cfg(target="native")
#warnings("-unused_value")
fn quic_decode_stream_frame(
  data : Bytes,
  offset : Int,
) -> (QuicStreamFrame, Int) raise {
  let (frame_type, pos) = quic_read_varint(data, offset)
  guard (frame_type & 0xf8) == quic_frame_stream_base else {
    raise QuicFrameUnsupported
  }
  let has_offset = (frame_type & 0x04) != 0
  let has_length = (frame_type & 0x02) != 0
  let fin = (frame_type & 0x01) != 0
  let (stream_id, pos) = quic_read_varint(data, pos)
  let (stream_offset, pos) = if has_offset {
    quic_read_varint(data, pos)
  } else {
    (0, pos)
  }
  let (len, pos) = if has_length {
    quic_read_varint(data, pos)
  } else {
    (data.length() - pos, pos)
  }
  guard pos + len <= data.length() else { raise QuicFrameTruncated }
  (
    {
      stream_id,
      offset: stream_offset,
      fin,
      data: quic_copy_slice(data, pos, pos + len),
    },
    pos + len,
  )
}

///|
#cfg(target="native")
#warnings("-unused_value")
fn quic_collect_crypto_data(payload : Bytes) -> Bytes raise {
  let out = @buffer.new()
  for offset = 0; offset < payload.length(); {
    let (frame_type, _) = quic_read_varint(payload, offset)
    if frame_type == quic_frame_padding {
      continue offset + 1
    } else if frame_type == quic_frame_ping {
      continue offset + 1
    } else if frame_type == quic_frame_ack {
      let (_, next) = quic_decode_ack(payload, offset)
      continue next
    } else if frame_type == quic_frame_crypto {
      let (frame, next) = quic_decode_crypto_frame(payload, offset)
      guard frame.offset == out.length() else { raise QuicCryptoOutOfOrder }
      out.write_bytes(frame.data)
      continue next
    } else {
      raise QuicFrameUnsupported
    }
  }
  out.contents()
}

///|
#cfg(target="native")
#warnings("-unused_value")
fn quic_collect_stream_data(payload : Bytes, stream_id : Int) -> Bytes raise {
  let out = @buffer.new()
  for offset = 0; offset < payload.length(); {
    let (frame_type, _) = quic_read_varint(payload, offset)
    if frame_type == quic_frame_padding {
      continue offset + 1
    } else if frame_type == quic_frame_ping {
      continue offset + 1
    } else if frame_type == quic_frame_ack {
      let (_, next) = quic_decode_ack(payload, offset)
      continue next
    } else if (frame_type & 0xf8) == quic_frame_stream_base {
      let (frame, next) = quic_decode_stream_frame(payload, offset)
      if frame.stream_id == stream_id {
        guard frame.offset == out.length() else { raise QuicCryptoOutOfOrder }
        out.write_bytes(frame.data)
      }
      continue next
    } else {
      raise QuicFrameUnsupported
    }
  }
  out.contents()
}