///|
/// The explicit inputs and output target for one generator run.
pub(all) struct GenerationPlan {
  input_paths : Array[String]
  output_path : String
}

///|
/// A successfully parsed MoonBit source input.
pub(all) struct ParsedSource {
  path : String
  ast : @syntax.Impls
}

///|
/// Callback-level failure reported by a downstream generator.
pub(all) suberror GenerationFailure {
  GenerationFailure(String)
} derive(Debug)

///|
/// Runner-level failure caused by an input source that cannot be parsed.
pub(all) suberror ParseFailure {
  ParseFailure(String)
} derive(Debug)

///|
pub fn GenerationFailure::message(self : GenerationFailure) -> String {
  match self {
    GenerationFailure(message) => message
  }
}

///|
pub fn ParseFailure::message(self : ParseFailure) -> String {
  match self {
    ParseFailure(message) => message
  }
}

///|
/// A downstream generator callback that emits into the runner-owned builder.
pub type GenerationCallback = (Array[ParsedSource], StringBuilder) -> Unit raise GenerationFailure

///|
/// Run generation and write the emitted source to the explicit output target.
pub fn generate(
  plan : GenerationPlan,
  callback : GenerationCallback,
) -> Unit raise {
  let generated = generate_to_string(plan, callback)
  create_parent_dirs(@path.Path(plan.output_path).dirname().to_string())
  @fs.write_string_to_file(plan.output_path, generated)
}

///|
/// Parse the input sources and invoke the callback with a runner-owned builder.
pub fn generate_to_string(
  plan : GenerationPlan,
  callback : GenerationCallback,
) -> String raise {
  if plan.input_paths.is_empty() {
    raise ParseFailure::ParseFailure("expected at least one input source")
  }
  let sources = []
  for path in plan.input_paths {
    sources.push(parse_source(path))
  }
  let out = StringBuilder::new()
  callback(sources, out)
  out.to_string()
}

///|
fn parse_source(path : String) -> ParsedSource raise ParseFailure {
  let (ast, reports) = @parser.parse_file(path) catch {
    _ =>
      raise ParseFailure::ParseFailure("failed to read input source: \{path}")
  }
  if !reports.is_empty() {
    raise ParseFailure::ParseFailure(format_parse_reports(reports))
  }
  ParsedSource::{ path, ast }
}

///|
fn format_parse_reports(reports : Array[@basic.Report]) -> String {
  reports.map(report => report.msg).join("\n")
}

///|
fn create_parent_dirs(path : String) -> Unit raise @fs.IOError {
  if path == "" || @fs.path_exists(path) {
    return
  }
  let parent = @path.Path(path).dirname().to_string()
  if parent != path {
    create_parent_dirs(parent)
  }
  @fs.create_dir(path)
}