// Deterministic, filesystem-independent workloads for benchmarks and QA.
//
// The data models common repositories: source files, tests, documentation,
// generated artifacts, hidden caches, and nested resources.  Keeping the
// generator in the library makes benchmark claims reproducible on every OS.

///|
pub enum WorkloadError {
  InvalidSize
  TooLarge
} derive(Debug, Eq)

///|
pub impl Show for WorkloadError with fn output(self, logger) {
  match self {
    WorkloadError::InvalidSize => logger.write_string("InvalidSize")
    WorkloadError::TooLarge => logger.write_string("TooLarge")
  }
}

///|
/// A named collection of patterns and deterministic candidate paths.
pub(all) struct GlobWorkload {
  name : String
  patterns : Array[String]
  paths : Array[String]
} derive(Debug, Eq)

///|
/// Aggregate results from evaluating every pattern against every candidate.
pub(all) struct WorkloadResult {
  workload_name : String
  pattern_count : Int
  path_count : Int
  matches : Int
  patterns_with_matches : Int
  invalid_patterns : Int
  literal_patterns : Int
  recursive_patterns : Int
} derive(Debug, Eq)

///|
fn two_digits(value : Int) -> String {
  if value < 10 {
    "0" + value.to_string()
  } else {
    value.to_string()
  }
}

///|
fn default_patterns() -> Array[String] {
  [
    "**/*.mbt", "**/*_test.mbt", "src/**/*.mbt", "tests/**/*.mbt", "docs/**/*.md",
    "**/*.json", "generated/**/*.gen.mbt", ".cache/**", "**/nested/*.txt", "**/missing/*.none",
  ]
}

///|
fn append_path_family(
  paths : Array[String],
  directory : String,
  index : Int,
  include_deep : Bool,
) -> Unit {
  let suffix = two_digits(index)
  paths.push(directory + "/module" + suffix + ".mbt")
  paths.push(directory + "/module" + suffix + "_test.mbt")
  paths.push(directory + "/data" + suffix + ".json")
  paths.push(directory + "/note" + suffix + ".md")
  paths.push("generated/" + directory + "/module" + suffix + ".gen.mbt")
  if include_deep {
    paths.push(directory + "/nested/leaf" + suffix + ".txt")
    paths.push(directory + "/nested/leaf" + suffix + ".bin")
  }
}

///|
fn build_paths(depth : Int, width : Int) -> Array[String] {
  let paths : Array[String] = [
    ".cache/index.bin", ".cache/state.json", "README.md", "LICENSE",
  ]
  let directories = ["src", "tests", "docs", "bench", "generated"]
  for directory in directories {
    let mut i = 0
    while i < width {
      append_path_family(paths, directory, i, depth > 1)
      i = i + 1
    }
  }
  paths
}

///|
/// Builds a reproducible repository-shaped workload.
pub fn build_workload(
  depth : Int,
  width : Int,
) -> Result[GlobWorkload, WorkloadError] {
  if depth < 1 || width < 1 {
    return Err(WorkloadError::InvalidSize)
  }
  if depth > 16 || width > 1000 {
    return Err(WorkloadError::TooLarge)
  }
  Ok({
    name: "custom-" + depth.to_string() + "x" + width.to_string(),
    patterns: default_patterns(),
    paths: build_paths(depth, width),
  })
}

///|
/// A compact workload suitable for every CI run.
pub fn build_default_workload() -> GlobWorkload {
  let workload = build_workload(3, 8).unwrap()
  { ..workload, name: "default" }
}

///|
/// A larger workload for local performance checks and release notes.
pub fn build_large_workload() -> GlobWorkload {
  let workload = build_workload(8, 40).unwrap()
  { ..workload, name: "large" }
}

///|
/// Returns the workload name.
pub fn GlobWorkload::name(self : GlobWorkload) -> String {
  self.name
}

///|
pub fn GlobWorkload::pattern_count(self : GlobWorkload) -> Int {
  self.patterns.length()
}

///|
pub fn GlobWorkload::path_count(self : GlobWorkload) -> Int {
  self.paths.length()
}

///|
pub fn GlobWorkload::patterns(self : GlobWorkload) -> Array[String] {
  self.patterns.copy()
}

///|
pub fn GlobWorkload::paths(self : GlobWorkload) -> Array[String] {
  self.paths.copy()
}

///|
/// Adds a path if it is not already present.
pub fn GlobWorkload::with_path(
  self : GlobWorkload,
  path : String,
) -> GlobWorkload {
  let paths = self.paths.copy()
  if !paths.contains(path) {
    paths.push(normalize_path(path))
  }
  { ..self, paths, }
}

///|
/// Adds a pattern while preserving input order.
pub fn GlobWorkload::with_pattern(
  self : GlobWorkload,
  pattern : String,
) -> GlobWorkload {
  let patterns = self.patterns.copy()
  patterns.push(pattern)
  { ..self, patterns, }
}

///|
fn evaluate_compiled(compiled : CompiledPattern, paths : Array[String]) -> Int {
  let mut matches = 0
  for path in paths {
    if compiled.matches(path) {
      matches = matches + 1
    }
  }
  matches
}

///|
/// Evaluates a workload with one compilation per pattern and no timing noise.
pub fn evaluate_workload(workload : GlobWorkload) -> WorkloadResult {
  let mut matches = 0
  let mut patterns_with_matches = 0
  let mut invalid_patterns = 0
  let mut literal_patterns = 0
  let mut recursive_patterns = 0
  for pattern in workload.patterns {
    match compile_pattern(pattern) {
      Err(_) => invalid_patterns = invalid_patterns + 1
      Ok(compiled) => {
        if compiled.is_literal() {
          literal_patterns = literal_patterns + 1
        }
        if compiled.has_recursive_wildcard() {
          recursive_patterns = recursive_patterns + 1
        }
        let count = evaluate_compiled(compiled, workload.paths)
        matches = matches + count
        if count > 0 {
          patterns_with_matches = patterns_with_matches + 1
        }
      }
    }
  }
  {
    workload_name: workload.name,
    pattern_count: workload.patterns.length(),
    path_count: workload.paths.length(),
    matches,
    patterns_with_matches,
    invalid_patterns,
    literal_patterns,
    recursive_patterns,
  }
}

///|
pub fn WorkloadResult::coverage_percent(self : WorkloadResult) -> Int {
  let total = self.pattern_count * self.path_count
  if total == 0 {
    0
  } else {
    self.matches * 100 / total
  }
}

///|
pub fn WorkloadResult::summary(self : WorkloadResult) -> String {
  "workload=" +
  self.workload_name +
  " patterns=" +
  self.pattern_count.to_string() +
  " paths=" +
  self.path_count.to_string() +
  " matches=" +
  self.matches.to_string() +
  " invalid=" +
  self.invalid_patterns.to_string()
}

///|
pub fn WorkloadResult::csv_header(_self : WorkloadResult) -> String {
  "workload_name,pattern_count,path_count,matches,patterns_with_matches,invalid_patterns"
}

///|
pub fn WorkloadResult::csv_row(self : WorkloadResult) -> String {
  self.workload_name +
  "," +
  self.pattern_count.to_string() +
  "," +
  self.path_count.to_string() +
  "," +
  self.matches.to_string() +
  "," +
  self.patterns_with_matches.to_string() +
  "," +
  self.invalid_patterns.to_string()
}

///|
pub fn WorkloadResult::has_invalid_patterns(self : WorkloadResult) -> Bool {
  self.invalid_patterns > 0
}

///|
pub fn WorkloadResult::is_useful(self : WorkloadResult) -> Bool {
  self.matches > 0 &&
  self.patterns_with_matches > 0 &&
  self.invalid_patterns == 0
}