///|
/// Diffing compares two folio inventories without re-parsing Markdown. This
/// is useful for release notes: changed content should be visible before a
/// site or documentation set is published.

///|
pub(all) enum FolioChangeKind {
  Added
  Removed
  Changed
  Renamed
  Unchanged
} derive(Eq, Debug)

///|
pub(all) struct FolioSnapshotItem {
  path : String
  slug : String
  fingerprint : String
  title : String
  permalink : String
  format : MatterFormat
} derive(Eq, Debug)

///|
pub(all) struct FolioSnapshot {
  items : Array[FolioSnapshotItem]
} derive(Eq, Debug)

///|
pub(all) struct FolioChange {
  kind : FolioChangeKind
  before_path : String
  after_path : String
  slug : String
  before_fingerprint : String
  after_fingerprint : String
  title : String
  message : String
} derive(Eq, Debug)

///|
pub(all) struct DiffReport {
  has_changes : Bool
  added : Int
  removed : Int
  changed : Int
  renamed : Int
  unchanged : Int
  changes : Array[FolioChange]
} derive(Eq, Debug)

///|
pub fn snapshot_from_registry(registry : FolioRegistry) -> FolioSnapshot {
  let items : Array[FolioSnapshotItem] = []
  for record in registry.records {
    items.push(snapshot_item_from_record(record))
  }
  { items, }
}

///|
pub fn snapshot_item_from_record(record : FolioRecord) -> FolioSnapshotItem {
  {
    path: record.path,
    slug: record.folio.slug,
    fingerprint: record.folio.fingerprint,
    title: record.folio.title,
    permalink: record.folio.permalink,
    format: record.folio.format,
  }
}

///|
pub fn snapshot_from_manifest(manifest : String) -> FolioSnapshot {
  let items : Array[FolioSnapshotItem] = []
  let lines = manifest.split("\n").map(view => view.to_owned()).to_array()
  for line in lines {
    let trimmed = line.trim().to_owned()
    if !trimmed.is_empty() {
      match snapshot_item_from_manifest_line(trimmed) {
        Some(item) => items.push(item)
        None => ()
      }
    }
  }
  { items, }
}

///|
pub fn snapshot_item_from_manifest_line(line : String) -> FolioSnapshotItem? {
  let parts = line.split("|").map(view => view.to_owned()).to_array()
  if parts.length() < 4 {
    return None
  }
  let slug = parts[0]
  let fingerprint = parts[1]
  let format = matter_format_from_name(parts[2])
  let title = join_manifest_title(parts, 3)
  Some({
    path: slug + ".md",
    slug,
    fingerprint,
    title,
    permalink: "/" + slug,
    format,
  })
}

///|
pub fn diff_registries(
  before : FolioRegistry,
  after : FolioRegistry,
) -> DiffReport {
  diff_snapshots(snapshot_from_registry(before), snapshot_from_registry(after))
}

///|
pub fn diff_manifests(before : String, after : String) -> DiffReport {
  diff_snapshots(snapshot_from_manifest(before), snapshot_from_manifest(after))
}

///|
pub fn diff_snapshots(
  before : FolioSnapshot,
  after : FolioSnapshot,
) -> DiffReport {
  let changes : Array[FolioChange] = []
  for before_item in before.items {
    match snapshot_find_by_path(after, before_item.path) {
      Some(after_item) =>
        if before_item.fingerprint == after_item.fingerprint {
          changes.push(
            make_change(
              Unchanged,
              before_item,
              after_item,
              "unchanged folio `" + before_item.slug + "`",
            ),
          )
        } else {
          changes.push(
            make_change(
              Changed,
              before_item,
              after_item,
              "changed folio `" + before_item.slug + "`",
            ),
          )
        }
      None =>
        match snapshot_find_by_fingerprint(after, before_item.fingerprint) {
          Some(after_item) =>
            changes.push(
              make_change(
                Renamed,
                before_item,
                after_item,
                "renamed folio `" +
                before_item.path +
                "` -> `" +
                after_item.path +
                "`",
              ),
            )
          None => changes.push(make_removed_change(before_item))
        }
    }
  }
  for after_item in after.items {
    if snapshot_find_by_path(before, after_item.path) is Some(_) {
      continue
    }
    if snapshot_find_by_fingerprint(before, after_item.fingerprint) is Some(_) {
      continue
    }
    changes.push(make_added_change(after_item))
  }
  summarize_changes(changes)
}

///|
pub fn diff_added(report : DiffReport) -> Array[FolioChange] {
  changes_by_kind(report, Added)
}

///|
pub fn diff_removed(report : DiffReport) -> Array[FolioChange] {
  changes_by_kind(report, Removed)
}

///|
pub fn diff_changed(report : DiffReport) -> Array[FolioChange] {
  changes_by_kind(report, Changed)
}

///|
pub fn diff_renamed(report : DiffReport) -> Array[FolioChange] {
  changes_by_kind(report, Renamed)
}

///|
pub fn diff_unchanged(report : DiffReport) -> Array[FolioChange] {
  changes_by_kind(report, Unchanged)
}

///|
pub fn diff_is_release_safe(report : DiffReport) -> Bool {
  report.removed == 0 && report.changed == 0
}

///|
pub fn diff_summary_line(report : DiffReport) -> String {
  "added=" +
  report.added.to_string() +
  ", removed=" +
  report.removed.to_string() +
  ", changed=" +
  report.changed.to_string() +
  ", renamed=" +
  report.renamed.to_string() +
  ", unchanged=" +
  report.unchanged.to_string()
}

///|
pub fn diff_change_kind_name(kind : FolioChangeKind) -> String {
  match kind {
    Added => "added"
    Removed => "removed"
    Changed => "changed"
    Renamed => "renamed"
    Unchanged => "unchanged"
  }
}

///|
pub fn diff_change_lines(report : DiffReport) -> Array[String] {
  let lines : Array[String] = []
  for change in report.changes {
    lines.push(diff_change_kind_name(change.kind) + ": " + change.message)
  }
  lines
}

///|
pub fn diff_paths(report : DiffReport) -> Array[String] {
  let paths : Array[String] = []
  for change in report.changes {
    let path = if change.after_path.is_empty() {
      change.before_path
    } else {
      change.after_path
    }
    if !contains_string(paths, path) {
      paths.push(path)
    }
  }
  sorted_strings(paths)
}

///|
pub fn snapshot_paths(snapshot : FolioSnapshot) -> Array[String] {
  let paths : Array[String] = []
  for item in snapshot.items {
    paths.push(item.path)
  }
  sorted_strings(paths)
}

///|
pub fn snapshot_slugs(snapshot : FolioSnapshot) -> Array[String] {
  let slugs : Array[String] = []
  for item in snapshot.items {
    if !contains_string(slugs, item.slug) {
      slugs.push(item.slug)
    }
  }
  sorted_strings(slugs)
}

///|
pub fn snapshot_fingerprints(snapshot : FolioSnapshot) -> Array[String] {
  let values : Array[String] = []
  for item in snapshot.items {
    if !contains_string(values, item.fingerprint) {
      values.push(item.fingerprint)
    }
  }
  sorted_strings(values)
}

///|
pub fn snapshot_find_by_path(
  snapshot : FolioSnapshot,
  path : String,
) -> FolioSnapshotItem? {
  for item in snapshot.items {
    if item.path == path {
      return Some(item)
    }
  }
  None
}

///|
pub fn snapshot_find_by_slug(
  snapshot : FolioSnapshot,
  slug : String,
) -> FolioSnapshotItem? {
  for item in snapshot.items {
    if item.slug == slug {
      return Some(item)
    }
  }
  None
}

///|
pub fn snapshot_find_by_fingerprint(
  snapshot : FolioSnapshot,
  fingerprint : String,
) -> FolioSnapshotItem? {
  for item in snapshot.items {
    if item.fingerprint == fingerprint {
      return Some(item)
    }
  }
  None
}

///|
pub fn snapshot_manifest(snapshot : FolioSnapshot) -> String {
  let lines : Array[String] = []
  for item in snapshot.items {
    lines.push(
      item.slug +
      "|" +
      item.fingerprint +
      "|" +
      format_name(item.format) +
      "|" +
      item.title,
    )
  }
  lines.join("\n")
}

///|
pub fn snapshot_sorted(snapshot : FolioSnapshot) -> FolioSnapshot {
  let items : Array[FolioSnapshotItem] = []
  for item in snapshot.items {
    insert_snapshot_item_by_path(items, item)
  }
  { items, }
}

///|
pub fn snapshot_without_unchanged(report : DiffReport) -> DiffReport {
  let changes : Array[FolioChange] = []
  for change in report.changes {
    if change.kind != Unchanged {
      changes.push(change)
    }
  }
  summarize_changes(changes)
}

///|
fn changes_by_kind(
  report : DiffReport,
  kind : FolioChangeKind,
) -> Array[FolioChange] {
  let values : Array[FolioChange] = []
  for change in report.changes {
    if change.kind == kind {
      values.push(change)
    }
  }
  values
}

///|
fn summarize_changes(changes : Array[FolioChange]) -> DiffReport {
  let mut added = 0
  let mut removed = 0
  let mut changed = 0
  let mut renamed = 0
  let mut unchanged = 0
  for change in changes {
    match change.kind {
      Added => added = added + 1
      Removed => removed = removed + 1
      Changed => changed = changed + 1
      Renamed => renamed = renamed + 1
      Unchanged => unchanged = unchanged + 1
    }
  }
  {
    has_changes: added + removed + changed + renamed > 0,
    added,
    removed,
    changed,
    renamed,
    unchanged,
    changes,
  }
}

///|
fn make_added_change(item : FolioSnapshotItem) -> FolioChange {
  {
    kind: Added,
    before_path: "",
    after_path: item.path,
    slug: item.slug,
    before_fingerprint: "",
    after_fingerprint: item.fingerprint,
    title: item.title,
    message: "added folio `" + item.path + "`",
  }
}

///|
fn make_removed_change(item : FolioSnapshotItem) -> FolioChange {
  {
    kind: Removed,
    before_path: item.path,
    after_path: "",
    slug: item.slug,
    before_fingerprint: item.fingerprint,
    after_fingerprint: "",
    title: item.title,
    message: "removed folio `" + item.path + "`",
  }
}

///|
fn make_change(
  kind : FolioChangeKind,
  before : FolioSnapshotItem,
  after : FolioSnapshotItem,
  message : String,
) -> FolioChange {
  {
    kind,
    before_path: before.path,
    after_path: after.path,
    slug: after.slug,
    before_fingerprint: before.fingerprint,
    after_fingerprint: after.fingerprint,
    title: after.title,
    message,
  }
}

///|
fn matter_format_from_name(name : String) -> MatterFormat {
  match name {
    "yaml" => Yaml
    "toml" => Toml
    "json" => Json
    _ => None
  }
}

///|
fn join_manifest_title(parts : Array[String], start : Int) -> String {
  let rest : Array[String] = []
  for i in start.. Unit {
  for i in 0..