///|
pub(all) struct InfoCatalog {
  title : String
  artist : String
  comment : String
  software : String
  raw_tags : Array[InfoTag]
} derive(Eq, @debug.Debug)

///|
pub fn InfoCatalog::empty() -> InfoCatalog {
  { title: "", artist: "", comment: "", software: "", raw_tags: [] }
}

///|
pub fn InfoCatalog::has_metadata(self : InfoCatalog) -> Bool {
  self.raw_tags.length() > 0
}

///|
pub fn InfoCatalog::display_title(self : InfoCatalog) -> String {
  if self.title.length() > 0 {
    self.title
  } else {
    "untitled"
  }
}

///|
pub(all) struct CueTimeline {
  count : Int
  first_sample : Int
  last_sample : Int
  sample_rate : Int
  duration_seconds : Double
  cue_points : Array[CuePoint]
} derive(Eq, @debug.Debug)

///|
pub fn CueTimeline::empty() -> CueTimeline {
  {
    count: 0,
    first_sample: 0,
    last_sample: 0,
    sample_rate: 0,
    duration_seconds: 0.0,
    cue_points: [],
  }
}

///|
pub fn CueTimeline::is_empty(self : CueTimeline) -> Bool {
  self.count == 0
}

///|
pub(all) struct FormatSummary {
  encoding : String
  channels : Int
  sample_rate : Int
  bits_per_sample : Int
  frame_count : Int
  duration_seconds : Double
  byte_rate : Int
  data_bytes : Int
} derive(Eq, @debug.Debug)

///|
pub fn FormatSummary::empty() -> FormatSummary {
  {
    encoding: "",
    channels: 0,
    sample_rate: 0,
    bits_per_sample: 0,
    frame_count: 0,
    duration_seconds: 0.0,
    byte_rate: 0,
    data_bytes: 0,
  }
}

///|
pub fn info_tag_value(wav : ParsedWav, key : String) -> String? {
  for tag in wav.info_tags {
    if tag.key == key {
      break Some(tag.value)
    }
  } nobreak {
    None
  }
}

///|
pub fn has_info_tag(wav : ParsedWav, key : String) -> Bool {
  info_tag_value(wav, key) is Some(_)
}

///|
fn tag_or_empty(wav : ParsedWav, key : String) -> String {
  match info_tag_value(wav, key) {
    Some(value) => value
    None => ""
  }
}

///|
pub fn collect_info_catalog(wav : ParsedWav) -> InfoCatalog {
  {
    title: tag_or_empty(wav, "INAM"),
    artist: tag_or_empty(wav, "IART"),
    comment: tag_or_empty(wav, "ICMT"),
    software: tag_or_empty(wav, "ISFT"),
    raw_tags: wav.info_tags,
  }
}

///|
pub fn cue_sample_offsets(wav : ParsedWav) -> Array[Int] {
  Array::makei(wav.cue_points.length(), i => wav.cue_points[i].sample_offset)
}

///|
pub fn cue_times_seconds(wav : ParsedWav) -> Array[Double] {
  if wav.format.sample_rate <= 0 {
    []
  } else {
    Array::makei(wav.cue_points.length(), i => {
      wav.cue_points[i].sample_offset.to_double() /
      wav.format.sample_rate.to_double()
    })
  }
}

///|
pub fn collect_cue_timeline(wav : ParsedWav) -> CueTimeline {
  if wav.cue_points.length() == 0 || wav.format.sample_rate <= 0 {
    CueTimeline::empty()
  } else {
    let first = for
      point in wav.cue_points
      value = wav.cue_points[0].sample_offset {
      if point.sample_offset < value {
        continue point.sample_offset
      } else {
        continue value
      }
    } nobreak {
      value
    }
    let last = for
      point in wav.cue_points
      value = wav.cue_points[0].sample_offset {
      if point.sample_offset > value {
        continue point.sample_offset
      } else {
        continue value
      }
    } nobreak {
      value
    }
    {
      count: wav.cue_points.length(),
      first_sample: first,
      last_sample: last,
      sample_rate: wav.format.sample_rate,
      duration_seconds: (last - first).to_double() /
      wav.format.sample_rate.to_double(),
      cue_points: wav.cue_points,
    }
  }
}

///|
pub fn summarize_format(wav : ParsedWav) -> FormatSummary {
  let info = wav.info()
  {
    encoding: wav.format.encoding.label(),
    channels: wav.format.channels,
    sample_rate: wav.format.sample_rate,
    bits_per_sample: wav.format.bits_per_sample,
    frame_count: info.frame_count,
    duration_seconds: info.duration_seconds,
    byte_rate: wav.format.byte_rate,
    data_bytes: wav.data_size,
  }
}

///|
pub fn chunk_table_to_text(wav : ParsedWav) -> String {
  if wav.chunks.length() == 0 {
    "chunks: empty"
  } else {
    for chunk in wav.chunks; text = "chunks" {
      continue "\{text}\n\{chunk.id} offset=\{chunk.offset} size=\{chunk.size}"
    } nobreak {
      text
    }
  }
}

///|
pub fn cue_timeline_to_text(timeline : CueTimeline) -> String {
  if timeline.count == 0 {
    "cue timeline: empty"
  } else {
    let header = "cue timeline\ncount: \{timeline.count}\nfirst: \{timeline.first_sample}\nlast: \{timeline.last_sample}"
    for point in timeline.cue_points; text = header {
      continue "\{text}\ncue#\{point.id} sample=\{point.sample_offset}"
    } nobreak {
      text
    }
  }
}

///|
pub fn describe_wav(wav : ParsedWav) -> String {
  let summary = summarize_format(wav)
  let catalog = collect_info_catalog(wav)
  let title = catalog.display_title()
  "MoonWavKit description\ntitle: \{title}\nencoding: \{summary.encoding}\nchannels: \{summary.channels}\nsample_rate: \{summary.sample_rate}\nbits: \{summary.bits_per_sample}\nframes: \{summary.frame_count}\nduration_seconds: \{summary.duration_seconds}\nchunks: \{wav.chunks.length()}"
}

///|
pub fn parse_and_describe(bytes : Array[Int]) -> String {
  let parsed = parse_wav(bytes)
  if !parsed.ok {
    "MoonWavKit description\nerror: \{parsed.error.code()}"
  } else {
    describe_wav(parsed.wav)
  }
}