///|
/// Raw blocks are retained, including reserved types 7..126.
pub(all) struct MetadataBlock {
  kind : Int32
  data : Bytes
} derive(Show)

///|
pub(all) struct SeekPoint {
  sample : UInt64
  offset : UInt64
  samples : Int32
} derive(Show)

///|
pub(all) struct VorbisComment {
  vendor : String
  entries : Array[String]
} derive(Show)

///|
pub(all) struct CueIndex {
  offset : UInt64
  number : Int32
} derive(Show)

///|
pub(all) struct CueTrack {
  offset : UInt64
  number : Int32
  isrc : Bytes
  non_audio : Bool
  pre_emphasis : Bool
  indices : Array[CueIndex]
} derive(Show)

///|
pub(all) struct CueSheet {
  catalog : Bytes
  lead_in : UInt64
  is_cd : Bool
  tracks : Array[CueTrack]
} derive(Show)

///|
pub(all) struct Picture {
  kind : UInt32
  media_type : String
  description : String
  width : UInt32
  height : UInt32
  depth : UInt32
  colors : UInt32
  data : Bytes
} derive(Show)

///|
pub(all) struct Metadata {
  info : StreamInfo
  blocks : Array[MetadataBlock]
  seek_points : Array[SeekPoint]
  comments : VorbisComment?
  cuesheet : CueSheet?
  cuesheets : Array[CueSheet]
  pictures : Array[Picture]
} derive(Show)

///|
priv struct MetaReader {
  data : Bytes
  base : Int32
  mut pos : Int32
}

///|
fn MetaReader::bad(r : MetaReader, message : String) -> Unit raise FlacError {
  raise Invalid((r.base + r.pos).to_int64(), message)
}

///|
fn MetaReader::take(r : MetaReader, n : Int32) -> Bytes raise FlacError {
  if n < 0 || n > r.data.length() - r.pos {
    r.bad("metadata field exceeds block")
  }
  let result : FixedArray[Byte] = FixedArray::make(n, 0)
  for i = 0; i < n; i = i + 1 {
    result[i] = r.data[r.pos + i]
  }
  r.pos += n
  Bytes::from_fixedarray(result)
}

///|
fn MetaReader::uint(
  r : MetaReader,
  n : Int32,
  little~ : Bool = false,
) -> UInt64 raise FlacError {
  let b = r.take(n)
  let mut v = 0UL
  for i = 0; i < n; i = i + 1 {
    let j = if little { n - 1 - i } else { i }
    v = (v << 8) | b[j].to_uint64()
  }
  v
}

///|
fn MetaReader::size(
  r : MetaReader,
  little~ : Bool = false,
) -> Int32 raise FlacError {
  let n = r.uint(4, little~)
  if n > (r.data.length() - r.pos).to_uint64() {
    r.bad("metadata length exceeds block")
  }
  n.to_int()
}

///|
fn MetaReader::text(r : MetaReader, n : Int32) -> String raise FlacError {
  let b = r.take(n)
  @utf8.decode(b) catch {
    _ => {
      r.bad("invalid metadata UTF-8")
      ""
    }
  }
}

///|
fn MetaReader::zero(r : MetaReader, n : Int32) -> Unit raise FlacError {
  for b in r.take(n) {
    if b != 0 {
      r.bad("nonzero reserved metadata bits")
    }
  }
}

///|
fn MetaReader::end(r : MetaReader) -> Unit raise FlacError {
  if r.pos != r.data.length() {
    r.bad("trailing metadata bytes")
  }
}

///|
fn meta_cue(r : MetaReader) -> CueSheet raise FlacError {
  let catalog = r.take(128)
  let mut padding = false
  for b in catalog {
    if b == 0 {
      padding = true
    } else if padding || b < 32 || b > 126 {
      r.bad("invalid catalog number")
    }
  }
  let lead_in = r.uint(8)
  let flags = r.uint(1).to_int()
  if (flags & 127) != 0 {
    r.bad("reserved cuesheet flags")
  }
  let is_cd = (flags & 128) != 0
  r.zero(258)
  let count = r.uint(1).to_int()
  if count == 0 || (is_cd && (count > 100 || lead_in < 88200UL)) {
    r.bad("invalid cuesheet track count or lead-in")
  }
  let tracks = []
  let seen : Array[Int32] = []
  for t = 0; t < count; t = t + 1 {
    let offset = r.uint(8)
    let number = r.uint(1).to_int()
    let leadout = t == count - 1
    if number == 0 ||
      seen.contains(number) ||
      (leadout && number != (if is_cd { 170 } else { 255 })) ||
      (is_cd && !leadout && number > 99) ||
      (is_cd && offset % 588UL != 0UL) {
      r.bad("invalid cuesheet track")
    }
    seen.push(number)
    let isrc = r.take(12)
    let mut empty = true
    for b in isrc {
      if b != 0 {
        empty = false
      }
    }
    if !empty {
      for b in isrc {
        if !((b >= 48 && b <= 57) ||
          (b >= 65 && b <= 90) ||
          (b >= 97 && b <= 122)) {
          r.bad("invalid ISRC")
        }
      }
    }
    let flags = r.uint(1).to_int()
    if (flags & 63) != 0 {
      r.bad("reserved track flags")
    }
    r.zero(13)
    let count = r.uint(1).to_int()
    if (leadout && count != 0) ||
      (!leadout && count == 0) ||
      (is_cd && count > 100) {
      r.bad("invalid cue index count")
    }
    let indices = []
    let mut last = -1
    for j = 0; j < count; j = j + 1 {
      let offset = r.uint(8)
      let number = r.uint(1).to_int()
      r.zero(3)
      if (j == 0 && number > 1) ||
        (j > 0 && number != last + 1) ||
        (is_cd && offset % 588UL != 0UL) {
        r.bad("invalid cue index")
      }
      last = number
      indices.push(CueIndex::{ offset, number, })
    }
    tracks.push(CueTrack::{
      offset,
      number,
      isrc,
      non_audio: (flags & 128) != 0,
      pre_emphasis: (flags & 64) != 0,
      indices,
    })
  }
  r.end()
  { catalog, lead_in, is_cd, tracks, }
}

///|
/// Parse native metadata; incomplete input raises NeedMore. Limits apply before copying payloads.
pub fn parse_metadata(
  data : Bytes,
  max_metadata_bytes~ : Int32 = 67108864,
) -> (Metadata, Int32) raise FlacError {
  if data.length() < 4 {
    raise NeedMore
  }
  if data[0] != 102 || data[1] != 76 || data[2] != 97 || data[3] != 67 {
    raise Invalid(0, "missing fLaC signature")
  }
  let mut pos = 4
  let mut info : StreamInfo? = None
  let blocks = []
  let seek_points = []
  let mut comments = None
  let mut cuesheet = None
  let cuesheets = []
  let pictures : Array[Picture] = []
  let mut seen_seek = false
  while true {
    if data.length() - pos < 4 {
      raise NeedMore
    }
    let flags = data[pos].to_int()
    let kind = flags & 127
    let n = (data[pos + 1].to_int() << 16) |
      (data[pos + 2].to_int() << 8) |
      data[pos + 3].to_int()
    if kind == 127 || (pos == 4 && kind != 0) {
      raise Invalid(pos.to_int64(), "invalid metadata type/order")
    }
    if n > max_metadata_bytes - pos - 4 {
      raise Limit("metadata byte limit")
    }
    if n > data.length() - pos - 4 {
      raise NeedMore
    }
    let source = MetaReader::{ data, base: 0, pos: pos + 4, }
    let payload = source.take(n)
    let r = MetaReader::{ data: payload, base: pos + 4, pos: 0, }
    match kind {
      0 => {
        if info is Some(_) || n != 34 {
          r.bad("duplicate or invalid STREAMINFO")
        }
        let min_block_size = r.uint(2).to_int()
        let max_block_size = r.uint(2).to_int()
        let min_frame_size = r.uint(3).to_int()
        let max_frame_size = r.uint(3).to_int()
        let packed = r.uint(8)
        let sample_rate = (packed >> 44).to_int()
        let channels = ((packed >> 41) & 7UL).to_int() + 1
        let bits_per_sample = ((packed >> 36) & 31UL).to_int() + 1
        let total_samples = (packed & 0xfffffffffUL).to_int64()
        let md5 = r.take(16)
        if min_block_size < 16 ||
          max_block_size < min_block_size ||
          bits_per_sample < 4 ||
          (max_frame_size != 0 && min_frame_size > max_frame_size) {
          r.bad("invalid STREAMINFO ranges")
        }
        info = Some({
          min_block_size,
          max_block_size,
          min_frame_size,
          max_frame_size,
          sample_rate,
          channels,
          bits_per_sample,
          total_samples,
          md5,
        })
      }
      1 => r.zero(n)
      2 => if n < 4 { r.bad("APPLICATION missing identifier") }
      3 => {
        if seen_seek || n % 18 != 0 {
          r.bad("duplicate or malformed SEEKTABLE")
        }
        seen_seek = true
        let mut previous : UInt64? = None
        let mut placeholders = false
        for _i = 0; _i < n / 18; _i = _i + 1 {
          let sample = r.uint(8)
          let offset = r.uint(8)
          let samples = r.uint(2).to_int()
          if sample == 0xffffffffffffffffUL {
            placeholders = true
          } else {
            if placeholders {
              r.bad("seek point after placeholder")
            }
            if previous is Some(p) {
              if sample <= p {
                r.bad("unordered seek points")
              }
            }
            previous = Some(sample)
          }
          seek_points.push({ sample, offset, samples, })
        }
      }
      4 => {
        if comments is Some(_) {
          r.bad("duplicate VORBIS_COMMENT")
        }
        let vendor = r.text(r.size(little=true))
        let count = r.uint(4, little=true)
        if count > ((n - r.pos) / 4).to_uint64() {
          r.bad("comment count exceeds block")
        }
        let entries = []
        for _i = 0; _i < count.to_int(); _i = _i + 1 {
          let entry = r.text(r.size(little=true))
          let mut equal = false
          for c in entry {
            if c == '=' {
              equal = true
              break
            }
            if c.to_int() < 32 || c.to_int() > 126 {
              r.bad("invalid comment field name")
            }
          }
          if !equal {
            r.bad("comment has no field separator")
          }
          entries.push(entry)
        }
        r.end()
        comments = Some({ vendor, entries, })
      }
      5 => {
        let cue = meta_cue(r)
        if cuesheet is None {
          cuesheet = Some(cue)
        }
        cuesheets.push(cue)
      }
      6 => {
        let kind = r.uint(4).to_uint()
        let media_type = r.text(r.size())
        for c in media_type {
          if c.to_int() < 32 || c.to_int() > 126 {
            r.bad("invalid picture media type")
          }
        }
        let description = r.text(r.size())
        let width = r.uint(4).to_uint()
        let height = r.uint(4).to_uint()
        let depth = r.uint(4).to_uint()
        let colors = r.uint(4).to_uint()
        let data = r.take(r.size())
        r.end()
        if kind == 1U || kind == 2U {
          for p in pictures {
            if p.kind == kind {
              r.bad("duplicate picture icon")
            }
          }
        }
        pictures.push(Picture::{
          kind,
          media_type,
          description,
          width,
          height,
          depth,
          colors,
          data,
        })
      }
      _ => ()
    }
    blocks.push({ kind, data: payload, })
    pos += 4 + n
    if (flags & 128) != 0 {
      break
    }
  }
  match info {
    Some(info) =>
      (
        { info, blocks, seek_points, comments, cuesheet, cuesheets, pictures, },
        pos,
      )
    None => raise Invalid(4, "missing STREAMINFO")
  }
}

///|
/// Return all values of a tag name, comparing printable ASCII names without case.
pub fn VorbisComment::values(
  self : VorbisComment,
  name : String,
) -> Array[String] {
  let result = []
  for entry in self.entries {
    if entry.split_once("=") is Some((key, value)) {
      if key.to_string().to_lower() == name.to_lower() {
        result.push(value.to_string())
      }
    }
  }
  result
}