///|
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 : Int32, size : Int32) -> String {
  if size <= 0 || !reader.slice_can_read(offset, size) {
    ""
  } else {
    let text = StringBuilder()
    for i in 0..= 32 && byte <= 126 {
        text.write_char(byte.to_uint16().unsafe_to_char())
      }
    }
    text.to_string()
  }
}

///|
fn parse_info_tags(
  reader : ByteReader,
  chunk : WaveChunk,
) -> Result[Array[InfoTag], WavError] {
  let tags = []
  if chunk.size < 4 || reader.fourcc_at(chunk.data_offset) != "INFO" {
    return Ok(tags)
  }
  let end = chunk.data_offset + chunk.size
  for offset = chunk.data_offset + 4; offset < end; {
    if end - offset < 8 {
      return Err(
        WavError::new(
          ErrorChunkOutOfBounds,
          "truncated INFO tag header",
          offset~,
        ),
      )
    }
    let size = reader.u32_le_at(offset + 4)
    let start = offset + 8
    let room = end - start
    if size < 0 || size > room || (size % 2 == 1 && size == room) {
      return Err(
        WavError::new(
          ErrorChunkOutOfBounds,
          "INFO tag exceeds LIST chunk",
          offset~,
        ),
      )
    }
    tags.push(
      InfoTag::new(
        reader.fourcc_at(offset),
        ascii_value_at(reader, start, size),
      ),
    )
    continue start + size + size % 2
  }
  Ok(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.. Int32 {
  if chunk.size >= 4 && reader.slice_can_read(chunk.data_offset, 4) {
    reader.u32_le_at(chunk.data_offset)
  } else {
    0
  }
}

///|
/// 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[Int32]) -> 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 declared = header_result.wav.header.file_size_minus_8
    if declared < 4 {
      return WavParseResult::failure(
        WavError::new(ErrorInvalidFormat, "unsupported RIFF extent", offset=4),
      )
    }
    // Tolerate an overstated RIFF size for diagnostics, but never scan a trailing container.
    let end = if declared < reader.length() - 8 {
      declared + 8
    } else {
      reader.length()
    }
    let parse_error = for offset = 12; offset < end; {
      if end - offset < 8 {
        break Some(
          WavError::new(
            ErrorChunkOutOfBounds,
            "truncated chunk header",
            offset~,
          ),
        )
      }
      let id = reader.fourcc_at(offset)
      let size = reader.u32_le_at(offset + 4)
      let start = offset + 8
      let room = end - start
      if size < 0 ||
        size > room ||
        (size % 2 == 1 && size == room && id != "data") {
        break Some(
          WavError::new(
            ErrorChunkOutOfBounds,
            "chunk extends beyond the available byte array",
            offset~,
          ),
        )
      }
      let chunk = WaveChunk::new(id, offset, start, size)
      // Python wave and common PCM encoders omit the pad after an odd final data chunk.
      let chunk = if size == room {
        { ..chunk, padded_size: size, }
      } else {
        chunk
      }
      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" {
        match parse_info_tags(reader, chunk) {
          Err(error) => break Some(error)
          Ok(tags) =>
            for tag in tags {
              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 {
          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[Int32]) -> 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()
  }
}