///|
pub(all) enum FileCategory {
  SourceCode
  Documents
  Images
  Audio
  Video
  Archives
  Data
  Executables
  Fonts
  Logs
  Temporary
  OtherFiles
} derive(Eq, Debug, ToJson, FromJson)

///|
fn string_in(value : String, choices : Array[String]) -> Bool {
  for choice in choices {
    if value == choice {
      return true
    }
  }
  false
}

///|
pub fn file_category(entry : Entry) -> FileCategory {
  let extension = entry.extension()
  let name = entry.name().to_lower()
  if string_in(extension, [
      "mbt", "c", "h", "cc", "cpp", "hpp", "rs", "go", "py", "js", "jsx", "ts", "tsx",
      "java", "kt", "swift", "rb", "php", "cs", "fs", "scala", "lua", "sh", "bash",
      "zsh", "ps1", "sql", "html", "css", "scss", "vue",
    ]) ||
    string_in(name, ["makefile", "dockerfile", "justfile"]) {
    SourceCode
  } else if string_in(extension, [
      "md", "txt", "rst", "pdf", "doc", "docx", "odt", "rtf", "ppt", "pptx", "odp",
      "xls", "xlsx", "ods", "epub", "tex",
    ]) {
    Documents
  } else if string_in(extension, [
      "png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "ico", "tif", "tiff", "avif",
      "heic", "raw",
    ]) {
    Images
  } else if string_in(extension, [
      "mp3", "wav", "flac", "aac", "ogg", "m4a", "opus", "wma",
    ]) {
    Audio
  } else if string_in(extension, [
      "mp4", "mkv", "mov", "avi", "webm", "wmv", "flv", "m4v", "mpeg", "mpg",
    ]) {
    Video
  } else if string_in(extension, [
      "zip", "7z", "rar", "tar", "gz", "bz2", "xz", "zst", "tgz", "cab",
    ]) {
    Archives
  } else if string_in(extension, [
      "json", "jsonl", "csv", "tsv", "xml", "yaml", "yml", "toml", "ini", "db", "sqlite",
      "sqlite3", "parquet", "arrow", "pkl",
    ]) {
    Data
  } else if string_in(extension, [
      "exe", "dll", "so", "dylib", "bin", "app", "msi", "apk", "wasm", "o", "obj",
      "a", "lib", "class", "jar",
    ]) {
    Executables
  } else if string_in(extension, ["ttf", "otf", "woff", "woff2", "eot"]) {
    Fonts
  } else if string_in(extension, ["log", "trace", "out"]) {
    Logs
  } else if string_in(extension, ["tmp", "temp", "bak", "swp"]) ||
    name.has_suffix("~") {
    Temporary
  } else {
    OtherFiles
  }
}

///|
pub fn file_category_name(category : FileCategory) -> String {
  match category {
    SourceCode => "Source code"
    Documents => "Documents"
    Images => "Images"
    Audio => "Audio"
    Video => "Video"
    Archives => "Archives"
    Data => "Structured data"
    Executables => "Executables and build artifacts"
    Fonts => "Fonts"
    Logs => "Logs"
    Temporary => "Temporary files"
    OtherFiles => "Other"
  }
}

///|
pub fn file_category_key(category : FileCategory) -> String {
  match category {
    SourceCode => "source-code"
    Documents => "documents"
    Images => "images"
    Audio => "audio"
    Video => "video"
    Archives => "archives"
    Data => "data"
    Executables => "executables"
    Fonts => "fonts"
    Logs => "logs"
    Temporary => "temporary"
    OtherFiles => "other"
  }
}

///|
pub fn file_category_from_key(key : String) -> FileCategory raise {
  match key.to_lower() {
    "source" | "source-code" | "code" => SourceCode
    "document" | "documents" => Documents
    "image" | "images" => Images
    "audio" => Audio
    "video" => Video
    "archive" | "archives" => Archives
    "data" => Data
    "executable" | "executables" | "binary" => Executables
    "font" | "fonts" => Fonts
    "log" | "logs" => Logs
    "temporary" | "temp" => Temporary
    "other" => OtherFiles
    value => raise Failure("Unknown file category: " + value)
  }
}

///|
pub fn category_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 name = file_category_name(file_category(entry))
      map_add_size(sizes, name, entry.size)
      map_add_count(counts, name, 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
}

///|
pub fn files_in_category(
  snapshot : Snapshot,
  category : FileCategory,
) -> Array[Entry] {
  let result : Array[Entry] = []
  for entry in snapshot.entries {
    if entry.is_file() && file_category(entry) == category {
      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 {
      1
    }
  })
  result
}