///|
/// Errors raised while parsing Ogg pages and Opus stream headers.
pub(all) suberror OggOpusError {
  InvalidOggPage(reason~ : String)
  InvalidOpusStream(reason~ : String)
} derive(Debug, Eq)

///|
fn ogg_crc_table() -> FixedArray[UInt] {
  FixedArray::makei(256, index => {
    let mut value = index.reinterpret_as_uint() << 24
    for _ in 0..<8 {
      value = if (value & 0x80000000U) != 0 {
        (value << 1) ^ 0x04C11DB7U
      } else {
        value << 1
      }
    }
    value
  })
}

///|
fn ogg_page_crc(page : ArrayView[Byte]) -> UInt {
  let table = ogg_crc_table()
  let mut crc = 0U
  for index, byte in page {
    let value : Byte = if index >= 22 && index < 26 { 0 } else { byte }
    let table_index = ((crc >> 24) ^ value.to_uint()).reinterpret_as_int()
    crc = (crc << 8) ^ table[table_index]
  }
  crc
}

///|
fn read_array_u32_le(bytes : Array[Byte], offset : Int) -> UInt {
  bytes[offset].to_uint() |
  (bytes[offset + 1].to_uint() << 8) |
  (bytes[offset + 2].to_uint() << 16) |
  (bytes[offset + 3].to_uint() << 24)
}

///|
/// Streaming Ogg packet reassembler. Feed arbitrary chunks with `push`, then
/// pull complete packets with `next_packet`.
pub struct OggPacketReader {
  priv buffer : Array[Byte]
  priv mut cursor : Int
  priv partial_packet : Array[Byte]
  priv ready_packets : Array[Bytes]
  priv mut ready_cursor : Int
  priv mut stream_serial : UInt?
  priv mut expected_page_sequence : UInt?
}

///|
/// Create an empty reader; feed it Ogg data with `push` and drain packets
/// with `next_packet`.
pub fn OggPacketReader::new() -> OggPacketReader {
  {
    buffer: [],
    cursor: 0,
    partial_packet: [],
    ready_packets: [],
    ready_cursor: 0,
    stream_serial: None,
    expected_page_sequence: None,
  }
}

///|
/// Add bytes to the streaming input without requiring a page boundary.
pub fn OggPacketReader::push(self : OggPacketReader, bytes : Bytes) -> Unit {
  for byte in bytes {
    self.buffer.push(byte)
  }
}

///|
fn OggPacketReader::take_ready(self : OggPacketReader) -> Bytes? {
  if self.ready_cursor >= self.ready_packets.length() {
    self.ready_packets.clear()
    self.ready_cursor = 0
    return None
  }
  let packet = self.ready_packets[self.ready_cursor]
  self.ready_cursor += 1
  Some(packet)
}

///|
fn OggPacketReader::parse_page(
  self : OggPacketReader,
) -> Bool raise OggOpusError {
  let available = self.buffer.length() - self.cursor
  if available < 27 {
    return false
  }
  let start = self.cursor
  if self.buffer[start] != b'O' ||
    self.buffer[start + 1] != b'g' ||
    self.buffer[start + 2] != b'g' ||
    self.buffer[start + 3] != b'S' {
    raise InvalidOggPage(reason="missing OggS capture pattern")
  }
  if self.buffer[start + 4] != 0 {
    raise InvalidOggPage(reason="unsupported Ogg bitstream version")
  }
  let segment_count = self.buffer[start + 26].to_int()
  if available < 27 + segment_count {
    return false
  }
  let mut body_length = 0
  for index in 0.. self.stream_serial = Some(serial)
    Some(expected) if expected != serial =>
      raise InvalidOggPage(
        reason="multiple logical Ogg streams are unsupported",
      )
    Some(_) => ()
  }
  match self.expected_page_sequence {
    None => ()
    Some(expected) if expected != page_sequence =>
      raise InvalidOggPage(reason="non-contiguous Ogg page sequence")
    Some(_) => ()
  }
  self.expected_page_sequence = Some(page_sequence + 1)

  let continued = (self.buffer[start + 5].to_int() & 0x01) != 0
  if continued && self.partial_packet.is_empty() {
    raise InvalidOggPage(reason="continued page has no preceding packet")
  }
  if !continued && !self.partial_packet.is_empty() {
    raise InvalidOggPage(reason="continued packet is missing its page flag")
  }
  let mut body_cursor = start + 27 + segment_count
  for index in 0.. Bytes? raise OggOpusError {
  if self.take_ready() is Some(packet) {
    return Some(packet)
  }
  while self.parse_page() {
    if self.take_ready() is Some(packet) {
      return Some(packet)
    }
  }
  None
}

///|
fn OggPacketReader::has_incomplete_input(self : OggPacketReader) -> Bool {
  self.cursor < self.buffer.length() || !self.partial_packet.is_empty()
}

///|
fn has_magic(bytes : Bytes, magic : Bytes) -> Bool {
  bytes.length() >= magic.length() && bytes[:magic.length()] == magic
}

///|
/// In-memory Ogg/Opus source. Opus packets must already represent 20 ms audio
/// frames; this demuxer does not decode or resample them.
pub struct OggOpusSource {
  priv reader : OggPacketReader
  priv input : &@io.Reader?
  priv channels_ : Int
  priv pre_skip_ : Int
}

///|
fn validate_opus_headers(
  head : Bytes?,
  tags : Bytes?,
) -> (Int, Int) raise OggOpusError {
  guard head is Some(head) else {
    raise InvalidOpusStream(reason="missing OpusHead packet")
  }
  if !has_magic(head, b"OpusHead") || head.length() < 19 {
    raise InvalidOpusStream(reason="invalid OpusHead packet")
  }
  if head[8] != 1 {
    raise InvalidOpusStream(reason="unsupported OpusHead version")
  }
  let channels = head[9].to_int()
  if channels == 0 {
    raise InvalidOpusStream(reason="OpusHead channel count is zero")
  }
  let pre_skip = head[10].to_int() | (head[11].to_int() << 8)
  guard tags is Some(tags) else {
    raise InvalidOpusStream(reason="missing OpusTags packet")
  }
  if !has_magic(tags, b"OpusTags") {
    raise InvalidOpusStream(reason="invalid OpusTags packet")
  }
  (channels, pre_skip)
}

///|
/// Parse a complete in-memory Ogg Opus file into an audio source, validating
/// its `OpusHead`/`OpusTags` headers.
pub fn OggOpusSource::from_bytes(
  bytes : Bytes,
) -> OggOpusSource raise OggOpusError {
  let reader = OggPacketReader::new()
  reader.push(bytes)
  let head = reader.next_packet()
  let tags = reader.next_packet()
  let (channels, pre_skip) = validate_opus_headers(head, tags)
  { reader, input: None, channels_: channels, pre_skip_: pre_skip, }
}

///|
/// Open an Ogg/Opus source backed by an asynchronous reader. The constructor
/// consumes and validates the `OpusHead` and `OpusTags` packets; audio packets
/// are read lazily by `AudioSource::next_frame`.
///
/// # Example
/// ```mbt nocheck
/// async test {
///   let file = @fs.open("voice.ogg")
///   defer file.close()
///   let source = @voice.OggOpusSource::from_reader(file)
///   let audio : &@voice.AudioSource = source
///   while audio.next_frame() is Some(frame) {
///     play_opus_frame(frame)
///   }
/// }
/// ```
pub async fn OggOpusSource::from_reader(input : &@io.Reader) -> OggOpusSource {
  let reader = OggPacketReader::new()
  let mut head : Bytes? = None
  let mut tags : Bytes? = None
  for ;; {
    while reader.next_packet() is Some(packet) {
      if head is None {
        head = Some(packet)
      } else {
        tags = Some(packet)
        break
      }
    }
    if tags is Some(_) {
      break
    }
    match input.read_some(max_len=8192) {
      Some(chunk) => reader.push(chunk)
      None => break
    }
  }
  let (channels, pre_skip) = validate_opus_headers(head, tags)
  { reader, input: Some(input), channels_: channels, pre_skip_: pre_skip, }
}

///|
/// The channel count declared by the stream's `OpusHead` header.
pub fn OggOpusSource::channels(self : OggOpusSource) -> Int {
  self.channels_
}

///|
/// Samples (at 48 kHz) to drop before playback, from the `OpusHead` header.
pub fn OggOpusSource::pre_skip(self : OggOpusSource) -> Int {
  self.pre_skip_
}

///|
/// Yield the next raw Opus packet, pulling more input on demand for
/// streaming sources; `None` at end of stream.
pub impl AudioSource for OggOpusSource with fn next_frame(self) {
  for ;; {
    if self.reader.next_packet() is Some(packet) {
      return Some(packet)
    }
    guard self.input is Some(input) else { return None }
    match input.read_some(max_len=8192) {
      Some(chunk) => self.reader.push(chunk)
      None => {
        if self.reader.has_incomplete_input() {
          raise InvalidOpusStream(reason="truncated ogg stream")
        }
        return None
      }
    }
  }
}

///|
fn push_u16_le(output : Array[Byte], value : Int) -> Unit {
  output.push(value.to_byte())
  output.push((value >> 8).to_byte())
}

///|
fn push_u32_le(output : Array[Byte], value : UInt) -> Unit {
  output.push(value.to_byte())
  output.push((value >> 8).to_byte())
  output.push((value >> 16).to_byte())
  output.push((value >> 24).to_byte())
}

///|
fn push_u64_le(output : Array[Byte], value : UInt64) -> Unit {
  for shift in 0..<8 {
    output.push((value >> (shift * 8)).to_byte())
  }
}

///|
fn opus_packet_segment_count(packet : Bytes) -> Int {
  packet.length() / 255 + 1
}

///|
fn append_opus_packet_lacing(laces : Array[Byte], packet : Bytes) -> Unit {
  let mut remaining = packet.length()
  while remaining >= 255 {
    laces.push(b'\xFF')
    remaining -= 255
  }
  laces.push(remaining.to_byte())
}

///|
/// Pure in-memory Ogg/Opus muxer. Header pages are available immediately from
/// `take_output`; audio pages become available as packet or lacing limits are
/// reached, and `finish` emits the final EOS page.
pub struct OggOpusWriter {
  priv serial : UInt
  priv max_packets_per_page : Int
  priv mut page_sequence : UInt
  priv mut granule : UInt64
  priv audio_packets : Array[Bytes]
  priv mut audio_segments : Int
  priv output : Array[Byte]
  priv mut finished : Bool
}

///|
fn OggOpusWriter::emit_page(
  self : OggOpusWriter,
  header_type : Byte,
  granule : UInt64,
  packets : Array[Bytes],
) -> Unit {
  let laces : Array[Byte] = []
  for packet in packets {
    append_opus_packet_lacing(laces, packet)
  }
  let page : Array[Byte] = [b'O', b'g', b'g', b'S', 0, header_type]
  push_u64_le(page, granule)
  push_u32_le(page, self.serial)
  push_u32_le(page, self.page_sequence)
  push_u32_le(page, 0U)
  page.push(laces.length().to_byte())
  page.append(laces)
  for packet in packets {
    for byte in packet {
      page.push(byte)
    }
  }
  let checksum = ogg_page_crc(page)
  page[22] = checksum.to_byte()
  page[23] = (checksum >> 8).to_byte()
  page[24] = (checksum >> 16).to_byte()
  page[25] = (checksum >> 24).to_byte()
  self.output.append(page)
  self.page_sequence += 1
}

///|
fn OggOpusWriter::flush_audio_page(self : OggOpusWriter, eos~ : Bool) -> Unit {
  if self.audio_packets.is_empty() {
    if eos {
      self.emit_page(b'\x04', self.granule, [])
    }
    return
  }
  self.emit_page(
    if eos {
      b'\x04'
    } else {
      0
    },
    self.granule,
    self.audio_packets,
  )
  self.audio_packets.clear()
  self.audio_segments = 0
}

///|
/// Create an Ogg/Opus muxer and queue its `OpusHead` and `OpusTags` pages.
///
/// # Example
/// ```mbt nocheck
/// async test {
///   let writer = @voice.OggOpusWriter::new(channels=2, pre_skip=312)
///   for frame in recorded_opus_frames() {
///     writer.write_frame(frame)
///   }
///   writer.finish()
///   let file = @fs.create("recording.ogg")
///   defer file.close()
///   file.write(writer.take_output())
/// }
/// ```
///
/// # Panics
/// Panics if `channels` or `pre_skip` cannot be represented in an Opus header,
/// or if `max_packets_per_page` is not positive.
pub fn OggOpusWriter::new(
  channels? : Int = 2,
  pre_skip? : Int = 0,
  serial? : UInt = 0U,
  max_packets_per_page? : Int = 50,
) -> OggOpusWriter {
  if channels <= 0 || channels > 255 {
    abort("OggOpusWriter channels must be between 1 and 255")
  }
  if pre_skip < 0 || pre_skip > 65535 {
    abort("OggOpusWriter pre_skip must be between 0 and 65535")
  }
  if max_packets_per_page <= 0 {
    abort("OggOpusWriter max_packets_per_page must be positive")
  }
  let writer = OggOpusWriter::{
    serial,
    max_packets_per_page,
    page_sequence: 0U,
    granule: 0UL,
    audio_packets: [],
    audio_segments: 0,
    output: [],
    finished: false,
  }
  let head : Array[Byte] = [
    b'O',
    b'p',
    b'u',
    b's',
    b'H',
    b'e',
    b'a',
    b'd',
    1,
    channels.to_byte(),
  ]
  push_u16_le(head, pre_skip)
  push_u32_le(head, 48000U)
  push_u16_le(head, 0)
  head.push(0)
  writer.emit_page(b'\x02', 0UL, [Bytes::from_array(head)])
  let tags = b"OpusTags\x0B\x00\x00\x00discord.mbt\x00\x00\x00\x00"
  writer.emit_page(0, 0UL, [tags])
  writer
}

///|
/// Queue one complete Opus packet and advance the 48 kHz granule position.
pub fn OggOpusWriter::write_frame(
  self : OggOpusWriter,
  opus : Bytes,
  samples? : Int = 960,
) -> Unit raise OggOpusError {
  if self.finished {
    raise InvalidOpusStream(reason="cannot write an Opus frame after finish")
  }
  if samples < 0 {
    raise InvalidOpusStream(reason="Opus frame sample count is negative")
  }
  let segments = opus_packet_segment_count(opus)
  if segments > 255 {
    raise InvalidOpusStream(reason="Opus packet exceeds one Ogg page")
  }
  if !self.audio_packets.is_empty() && self.audio_segments + segments > 255 {
    self.flush_audio_page(eos=false)
  }
  self.audio_packets.push(opus)
  self.audio_segments += segments
  self.granule += samples.to_uint64()
  if self.audio_packets.length() >= self.max_packets_per_page {
    self.flush_audio_page(eos=false)
  }
}

///|
/// Flush remaining packets in a final EOS page. Repeated calls are no-ops.
pub fn OggOpusWriter::finish(self : OggOpusWriter) -> Unit {
  guard !self.finished else { return }
  self.flush_audio_page(eos=true)
  self.finished = true
}

///|
/// Drain all complete Ogg pages generated so far.
pub fn OggOpusWriter::take_output(self : OggOpusWriter) -> Bytes {
  let result = Bytes::from_array(self.output)
  self.output.clear()
  result
}