///|
/// Error categories returned by the parser, decoder, validator, and CLI model.
pub(all) enum WavErrorKind {
  ErrorNone
  ErrorTooShort
  ErrorNotRiff
  ErrorNotWave
  ErrorChunkOutOfBounds
  ErrorMissingFormat
  ErrorMissingData
  ErrorUnsupportedFormat
  ErrorInvalidFormat
  ErrorTruncatedData
  ErrorInvalidArgument
} derive(Eq, @debug.Debug)

///|
pub fn WavErrorKind::code(self : WavErrorKind) -> String {
  match self {
    ErrorNone => "none"
    ErrorTooShort => "too-short"
    ErrorNotRiff => "not-riff"
    ErrorNotWave => "not-wave"
    ErrorChunkOutOfBounds => "chunk-out-of-bounds"
    ErrorMissingFormat => "missing-fmt"
    ErrorMissingData => "missing-data"
    ErrorUnsupportedFormat => "unsupported-format"
    ErrorInvalidFormat => "invalid-format"
    ErrorTruncatedData => "truncated-data"
    ErrorInvalidArgument => "invalid-argument"
  }
}

///|
pub(all) struct WavError {
  kind : WavErrorKind
  offset : Int
  message : String
} derive(Eq, @debug.Debug)

///|
pub fn WavError::none() -> WavError {
  { kind: ErrorNone, offset: 0, message: "" }
}

///|
pub fn WavError::new(
  kind : WavErrorKind,
  message : String,
  offset? : Int = 0,
) -> WavError {
  { kind, offset, message }
}

///|
pub fn WavError::is_error(self : WavError) -> Bool {
  self.kind != ErrorNone
}

///|
pub fn WavError::code(self : WavError) -> String {
  self.kind.code()
}

///|
/// Non-owning reader over an integer byte array. Values outside 0..255 are masked.
pub(all) struct ByteReader {
  bytes : Array[Int]
  offset : Int
} derive(Eq, @debug.Debug)

///|
pub fn ByteReader::new(bytes : Array[Int]) -> ByteReader {
  { bytes, offset: 0 }
}

///|
pub fn ByteReader::at(bytes : Array[Int], offset : Int) -> ByteReader {
  { bytes, offset }
}

///|
pub fn ByteReader::length(self : ByteReader) -> Int {
  self.bytes.length()
}

///|
pub fn ByteReader::remaining(self : ByteReader) -> Int {
  self.bytes.length() - self.offset
}

///|
pub fn ByteReader::can_read(self : ByteReader, count : Int) -> Bool {
  count >= 0 && self.offset >= 0 && self.offset + count <= self.bytes.length()
}

///|
pub fn ByteReader::slice_can_read(
  self : ByteReader,
  offset : Int,
  count : Int,
) -> Bool {
  offset >= 0 && count >= 0 && offset + count <= self.bytes.length()
}

///|
pub fn ByteReader::byte_at(self : ByteReader, offset : Int) -> Int {
  if self.slice_can_read(offset, 1) {
    self.bytes[offset] & 0xff
  } else {
    0
  }
}

///|
pub fn ByteReader::u16_le_at(self : ByteReader, offset : Int) -> Int {
  self.byte_at(offset) | (self.byte_at(offset + 1) << 8)
}

///|
pub fn ByteReader::u24_le_at(self : ByteReader, offset : Int) -> Int {
  self.byte_at(offset) |
  (self.byte_at(offset + 1) << 8) |
  (self.byte_at(offset + 2) << 16)
}

///|
pub fn ByteReader::u32_le_at(self : ByteReader, offset : Int) -> Int {
  self.byte_at(offset) |
  (self.byte_at(offset + 1) << 8) |
  (self.byte_at(offset + 2) << 16) |
  (self.byte_at(offset + 3) << 24)
}

///|
pub fn ByteReader::i16_le_at(self : ByteReader, offset : Int) -> Int {
  let raw = self.u16_le_at(offset)
  if raw >= 0x8000 {
    raw - 0x10000
  } else {
    raw
  }
}

///|
pub fn ByteReader::i24_le_at(self : ByteReader, offset : Int) -> Int {
  let raw = self.u24_le_at(offset)
  if raw >= 0x800000 {
    raw - 0x1000000
  } else {
    raw
  }
}

///|
pub fn ByteReader::i32_le_at(self : ByteReader, offset : Int) -> Int {
  let raw = self.u32_le_at(offset)
  raw
}

///|
pub fn ByteReader::matches_ascii(
  self : ByteReader,
  offset : Int,
  a : Int,
  b : Int,
  c : Int,
  d : Int,
) -> Bool {
  self.byte_at(offset) == a &&
  self.byte_at(offset + 1) == b &&
  self.byte_at(offset + 2) == c &&
  self.byte_at(offset + 3) == d
}

///|
pub fn ByteReader::fourcc_at(self : ByteReader, offset : Int) -> String {
  if self.matches_ascii(offset, 82, 73, 70, 70) {
    "RIFF"
  } else if self.matches_ascii(offset, 87, 65, 86, 69) {
    "WAVE"
  } else if self.matches_ascii(offset, 102, 109, 116, 32) {
    "fmt "
  } else if self.matches_ascii(offset, 100, 97, 116, 97) {
    "data"
  } else if self.matches_ascii(offset, 102, 97, 99, 116) {
    "fact"
  } else if self.matches_ascii(offset, 76, 73, 83, 84) {
    "LIST"
  } else if self.matches_ascii(offset, 73, 78, 70, 79) {
    "INFO"
  } else if self.matches_ascii(offset, 99, 117, 101, 32) {
    "cue "
  } else if self.slice_can_read(offset, 4) {
    let b0 = self.byte_at(offset)
    let b1 = self.byte_at(offset + 1)
    let b2 = self.byte_at(offset + 2)
    let b3 = self.byte_at(offset + 3)
    if b0 >= 32 &&
      b0 <= 126 &&
      b1 >= 32 &&
      b1 <= 126 &&
      b2 >= 32 &&
      b2 <= 126 &&
      b3 >= 32 &&
      b3 <= 126 {
      String::from_array([
        b0.to_uint16().unsafe_to_char(),
        b1.to_uint16().unsafe_to_char(),
        b2.to_uint16().unsafe_to_char(),
        b3.to_uint16().unsafe_to_char(),
      ])
    } else {
      "unknown"
    }
  } else {
    "unknown"
  }
}

///|
pub(all) struct RiffHeader {
  riff_id : String
  file_size_minus_8 : Int
  wave_id : String
} derive(Eq, @debug.Debug)

///|
pub fn RiffHeader::empty() -> RiffHeader {
  { riff_id: "", file_size_minus_8: 0, wave_id: "" }
}

///|
pub fn RiffHeader::is_valid(self : RiffHeader) -> Bool {
  self.riff_id == "RIFF" &&
  self.wave_id == "WAVE" &&
  self.file_size_minus_8 >= 4
}

///|
pub(all) struct WaveChunk {
  id : String
  offset : Int
  data_offset : Int
  size : Int
  padded_size : Int
} derive(Eq, @debug.Debug)

///|
pub fn WaveChunk::new(
  id : String,
  offset : Int,
  data_offset : Int,
  size : Int,
) -> WaveChunk {
  let padded_size = if size % 2 == 0 { size } else { size + 1 }
  { id, offset, data_offset, size, padded_size }
}

///|
pub fn WaveChunk::empty() -> WaveChunk {
  { id: "", offset: 0, data_offset: 0, size: 0, padded_size: 0 }
}

///|
pub fn WaveChunk::end_offset(self : WaveChunk) -> Int {
  self.data_offset + self.padded_size
}

///|
pub fn WaveChunk::contains(self : WaveChunk, offset : Int) -> Bool {
  offset >= self.data_offset && offset < self.data_offset + self.size
}

///|
pub fn WaveChunk::is_known(self : WaveChunk) -> Bool {
  self.id == "fmt " ||
  self.id == "data" ||
  self.id == "fact" ||
  self.id == "LIST" ||
  self.id == "cue "
}

///|
pub(all) enum WaveEncoding {
  EncodingUnknown
  EncodingPcm
  EncodingIeeeFloat
  EncodingALaw
  EncodingMuLaw
  EncodingExtensible
} derive(Eq, @debug.Debug)

///|
pub fn WaveEncoding::label(self : WaveEncoding) -> String {
  match self {
    EncodingUnknown => "unknown"
    EncodingPcm => "pcm"
    EncodingIeeeFloat => "ieee-float"
    EncodingALaw => "a-law"
    EncodingMuLaw => "mu-law"
    EncodingExtensible => "extensible"
  }
}

///|
pub fn encoding_from_code(code : Int) -> WaveEncoding {
  match code {
    1 => EncodingPcm
    3 => EncodingIeeeFloat
    6 => EncodingALaw
    7 => EncodingMuLaw
    65534 => EncodingExtensible
    _ => EncodingUnknown
  }
}

///|
pub(all) struct WaveFormat {
  audio_format : Int
  encoding : WaveEncoding
  channels : Int
  sample_rate : Int
  byte_rate : Int
  block_align : Int
  bits_per_sample : Int
  extra_size : Int
} derive(Eq, @debug.Debug)

///|
pub fn WaveFormat::empty() -> WaveFormat {
  {
    audio_format: 0,
    encoding: EncodingUnknown,
    channels: 0,
    sample_rate: 0,
    byte_rate: 0,
    block_align: 0,
    bits_per_sample: 0,
    extra_size: 0,
  }
}

///|
pub fn WaveFormat::new(
  audio_format : Int,
  channels : Int,
  sample_rate : Int,
  byte_rate : Int,
  block_align : Int,
  bits_per_sample : Int,
  extra_size? : Int = 0,
) -> WaveFormat {
  {
    audio_format,
    encoding: encoding_from_code(audio_format),
    channels,
    sample_rate,
    byte_rate,
    block_align,
    bits_per_sample,
    extra_size,
  }
}

///|
pub fn WaveFormat::bytes_per_sample(self : WaveFormat) -> Int {
  self.bits_per_sample / 8
}

///|
pub fn WaveFormat::expected_block_align(self : WaveFormat) -> Int {
  self.channels * self.bytes_per_sample()
}

///|
pub fn WaveFormat::expected_byte_rate(self : WaveFormat) -> Int {
  self.sample_rate * self.expected_block_align()
}

///|
pub fn WaveFormat::is_pcm(self : WaveFormat) -> Bool {
  self.encoding == EncodingPcm
}

///|
pub fn WaveFormat::is_float(self : WaveFormat) -> Bool {
  self.encoding == EncodingIeeeFloat
}

///|
pub fn WaveFormat::is_supported(self : WaveFormat) -> Bool {
  (
    self.is_pcm() &&
    (
      self.bits_per_sample == 8 ||
      self.bits_per_sample == 16 ||
      self.bits_per_sample == 24 ||
      self.bits_per_sample == 32
    )
  ) ||
  (self.is_float() && self.bits_per_sample == 32)
}

///|
pub fn WaveFormat::is_valid(self : WaveFormat) -> Bool {
  self.channels > 0 &&
  self.sample_rate > 0 &&
  self.byte_rate > 0 &&
  self.block_align > 0 &&
  self.bits_per_sample > 0 &&
  self.expected_block_align() == self.block_align &&
  self.expected_byte_rate() == self.byte_rate
}

///|
pub(all) struct InfoTag {
  key : String
  value : String
} derive(Eq, @debug.Debug)

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

///|
pub fn InfoTag::empty() -> InfoTag {
  { key: "", value: "" }
}

///|
pub fn InfoTag::is_valid(self : InfoTag) -> Bool {
  self.key.length() > 0
}

///|
pub(all) struct CuePoint {
  id : Int
  position : Int
  sample_offset : Int
} derive(Eq, @debug.Debug)

///|
pub fn CuePoint::new(id : Int, position : Int, sample_offset : Int) -> CuePoint {
  { id, position, sample_offset }
}

///|
pub fn CuePoint::empty() -> CuePoint {
  { id: 0, position: 0, sample_offset: 0 }
}

///|
pub fn CuePoint::is_valid(self : CuePoint) -> Bool {
  self.id >= 0 && self.position >= 0 && self.sample_offset >= 0
}

///|
pub(all) struct WaveInfo {
  duration_seconds : Double
  frame_count : Int
  sample_count : Int
  data_bytes : Int
} derive(Eq, @debug.Debug)

///|
pub fn WaveInfo::empty() -> WaveInfo {
  { duration_seconds: 0.0, frame_count: 0, sample_count: 0, data_bytes: 0 }
}

///|
pub fn WaveInfo::is_valid(self : WaveInfo) -> Bool {
  self.duration_seconds >= 0.0 &&
  self.frame_count >= 0 &&
  self.sample_count >= 0 &&
  self.data_bytes >= 0
}

///|
pub(all) struct ParsedWav {
  header : RiffHeader
  format : WaveFormat
  chunks : Array[WaveChunk]
  info_tags : Array[InfoTag]
  cue_points : Array[CuePoint]
  fact_sample_count : Int
  data_offset : Int
  data_size : Int
  source_length : Int
} derive(Eq, @debug.Debug)

///|
pub fn ParsedWav::empty() -> ParsedWav {
  {
    header: RiffHeader::empty(),
    format: WaveFormat::empty(),
    chunks: [],
    info_tags: [],
    cue_points: [],
    fact_sample_count: 0,
    data_offset: 0,
    data_size: 0,
    source_length: 0,
  }
}

///|
pub fn ParsedWav::has_chunk(self : ParsedWav, id : String) -> Bool {
  for chunk in self.chunks {
    if chunk.id == id {
      break true
    }
  } nobreak {
    false
  }
}

///|
pub fn ParsedWav::info(self : ParsedWav) -> WaveInfo {
  if self.format.block_align <= 0 || self.format.sample_rate <= 0 {
    WaveInfo::empty()
  } else {
    let frame_count = self.data_size / self.format.block_align
    let sample_count = frame_count * self.format.channels
    {
      duration_seconds: frame_count.to_double() /
      self.format.sample_rate.to_double(),
      frame_count,
      sample_count,
      data_bytes: self.data_size,
    }
  }
}

///|
pub fn ParsedWav::is_complete(self : ParsedWav) -> Bool {
  self.header.is_valid() &&
  self.format.is_valid() &&
  self.data_offset > 0 &&
  self.data_size > 0
}

///|
pub(all) struct WavParseResult {
  ok : Bool
  wav : ParsedWav
  error : WavError
} derive(Eq, @debug.Debug)

///|
pub fn WavParseResult::success(wav : ParsedWav) -> WavParseResult {
  { ok: true, wav, error: WavError::none() }
}

///|
pub fn WavParseResult::failure(error : WavError) -> WavParseResult {
  { ok: false, wav: ParsedWav::empty(), error }
}

///|
pub fn WavParseResult::is_ok(self : WavParseResult) -> Bool {
  self.ok
}

///|
pub(all) struct DecodeOptions {
  normalize : Bool
  clamp : Bool
  target_channels : Int
} derive(Eq, @debug.Debug)

///|
pub fn DecodeOptions::new(
  normalize? : Bool = true,
  clamp? : Bool = true,
  target_channels? : Int = 0,
) -> DecodeOptions {
  { normalize, clamp, target_channels }
}

///|
pub fn DecodeOptions::is_valid(self : DecodeOptions) -> Bool {
  self.target_channels >= 0
}

///|
pub(all) struct PcmBuffer {
  channels : Int
  sample_rate : Int
  bits_per_sample : Int
  samples : Array[Int]
} derive(Eq, @debug.Debug)

///|
pub fn PcmBuffer::empty() -> PcmBuffer {
  { channels: 0, sample_rate: 0, bits_per_sample: 0, samples: [] }
}

///|
pub fn PcmBuffer::new(
  channels : Int,
  sample_rate : Int,
  bits_per_sample : Int,
  samples : Array[Int],
) -> PcmBuffer {
  { channels, sample_rate, bits_per_sample, samples }
}

///|
pub fn PcmBuffer::frame_count(self : PcmBuffer) -> Int {
  if self.channels <= 0 {
    0
  } else {
    self.samples.length() / self.channels
  }
}

///|
pub fn PcmBuffer::duration_seconds(self : PcmBuffer) -> Double {
  if self.sample_rate <= 0 {
    0.0
  } else {
    self.frame_count().to_double() / self.sample_rate.to_double()
  }
}

///|
pub fn PcmBuffer::is_valid(self : PcmBuffer) -> Bool {
  self.channels > 0 &&
  self.sample_rate > 0 &&
  self.bits_per_sample > 0 &&
  self.samples.length() % self.channels == 0
}

///|
pub(all) struct FloatBuffer {
  channels : Int
  sample_rate : Int
  samples : Array[Double]
} derive(Eq, @debug.Debug)

///|
pub fn FloatBuffer::empty() -> FloatBuffer {
  { channels: 0, sample_rate: 0, samples: [] }
}

///|
pub fn FloatBuffer::new(
  channels : Int,
  sample_rate : Int,
  samples : Array[Double],
) -> FloatBuffer {
  { channels, sample_rate, samples }
}

///|
pub fn FloatBuffer::frame_count(self : FloatBuffer) -> Int {
  if self.channels <= 0 {
    0
  } else {
    self.samples.length() / self.channels
  }
}

///|
pub fn FloatBuffer::duration_seconds(self : FloatBuffer) -> Double {
  if self.sample_rate <= 0 {
    0.0
  } else {
    self.frame_count().to_double() / self.sample_rate.to_double()
  }
}

///|
pub fn FloatBuffer::is_valid(self : FloatBuffer) -> Bool {
  self.channels > 0 &&
  self.sample_rate > 0 &&
  self.samples.length() % self.channels == 0
}

///|
pub(all) struct DecodeResult {
  ok : Bool
  pcm : PcmBuffer
  float_buffer : FloatBuffer
  error : WavError
} derive(Eq, @debug.Debug)

///|
pub fn DecodeResult::success(
  pcm : PcmBuffer,
  float_buffer : FloatBuffer,
) -> DecodeResult {
  { ok: true, pcm, float_buffer, error: WavError::none() }
}

///|
pub fn DecodeResult::failure(error : WavError) -> DecodeResult {
  {
    ok: false,
    pcm: PcmBuffer::empty(),
    float_buffer: FloatBuffer::empty(),
    error,
  }
}

///|
pub(all) struct AudioStats {
  duration_seconds : Double
  frame_count : Int
  sample_count : Int
  peak : Double
  rms : Double
  silent_samples : Int
  clipped_samples : Int
  dc_offset : Double
} derive(Eq, @debug.Debug)

///|
pub fn AudioStats::empty() -> AudioStats {
  {
    duration_seconds: 0.0,
    frame_count: 0,
    sample_count: 0,
    peak: 0.0,
    rms: 0.0,
    silent_samples: 0,
    clipped_samples: 0,
    dc_offset: 0.0,
  }
}

///|
pub fn AudioStats::has_clipping(self : AudioStats) -> Bool {
  self.clipped_samples > 0
}

///|
pub fn AudioStats::silence_ratio(self : AudioStats) -> Double {
  if self.sample_count == 0 {
    0.0
  } else {
    self.silent_samples.to_double() / self.sample_count.to_double()
  }
}

///|
pub(all) struct WaveformBucket {
  index : Int
  min : Double
  max : Double
  average_abs : Double
} derive(Eq, @debug.Debug)

///|
pub fn WaveformBucket::empty(index : Int) -> WaveformBucket {
  { index, min: 0.0, max: 0.0, average_abs: 0.0 }
}

///|
pub(all) struct WaveformSummary {
  buckets : Array[WaveformBucket]
  bucket_count : Int
  source_samples : Int
} derive(Eq, @debug.Debug)

///|
pub fn WaveformSummary::empty() -> WaveformSummary {
  { buckets: [], bucket_count: 0, source_samples: 0 }
}

///|
pub fn WaveformSummary::is_valid(self : WaveformSummary) -> Bool {
  self.bucket_count == self.buckets.length() && self.source_samples >= 0
}