///|
pub(all) enum DeltaKind {
  SegmentAdded
  SegmentRemoved
  SegmentChanged
  SegmentReordered
  SequenceAdvanced
  SequenceRegressed
  TargetDurationChanged
  EndListChanged
  VariantAdded
  VariantRemoved
  VariantChanged
  RenditionAdded
  RenditionRemoved
  RenditionChanged
} derive(Eq, @debug.Debug)

///|
pub(all) struct DeltaChange {
  kind : DeltaKind
  code : String
  message : String
  old_line : Int
  new_line : Int
} derive(Eq, @debug.Debug)

///|
pub(all) struct DeltaReport {
  compatible : Bool
  breaking_changes : Int
  additive_changes : Int
  informational_changes : Int
  old_sequence : Int
  new_sequence : Int
  changes : Array[DeltaChange]
} derive(Eq, @debug.Debug)

///|
pub fn compare_playlists(
  old_playlist : HlsPlaylist,
  new_playlist : HlsPlaylist,
) -> DeltaReport {
  let changes : Array[DeltaChange] = []
  compare_media_header(old_playlist, new_playlist, changes)
  compare_segments(old_playlist, new_playlist, changes)
  compare_variants(old_playlist, new_playlist, changes)
  compare_renditions(old_playlist, new_playlist, changes)
  report_from_changes(old_playlist, new_playlist, changes)
}

///|
pub fn format_delta_report(report : DeltaReport) -> String {
  let out = StringBuilder()
  out.write_string("delta=compatible=")
  out.write_string(report.compatible.to_string())
  out.write_string(", breaking=")
  out.write_string(report.breaking_changes.to_string())
  out.write_string(", additive=")
  out.write_string(report.additive_changes.to_string())
  out.write_string(", info=")
  out.write_string(report.informational_changes.to_string())
  out.write_string(", seq=")
  out.write_string(report.old_sequence.to_string())
  out.write_string("->")
  out.write_string(report.new_sequence.to_string())
  for change in report.changes {
    out.write_char('\n')
    out.write_string(delta_kind_to_string(change.kind))
    out.write_string(" ")
    out.write_string(change.code)
    if change.old_line > 0 {
      out.write_string(" old_line=")
      out.write_string(change.old_line.to_string())
    }
    if change.new_line > 0 {
      out.write_string(" new_line=")
      out.write_string(change.new_line.to_string())
    }
    out.write_string(": ")
    out.write_string(change.message)
  }
  out.to_string()
}

///|
pub fn delta_is_append_only(report : DeltaReport) -> Bool {
  if !report.compatible {
    return false
  }
  for change in report.changes {
    match change.kind {
      SegmentAdded | SequenceAdvanced | EndListChanged => ()
      _ => return false
    }
  }
  true
}

///|
pub fn count_delta_kind(report : DeltaReport, kind : DeltaKind) -> Int {
  let mut count = 0
  for change in report.changes {
    if change.kind == kind {
      count += 1
    }
  }
  count
}

///|
fn compare_media_header(
  old_playlist : HlsPlaylist,
  new_playlist : HlsPlaylist,
  changes : Array[DeltaChange],
) -> Unit {
  if new_playlist.media_sequence > old_playlist.media_sequence {
    push_change(
      changes,
      SequenceAdvanced,
      "media-sequence-advanced",
      "media sequence advanced by \{new_playlist.media_sequence - old_playlist.media_sequence}",
      0,
      0,
    )
  } else if new_playlist.media_sequence < old_playlist.media_sequence {
    push_change(
      changes,
      SequenceRegressed,
      "media-sequence-regressed",
      "media sequence moved backward from \{old_playlist.media_sequence} to \{new_playlist.media_sequence}",
      0,
      0,
    )
  }
  if old_playlist.target_duration != new_playlist.target_duration {
    push_change(
      changes,
      TargetDurationChanged,
      "target-duration-changed",
      "target duration changed from \{old_playlist.target_duration} to \{new_playlist.target_duration}",
      0,
      0,
    )
  }
  if old_playlist.end_list != new_playlist.end_list {
    push_change(
      changes,
      EndListChanged,
      "endlist-changed",
      "EXT-X-ENDLIST changed from \{old_playlist.end_list} to \{new_playlist.end_list}",
      0,
      0,
    )
  }
}

///|
fn compare_segments(
  old_playlist : HlsPlaylist,
  new_playlist : HlsPlaylist,
  changes : Array[DeltaChange],
) -> Unit {
  for old_index, old_segment in old_playlist.segments {
    let old_sequence = old_playlist.media_sequence + old_index
    match find_segment_by_sequence(new_playlist, old_sequence) {
      Some(new_segment) =>
        if old_segment.uri != new_segment.uri {
          push_change(
            changes,
            SegmentReordered,
            "segment-uri-at-sequence-changed",
            "sequence \{old_sequence} changed URI from \{old_segment.uri} to \{new_segment.uri}",
            old_segment.line,
            new_segment.line,
          )
        } else if segment_signature(old_segment) !=
          segment_signature(new_segment) {
          push_change(
            changes,
            SegmentChanged,
            "segment-metadata-changed",
            "segment \{old_segment.uri} changed duration, key, map, byte range, or discontinuity metadata",
            old_segment.line,
            new_segment.line,
          )
        }
      None =>
        if !sequence_before_playlist(new_playlist, old_sequence) {
          push_change(
            changes,
            SegmentRemoved,
            "segment-removed",
            "segment sequence \{old_sequence} disappeared from the new playlist",
            old_segment.line,
            0,
          )
        }
    }
  }
  for new_index, new_segment in new_playlist.segments {
    let new_sequence = new_playlist.media_sequence + new_index
    if find_segment_by_sequence(old_playlist, new_sequence) == None {
      push_change(
        changes,
        SegmentAdded,
        "segment-added",
        "segment sequence \{new_sequence} was added: \{new_segment.uri}",
        0,
        new_segment.line,
      )
    }
  }
}

///|
fn compare_variants(
  old_playlist : HlsPlaylist,
  new_playlist : HlsPlaylist,
  changes : Array[DeltaChange],
) -> Unit {
  for old_variant in old_playlist.variants {
    match find_variant_by_uri(new_playlist, old_variant.uri) {
      Some(new_variant) =>
        if variant_signature(old_variant) != variant_signature(new_variant) {
          push_change(
            changes,
            VariantChanged,
            "variant-changed",
            "variant \{old_variant.uri} changed bandwidth, codecs, groups, or display attributes",
            old_variant.line,
            new_variant.line,
          )
        }
      None =>
        push_change(
          changes,
          VariantRemoved,
          "variant-removed",
          "variant was removed: \{old_variant.uri}",
          old_variant.line,
          0,
        )
    }
  }
  for new_variant in new_playlist.variants {
    if find_variant_by_uri(old_playlist, new_variant.uri) == None {
      push_change(
        changes,
        VariantAdded,
        "variant-added",
        "variant was added: \{new_variant.uri}",
        0,
        new_variant.line,
      )
    }
  }
}

///|
fn compare_renditions(
  old_playlist : HlsPlaylist,
  new_playlist : HlsPlaylist,
  changes : Array[DeltaChange],
) -> Unit {
  for old_rendition in old_playlist.renditions {
    match find_rendition(new_playlist, old_rendition) {
      Some(new_rendition) =>
        if rendition_signature(old_rendition) !=
          rendition_signature(new_rendition) {
          push_change(
            changes,
            RenditionChanged,
            "rendition-changed",
            "rendition \{rendition_key(old_rendition)} changed URI or selection flags",
            old_rendition.line,
            new_rendition.line,
          )
        }
      None =>
        push_change(
          changes,
          RenditionRemoved,
          "rendition-removed",
          "rendition was removed: \{rendition_key(old_rendition)}",
          old_rendition.line,
          0,
        )
    }
  }
  for new_rendition in new_playlist.renditions {
    if find_rendition(old_playlist, new_rendition) == None {
      push_change(
        changes,
        RenditionAdded,
        "rendition-added",
        "rendition was added: \{rendition_key(new_rendition)}",
        0,
        new_rendition.line,
      )
    }
  }
}

///|
fn report_from_changes(
  old_playlist : HlsPlaylist,
  new_playlist : HlsPlaylist,
  changes : Array[DeltaChange],
) -> DeltaReport {
  let mut breaking_changes = 0
  let mut additive_changes = 0
  let mut informational_changes = 0
  for change in changes {
    match delta_bucket(change.kind) {
      "breaking" => breaking_changes += 1
      "additive" => additive_changes += 1
      _ => informational_changes += 1
    }
  }
  {
    compatible: breaking_changes == 0,
    breaking_changes,
    additive_changes,
    informational_changes,
    old_sequence: old_playlist.media_sequence,
    new_sequence: new_playlist.media_sequence,
    changes,
  }
}

///|
fn find_segment_by_sequence(playlist : HlsPlaylist, sequence : Int) -> Segment? {
  let index = sequence - playlist.media_sequence
  if index < 0 || index >= playlist.segments.length() {
    None
  } else {
    Some(playlist.segments[index])
  }
}

///|
fn sequence_before_playlist(playlist : HlsPlaylist, sequence : Int) -> Bool {
  sequence < playlist.media_sequence
}

///|
fn find_variant_by_uri(playlist : HlsPlaylist, uri : String) -> VariantStream? {
  for variant in playlist.variants {
    if variant.uri == uri {
      return Some(variant)
    }
  }
  None
}

///|
fn find_rendition(playlist : HlsPlaylist, probe : Rendition) -> Rendition? {
  for rendition in playlist.renditions {
    if rendition.rendition_type == probe.rendition_type &&
      rendition.group_id == probe.group_id &&
      rendition.name == probe.name {
      return Some(rendition)
    }
  }
  None
}

///|
fn push_change(
  changes : Array[DeltaChange],
  kind : DeltaKind,
  code : String,
  message : String,
  old_line : Int,
  new_line : Int,
) -> Unit {
  changes.push({ kind, code, message, old_line, new_line })
}

///|
fn segment_signature(segment : Segment) -> String {
  segment.uri +
  "|" +
  segment.duration_ms.to_string() +
  "|" +
  option_range_signature(segment.byte_range) +
  "|" +
  option_key_signature(segment.key) +
  "|" +
  option_map_signature(segment.map) +
  "|" +
  segment.discontinuity.to_string() +
  "|" +
  option_string(segment.program_date_time)
}

///|
fn option_range_signature(range : ByteRange?) -> String {
  match range {
    Some(value) => byte_range_to_string(value)
    None => "-"
  }
}

///|
fn option_key_signature(key : KeyInfo?) -> String {
  match key {
    Some(value) =>
      value.key_method +
      ":" +
      option_string(value.uri) +
      ":" +
      option_string(value.iv) +
      ":" +
      option_string(value.key_format) +
      ":" +
      option_string(value.key_format_versions)
    None => "-"
  }
}

///|
fn option_map_signature(map : SegmentMap?) -> String {
  match map {
    Some(value) => value.uri + ":" + option_range_signature(value.byte_range)
    None => "-"
  }
}

///|
fn variant_signature(variant : VariantStream) -> String {
  variant.bandwidth.to_string() +
  "|" +
  option_int(variant.average_bandwidth) +
  "|" +
  option_string(variant.codecs) +
  "|" +
  option_resolution(variant.resolution) +
  "|" +
  option_int(variant.frame_rate_milli) +
  "|" +
  option_string(variant.audio_group) +
  "|" +
  option_string(variant.subtitles_group) +
  "|" +
  option_string(variant.closed_captions)
}

///|
fn rendition_signature(rendition : Rendition) -> String {
  rendition_key(rendition) +
  "|" +
  option_string(rendition.language) +
  "|" +
  option_string(rendition.uri) +
  "|" +
  rendition.default.to_string() +
  "|" +
  rendition.autoselect.to_string() +
  "|" +
  rendition.forced.to_string()
}

///|
fn rendition_key(rendition : Rendition) -> String {
  rendition.rendition_type + ":" + rendition.group_id + ":" + rendition.name
}

///|
fn option_string(value : String?) -> String {
  match value {
    Some(text) => text
    None => "-"
  }
}

///|
fn option_int(value : Int?) -> String {
  match value {
    Some(number) => number.to_string()
    None => "-"
  }
}

///|
fn option_resolution(value : Resolution?) -> String {
  match value {
    Some(resolution) => resolution_to_string(resolution)
    None => "-"
  }
}

///|
fn delta_bucket(kind : DeltaKind) -> String {
  match kind {
    SegmentRemoved => "breaking"
    SegmentChanged => "breaking"
    SegmentReordered => "breaking"
    SequenceRegressed => "breaking"
    TargetDurationChanged => "info"
    VariantRemoved => "breaking"
    VariantChanged => "breaking"
    RenditionRemoved => "breaking"
    RenditionChanged => "breaking"
    SegmentAdded => "additive"
    SequenceAdvanced => "info"
    EndListChanged => "info"
    VariantAdded => "additive"
    RenditionAdded => "additive"
  }
}

///|
fn delta_kind_to_string(kind : DeltaKind) -> String {
  match kind {
    SegmentAdded => "segment-added"
    SegmentRemoved => "segment-removed"
    SegmentChanged => "segment-changed"
    SegmentReordered => "segment-reordered"
    SequenceAdvanced => "sequence-advanced"
    SequenceRegressed => "sequence-regressed"
    TargetDurationChanged => "target-duration-changed"
    EndListChanged => "endlist-changed"
    VariantAdded => "variant-added"
    VariantRemoved => "variant-removed"
    VariantChanged => "variant-changed"
    RenditionAdded => "rendition-added"
    RenditionRemoved => "rendition-removed"
    RenditionChanged => "rendition-changed"
  }
}