///|
/// Aggregated file activity for one directory bucket between two snapshots.
pub(all) struct DirectoryHotspot {
  path : String
  added_files : Int
  removed_files : Int
  changed_files : Int
  added_bytes : Int64
  removed_bytes : Int64
  grown_bytes : Int64
  shrunk_bytes : Int64
  net_bytes : Int64
} derive(Eq, Debug, ToJson, FromJson)

///|
fn hotspot_bucket(path : String, depth : Int) -> String {
  let parent = parent_path(path)
  if parent == "" || depth <= 0 {
    return ""
  }
  let parts = parent.split("/").to_array()
  let count = if depth < parts.length() { depth } else { parts.length() }
  parts[:count].join("/")
}

///|
fn hotspot_add_int(map : Map[String, Int], path : String, value : Int) -> Unit {
  map[path] = map.get(path).unwrap_or(0) + value
}

///|
fn hotspot_add_bytes(
  map : Map[String, Int64],
  path : String,
  value : Int64,
) -> Unit {
  map[path] = map.get(path).unwrap_or(0L) + value
}

///|
fn changed_file_before(change : EntryChange) -> Entry? {
  match change.before {
    Some(entry) if entry.is_file() => Some(entry)
    _ => None
  }
}

///|
fn changed_file_after(change : EntryChange) -> Entry? {
  match change.after {
    Some(entry) if entry.is_file() => Some(entry)
    _ => None
  }
}

///|
/// Group changed files by the first `depth` directory segments.
/// A depth of one answers which top-level directory caused the most activity;
/// zero combines all root-relative changes into one bucket.
pub fn directory_hotspots(
  diff : SnapshotDiff,
  depth? : Int = 1,
) -> Array[DirectoryHotspot] {
  let added_counts : Map[String, Int] = Map([])
  let removed_counts : Map[String, Int] = Map([])
  let changed_counts : Map[String, Int] = Map([])
  let added_sizes : Map[String, Int64] = Map([])
  let removed_sizes : Map[String, Int64] = Map([])
  let grown_sizes : Map[String, Int64] = Map([])
  let shrunk_sizes : Map[String, Int64] = Map([])
  let paths : Map[String, Bool] = Map([])
  for change in diff.changes {
    let before = changed_file_before(change)
    let after = changed_file_after(change)
    if before is None && after is None {
      continue
    }
    let path = hotspot_bucket(change.path, depth)
    paths[path] = true
    match (before, after) {
      (None, Some(entry)) => {
        hotspot_add_int(added_counts, path, 1)
        hotspot_add_bytes(added_sizes, path, entry.size)
      }
      (Some(entry), None) => {
        hotspot_add_int(removed_counts, path, 1)
        hotspot_add_bytes(removed_sizes, path, entry.size)
      }
      (Some(old), Some(current)) => {
        hotspot_add_int(changed_counts, path, 1)
        if current.size > old.size {
          hotspot_add_bytes(grown_sizes, path, current.size - old.size)
        } else if old.size > current.size {
          hotspot_add_bytes(shrunk_sizes, path, old.size - current.size)
        }
      }
      (None, None) => ()
    }
  }
  let result : Array[DirectoryHotspot] = []
  for path, _ in paths {
    let added = added_sizes.get(path).unwrap_or(0L)
    let removed = removed_sizes.get(path).unwrap_or(0L)
    let grown = grown_sizes.get(path).unwrap_or(0L)
    let shrunk = shrunk_sizes.get(path).unwrap_or(0L)
    result.push(DirectoryHotspot::{
      path,
      added_files: added_counts.get(path).unwrap_or(0),
      removed_files: removed_counts.get(path).unwrap_or(0),
      changed_files: changed_counts.get(path).unwrap_or(0),
      added_bytes: added,
      removed_bytes: removed,
      grown_bytes: grown,
      shrunk_bytes: shrunk,
      net_bytes: added + grown - removed - shrunk,
    })
  }
  result.sort_by(fn(a, b) {
    let a_activity = a.added_bytes +
      a.removed_bytes +
      a.grown_bytes +
      a.shrunk_bytes
    let b_activity = b.added_bytes +
      b.removed_bytes +
      b.grown_bytes +
      b.shrunk_bytes
    if a_activity > b_activity {
      -1
    } else if a_activity < b_activity {
      1
    } else if a.path < b.path {
      -1
    } else if a.path > b.path {
      1
    } else {
      0
    }
  })
  result
}

///|
pub fn render_directory_hotspots(
  hotspots : Array[DirectoryHotspot],
  limit? : Int = 20,
) -> String {
  let lines : Array[String] = [
    "MoonDisk directory growth hotspots",
    "Directories: " + hotspots.length().to_string(),
    "",
    "Net change     Added    Removed    Changed  Directory",
  ]
  let count = if limit < 0 {
    0
  } else if limit < hotspots.length() {
    limit
  } else {
    hotspots.length()
  }
  for index = 0; index < count; index = index + 1 {
    let item = hotspots[index]
    let display_path = if item.path == "" { "." } else { item.path }
    lines.push(
      pad_left(format_bytes(item.net_bytes), 12) +
      "  " +
      pad_left(item.added_files.to_string(), 8) +
      "  " +
      pad_left(item.removed_files.to_string(), 9) +
      "  " +
      pad_left(item.changed_files.to_string(), 9) +
      "  " +
      display_path,
    )
  }
  lines.join("\n")
}

///|
pub fn directory_hotspots_to_json(
  hotspots : Array[DirectoryHotspot],
  pretty? : Bool = true,
) -> String {
  let value = ToJson::to_json(hotspots)
  if pretty {
    value.stringify(indent=2)
  } else {
    value.stringify()
  }
}