///|
pub(all) enum ChangeKind {
  Added
  Removed
  Grew
  Shrank
  Modified
  KindChanged
} derive(Eq, Debug)

///|
pub(all) struct EntryChange {
  path : String
  kind : ChangeKind
  before : Entry?
  after : Entry?
  size_delta : Int64
} derive(Eq, Debug)

///|
pub(all) struct DirectoryChange {
  path : String
  before_size : Int64
  after_size : Int64
  size_delta : Int64
} derive(Eq, Debug)

///|
pub(all) struct SnapshotDiff {
  changes : Array[EntryChange]
  directories : Array[DirectoryChange]
  added_size : Int64
  removed_size : Int64
  net_size : Int64
} derive(Eq, Debug)

///|
fn entry_map(snapshot : Snapshot) -> Map[String, Entry] {
  let result : Map[String, Entry] = Map([])
  for entry in snapshot.entries {
    result[entry.path] = entry
  }
  result
}

///|
fn classify_existing(before : Entry, after : Entry) -> ChangeKind? {
  if before.kind != after.kind {
    Some(KindChanged)
  } else if before.size < after.size {
    Some(Grew)
  } else if before.size > after.size {
    Some(Shrank)
  } else if before.modified_at != after.modified_at {
    Some(Modified)
  } else {
    None
  }
}

///|
fn directory_size_map(snapshot : Snapshot) -> Map[String, Int64] {
  let result : Map[String, Int64] = Map([])
  for stat in directory_stats(snapshot) {
    result[stat.path] = stat.recursive_size
  }
  result
}

///|
pub fn compare_snapshots(before : Snapshot, after : Snapshot) -> SnapshotDiff {
  let old = entry_map(before)
  let current = entry_map(after)
  let changes : Array[EntryChange] = []
  let mut added_size = 0L
  let mut removed_size = 0L
  for path, previous in old {
    match current.get(path) {
      None => {
        let size = if previous.is_file() { previous.size } else { 0L }
        removed_size += size
        changes.push(EntryChange::{
          path,
          kind: Removed,
          before: Some(previous),
          after: None,
          size_delta: -size,
        })
      }
      Some(now) =>
        match classify_existing(previous, now) {
          Some(kind) =>
            changes.push(EntryChange::{
              path,
              kind,
              before: Some(previous),
              after: Some(now),
              size_delta: now.size - previous.size,
            })
          None => ()
        }
    }
  }
  for path, now in current {
    if old.get(path) is None {
      let size = if now.is_file() { now.size } else { 0L }
      added_size += size
      changes.push(EntryChange::{
        path,
        kind: Added,
        before: None,
        after: Some(now),
        size_delta: size,
      })
    }
  }
  changes.sort_by(fn(a, b) {
    let aa = if a.size_delta < 0L { -a.size_delta } else { a.size_delta }
    let bb = if b.size_delta < 0L { -b.size_delta } else { b.size_delta }
    if aa > bb {
      -1
    } else if aa < bb {
      1
    } else if a.path < b.path {
      -1
    } else {
      1
    }
  })
  let old_dirs = directory_size_map(before)
  let new_dirs = directory_size_map(after)
  let all_dirs : Map[String, Bool] = Map([])
  for path, _ in old_dirs {
    all_dirs[path] = true
  }
  for path, _ in new_dirs {
    all_dirs[path] = true
  }
  let directories : Array[DirectoryChange] = []
  for path, _ in all_dirs {
    let old_size = old_dirs.get(path).unwrap_or(0L)
    let new_size = new_dirs.get(path).unwrap_or(0L)
    if old_size != new_size {
      directories.push(DirectoryChange::{
        path,
        before_size: old_size,
        after_size: new_size,
        size_delta: new_size - old_size,
      })
    }
  }
  directories.sort_by(fn(a, b) {
    if a.size_delta > b.size_delta {
      -1
    } else if a.size_delta < b.size_delta {
      1
    } else if a.path < b.path {
      -1
    } else {
      1
    }
  })
  SnapshotDiff::{
    changes,
    directories,
    added_size,
    removed_size,
    net_size: after.total_size() - before.total_size(),
  }
}

///|
pub fn SnapshotDiff::added_count(self : SnapshotDiff) -> Int {
  self.changes.filter(fn(change) { change.kind == Added }).length()
}

///|
pub fn SnapshotDiff::removed_count(self : SnapshotDiff) -> Int {
  self.changes.filter(fn(change) { change.kind == Removed }).length()
}

///|
pub fn SnapshotDiff::changed_count(self : SnapshotDiff) -> Int {
  self.changes.length() - self.added_count() - self.removed_count()
}