///|
/// A MoonBit package discovered from a moon.pkg file.
pub(all) struct MoonPackage {
  path : String
  import_path : String
  manifest : MoonManifest
  source_files : Array[ProjectFile]
  test_files : Array[ProjectFile]
} derive(Debug, Eq)

///|
pub fn MoonPackage::new(
  path : String,
  import_path : String,
  manifest : MoonManifest,
  source_files : Array[ProjectFile],
  test_files : Array[ProjectFile],
) -> MoonPackage {
  { path, import_path, manifest, source_files, test_files }
}

///|
pub fn MoonPackage::has_tests(self : MoonPackage) -> Bool {
  self.test_files.length() > 0
}

///|
pub fn MoonPackage::has_source(self : MoonPackage) -> Bool {
  self.source_files.length() > 0
}

///|
/// Directed graph of local package imports in one MoonBit module.
pub(all) struct PackageGraph {
  module_name : String
  packages : Array[MoonPackage]
  local_edges : Map[String, Array[String]]
  missing_local_imports : Array[(String, String)]
  cycles : Array[Array[String]]
} derive(Debug, Eq)

///|
pub fn PackageGraph::new(
  module_name : String,
  packages : Array[MoonPackage],
  local_edges : Map[String, Array[String]],
  missing_local_imports : Array[(String, String)],
  cycles : Array[Array[String]],
) -> PackageGraph {
  { module_name, packages, local_edges, missing_local_imports, cycles }
}

///|
pub fn PackageGraph::find_package(
  self : PackageGraph,
  import_path : String,
) -> MoonPackage? {
  for node in self.packages {
    if node.import_path == import_path {
      return Some(node)
    }
  }
  None
}

///|
pub fn PackageGraph::package_count(self : PackageGraph) -> Int {
  self.packages.length()
}

///|
pub fn PackageGraph::edge_count(self : PackageGraph) -> Int {
  let mut count = 0
  for _, targets in self.local_edges {
    count += targets.length()
  }
  count
}

///|
fn package_directory(manifest_path : String) -> String {
  let pieces = manifest_path.split("/").to_array()
  if pieces.length() <= 1 {
    ""
  } else {
    pieces[:pieces.length() - 1].join("/")
  }
}

///|
fn package_import_path(module_name : String, directory : String) -> String {
  if directory.length() == 0 {
    module_name
  } else {
    module_name + "/" + directory
  }
}

///|
fn package_files(
  inventory : ProjectInventory,
  directory : String,
) -> Array[ProjectFile] {
  let prefix = if directory.length() == 0 { "" } else { directory + "/" }
  let files : Array[ProjectFile] = []
  for file in inventory.files {
    if file.path.has_prefix(prefix) {
      let nested_path = file.path[prefix.length():]
      if !nested_path.contains("/") && file.path.has_suffix(".mbt") {
        files.push(file)
      }
    }
  }
  files
}

///|
fn split_test_files(
  files : Array[ProjectFile],
) -> (Array[ProjectFile], Array[ProjectFile]) {
  let source_files : Array[ProjectFile] = []
  let test_files : Array[ProjectFile] = []
  for file in files {
    if file.path.has_suffix("_test.mbt") || file.path.has_suffix("_wbtest.mbt") {
      test_files.push(file)
    } else {
      source_files.push(file)
    }
  }
  (source_files, test_files)
}

///|
fn discover_packages(
  inventory : ProjectInventory,
  module_name : String,
) -> Array[MoonPackage] {
  let packages : Array[MoonPackage] = []
  for manifest_file in inventory.files_with_suffix("moon.pkg") {
    let directory = package_directory(manifest_file.path)
    let all_files = package_files(inventory, directory)
    let (source_files, test_files) = split_test_files(all_files)
    packages.push(
      MoonPackage::new(
        directory,
        package_import_path(module_name, directory),
        parse_manifest_file(manifest_file),
        source_files,
        test_files,
      ),
    )
  }
  packages
}

///|
fn local_imports(
  module_name : String,
  imports : Array[String],
) -> Array[String] {
  let local_imports : Array[String] = []
  for import_name in imports {
    if import_name == module_name || import_name.has_prefix(module_name + "/") {
      local_imports.push(import_name)
    }
  }
  local_imports
}

///|
fn package_paths(packages : Array[MoonPackage]) -> Map[String, Unit] {
  let paths : Map[String, Unit] = {}
  for node in packages {
    paths[node.import_path] = ()
  }
  paths
}

///|
fn graph_edges(
  module_name : String,
  packages : Array[MoonPackage],
) -> (Map[String, Array[String]], Array[(String, String)]) {
  let edges : Map[String, Array[String]] = {}
  let missing : Array[(String, String)] = []
  let known = package_paths(packages)
  for node in packages {
    let imports = local_imports(module_name, node.manifest.imports)
    edges[node.import_path] = imports
    for target in imports {
      if !known.contains(target) {
        missing.push((node.import_path, target))
      }
    }
  }
  (edges, missing)
}

///|
fn find_cycle_from(
  current : String,
  edges : Map[String, Array[String]],
  visiting : Map[String, Unit],
  visited : Map[String, Unit],
  path : Array[String],
  cycles : Array[Array[String]],
) -> Unit {
  if visited.contains(current) {
    return
  }
  if visiting.contains(current) {
    let cycle : Array[String] = []
    let mut collecting = false
    for item in path {
      if item == current {
        collecting = true
      }
      if collecting {
        cycle.push(item)
      }
    }
    cycle.push(current)
    if cycle.length() > 1 && !cycles.contains(cycle) {
      cycles.push(cycle)
    }
    return
  }
  visiting[current] = ()
  path.push(current)
  match edges.get(current) {
    Some(targets) =>
      for target in targets {
        find_cycle_from(target, edges, visiting, visited, path, cycles)
      }
    None => ()
  }
  ignore(path.pop())
  visiting.remove(current)
  visited[current] = ()
}

///|
fn find_cycles(edges : Map[String, Array[String]]) -> Array[Array[String]] {
  let cycles : Array[Array[String]] = []
  let visited : Map[String, Unit] = {}
  for start in edges.keys() {
    let visiting : Map[String, Unit] = {}
    find_cycle_from(start, edges, visiting, visited, [], cycles)
  }
  cycles
}

///|
/// Build the local package dependency graph from a recursive inventory.
pub fn build_package_graph(inventory : ProjectInventory) -> PackageGraph {
  let module_name = match parse_root_manifest(inventory) {
    Some(manifest) =>
      match manifest.text("name") {
        Some(name) => name
        None => ""
      }
    None => ""
  }
  let packages = discover_packages(inventory, module_name)
  let (edges, missing) = graph_edges(module_name, packages)
  PackageGraph::new(module_name, packages, edges, missing, find_cycles(edges))
}