///|
/// Paired display excerpts around the first changed character, with up to 100
/// unchanged characters on either side. Each excerpt is at most 1600 characters;
/// ellipses mark omitted text. This is not a complete diff or a snapshot.
pub fn document_change_context(
  previous : String,
  current : String,
) -> (String, String) {
  let before = previous.to_array()
  let after = current.to_array()
  let mut prefix = 0
  while prefix < before.length() &&
        prefix < after.length() &&
        before[prefix] == after[prefix] {
    prefix += 1
  }
  let mut suffix = 0
  while suffix < before.length() - prefix &&
        suffix < after.length() - prefix &&
        before[before.length() - suffix - 1] ==
        after[after.length() - suffix - 1] {
    suffix += 1
  }
  let start = if previous == current { 0 } else { Int::max(0, prefix - 100) }
  (
    document_change_excerpt(before, start, before.length() - suffix + 100),
    document_change_excerpt(after, start, after.length() - suffix + 100),
  )
}

///|
fn document_change_excerpt(
  chars : Array[Char],
  start : Int,
  end : Int,
) -> String {
  let end = Int::min(chars.length(), Int::min(end, start + 1596))
  let text = String::from_array(chars[start:end])
  let text = if start > 0 { "…\n" + text } else { text }
  if end < chars.length() {
    text + "\n…"
  } else {
    text
  }
}

///|
pub(all) struct DocumentSnapshotChange {
  document : String
  source : String
  kind : String
  excerpt : String?
  change : String
  previous : String?
  current : String?
} derive(ToJson)

///|
pub extend DocumentSnapshotChange with ToJson::{to_json}

///|
fn document_snapshot_key(entry : DocumentSnapshotEntry) -> String {
  @json.to_json([
    entry.document,
    entry.source,
    entry.kind,
    @json.to_json(entry.excerpt).stringify(),
  ]).stringify()
}

///|
/// Compare complete verified snapshots. Line movement is ignored, repeated
/// references share one identity, and excerpt edits are removal + addition.
pub fn compare_document_snapshots(
  previous : DocumentSnapshot,
  current : DocumentSnapshot,
) -> Array[DocumentSnapshotChange] {
  let old : Map[String, DocumentSnapshotEntry] = Map([])
  let present : Map[String, Bool] = Map([])
  let changes : Array[DocumentSnapshotChange] = []
  for entry in previous.entries {
    let key = document_snapshot_key(entry)
    if old.get(key) is None {
      old.set(key, entry)
    }
  }
  for entry in current.entries {
    let key = document_snapshot_key(entry)
    if present.get(key) is Some(_) {
      continue
    }
    present.set(key, true)
    let before = old.get(key)
    if before is Some(value) && value.observed == entry.observed {
      continue
    }
    changes.push({
      document: entry.document,
      source: entry.source,
      kind: entry.kind,
      excerpt: entry.excerpt,
      change: if before is Some(_) {
        "changed"
      } else {
        "added"
      },
      previous: before.map(value => value.observed),
      current: Some(entry.observed),
    })
  }
  for entry in previous.entries {
    let key = document_snapshot_key(entry)
    if present.get(key) is Some(_) {
      continue
    }
    present.set(key, true)
    changes.push({
      document: entry.document,
      source: entry.source,
      kind: entry.kind,
      excerpt: entry.excerpt,
      change: "removed",
      previous: Some(entry.observed),
      current: None,
    })
  }
  changes
}