///|
pub(all) struct DirectoryStat {
  path : String
  direct_size : Int64
  recursive_size : Int64
  direct_files : Int
  recursive_files : Int
} derive(Eq, Debug)

///|
pub(all) struct CategoryStat {
  name : String
  size : Int64
  files : Int
} derive(Eq, Debug)

///|
pub(all) struct AgeBuckets {
  day : CategoryStat
  week : CategoryStat
  month : CategoryStat
  quarter : CategoryStat
  year : CategoryStat
  older : CategoryStat
} derive(Eq, Debug)

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

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

///|
fn ancestor_paths(path : String) -> Array[String] {
  let result = [""]
  let normalized = normalize_relative_path(path)
  if normalized == "" {
    return result
  }
  let parts = normalized.split("/").to_array()
  let mut current = ""
  for part in parts {
    current = if current == "" {
      part.to_owned()
    } else {
      current + "/" + part.to_owned()
    }
    result.push(current)
  }
  result
}

///|
pub fn directory_stats(snapshot : Snapshot) -> Array[DirectoryStat] {
  let direct_sizes : Map[String, Int64] = Map([])
  let recursive_sizes : Map[String, Int64] = Map([])
  let direct_counts : Map[String, Int] = Map([])
  let recursive_counts : Map[String, Int] = Map([])
  let directories : Map[String, Bool] = Map([("", true)])
  for entry in snapshot.entries {
    if entry.is_dir() {
      directories[entry.path] = true
    } else if entry.is_file() {
      let parent = entry.parent()
      directories[parent] = true
      map_add_size(direct_sizes, parent, entry.size)
      map_add_count(direct_counts, parent, 1)
      for ancestor in ancestor_paths(parent) {
        directories[ancestor] = true
        map_add_size(recursive_sizes, ancestor, entry.size)
        map_add_count(recursive_counts, ancestor, 1)
      }
    }
  }
  let result : Array[DirectoryStat] = []
  for path, _ in directories {
    result.push(DirectoryStat::{
      path,
      direct_size: direct_sizes.get(path).unwrap_or(0L),
      recursive_size: recursive_sizes.get(path).unwrap_or(0L),
      direct_files: direct_counts.get(path).unwrap_or(0),
      recursive_files: recursive_counts.get(path).unwrap_or(0),
    })
  }
  result.sort_by(fn(a, b) {
    if a.path < b.path {
      -1
    } else if a.path > b.path {
      1
    } else {
      0
    }
  })
  result
}

///|
pub fn stat_for_directory(snapshot : Snapshot, path : String) -> DirectoryStat? {
  let wanted = normalize_relative_path(path)
  for stat in directory_stats(snapshot) {
    if stat.path == wanted {
      return Some(stat)
    }
  }
  None
}

///|
pub fn largest_files(snapshot : Snapshot, limit : Int) -> Array[Entry] {
  let result : Array[Entry] = []
  for entry in snapshot.entries {
    if entry.is_file() {
      result.push(entry)
    }
  }
  result.sort_by(fn(a, b) {
    if a.size > b.size {
      -1
    } else if a.size < b.size {
      1
    } else if a.path < b.path {
      -1
    } else if a.path > b.path {
      1
    } else {
      0
    }
  })
  if limit < result.length() {
    result[:limit].to_owned()
  } else {
    result
  }
}

///|
pub fn largest_directories(
  snapshot : Snapshot,
  limit : Int,
) -> Array[DirectoryStat] {
  let result = directory_stats(snapshot)
  result.sort_by(fn(a, b) {
    if a.recursive_size > b.recursive_size {
      -1
    } else if a.recursive_size < b.recursive_size {
      1
    } else if a.path < b.path {
      -1
    } else if a.path > b.path {
      1
    } else {
      0
    }
  })
  if limit < result.length() {
    result[:limit].to_owned()
  } else {
    result
  }
}

///|
pub fn extension_stats(snapshot : Snapshot) -> Array[CategoryStat] {
  let sizes : Map[String, Int64] = Map([])
  let counts : Map[String, Int] = Map([])
  for entry in snapshot.entries {
    if entry.is_file() {
      let ext = entry.extension()
      let category = if ext == "" { "(no extension)" } else { "." + ext }
      map_add_size(sizes, category, entry.size)
      map_add_count(counts, category, 1)
    }
  }
  let result : Array[CategoryStat] = []
  for name, size in sizes {
    result.push(CategoryStat::{ name, size, files: counts[name], })
  }
  result.sort_by(fn(a, b) {
    if a.size > b.size {
      -1
    } else if a.size < b.size {
      1
    } else if a.name < b.name {
      -1
    } else {
      1
    }
  })
  result
}

///|
fn empty_category(name : String) -> CategoryStat {
  CategoryStat::{ name, size: 0L, files: 0, }
}

///|
fn category_with_entry(category : CategoryStat, entry : Entry) -> CategoryStat {
  { ..category, size: category.size + entry.size, files: category.files + 1, }
}

///|
pub fn age_buckets(snapshot : Snapshot, now : Int64) -> AgeBuckets {
  let mut day = empty_category("less than 1 day")
  let mut week = empty_category("1 to 7 days")
  let mut month = empty_category("7 to 30 days")
  let mut quarter = empty_category("30 to 90 days")
  let mut year = empty_category("90 to 365 days")
  let mut older = empty_category("older than 365 days")
  for entry in snapshot.entries {
    if !entry.is_file() {
      continue
    }
    let age = if now > entry.modified_at { now - entry.modified_at } else { 0L }
    if age < 86400L {
      day = category_with_entry(day, entry)
    } else if age < 604800L {
      week = category_with_entry(week, entry)
    } else if age < 2592000L {
      month = category_with_entry(month, entry)
    } else if age < 7776000L {
      quarter = category_with_entry(quarter, entry)
    } else if age < 31536000L {
      year = category_with_entry(year, entry)
    } else {
      older = category_with_entry(older, entry)
    }
  }
  AgeBuckets::{ day, week, month, quarter, year, older, }
}

///|
pub fn empty_directories(snapshot : Snapshot) -> Array[Entry] {
  let stats = directory_stats(snapshot)
  let result : Array[Entry] = []
  for entry in snapshot.entries {
    if entry.is_dir() {
      for stat in stats {
        if stat.path == entry.path && stat.recursive_files == 0 {
          result.push(entry)
          break
        }
      }
    }
  }
  result.sort_by(fn(a, b) { if a.path < b.path { -1 } else { 1 } })
  result
}