///|
enum VisitState {
  Visiting
  Done
} derive(Eq, @debug.Debug)

///|
pub fn validate_project(project : Project) -> Unit raise MoonforgeError {
  ignore(task_order(project, None))
  ignore(duplicate_outputs(project))
}

///|
pub fn task_order(
  project : Project,
  target : String?,
) -> Array[String] raise MoonforgeError {
  let target_name = match target {
    Some(name) => name
    None => default_task_name(project)
  }
  guard project.tasks.contains(target_name) else {
    raise MissingTask("unknown task '\{target_name}'")
  }
  let order = []
  let states : Map[String, VisitState] = Map([])
  let stack = []
  dfs_task(project, target_name, states, order, stack)
  order
}

///|
fn dfs_task(
  project : Project,
  name : String,
  states : Map[String, VisitState],
  order : Array[String],
  stack : Array[String],
) -> Unit raise MoonforgeError {
  match states.get(name) {
    Some(Done) => ()
    Some(Visiting) => {
      let cycle = stack + [name]
      raise CycleDetected(join_strings(cycle, " -> "))
    }
    None => {
      states[name] = Visiting
      stack.push(name)
      let task = get_task(project, name)
      for dep in task.deps {
        guard project.tasks.contains(dep) else {
          config_error("task '\{name}' depends on missing task '\{dep}'")
        }
        dfs_task(project, dep, states, order, stack)
      }
      ignore(stack.pop())
      states[name] = Done
      order.push(name)
    }
  }
}

///|
pub fn task_levels(
  project : Project,
  order : Array[String],
) -> Array[Array[String]] raise MoonforgeError {
  let levels : Map[String, Int] = Map([])
  let max_level = Ref(0)
  for name in order {
    let task = get_task(project, name)
    let mut level = 0
    for dep in task.deps {
      let dep_level = levels.get(dep).unwrap()
      if dep_level + 1 > level {
        level = dep_level + 1
      }
    }
    levels[name] = level
    if level > max_level.val {
      max_level.val = level
    }
  }
  let grouped = []
  for _ in 0..<=max_level.val {
    grouped.push([])
  }
  for name in order {
    let level = levels.get(name).unwrap()
    grouped[level].push(name)
  }
  grouped
}

///|
pub fn duplicate_outputs(project : Project) -> Array[(String, String, String)] {
  let seen : Map[String, String] = Map([])
  let duplicates = []
  for task_name, task in project.tasks {
    for output in task.outputs {
      match seen.get(output) {
        Some(other_task) => duplicates.push((output, other_task, task_name))
        None => seen[output] = task_name
      }
    }
  }
  duplicates
}

///|
fn default_task_name(project : Project) -> String {
  if project.tasks.contains("build") {
    "build"
  } else if project.tasks.contains("default") {
    "default"
  } else {
    let names = project.tasks.keys().to_array()
    names.sort()
    names[0]
  }
}

///|
fn get_task(project : Project, name : String) -> Task raise MoonforgeError {
  match project.tasks.get(name) {
    Some(task) => task
    None => raise MissingTask("unknown task '\{name}'")
  }
}