///|
pub fn playlist_stats(playlist : HlsPlaylist) -> PlaylistStats {
  let mut total_duration_ms = 0
  let mut max_segment_ms = 0
  let mut encrypted_segments = 0
  let mut discontinuities = 0
  for segment in playlist.segments {
    total_duration_ms += segment.duration_ms
    if segment.duration_ms > max_segment_ms {
      max_segment_ms = segment.duration_ms
    }
    if segment.discontinuity {
      discontinuities += 1
    }
    match segment.key {
      Some(key) =>
        if key.key_method != "NONE" {
          encrypted_segments += 1
        }
      None => ()
    }
  }
  {
    kind: playlist.kind,
    segment_count: playlist.segments.length(),
    variant_count: playlist.variants.length(),
    rendition_count: playlist.renditions.length(),
    total_duration_ms,
    max_segment_ms,
    encrypted_segments,
    discontinuities,
  }
}

///|
pub fn summarize_playlist(playlist : HlsPlaylist) -> String {
  let stats = playlist_stats(playlist)
  "kind=\{kind_to_string(stats.kind)}, segments=\{stats.segment_count}, variants=\{stats.variant_count}, renditions=\{stats.rendition_count}, duration=\{millis_to_string(stats.total_duration_ms)}"
}

///|
pub fn summarize_pipeline_readiness(playlist : HlsPlaylist) -> String {
  let validation = validate_playlist(playlist)
  let uris = audit_uris(playlist)
  let ladder = ladder_report(playlist)
  summarize_playlist(playlist) +
  "; " +
  format_validation_report(validation).split_once("\n").map_or(
    format_validation_report(validation),
    pair => pair.0.to_owned(),
  ) +
  "; " +
  format_uri_audit(uris) +
  "; " +
  format_ladder_report(ladder)
}

///|
fn kind_to_string(kind : PlaylistKind) -> String {
  match kind {
    Empty => "empty"
    Media => "media"
    Master => "master"
    Hybrid => "hybrid"
  }
}

///|
pub fn format_playlist(playlist : HlsPlaylist) -> String {
  let out = StringBuilder()
  out.write_string("#EXTM3U\n")
  if playlist.version > 0 {
    out.write_string("#EXT-X-VERSION:")
    out.write_string(playlist.version.to_string())
    out.write_char('\n')
  }
  if playlist.independent_segments {
    out.write_string("#EXT-X-INDEPENDENT-SEGMENTS\n")
  }
  match playlist.start_offset_ms {
    Some(offset) => {
      out.write_string("#EXT-X-START:TIME-OFFSET=")
      out.write_string(millis_to_string(offset))
      out.write_char('\n')
    }
    None => ()
  }
  for rendition in playlist.renditions {
    out.write_string(format_rendition(rendition))
    out.write_char('\n')
  }
  for variant in playlist.variants {
    out.write_string(format_variant(variant))
    out.write_char('\n')
    out.write_string(variant.uri)
    out.write_char('\n')
  }
  if playlist.segments.length() > 0 {
    if playlist.target_duration > 0 {
      out.write_string("#EXT-X-TARGETDURATION:")
      out.write_string(playlist.target_duration.to_string())
      out.write_char('\n')
    }
    if playlist.media_sequence > 0 {
      out.write_string("#EXT-X-MEDIA-SEQUENCE:")
      out.write_string(playlist.media_sequence.to_string())
      out.write_char('\n')
    }
    if playlist.discontinuity_sequence > 0 {
      out.write_string("#EXT-X-DISCONTINUITY-SEQUENCE:")
      out.write_string(playlist.discontinuity_sequence.to_string())
      out.write_char('\n')
    }
    write_segments(out, playlist.segments)
    if playlist.end_list {
      out.write_string("#EXT-X-ENDLIST\n")
    }
  }
  out.to_string()
}

///|
fn write_segments(out : StringBuilder, segments : Array[Segment]) -> Unit {
  let mut last_key : KeyInfo? = None
  let mut last_map : SegmentMap? = None
  for segment in segments {
    if segment.discontinuity {
      out.write_string("#EXT-X-DISCONTINUITY\n")
    }
    match segment.program_date_time {
      Some(value) => {
        out.write_string("#EXT-X-PROGRAM-DATE-TIME:")
        out.write_string(value)
        out.write_char('\n')
      }
      None => ()
    }
    match segment.key {
      Some(key) =>
        if last_key != Some(key) {
          out.write_string(format_key(key))
          out.write_char('\n')
          last_key = Some(key)
        }
      None => ()
    }
    match segment.map {
      Some(map) =>
        if last_map != Some(map) {
          out.write_string(format_map(map))
          out.write_char('\n')
          last_map = Some(map)
        }
      None => ()
    }
    match segment.byte_range {
      Some(range) => {
        out.write_string("#EXT-X-BYTERANGE:")
        out.write_string(byte_range_to_string(range))
        out.write_char('\n')
      }
      None => ()
    }
    out.write_string("#EXTINF:")
    out.write_string(millis_to_string(segment.duration_ms))
    out.write_char(',')
    out.write_string(segment.title)
    out.write_char('\n')
    out.write_string(segment.uri)
    out.write_char('\n')
  }
}

///|
fn format_key(key : KeyInfo) -> String {
  let attrs : Array[String] = []
  write_attr(attrs, "METHOD", key.key_method)
  match key.uri {
    Some(uri) => write_attr(attrs, "URI", uri, quoted=true)
    None => ()
  }
  match key.iv {
    Some(iv) => write_attr(attrs, "IV", iv)
    None => ()
  }
  match key.key_format {
    Some(value) => write_attr(attrs, "KEYFORMAT", value, quoted=true)
    None => ()
  }
  match key.key_format_versions {
    Some(value) => write_attr(attrs, "KEYFORMATVERSIONS", value, quoted=true)
    None => ()
  }
  "#EXT-X-KEY:" + join_attrs(attrs)
}

///|
fn format_map(map : SegmentMap) -> String {
  let attrs : Array[String] = []
  write_attr(attrs, "URI", map.uri, quoted=true)
  match map.byte_range {
    Some(range) =>
      write_attr(attrs, "BYTERANGE", byte_range_to_string(range), quoted=true)
    None => ()
  }
  "#EXT-X-MAP:" + join_attrs(attrs)
}

///|
fn format_variant(variant : VariantStream) -> String {
  let attrs : Array[String] = []
  write_attr(attrs, "BANDWIDTH", variant.bandwidth.to_string())
  match variant.average_bandwidth {
    Some(value) => write_attr(attrs, "AVERAGE-BANDWIDTH", value.to_string())
    None => ()
  }
  match variant.codecs {
    Some(value) => write_attr(attrs, "CODECS", value, quoted=true)
    None => ()
  }
  match variant.resolution {
    Some(value) => write_attr(attrs, "RESOLUTION", resolution_to_string(value))
    None => ()
  }
  match variant.frame_rate_milli {
    Some(value) => write_attr(attrs, "FRAME-RATE", millis_to_string(value))
    None => ()
  }
  match variant.audio_group {
    Some(value) => write_attr(attrs, "AUDIO", value, quoted=true)
    None => ()
  }
  match variant.subtitles_group {
    Some(value) => write_attr(attrs, "SUBTITLES", value, quoted=true)
    None => ()
  }
  match variant.closed_captions {
    Some(value) => write_attr(attrs, "CLOSED-CAPTIONS", value, quoted=true)
    None => ()
  }
  "#EXT-X-STREAM-INF:" + join_attrs(attrs)
}

///|
fn format_rendition(rendition : Rendition) -> String {
  let attrs : Array[String] = []
  write_attr(attrs, "TYPE", rendition.rendition_type)
  write_attr(attrs, "GROUP-ID", rendition.group_id, quoted=true)
  write_attr(attrs, "NAME", rendition.name, quoted=true)
  match rendition.language {
    Some(value) => write_attr(attrs, "LANGUAGE", value, quoted=true)
    None => ()
  }
  match rendition.uri {
    Some(value) => write_attr(attrs, "URI", value, quoted=true)
    None => ()
  }
  write_attr(attrs, "DEFAULT", yes_no(rendition.default))
  write_attr(attrs, "AUTOSELECT", yes_no(rendition.autoselect))
  if rendition.forced {
    write_attr(attrs, "FORCED", "YES")
  }
  "#EXT-X-MEDIA:" + join_attrs(attrs)
}

///|
pub fn format_validation_report(report : ValidationReport) -> String {
  let out = StringBuilder()
  out.write_string("ok=")
  out.write_string(report.ok.to_string())
  out.write_string(", errors=")
  out.write_string(report.errors.to_string())
  out.write_string(", warnings=")
  out.write_string(report.warnings.to_string())
  for issue in report.issues {
    out.write_char('\n')
    out.write_string(severity_to_string(issue.severity))
    out.write_string(" ")
    out.write_string(issue.code)
    if issue.line > 0 {
      out.write_string(" line=")
      out.write_string(issue.line.to_string())
    }
    out.write_string(": ")
    out.write_string(issue.message)
  }
  out.to_string()
}

///|
fn severity_to_string(severity : Severity) -> String {
  match severity {
    Error => "error"
    Warning => "warning"
    Info => "info"
  }
}