///|
pub(all) enum LowLatencyHintType {
  Part
  Map
} derive(Eq, @debug.Debug)

///|
pub(all) struct PartInfo {
  target_duration_ms : Int
  line : Int
} derive(Eq, @debug.Debug)

///|
pub(all) struct ServerControl {
  can_skip_until_ms : Int?
  can_block_reload : Bool
  hold_back_ms : Int?
  part_hold_back_ms : Int?
  can_skip_dateranges : Bool
  line : Int
} derive(Eq, @debug.Debug)

///|
pub(all) struct PartialSegment {
  uri : String
  duration_ms : Int
  independent : Bool
  gap : Bool
  byte_range : ByteRange?
  line : Int
} derive(Eq, @debug.Debug)

///|
pub(all) struct PreloadHint {
  hint_type : LowLatencyHintType
  uri : String
  byte_range_start : Int?
  byte_range_length : Int?
  line : Int
} derive(Eq, @debug.Debug)

///|
pub(all) struct RenditionReport {
  uri : String
  last_msn : Int?
  last_part : Int?
  line : Int
} derive(Eq, @debug.Debug)

///|
pub(all) struct SkipInfo {
  skipped_segments : Int
  recently_removed_dateranges : String?
  line : Int
} derive(Eq, @debug.Debug)

///|
pub(all) struct LowLatencyProfile {
  part_info : PartInfo?
  server_control : ServerControl?
  parts : Array[PartialSegment]
  preload_hints : Array[PreloadHint]
  rendition_reports : Array[RenditionReport]
  skip : SkipInfo?
  unknown_low_latency_tags : Array[String]
} derive(Eq, @debug.Debug)

///|
pub fn parse_low_latency_profile(
  input : String,
) -> Result[LowLatencyProfile, ParseError] {
  let parts : Array[PartialSegment] = []
  let preload_hints : Array[PreloadHint] = []
  let rendition_reports : Array[RenditionReport] = []
  let unknown_low_latency_tags : Array[String] = []
  let mut part_info : PartInfo? = None
  let mut server_control : ServerControl? = None
  let mut skip : SkipInfo? = None
  let lines = split_char(normalize_newlines(input), '\n')
  let mut seen_header = false
  for index, raw_line in lines {
    let line = index + 1
    let text = clean(raw_line[:])
    if text == "" {
      continue
    }
    if text == "#EXTM3U" {
      seen_header = true
      continue
    }
    if !seen_header {
      return Err(MissingHeader(line))
    }
    if text.has_prefix("#EXT-X-PART-INF:") {
      match parse_part_info(after_llhls_colon(text), line) {
        Ok(value) => part_info = Some(value)
        Err(err) => return Err(err)
      }
    } else if text.has_prefix("#EXT-X-SERVER-CONTROL:") {
      match parse_server_control(after_llhls_colon(text), line) {
        Ok(value) => server_control = Some(value)
        Err(err) => return Err(err)
      }
    } else if text.has_prefix("#EXT-X-PART:") {
      match parse_partial_segment(after_llhls_colon(text), line) {
        Ok(value) => parts.push(value)
        Err(err) => return Err(err)
      }
    } else if text.has_prefix("#EXT-X-PRELOAD-HINT:") {
      match parse_preload_hint(after_llhls_colon(text), line) {
        Ok(value) => preload_hints.push(value)
        Err(err) => return Err(err)
      }
    } else if text.has_prefix("#EXT-X-RENDITION-REPORT:") {
      match parse_rendition_report(after_llhls_colon(text), line) {
        Ok(value) => rendition_reports.push(value)
        Err(err) => return Err(err)
      }
    } else if text.has_prefix("#EXT-X-SKIP:") {
      match parse_skip_info(after_llhls_colon(text), line) {
        Ok(value) => skip = Some(value)
        Err(err) => return Err(err)
      }
    } else if text.has_prefix("#EXT-X-PART") ||
      text.has_prefix("#EXT-X-SERVER-CONTROL") ||
      text.has_prefix("#EXT-X-PRELOAD-HINT") ||
      text.has_prefix("#EXT-X-RENDITION-REPORT") ||
      text.has_prefix("#EXT-X-SKIP") {
      push_unknown(unknown_low_latency_tags, line, text)
    }
  }
  Ok({
    part_info,
    server_control,
    parts,
    preload_hints,
    rendition_reports,
    skip,
    unknown_low_latency_tags,
  })
}

///|
pub fn validate_low_latency_profile(
  playlist : HlsPlaylist,
  profile : LowLatencyProfile,
) -> ValidationReport {
  let issues : Array[ValidationIssue] = []
  validate_part_info(profile, issues)
  validate_server_control(playlist, profile, issues)
  validate_parts(profile, issues)
  validate_preload_hints(profile, issues)
  validate_rendition_reports(profile, issues)
  validate_skip(profile, issues)
  report_from_issues(issues)
}

///|
pub fn summarize_low_latency_profile(profile : LowLatencyProfile) -> String {
  let part_target = match profile.part_info {
    Some(info) => millis_to_string(info.target_duration_ms)
    None => "-"
  }
  let can_block = match profile.server_control {
    Some(control) => control.can_block_reload.to_string()
    None => "false"
  }
  let skip_segments = match profile.skip {
    Some(info) => info.skipped_segments.to_string()
    None => "0"
  }
  "llhls=parts=\{profile.parts.length()}, part_target=\{part_target}, preload=\{profile.preload_hints.length()}, reports=\{profile.rendition_reports.length()}, block_reload=\{can_block}, skip=\{skip_segments}"
}

///|
pub fn part_duration_total(profile : LowLatencyProfile) -> Int {
  let mut total = 0
  for part in profile.parts {
    total += part.duration_ms
  }
  total
}

///|
pub fn independent_part_count(profile : LowLatencyProfile) -> Int {
  let mut count = 0
  for part in profile.parts {
    if part.independent {
      count += 1
    }
  }
  count
}

///|
pub fn gap_part_count(profile : LowLatencyProfile) -> Int {
  let mut count = 0
  for part in profile.parts {
    if part.gap {
      count += 1
    }
  }
  count
}

///|
pub fn low_latency_uri_audit(profile : LowLatencyProfile) -> UriAudit {
  let entries : Array[UriEntry] = []
  for part in profile.parts {
    entries.push({ role: SegmentUri, uri: part.uri, line: part.line })
  }
  for hint in profile.preload_hints {
    entries.push({ role: SegmentUri, uri: hint.uri, line: hint.line })
  }
  for report in profile.rendition_reports {
    entries.push({ role: VariantUri, uri: report.uri, line: report.line })
  }
  audit_uri_entries(entries)
}

///|
fn parse_part_info(raw : String, line : Int) -> Result[PartInfo, ParseError] {
  match parse_attribute_list(raw, line~) {
    Ok(attrs) =>
      match required_attr(attrs, "PART-TARGET", line) {
        Ok(value) =>
          match parse_millis(value, line, "PART-TARGET") {
            Ok(target_duration_ms) => Ok({ target_duration_ms, line })
            Err(err) => Err(err)
          }
        Err(err) => Err(err)
      }
    Err(err) => Err(err)
  }
}

///|
fn parse_server_control(
  raw : String,
  line : Int,
) -> Result[ServerControl, ParseError] {
  match parse_attribute_list(raw, line~) {
    Ok(attrs) => {
      let can_skip_until_ms = match maybe_attr(attrs, "CAN-SKIP-UNTIL") {
        Some(value) =>
          match parse_millis(value, line, "CAN-SKIP-UNTIL") {
            Ok(parsed) => Some(parsed)
            Err(err) => return Err(err)
          }
        None => None
      }
      let hold_back_ms = match maybe_attr(attrs, "HOLD-BACK") {
        Some(value) =>
          match parse_millis(value, line, "HOLD-BACK") {
            Ok(parsed) => Some(parsed)
            Err(err) => return Err(err)
          }
        None => None
      }
      let part_hold_back_ms = match maybe_attr(attrs, "PART-HOLD-BACK") {
        Some(value) =>
          match parse_millis(value, line, "PART-HOLD-BACK") {
            Ok(parsed) => Some(parsed)
            Err(err) => return Err(err)
          }
        None => None
      }
      Ok({
        can_skip_until_ms,
        can_block_reload: attr_yes_no(attrs, "CAN-BLOCK-RELOAD"),
        hold_back_ms,
        part_hold_back_ms,
        can_skip_dateranges: attr_yes_no(attrs, "CAN-SKIP-DATERANGES"),
        line,
      })
    }
    Err(err) => Err(err)
  }
}

///|
fn parse_partial_segment(
  raw : String,
  line : Int,
) -> Result[PartialSegment, ParseError] {
  match parse_attribute_list(raw, line~) {
    Ok(attrs) => {
      let uri = match required_attr(attrs, "URI", line) {
        Ok(value) => value
        Err(err) => return Err(err)
      }
      let duration_ms = match required_attr(attrs, "DURATION", line) {
        Ok(value) =>
          match parse_millis(value, line, "DURATION") {
            Ok(parsed) => parsed
            Err(err) => return Err(err)
          }
        Err(err) => return Err(err)
      }
      let byte_range = match maybe_attr(attrs, "BYTERANGE") {
        Some(value) =>
          match parse_byte_range(value, line) {
            Ok(parsed) => Some(parsed)
            Err(err) => return Err(err)
          }
        None => None
      }
      Ok({
        uri,
        duration_ms,
        independent: attr_yes_no(attrs, "INDEPENDENT"),
        gap: attr_yes_no(attrs, "GAP"),
        byte_range,
        line,
      })
    }
    Err(err) => Err(err)
  }
}

///|
fn parse_preload_hint(
  raw : String,
  line : Int,
) -> Result[PreloadHint, ParseError] {
  match parse_attribute_list(raw, line~) {
    Ok(attrs) => {
      let hint_type = match required_attr(attrs, "TYPE", line) {
        Ok(value) =>
          match value {
            "PART" => Part
            "MAP" => Map
            _ =>
              return Err(
                InvalidAttribute(line, "unsupported PRELOAD-HINT TYPE \{value}"),
              )
          }
        Err(err) => return Err(err)
      }
      let uri = match required_attr(attrs, "URI", line) {
        Ok(value) => value
        Err(err) => return Err(err)
      }
      let byte_range_start = match maybe_attr(attrs, "BYTERANGE-START") {
        Some(value) =>
          match parse_unsigned_int(value, line, "BYTERANGE-START") {
            Ok(parsed) => Some(parsed)
            Err(err) => return Err(err)
          }
        None => None
      }
      let byte_range_length = match maybe_attr(attrs, "BYTERANGE-LENGTH") {
        Some(value) =>
          match parse_unsigned_int(value, line, "BYTERANGE-LENGTH") {
            Ok(parsed) => Some(parsed)
            Err(err) => return Err(err)
          }
        None => None
      }
      Ok({ hint_type, uri, byte_range_start, byte_range_length, line })
    }
    Err(err) => Err(err)
  }
}

///|
fn parse_rendition_report(
  raw : String,
  line : Int,
) -> Result[RenditionReport, ParseError] {
  match parse_attribute_list(raw, line~) {
    Ok(attrs) => {
      let uri = match required_attr(attrs, "URI", line) {
        Ok(value) => value
        Err(err) => return Err(err)
      }
      let last_msn = match maybe_attr(attrs, "LAST-MSN") {
        Some(value) =>
          match parse_unsigned_int(value, line, "LAST-MSN") {
            Ok(parsed) => Some(parsed)
            Err(err) => return Err(err)
          }
        None => None
      }
      let last_part = match maybe_attr(attrs, "LAST-PART") {
        Some(value) =>
          match parse_unsigned_int(value, line, "LAST-PART") {
            Ok(parsed) => Some(parsed)
            Err(err) => return Err(err)
          }
        None => None
      }
      Ok({ uri, last_msn, last_part, line })
    }
    Err(err) => Err(err)
  }
}

///|
fn parse_skip_info(raw : String, line : Int) -> Result[SkipInfo, ParseError] {
  match parse_attribute_list(raw, line~) {
    Ok(attrs) => {
      let skipped_segments = match
        required_attr(attrs, "SKIPPED-SEGMENTS", line) {
        Ok(value) =>
          match parse_unsigned_int(value, line, "SKIPPED-SEGMENTS") {
            Ok(parsed) => parsed
            Err(err) => return Err(err)
          }
        Err(err) => return Err(err)
      }
      Ok({
        skipped_segments,
        recently_removed_dateranges: maybe_attr(
          attrs, "RECENTLY-REMOVED-DATERANGES",
        ),
        line,
      })
    }
    Err(err) => Err(err)
  }
}

///|
fn validate_part_info(
  profile : LowLatencyProfile,
  issues : Array[ValidationIssue],
) -> Unit {
  if profile.parts.length() > 0 && profile.part_info == None {
    issues.push(
      ValidationIssue::warning(
        "llhls-part-info-missing", "EXT-X-PART entries are present but EXT-X-PART-INF is missing",
      ),
    )
  }
  match profile.part_info {
    Some(info) =>
      if info.target_duration_ms <= 0 {
        issues.push(
          ValidationIssue::error(
            "llhls-part-target-invalid",
            "PART-TARGET must be positive",
            line=info.line,
          ),
        )
      }
    None => ()
  }
}

///|
fn validate_server_control(
  playlist : HlsPlaylist,
  profile : LowLatencyProfile,
  issues : Array[ValidationIssue],
) -> Unit {
  match profile.server_control {
    Some(control) => {
      match control.hold_back_ms {
        Some(value) =>
          if playlist.target_duration > 0 &&
            value < playlist.target_duration * 3000 {
            issues.push(
              ValidationIssue::warning(
                "llhls-hold-back-low",
                "HOLD-BACK is usually at least three target durations",
                line=control.line,
              ),
            )
          }
        None => ()
      }
      match control.part_hold_back_ms {
        Some(value) =>
          match profile.part_info {
            Some(info) =>
              if value < info.target_duration_ms * 2 {
                issues.push(
                  ValidationIssue::warning(
                    "llhls-part-hold-back-low",
                    "PART-HOLD-BACK is usually at least two part target durations",
                    line=control.line,
                  ),
                )
              }
            None => ()
          }
        None => ()
      }
      match control.can_skip_until_ms {
        Some(value) =>
          if value <= 0 {
            issues.push(
              ValidationIssue::error(
                "llhls-skip-window-invalid",
                "CAN-SKIP-UNTIL must be positive when present",
                line=control.line,
              ),
            )
          }
        None => ()
      }
    }
    None =>
      if profile.preload_hints.length() > 0 || profile.skip != None {
        issues.push(
          ValidationIssue::info(
            "llhls-server-control-missing", "low latency hints are present without EXT-X-SERVER-CONTROL",
          ),
        )
      }
  }
}

///|
fn validate_parts(
  profile : LowLatencyProfile,
  issues : Array[ValidationIssue],
) -> Unit {
  for part in profile.parts {
    if part.uri == "" {
      issues.push(
        ValidationIssue::error(
          "llhls-part-uri-empty",
          "partial segment URI must not be empty",
          line=part.line,
        ),
      )
    }
    if part.duration_ms <= 0 {
      issues.push(
        ValidationIssue::error(
          "llhls-part-duration-invalid",
          "partial segment duration must be positive",
          line=part.line,
        ),
      )
    }
    match part.byte_range {
      Some(range) => validate_byte_range(range, part.line, issues)
      None => ()
    }
    match profile.part_info {
      Some(info) =>
        if info.target_duration_ms > 0 &&
          part.duration_ms > info.target_duration_ms {
          issues.push(
            ValidationIssue::warning(
              "llhls-part-exceeds-target",
              "partial segment duration exceeds PART-TARGET",
              line=part.line,
            ),
          )
        }
      None => ()
    }
  }
}

///|
fn validate_preload_hints(
  profile : LowLatencyProfile,
  issues : Array[ValidationIssue],
) -> Unit {
  for hint in profile.preload_hints {
    if hint.uri == "" {
      issues.push(
        ValidationIssue::error(
          "llhls-preload-uri-empty",
          "preload hint URI must not be empty",
          line=hint.line,
        ),
      )
    }
    match hint.byte_range_length {
      Some(value) =>
        if value <= 0 {
          issues.push(
            ValidationIssue::error(
              "llhls-preload-range-invalid",
              "preload hint byte range length must be positive",
              line=hint.line,
            ),
          )
        }
      None => ()
    }
  }
}

///|
fn validate_rendition_reports(
  profile : LowLatencyProfile,
  issues : Array[ValidationIssue],
) -> Unit {
  for report in profile.rendition_reports {
    if report.uri == "" {
      issues.push(
        ValidationIssue::error(
          "llhls-rendition-report-uri-empty",
          "rendition report URI must not be empty",
          line=report.line,
        ),
      )
    }
    match report.last_part {
      Some(value) =>
        if value < 0 {
          issues.push(
            ValidationIssue::error(
              "llhls-rendition-last-part-invalid",
              "LAST-PART must be non-negative",
              line=report.line,
            ),
          )
        }
      None => ()
    }
  }
}

///|
fn validate_skip(
  profile : LowLatencyProfile,
  issues : Array[ValidationIssue],
) -> Unit {
  match profile.skip {
    Some(skip) =>
      if skip.skipped_segments <= 0 {
        issues.push(
          ValidationIssue::error(
            "llhls-skip-invalid",
            "SKIPPED-SEGMENTS must be positive",
            line=skip.line,
          ),
        )
      }
    None => ()
  }
}

///|
fn attr_yes_no(attrs : Array[HlsAttribute], name : String) -> Bool {
  match maybe_attr(attrs, name) {
    Some(value) => parse_yes_no(value)
    None => false
  }
}

///|
fn after_llhls_colon(text : String) -> String {
  match text.split_once(":") {
    Some((_, value)) => value.to_owned()
    None => ""
  }
}