///|
pub(all) struct TimelineEntry {
  sequence : Int
  uri : String
  start_ms : Int
  end_ms : Int
  duration_ms : Int
  discontinuity_group : Int
  encrypted : Bool
  line : Int
} derive(Eq, @debug.Debug)

///|
pub(all) struct TimelineReport {
  entries : Array[TimelineEntry]
  first_sequence : Int
  last_sequence : Int
  total_duration_ms : Int
  discontinuity_groups : Int
  encrypted_segments : Int
  longest_segment_ms : Int
  shortest_segment_ms : Int
  average_segment_ms : Int
  open_ended : Bool
} derive(Eq, @debug.Debug)

///|
pub(all) struct WindowPlan {
  start_index : Int
  dropped_segments : Int
  kept_segments : Int
  kept_duration_ms : Int
  first_sequence : Int
  last_sequence : Int
} derive(Eq, @debug.Debug)

///|
pub fn build_timeline(playlist : HlsPlaylist) -> TimelineReport {
  let entries : Array[TimelineEntry] = []
  let mut cursor = 0
  let mut group = playlist.discontinuity_sequence
  let mut encrypted_segments = 0
  let mut longest_segment_ms = 0
  let mut shortest_segment_ms = 0
  for index, segment in playlist.segments {
    if segment.discontinuity {
      group += 1
    }
    let encrypted = segment_is_encrypted(segment)
    if encrypted {
      encrypted_segments += 1
    }
    if index == 0 || segment.duration_ms < shortest_segment_ms {
      shortest_segment_ms = segment.duration_ms
    }
    if segment.duration_ms > longest_segment_ms {
      longest_segment_ms = segment.duration_ms
    }
    let start_ms = cursor
    cursor += segment.duration_ms
    entries.push({
      sequence: playlist.media_sequence + index,
      uri: segment.uri,
      start_ms,
      end_ms: cursor,
      duration_ms: segment.duration_ms,
      discontinuity_group: group,
      encrypted,
      line: segment.line,
    })
  }
  let first_sequence = if entries.length() == 0 {
    playlist.media_sequence
  } else {
    entries[0].sequence
  }
  let last_sequence = if entries.length() == 0 {
    playlist.media_sequence - 1
  } else {
    entries[entries.length() - 1].sequence
  }
  let average_segment_ms = if entries.length() == 0 {
    0
  } else {
    cursor / entries.length()
  }
  {
    entries,
    first_sequence,
    last_sequence,
    total_duration_ms: cursor,
    discontinuity_groups: count_discontinuity_groups(entries),
    encrypted_segments,
    longest_segment_ms,
    shortest_segment_ms,
    average_segment_ms,
    open_ended: !playlist.end_list,
  }
}

///|
pub fn format_timeline_report(report : TimelineReport) -> String {
  "timeline=segments=\{report.entries.length()}, seq=\{report.first_sequence}-\{report.last_sequence}, duration=\{millis_to_string(report.total_duration_ms)}, groups=\{report.discontinuity_groups}, encrypted=\{report.encrypted_segments}, min=\{millis_to_string(report.shortest_segment_ms)}, max=\{millis_to_string(report.longest_segment_ms)}, avg=\{millis_to_string(report.average_segment_ms)}, open=\{report.open_ended}"
}

///|
pub fn timeline_entry_at(
  playlist : HlsPlaylist,
  position_ms : Int,
) -> TimelineEntry? {
  let report = build_timeline(playlist)
  for entry in report.entries {
    if position_ms >= entry.start_ms && position_ms < entry.end_ms {
      return Some(entry)
    }
  }
  if report.entries.length() > 0 && position_ms == report.total_duration_ms {
    Some(report.entries[report.entries.length() - 1])
  } else {
    None
  }
}

///|
pub fn timeline_entry_by_sequence(
  playlist : HlsPlaylist,
  sequence : Int,
) -> TimelineEntry? {
  let report = build_timeline(playlist)
  for entry in report.entries {
    if entry.sequence == sequence {
      return Some(entry)
    }
  }
  None
}

///|
pub fn live_window_plan(
  playlist : HlsPlaylist,
  min_window_ms : Int,
) -> WindowPlan {
  let mut start_index = 0
  let mut kept_duration_ms = 0
  let count = playlist.segments.length()
  let mut index = count
  while index > 0 {
    index -= 1
    kept_duration_ms += playlist.segments[index].duration_ms
    start_index = index
    if kept_duration_ms >= min_window_ms {
      break
    }
  }
  let kept_segments = count - start_index
  let first_sequence = playlist.media_sequence + start_index
  let last_sequence = if kept_segments == 0 {
    first_sequence - 1
  } else {
    first_sequence + kept_segments - 1
  }
  {
    start_index,
    dropped_segments: start_index,
    kept_segments,
    kept_duration_ms,
    first_sequence,
    last_sequence,
  }
}

///|
pub fn trim_live_window(
  playlist : HlsPlaylist,
  min_window_ms : Int,
) -> HlsPlaylist {
  let plan = live_window_plan(playlist, min_window_ms)
  trim_from_index(playlist, plan.start_index)
}

///|
pub fn trim_before_sequence(
  playlist : HlsPlaylist,
  first_sequence : Int,
) -> HlsPlaylist {
  if first_sequence <= playlist.media_sequence {
    return playlist
  }
  let offset = first_sequence - playlist.media_sequence
  if offset >= playlist.segments.length() {
    { ..playlist, media_sequence: first_sequence, segments: [] }
  } else {
    trim_from_index(playlist, offset)
  }
}

///|
pub fn format_window_plan(plan : WindowPlan) -> String {
  "window=drop=\{plan.dropped_segments}, keep=\{plan.kept_segments}, seq=\{plan.first_sequence}-\{plan.last_sequence}, duration=\{millis_to_string(plan.kept_duration_ms)}"
}

///|
pub fn segment_sequence(playlist : HlsPlaylist, index : Int) -> Int {
  playlist.media_sequence + index
}

///|
pub fn segment_end_sequence(playlist : HlsPlaylist) -> Int {
  playlist.media_sequence + playlist.segments.length() - 1
}

///|
pub fn playlist_duration_seconds_floor(playlist : HlsPlaylist) -> Int {
  build_timeline(playlist).total_duration_ms / 1000
}

///|
pub fn playlist_duration_seconds_ceil(playlist : HlsPlaylist) -> Int {
  let total = build_timeline(playlist).total_duration_ms
  if total == 0 {
    0
  } else {
    (total + 999) / 1000
  }
}

///|
pub fn discontinuity_sequence_for_segment(
  playlist : HlsPlaylist,
  sequence : Int,
) -> Int? {
  match timeline_entry_by_sequence(playlist, sequence) {
    Some(entry) => Some(entry.discontinuity_group)
    None => None
  }
}

///|
pub fn sequence_for_playhead(playlist : HlsPlaylist, position_ms : Int) -> Int? {
  match timeline_entry_at(playlist, position_ms) {
    Some(entry) => Some(entry.sequence)
    None => None
  }
}

///|
pub fn time_range_for_sequence(
  playlist : HlsPlaylist,
  sequence : Int,
) -> (Int, Int)? {
  match timeline_entry_by_sequence(playlist, sequence) {
    Some(entry) => Some((entry.start_ms, entry.end_ms))
    None => None
  }
}

///|
fn trim_from_index(playlist : HlsPlaylist, start_index : Int) -> HlsPlaylist {
  let segments : Array[Segment] = []
  for index, segment in playlist.segments {
    if index >= start_index {
      segments.push(segment)
    }
  }
  {
    ..playlist,
    media_sequence: playlist.media_sequence + start_index,
    segments,
  }
}

///|
fn segment_is_encrypted(segment : Segment) -> Bool {
  match segment.key {
    Some(key) => key.key_method != "NONE"
    None => false
  }
}

///|
fn count_discontinuity_groups(entries : Array[TimelineEntry]) -> Int {
  if entries.length() == 0 {
    return 0
  }
  let mut groups = 1
  let mut last = entries[0].discontinuity_group
  for entry in entries {
    if entry.discontinuity_group != last {
      groups += 1
      last = entry.discontinuity_group
    }
  }
  groups
}