///|
priv struct GeneratedFile {
  name : String
  content : String
}

///|
fn generated_header(kind : String) -> String {
  "// Generated by nanocake generate (\{kind}). DO NOT EDIT.\n\n"
}

///|
fn generated_source(header : String, body : String) -> String {
  let clean = body
    .split("\n")
    .map(line => line.trim_end().to_owned())
    .join("\n")
    .trim_end()
    .to_owned()
  header + clean + "\n"
}

///|
fn generated_package() -> String {
  let text =
    #|import {
    #|  "YumeXi/nanocake/pass",
    #|  "YumeXi/nanocake/unparser/sexp",
    #|}
    #|
    #|formatter(ignore: [ "ast.mbt", "codec.mbt", "pass.mbt" ])
    #|
    #|warnings = "-unused_value-unused_type_declaration-unused_constructor-unused_field-missing_priv-struct_never_constructed-unreachable_code"
    #|
  text
}

///|
fn build_generated_files(
  options : GenerateOptions,
) -> Array[GeneratedFile] raise {
  let language = @language.Language::from_spec(
    name=options.language,
    path=options.spec,
    mod=options.module_name,
    pkg=options.pkg,
  )
  let ast = generated_source(
    generated_header("typed AST"),
    @fmt.impls_to_string(language.gen()),
  )
  let codec = generated_source(
    generated_header("S-expression codecs"),
    @gen.gen_source(language),
  )
  let passes = generated_source(
    generated_header("pass APIs"),
    @fmt.impls_to_string(@pass.gen_surface(language)) +
    @fmt.impls_to_string(@pass.gen_semantics(language)) +
    @fmt.impls_to_string(@pass.gen_rewrite(language)),
  )
  [
    { name: "ast.mbt", content: ast },
    { name: "codec.mbt", content: codec },
    { name: "pass.mbt", content: passes },
    { name: "moon.pkg", content: generated_package() },
  ]
}

///|
fn join_path(dir : String, name : String) -> String {
  if dir.has_suffix("/") {
    dir + name
  } else {
    dir + "/" + name
  }
}

///|
fn ensure_dir(path : String) -> Unit raise {
  if @fs.path_exists(path) {
    return
  }
  match path.rev_find("/") {
    Some(index) => {
      let parent = path[:index].to_owned()
      if parent != "" {
        ensure_dir(parent)
      }
    }
    None => ()
  }
  @fs.create_dir(path)
}

///|
fn file_matches(path : String, expected : String) -> Bool raise {
  @fs.path_exists(path) &&
  normalize_newlines(@fs.read_file_to_string(path)) ==
  normalize_newlines(expected)
}

///|
fn normalize_newlines(text : String) -> String {
  text.replace_all(old="\r\n", new="\n")
}

///|
fn write_generated_files(options : GenerateOptions) -> Array[String] raise {
  let files = build_generated_files(options)
  let changed : Array[String] = []
  if !options.check {
    ensure_dir(options.out_dir)
  }
  for file in files {
    let path = join_path(options.out_dir, file.name)
    if !file_matches(path, file.content) {
      changed.push(path)
      if !options.check {
        @fs.write_string_to_file(path, file.content)
      }
    }
  }
  changed
}