///|
pub(all) struct SourceFile {
  path : String
  content : String
} derive(Debug, Eq, ToJson)

///|
pub(all) struct ProjectMutation {
  global_id : Int
  file_index : Int
  file : String
  local_id : Int
  candidate : MutationCandidate
  mutated_source : String
} derive(Debug, Eq, ToJson)

///|
pub(all) struct FileMutationPlan {
  file : String
  file_index : Int
  start_id : Int
  end_id : Int
  summary : MutationSummary
  candidates : Array[MutationCandidate]
} derive(Debug, Eq, ToJson)

///|
pub(all) struct ProjectMutationPlan {
  file_count : Int
  mutation_count : Int
  files : Array[FileMutationPlan]
  mutations : Array[ProjectMutation]
} derive(Debug, Eq, ToJson)

///|
pub fn source_file(path : String, content : String) -> SourceFile {
  { path, content }
}

///|
pub fn plan_project(files : ArrayView[SourceFile]) -> ProjectMutationPlan {
  plan_project_filtered(files, MutationFilter::all())
}

///|
pub fn plan_project_filtered(
  files : ArrayView[SourceFile],
  filter : MutationFilter,
) -> ProjectMutationPlan {
  let file_plans : Array[FileMutationPlan] = []
  let mutations : Array[ProjectMutation] = []
  let mut next_id = 0
  for file_index, source in files {
    let candidates = discover_filtered(source.content, filter, file=source.path)
    let start_id = next_id
    for candidate in candidates {
      mutations.push({
        global_id: next_id,
        file_index,
        file: source.path,
        local_id: candidate.id,
        candidate,
        mutated_source: apply_mutation(source.content, candidate),
      })
      next_id += 1
    }
    file_plans.push({
      file: source.path,
      file_index,
      start_id,
      end_id: next_id,
      summary: summarize_candidates(source.path, candidates),
      candidates,
    })
  }
  {
    file_count: files.length(),
    mutation_count: mutations.length(),
    files: file_plans,
    mutations,
  }
}

///|
pub fn project_mutation_by_id(
  plan : ProjectMutationPlan,
  global_id : Int,
) -> ProjectMutation? {
  for mutation in plan.mutations {
    if mutation.global_id == global_id {
      break Some(mutation)
    }
  } nobreak {
    None
  }
}

///|
pub fn format_project_mutation(mutation : ProjectMutation) -> String {
  "#\{mutation.global_id} " +
  "\{mutation.file}:\{mutation.candidate.span.line}:\{mutation.candidate.span.column} " +
  "\{mutation.candidate.rule.label} " +
  "\{mutation.candidate.original} -> \{mutation.candidate.replacement}"
}

///|
pub fn format_project_plan_summary(plan : ProjectMutationPlan) -> String {
  let lines : Array[String] = [
    "Project mutation plan",
    "files: \{plan.file_count}",
    "mutations: \{plan.mutation_count}",
  ]
  for file in plan.files {
    lines.push(
      "- \{file.file}: \{file.summary.candidate_count} candidates " +
      "(ids \{file.start_id}..\{file.end_id})",
    )
  }
  lines.join("\n")
}

///|
pub fn format_project_plan(plan : ProjectMutationPlan) -> String {
  let lines : Array[String] = [format_project_plan_summary(plan), ""]
  if plan.mutations.is_empty() {
    lines.push("No project mutation candidates found.")
  } else {
    for mutation in plan.mutations {
      lines.push(format_project_mutation(mutation))
    }
  }
  lines.join("\n")
}