///|
const OGG_PAGE_HEADER_SIZE : Int = 27

///|
const OGG_DEFAULT_PRE_SKIP : UInt16 = 3840

///|
const OGG_BOS : Byte = 0x02

///|
const OGG_EOS : Byte = 0x04

///|
const OGG_CONTINUED : Byte = 0x01

///|
fn ogg_crc_page(page : Array[Byte]) -> UInt {
  let mut crc = 0U
  for index = 0; index < page.length(); index = index + 1 {
    let byte : Byte = if index >= 22 && index < 26 { 0 } else { page[index] }
    crc = crc ^ (byte.to_uint() << 24)
    for bit = 0; bit < 8; bit = bit + 1 {
      crc = if (crc & 0x80000000U) != 0 {
        (crc << 1) ^ 0x04c11db7U
      } else {
        crc << 1
      }
    }
  }
  crc
}

///|
pub(all) struct UserComment {
  key : String
  value : String
} derive(Debug, Eq)

///|
pub fn UserComment::new(key~ : String, value~ : String) -> UserComment {
  { key, value, }
}

///|
pub fn UserComment::key(self : UserComment) -> String {
  self.key
}

///|
pub fn UserComment::value(self : UserComment) -> String {
  self.value
}

///|
pub(all) struct OpusTags {
  vendor : String
  comments : Array[UserComment]
} derive(Debug, Eq)

///|
pub fn OpusTags::new(
  vendor~ : String,
  comments? : Array[UserComment] = [],
) -> OpusTags {
  { vendor, comments: comments.copy(), }
}

///|
pub fn OpusTags::vendor(self : OpusTags) -> String {
  self.vendor
}

///|
pub fn OpusTags::comments(self : OpusTags) -> Array[UserComment] {
  self.comments.copy()
}

///|
pub(all) struct OggHeader {
  version : Byte
  channels : Byte
  pre_skip : UInt16
  sample_rate : UInt
  output_gain : UInt16
  channel_mapping_family : Byte
  stream_count : Byte
  coupled_count : Byte
  channel_mapping : Bytes
} derive(Debug, Eq)

///|
pub fn OggHeader::version(self : OggHeader) -> Byte {
  self.version
}

///|
pub fn OggHeader::channels(self : OggHeader) -> Byte {
  self.channels
}

///|
pub fn OggHeader::pre_skip(self : OggHeader) -> UInt16 {
  self.pre_skip
}

///|
pub fn OggHeader::sample_rate(self : OggHeader) -> UInt {
  self.sample_rate
}

///|
pub fn OggHeader::output_gain(self : OggHeader) -> UInt16 {
  self.output_gain
}

///|
pub fn OggHeader::channel_mapping_family(self : OggHeader) -> Byte {
  self.channel_mapping_family
}

///|
pub fn OggHeader::stream_count(self : OggHeader) -> Byte {
  self.stream_count
}

///|
pub fn OggHeader::coupled_count(self : OggHeader) -> Byte {
  self.coupled_count
}

///|
pub fn OggHeader::channel_mapping(self : OggHeader) -> Bytes {
  self.channel_mapping
}

///|
pub(all) struct OggPageHeader {
  header_type : Byte
  granule_position : UInt64
  serial : UInt
  sequence : UInt
  checksum : UInt
  segments : Bytes
} derive(Debug, Eq)

///|
pub fn OggPageHeader::header_type(self : OggPageHeader) -> Byte {
  self.header_type
}

///|
pub fn OggPageHeader::granule_position(self : OggPageHeader) -> UInt64 {
  self.granule_position
}

///|
pub fn OggPageHeader::serial(self : OggPageHeader) -> UInt {
  self.serial
}

///|
pub fn OggPageHeader::sequence(self : OggPageHeader) -> UInt {
  self.sequence
}

///|
pub fn OggPageHeader::checksum(self : OggPageHeader) -> UInt {
  self.checksum
}

///|
pub fn OggPageHeader::segments(self : OggPageHeader) -> Bytes {
  self.segments
}

///|
pub fn OggPageHeader::is_beginning_of_stream(self : OggPageHeader) -> Bool {
  (self.header_type & OGG_BOS) != 0
}

///|
pub fn OggPageHeader::is_end_of_stream(self : OggPageHeader) -> Bool {
  (self.header_type & OGG_EOS) != 0
}

///|
pub(all) struct OggPage {
  header : OggPageHeader
  payload : Bytes
} derive(Debug, Eq)

///|
pub fn OggPage::header(self : OggPage) -> OggPageHeader {
  self.header
}

///|
pub fn OggPage::payload(self : OggPage) -> Bytes {
  self.payload
}

///|
pub(all) struct OggPacket {
  data : Bytes
  granule_position : UInt64
  end_of_stream : Bool
} derive(Debug, Eq)

///|
pub fn OggPacket::data(self : OggPacket) -> Bytes {
  self.data
}

///|
pub fn OggPacket::granule_position(self : OggPacket) -> UInt64 {
  self.granule_position
}

///|
pub fn OggPacket::is_end_of_stream(self : OggPacket) -> Bool {
  self.end_of_stream
}

///|
fn ogg_encode_page(
  payload : Bytes,
  header_type : Byte,
  granule_position : UInt64,
  serial : UInt,
  sequence : UInt,
) -> Bytes raise MediaError {
  let segment_count = payload.length() / 255 + 1
  if segment_count > 255 {
    raise InvalidMedia("Ogg packet needs more than 255 lacing values")
  }
  let page : Array[Byte] = []
  for byte in b"OggS" {
    page.push(byte)
  }
  page.push(0)
  page.push(header_type)
  media_write_u64_le(page, granule_position)
  media_write_u32_le(page, serial)
  media_write_u32_le(page, sequence)
  media_write_u32_le(page, 0)
  page.push(segment_count.to_byte())
  let full_segments = payload.length() / 255
  for index = 0; index < full_segments; index = index + 1 {
    page.push(255)
  }
  page.push((payload.length() % 255).to_byte())
  for byte in payload {
    page.push(byte)
  }
  let checksum = ogg_crc_page(page)
  page[22] = checksum.to_byte()
  page[23] = (checksum >> 8).to_byte()
  page[24] = (checksum >> 16).to_byte()
  page[25] = (checksum >> 24).to_byte()
  Bytes::from_array(page)
}

///|
fn ogg_parse_page_at(
  data : Bytes,
  offset : Int,
  verify_checksum : Bool,
) -> (OggPage, Int) raise MediaError {
  if offset < 0 || offset + OGG_PAGE_HEADER_SIZE > data.length() {
    raise InvalidMedia("truncated Ogg page header")
  }
  if data[offset:offset + 4] != b"OggS" {
    raise InvalidMedia("invalid Ogg capture pattern")
  }
  if data[offset + 4] != 0 {
    raise InvalidMedia("unsupported Ogg bitstream version")
  }
  let segment_count = data[offset + 26].to_int()
  let table_start = offset + OGG_PAGE_HEADER_SIZE
  let body_start = table_start + segment_count
  if body_start > data.length() {
    raise InvalidMedia("truncated Ogg segment table")
  }
  let mut body_length = 0
  for lace in data[table_start:body_start] {
    body_length += lace.to_int()
  }
  let page_end = body_start + body_length
  if page_end > data.length() {
    raise InvalidMedia("Ogg lacing values exceed page data")
  }
  let checksum = media_read_u32_le(data, offset + 22)
  if verify_checksum {
    let page = data[offset:page_end].to_array()
    if ogg_crc_page(page) != checksum {
      raise InvalidMedia("Ogg page checksum mismatch")
    }
  }
  let header : OggPageHeader = {
    header_type: data[offset + 5],
    granule_position: media_read_u64_le(data, offset + 6),
    serial: media_read_u32_le(data, offset + 14),
    sequence: media_read_u32_le(data, offset + 18),
    checksum,
    segments: data[table_start:body_start].to_owned(),
  }
  ({ header, payload: data[body_start:page_end].to_owned(), }, page_end)
}

///|
fn ogg_opus_head(
  sample_rate : UInt,
  channels : Byte,
  pre_skip : UInt16,
) -> Bytes {
  let output : Array[Byte] = []
  for byte in b"OpusHead" {
    output.push(byte)
  }
  output.push(1)
  output.push(channels)
  media_write_u16_le(output, pre_skip)
  media_write_u32_le(output, sample_rate)
  media_write_u16_le(output, 0)
  output.push(0)
  Bytes::from_array(output)
}

///|
fn ogg_opus_tags(tags : OpusTags) -> Bytes {
  let output : Array[Byte] = []
  for byte in b"OpusTags" {
    output.push(byte)
  }
  let vendor = @utf8.encode(tags.vendor)
  media_write_u32_le(output, vendor.length().reinterpret_as_uint())
  for byte in vendor {
    output.push(byte)
  }
  media_write_u32_le(output, tags.comments.length().reinterpret_as_uint())
  for comment in tags.comments {
    let encoded = @utf8.encode(comment.key + "=" + comment.value)
    media_write_u32_le(output, encoded.length().reinterpret_as_uint())
    for byte in encoded {
      output.push(byte)
    }
  }
  Bytes::from_array(output)
}

///|
pub struct OggWriter {
  sample_rate : UInt
  channels : Byte
  pre_skip : UInt16
  serial : UInt
  output : Array[Byte]
  mut sequence : UInt
  mut last_granule_position : UInt64
  mut last_rtp_timestamp : UInt?
  mut closed : Bool
}

///|
pub fn OggWriter::new(
  sample_rate~ : UInt,
  channels~ : Byte,
  pre_skip? : UInt16 = OGG_DEFAULT_PRE_SKIP,
  serial? : UInt = 0x72746301U,
  tags? : OpusTags = { vendor: "rtc.mbt", comments: [], },
) -> OggWriter raise MediaError {
  if sample_rate == 0 || channels == 0 || channels > 2 {
    raise InvalidMedia("Ogg Opus mapping family zero supports mono or stereo")
  }
  let output : Array[Byte] = []
  let id_page = ogg_encode_page(
    ogg_opus_head(sample_rate, channels, pre_skip),
    OGG_BOS,
    0UL,
    serial,
    0U,
  )
  let tags_page = ogg_encode_page(ogg_opus_tags(tags), 0, 0UL, serial, 1U)
  for byte in id_page {
    output.push(byte)
  }
  for byte in tags_page {
    output.push(byte)
  }
  {
    sample_rate,
    channels,
    pre_skip,
    serial,
    output,
    sequence: 2U,
    last_granule_position: 0UL,
    last_rtp_timestamp: None,
    closed: false,
  }
}

///|
fn OggWriter::append_page(
  self : OggWriter,
  payload : Bytes,
  flags : Byte,
  granule_position : UInt64,
) -> Unit raise MediaError {
  let page = ogg_encode_page(
    payload,
    flags,
    granule_position,
    self.serial,
    self.sequence,
  )
  for byte in page {
    self.output.push(byte)
  }
  self.sequence += 1
}

///|
pub fn OggWriter::write_opus_packet(
  self : OggWriter,
  payload : Bytes,
  granule_position : UInt64,
) -> Unit raise MediaError {
  if self.closed {
    raise InvalidMedia("cannot write an Ogg packet after close")
  }
  if payload.is_empty() {
    raise InvalidMedia("cannot write an empty Opus packet")
  }
  if granule_position < self.last_granule_position {
    raise InvalidMedia("Ogg granule position moved backwards")
  }
  self.append_page(payload, 0, granule_position)
  self.last_granule_position = granule_position
}

///|
pub fn OggWriter::write_rtp(
  self : OggWriter,
  packet : @rtp.Packet,
) -> Unit raise MediaError {
  if packet.payload().is_empty() {
    return
  }
  let granule = match self.last_rtp_timestamp {
    None => self.pre_skip.to_uint64()
    Some(previous) =>
      self.last_granule_position + (packet.timestamp() - previous).to_uint64()
  }
  self.write_opus_packet(packet.payload(), granule)
  self.last_rtp_timestamp = Some(packet.timestamp())
}

///|
pub fn OggWriter::close(self : OggWriter) -> Unit raise MediaError {
  if self.closed {
    return
  }
  self.append_page(b"", OGG_EOS, self.last_granule_position)
  self.closed = true
}

///|
pub fn OggWriter::bytes(self : OggWriter) -> Bytes {
  Bytes::from_array(self.output)
}

///|
fn parse_opus_head(payload : Bytes) -> OggHeader raise MediaError {
  if payload.length() < 19 || payload[0:8] != b"OpusHead" {
    raise InvalidMedia("invalid OpusHead packet")
  }
  let version = payload[8]
  let channels = payload[9]
  let pre_skip = media_read_u16_le(payload, 10)
  let sample_rate = media_read_u32_le(payload, 12)
  let output_gain = media_read_u16_le(payload, 16)
  let family = payload[18]
  if version > 15 || channels == 0 || sample_rate == 0 {
    raise InvalidMedia("unsupported OpusHead parameters")
  }
  if family == 0 {
    if channels > 2 || payload.length() != 19 {
      raise InvalidMedia("invalid Opus mapping family zero header")
    }
    return {
      version,
      channels,
      pre_skip,
      sample_rate,
      output_gain,
      channel_mapping_family: family,
      stream_count: 0,
      coupled_count: 0,
      channel_mapping: b"",
    }
  }
  let expected = 21 + channels.to_int()
  if payload.length() < expected {
    raise InvalidMedia("truncated Opus channel mapping")
  }
  {
    version,
    channels,
    pre_skip,
    sample_rate,
    output_gain,
    channel_mapping_family: family,
    stream_count: payload[19],
    coupled_count: payload[20],
    channel_mapping: payload[21:expected].to_owned(),
  }
}

///|
fn parse_opus_tags(payload : Bytes) -> OpusTags raise MediaError {
  if payload.length() < 16 || payload[0:8] != b"OpusTags" {
    raise InvalidMedia("invalid OpusTags packet")
  }
  let vendor_length = media_read_u32_le(payload, 8).reinterpret_as_int()
  let vendor_start = 12
  let vendor_end = vendor_start + vendor_length
  if vendor_length < 0 || vendor_end + 4 > payload.length() {
    raise InvalidMedia("truncated OpusTags vendor")
  }
  let vendor = @utf8.decode(payload[vendor_start:vendor_end]) catch {
    _ => raise InvalidMedia("OpusTags vendor is not valid UTF-8")
  }
  let comment_count = media_read_u32_le(payload, vendor_end).reinterpret_as_int()
  let comments : Array[UserComment] = []
  let mut offset = vendor_end + 4
  for comment_index = 0
      comment_index < comment_count
      comment_index = comment_index + 1 {
    if offset + 4 > payload.length() {
      raise InvalidMedia("truncated OpusTags comment length")
    }
    let length = media_read_u32_le(payload, offset).reinterpret_as_int()
    offset += 4
    if length < 0 || offset + length > payload.length() {
      raise InvalidMedia("truncated OpusTags comment")
    }
    let value = @utf8.decode(payload[offset:offset + length]) catch {
      _ => raise InvalidMedia("OpusTags comment is not valid UTF-8")
    }
    match value.split_once("=") {
      Some((key, comment_value)) =>
        comments.push(
          UserComment::new(key=key.to_owned(), value=comment_value.to_owned()),
        )
      None => ()
    }
    offset += length
  }
  { vendor, comments, }
}

///|
fn ogg_single_packet(page : OggPage) -> Bytes raise MediaError {
  let segments = page.header.segments
  if segments.is_empty() || segments[segments.length() - 1] == 255 {
    raise InvalidMedia("Ogg header packet spans multiple pages")
  }
  let mut terminators = 0
  for value in segments {
    if value < 255 {
      terminators += 1
    }
  }
  if terminators != 1 {
    raise InvalidMedia("Ogg header page contains multiple packets")
  }
  page.payload
}

///|
pub struct OggReader {
  data : Bytes
  header : OggHeader
  tags : OpusTags
  verify_checksum : Bool
  mut offset : Int
  pending : Array[Byte]
  queued : Array[OggPacket]
  serial : UInt
  mut expected_sequence : UInt
  mut reached_eos : Bool
}

///|
pub fn OggReader::new(
  data : Bytes,
  verify_checksum? : Bool = true,
) -> OggReader raise MediaError {
  let (id_page, after_id) = ogg_parse_page_at(data, 0, verify_checksum)
  if !id_page.header.is_beginning_of_stream() {
    raise InvalidMedia("OpusHead page is not beginning-of-stream")
  }
  let serial = id_page.header.serial
  let header = parse_opus_head(ogg_single_packet(id_page))
  let (tags_page, after_tags) = ogg_parse_page_at(
    data, after_id, verify_checksum,
  )
  if tags_page.header.serial != serial || tags_page.header.sequence != 1U {
    raise InvalidMedia("Ogg Opus header page sequence is invalid")
  }
  let tags = parse_opus_tags(ogg_single_packet(tags_page))
  {
    data,
    header,
    tags,
    verify_checksum,
    offset: after_tags,
    pending: [],
    queued: [],
    serial,
    expected_sequence: 2U,
    reached_eos: false,
  }
}

///|
pub fn OggReader::header(self : OggReader) -> OggHeader {
  self.header
}

///|
pub fn OggReader::tags(self : OggReader) -> OpusTags {
  self.tags
}

///|
fn OggReader::read_page(self : OggReader) -> OggPage? raise MediaError {
  if self.offset == self.data.length() {
    return None
  }
  let (page, next) = ogg_parse_page_at(
    self.data,
    self.offset,
    self.verify_checksum,
  )
  if page.header.serial != self.serial {
    raise InvalidMedia("unexpected logical stream in Ogg Opus data")
  }
  if page.header.sequence != self.expected_sequence {
    raise InvalidMedia("Ogg page sequence number gap")
  }
  self.offset = next
  self.expected_sequence += 1
  Some(page)
}

///|
fn OggReader::queue_page_packets(
  self : OggReader,
  page : OggPage,
) -> Unit raise MediaError {
  let continued = (page.header.header_type & OGG_CONTINUED) != 0
  if continued != !self.pending.is_empty() {
    raise InvalidMedia("Ogg continued-packet flag does not match lacing state")
  }
  let mut body_offset = 0
  for lace in page.header.segments {
    let length = lace.to_int()
    if body_offset + length > page.payload.length() {
      raise InvalidMedia("Ogg segment exceeds page body")
    }
    for byte in page.payload[body_offset:body_offset + length] {
      self.pending.push(byte)
    }
    body_offset += length
    if lace < 255 {
      if !self.pending.is_empty() {
        self.queued.push({
          data: Bytes::from_array(self.pending),
          granule_position: page.header.granule_position,
          end_of_stream: page.header.is_end_of_stream(),
        })
      }
      self.pending.clear()
    }
  }
  if body_offset != page.payload.length() {
    raise InvalidMedia("Ogg page has unreferenced body bytes")
  }
  if page.header.is_end_of_stream() {
    self.reached_eos = true
    if !self.pending.is_empty() {
      raise InvalidMedia("Ogg stream ends with an incomplete packet")
    }
  }
}

///|
pub fn OggReader::next_packet(self : OggReader) -> OggPacket? raise MediaError {
  while self.queued.is_empty() {
    if self.reached_eos {
      return None
    }
    match self.read_page() {
      None => {
        if !self.pending.is_empty() {
          raise InvalidMedia("Ogg stream ends with an incomplete packet")
        }
        return None
      }
      Some(page) => self.queue_page_packets(page)
    }
  }
  Some(self.queued.remove(0))
}

///|
pub fn OggReader::bytes_read(self : OggReader) -> Int {
  self.offset
}