///|
fn parse_header(reader : ByteReader) -> WavParseResult {
  if !reader.slice_can_read(0, 12) {
    WavParseResult::failure(
      WavError::new(ErrorTooShort, "WAV data is shorter than RIFF header"),
    )
  } else if !reader.matches_ascii(0, 82, 73, 70, 70) {
    WavParseResult::failure(
      WavError::new(ErrorNotRiff, "expected RIFF signature"),
    )
  } else if !reader.matches_ascii(8, 87, 65, 86, 69) {
    WavParseResult::failure(
      WavError::new(ErrorNotWave, "expected WAVE signature", offset=8),
    )
  } else {
    let header = {
      riff_id: "RIFF",
      file_size_minus_8: reader.u32_le_at(4),
      wave_id: "WAVE",
    }
    WavParseResult::success({
      header,
      format: WaveFormat::empty(),
      chunks: [],
      info_tags: [],
      cue_points: [],
      fact_sample_count: 0,
      data_offset: 0,
      data_size: 0,
      source_length: reader.length(),
    })
  }
}

///|
fn parse_format(reader : ByteReader, chunk : WaveChunk) -> WaveFormat {
  if chunk.size < 16 || !reader.slice_can_read(chunk.data_offset, 16) {
    WaveFormat::empty()
  } else {
    let extra_size = if chunk.size >= 18 &&
      reader.slice_can_read(chunk.data_offset + 16, 2) {
      reader.u16_le_at(chunk.data_offset + 16)
    } else {
      0
    }
    WaveFormat::new(
      reader.u16_le_at(chunk.data_offset),
      reader.u16_le_at(chunk.data_offset + 2),
      reader.u32_le_at(chunk.data_offset + 4),
      reader.u32_le_at(chunk.data_offset + 8),
      reader.u16_le_at(chunk.data_offset + 12),
      reader.u16_le_at(chunk.data_offset + 14),
      extra_size~,
    )
  }
}

///|
fn ascii_value_at(reader : ByteReader, offset : Int, size : Int) -> String {
  if size <= 0 || !reader.slice_can_read(offset, size) {
    ""
  } else {
    let text = for i in 0..= 32 && byte <= 126 {
        continue acc + String::from_array([byte.to_uint16().unsafe_to_char()])
      } else {
        continue acc
      }
    } nobreak {
      acc
    }
    text
  }
}

///|
fn parse_info_tags(reader : ByteReader, chunk : WaveChunk) -> Array[InfoTag] {
  let tags : Array[InfoTag] = []
  if chunk.size < 4 || !reader.slice_can_read(chunk.data_offset, chunk.size) {
    tags
  } else {
    let list_kind = reader.fourcc_at(chunk.data_offset)
    if list_kind != "INFO" {
      tags
    } else {
      let end = chunk.data_offset + chunk.size
      for offset = chunk.data_offset + 4; offset + 8 <= end; {
        let key = reader.fourcc_at(offset)
        let size = reader.u32_le_at(offset + 4)
        let data_offset = offset + 8
        if data_offset + size > end {
          break tags
        }
        tags.push(InfoTag::new(key, ascii_value_at(reader, data_offset, size)))
        let padded = if size % 2 == 0 { size } else { size + 1 }
        continue data_offset + padded
      } nobreak {
        tags
      }
    }
  }
}

///|
fn parse_cue_points(reader : ByteReader, chunk : WaveChunk) -> Array[CuePoint] {
  let points : Array[CuePoint] = []
  if chunk.size < 4 || !reader.slice_can_read(chunk.data_offset, chunk.size) {
    points
  } else {
    let count = reader.u32_le_at(chunk.data_offset)
    let max_count = (chunk.size - 4) / 24
    let safe_count = if count < max_count { count } else { max_count }
    for i in 0.. Int {
  if chunk.size >= 4 && reader.slice_can_read(chunk.data_offset, 4) {
    reader.u32_le_at(chunk.data_offset)
  } else {
    0
  }
}

///|
fn validate_chunk_bounds(reader : ByteReader, chunk : WaveChunk) -> Bool {
  chunk.offset >= 12 &&
  chunk.size >= 0 &&
  chunk.data_offset >= chunk.offset + 8 &&
  chunk.end_offset() <= reader.length()
}

///|
/// Parse a RIFF/WAVE byte array into structural metadata. The parser skips
/// unknown chunks while preserving their id and bounds.
pub fn parse_wav(bytes : Array[Int]) -> WavParseResult {
  let reader = ByteReader::new(bytes)
  let header_result = parse_header(reader)
  if !header_result.ok {
    header_result
  } else {
    let chunks : Array[WaveChunk] = []
    let info_tags : Array[InfoTag] = []
    let cue_points : Array[CuePoint] = []
    let mut format = WaveFormat::empty()
    let mut data_offset = 0
    let mut data_size = 0
    let mut fact_sample_count = 0
    let parse_error = for offset = 12; offset + 8 <= reader.length(); {
      let id = reader.fourcc_at(offset)
      let size = reader.u32_le_at(offset + 4)
      let chunk = WaveChunk::new(id, offset, offset + 8, size)
      if !validate_chunk_bounds(reader, chunk) {
        break Some(
          WavError::new(
            ErrorChunkOutOfBounds,
            "chunk extends beyond the available byte array",
            offset~,
          ),
        )
      }
      chunks.push(chunk)
      if id == "fmt " {
        format = parse_format(reader, chunk)
      } else if id == "data" {
        data_offset = chunk.data_offset
        data_size = chunk.size
      } else if id == "fact" {
        fact_sample_count = parse_fact_sample_count(reader, chunk)
      } else if id == "LIST" {
        for tag in parse_info_tags(reader, chunk) {
          info_tags.push(tag)
        }
      } else if id == "cue " {
        for point in parse_cue_points(reader, chunk) {
          cue_points.push(point)
        }
      }
      continue chunk.end_offset()
    } nobreak {
      None
    }
    match parse_error {
      Some(error) => WavParseResult::failure(error)
      None =>
        if !format.is_valid() {
          WavParseResult::failure(
            WavError::new(ErrorMissingFormat, "missing or invalid fmt chunk"),
          )
        } else if data_offset == 0 || data_size == 0 {
          WavParseResult::failure(
            WavError::new(ErrorMissingData, "missing data chunk"),
          )
        } else {
          WavParseResult::success({
            header: header_result.wav.header,
            format,
            chunks,
            info_tags,
            cue_points,
            fact_sample_count,
            data_offset,
            data_size,
            source_length: bytes.length(),
          })
        }
    }
  }
}

///|
pub fn validate_wav(bytes : Array[Int]) -> WavError {
  let parsed = parse_wav(bytes)
  if !parsed.ok {
    parsed.error
  } else if !parsed.wav.format.is_supported() {
    WavError::new(
      ErrorUnsupportedFormat,
      "format is structurally valid but unsupported by v1",
    )
  } else if parsed.wav.data_size % parsed.wav.format.block_align != 0 {
    WavError::new(
      ErrorTruncatedData,
      "data chunk is not aligned to complete sample frames",
    )
  } else {
    WavError::none()
  }
}