// Emission of MoonBit source from the OpenCC snapshot.
//
// Dictionaries are embedded as their own text, split into chunks of raw string
// lines, and indexed at run time by `core/Dict::parse`. That keeps the generated
// files small, keeps the compiler fast, and — unlike one literal per entry —
// stays inside the plain wasm backend's limits. The embedded text is
// byte-identical to the file named in the header, so it can be audited.

///|
/// Lines per generated string chunk.
let chunk_lines : Int = 4000

///|
/// Derive a MoonBit identifier from a dictionary file name.
///
/// `STPhrases.txt` -> `st_phrases`, `CJK_Compatibility_Ideographs.txt` ->
/// `cjk_compatibility_ideographs`.
pub fn module_name(file : String) -> String {
  let stem = match file.strip_suffix(".txt") {
    Some(stem) => stem
    None => file.to_string_view()
  }
  let chars = stem.to_array()
  let out = StringBuilder()
  for index, ch in chars.iter2() {
    if ch >= 'A' && ch <= 'Z' {
      // Insert a boundary before an acronym tail (`STPhrases` -> `st_phrases`)
      // and before an uppercase letter that follows a lowercase one.
      let previous = if index > 0 { chars[index - 1] } else { ' ' }
      let next = if index + 1 < chars.length() { chars[index + 1] } else { ' ' }
      let after_word = (previous >= 'a' && previous <= 'z') ||
        (previous >= '0' && previous <= '9')
      let after_acronym = previous >= 'A' &&
        previous <= 'Z' &&
        next >= 'a' &&
        next <= 'z'
      if !out.is_empty() && (after_word || after_acronym) {
        out.write_char('_')
      }
      out.write_char(ch)
    } else if (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') {
      out.write_char(ch)
    } else {
      out.write_char('_')
    }
  }
  let mut name = out.to_string().to_lower()
  while name.contains("__") {
    name = name.replace_all(old="__", new="_")
  }
  name.trim(chars="_").to_owned()
}

///|
/// Escape a string for use inside a MoonBit string literal.
pub fn escape(text : String) -> String {
  let out = StringBuilder()
  for ch in text.iter() {
    if ch == '"' || ch == '\\' {
      out.write_char('\\')
    }
    out.write_char(ch)
  }
  out.to_string()
}

///|
/// Split text into lines, dropping the empty piece after a trailing newline.
pub fn lines_of(text : String) -> Array[String] {
  let lines : Array[String] = []
  for raw in text.split("\n") {
    lines.push(raw.to_owned())
  }
  if lines.length() > 0 && lines[lines.length() - 1].is_empty() {
    ignore(lines.pop())
  }
  lines
}

///|
/// Emit the data module for one dictionary.
///
/// The text is embedded verbatim through `#|` raw strings. Those strings drop a
/// single leading space, so a line that starts with a space would lose a
/// character; `tools/gen_dict` refuses such data instead of shipping it.
pub fn emit_dict_data(
  binding : String,
  source_label : String,
  text : String,
) -> String {
  let lines = lines_of(text)
  let out = StringBuilder()
  out.write_string("// Generated by tools/gen_dict from ")
  out.write_string(source_label)
  out.write_string(" — do not edit by hand.\n")
  out.write_string("//\n")
  out.write_string(
    "// The embedded text is byte-identical to that file; its SHA-256 is recorded\n",
  )
  out.write_string(
    "// in dict/manifest.mbt. Entries are indexed by core/Dict::parse.\n",
  )
  out.write_string("//\n")
  out.write_string(
    "// Only the first candidate of a multi-value entry is used for conversion;\n",
  )
  out.write_string(
    "// the remaining candidates take part in segmentation scoring upstream.\n\n",
  )

  let mut chunks = 0
  let mut index = 0
  while index < lines.length() {
    let stop = if index + chunk_lines < lines.length() {
      index + chunk_lines
    } else {
      lines.length()
    }
    out.write_string("///|\n")
    out.write_string("let ")
    out.write_string(binding)
    out.write_string("_chunk_")
    out.write_string(chunks.to_string())
    out.write_string(" : String =\n")
    let mut line_index = index
    while line_index < stop {
      out.write_string("  #|")
      out.write_string(lines[line_index])
      out.write_string("\n")
      line_index = line_index + 1
    }
    chunks = chunks + 1
    index = stop
  }
  if chunks == 0 {
    out.write_string("///|\nlet ")
    out.write_string(binding)
    out.write_string("_chunk_0 : String = \"\"\n")
    chunks = 1
  }

  out.write_string("\n///|\n")
  out.write_string("pub let ")
  out.write_string(binding)
  out.write_string(" : Array[String] = [\n")
  let mut chunk_index = 0
  while chunk_index < chunks {
    out.write_string("  ")
    out.write_string(binding)
    out.write_string("_chunk_")
    out.write_string(chunk_index.to_string())
    out.write_string(",\n")
    chunk_index = chunk_index + 1
  }
  out.write_string("]\n")
  out.to_string()
}

///|
/// Emit the package file of one dictionary package.
///
/// Each dictionary lives in its own package so that a consumer of a single
/// configuration links only the dictionaries that configuration references.
/// `dict` itself re-exports everything for name-based lookups.
pub fn emit_dict_subpkg() -> String {
  "// Generated by tools/gen_dict — do not edit by hand.\nformatter(ignore: [\"data.mbt\"])\n"
}

///|
/// Emit the package file of the aggregate `dict` package.
pub fn emit_dict_aggregate_pkg(
  module_path : String,
  bindings : Array[String],
) -> String {
  let out = StringBuilder()
  out.write_string("// Generated by tools/gen_dict — do not edit by hand.\n")
  out.write_string("//\n")
  out.write_string(
    "// This aggregate re-exports every dictionary, which is what the CLI, the tests\n",
  )
  out.write_string(
    "// and `lookup` need. Size-sensitive consumers should import a `config/`\n",
  )
  out.write_string(
    "// package instead: it references only the dictionaries that configuration uses.\n\n",
  )
  out.write_string("import {\n")
  for binding in bindings {
    out.write_string("  \"")
    out.write_string(module_path)
    out.write_string("/dict/")
    out.write_string(binding)
    out.write_string("\",\n")
  }
  out.write_string("}\n\n")
  out.write_string("formatter(ignore: [\"manifest.mbt\", \"reexport.mbt\"])\n")
  out.to_string()
}

///|
/// Emit the aggregate re-exports, so `@dict.st_phrases` keeps working.
pub fn emit_reexport(bindings : Array[String]) -> String {
  let out = StringBuilder()
  out.write_string(
    "// Generated by tools/gen_dict — do not edit by hand.\n\n",
  )
  for binding in bindings {
    out.write_string("///| Re-export of the `")
    out.write_string(binding)
    out.write_string("` dictionary.\npub using @")
    out.write_string(binding)
    out.write_string(" {")
    out.write_string(binding)
    out.write_string("}\n\n")
  }
  out.to_string()
}

///|
/// Emit the `lookup` function that resolves a binding name to its text chunks.
pub fn emit_lookup(bindings : Array[String]) -> String {
  let out = StringBuilder()
  out.write_string("\n///|\n")
  out.write_string("/// Resolve a dictionary binding by name.\n")
  out.write_string("///\n")
  out.write_string(
    "/// Generated from the snapshot: unknown names return `None` instead of\n",
  )
  out.write_string(
    "/// failing, so a configuration that references a dictionary this snapshot\n",
  )
  out.write_string("/// does not ship degrades to \"that pass is skipped\".\n")
  out.write_string("pub fn lookup(binding : String) -> Array[String]? {\n")
  out.write_string("  match binding {\n")
  for binding in bindings {
    out.write_string("    \"")
    out.write_string(binding)
    out.write_string("\" => Some(")
    out.write_string(binding)
    out.write_string(")\n")
  }
  out.write_string("    _ => None\n")
  out.write_string("  }\n")
  out.write_string("}\n")
  out.to_string()
}

///|
/// Write one pass node as MoonBit source.
fn emit_spec(out : StringBuilder, spec : Spec) -> Unit {
  match spec {
    Leaf(name) => {
      out.write_string("Spec::Leaf(\"")
      out.write_string(name)
      out.write_string("\")")
    }
    Group(policy, children) => {
      out.write_string("Spec::Group(")
      out.write_string(
        match policy {
          Union => "Policy::Union"
          ShortCircuit => "Policy::ShortCircuit"
        },
      )
      out.write_string(", [")
      for child in children {
        emit_spec(out, child)
        out.write_string(", ")
      }
      out.write_string("])")
    }
  }
}

///|
/// Record one leaf name if it is missing from the snapshot.
fn note_missing(
  pass : Spec,
  available : Array[String],
  missing : Array[String],
) -> Unit {
  for name in pass.leaves() {
    if !available.contains(name) && !missing.contains(name) {
      missing.push(name)
    }
  }
}

///|
/// Dictionaries a configuration asks for that the snapshot does not ship.
fn missing_of(spec : ChainSpec, available : Array[String]) -> Array[String] {
  let missing : Array[String] = []
  for pass in spec.normalization {
    note_missing(pass, available, missing)
  }
  match spec.segmentation {
    Some(pass) => note_missing(pass, available, missing)
    None => ()
  }
  for pass in spec.conversion {
    note_missing(pass, available, missing)
  }
  missing
}

///|
/// Write a list of passes as MoonBit source.
fn emit_passes(
  out : StringBuilder,
  specs : Array[Spec],
  indent : String,
) -> Unit {
  for spec in specs {
    out.write_string(indent)
    emit_spec(out, spec)
    out.write_string(",\n")
  }
}

///|
/// Emit `config/chains.mbt` from the parsed configurations.
pub fn emit_chains(
  specs : Array[ChainSpec],
  available : Array[String],
) -> String {
  let out = StringBuilder()
  out.write_string(
    "// Generated by tools/gen_dict from data/opencc/config/*.json — do not edit by hand.\n",
  )
  out.write_string("//\n")
  out.write_string(
    "// `passes` is the normalisation pass followed by the conversion chain, in the\n",
  )
  out.write_string(
    "// order OpenCC applies them. `missing` lists dictionaries the configuration\n",
  )
  out.write_string(
    "// references but the pinned snapshot does not ship (OpenCC produces them during\n",
  )
  out.write_string(
    "// its own build); the runtime builder skips those leaves and reports them.\n\n",
  )
  out.write_string("///|\n")
  out.write_string("pub(all) enum Policy {\n")
  out.write_string("  Union\n")
  out.write_string("  ShortCircuit\n")
  out.write_string("}\n\n")
  out.write_string("///|\n")
  out.write_string("pub(all) enum Spec {\n")
  out.write_string("  Leaf(String)\n")
  out.write_string("  Group(Policy, Array[Spec])\n")
  out.write_string("}\n\n")
  out.write_string("///|\n")
  out.write_string("pub struct ChainSpec {\n")
  out.write_string("  name : String\n")
  out.write_string("  title : String\n")
  out.write_string("  missing : Array[String]\n")
  out.write_string("  normalization : Array[Spec]\n")
  out.write_string("  segmentation : Spec?\n")
  out.write_string("  conversion : Array[Spec]\n")
  out.write_string("}\n\n")
  out.write_string("///| Every configuration of the pinned snapshot.\n")
  out.write_string("pub let chains : Array[ChainSpec] = [\n")
  for spec in specs {
    out.write_string("  {\n")
    out.write_string("    name: \"")
    out.write_string(spec.name)
    out.write_string("\",\n")
    out.write_string("    title: \"")
    out.write_string(escape(spec.title))
    out.write_string("\",\n")
    out.write_string("    missing: [")
    for name in missing_of(spec, available) {
      out.write_string("\"")
      out.write_string(name)
      out.write_string("\", ")
    }
    out.write_string("],\n")
    out.write_string("    normalization: [\n")
    emit_passes(out, spec.normalization, "      ")
    out.write_string("    ],\n")
    out.write_string("    segmentation: ")
    match spec.segmentation {
      Some(pass) => {
        out.write_string("Some(")
        emit_spec(out, pass)
        out.write_string(")")
      }
      None => out.write_string("None")
    }
    out.write_string(",\n")
    out.write_string("    conversion: [\n")
    emit_passes(out, spec.conversion, "      ")
    out.write_string("    ],\n")
    out.write_string("  },\n")
  }
  out.write_string("]\n")
  out.to_string()
}

///|
/// Record the dictionaries of a pass that the snapshot provides.
fn note_used(
  pass : Spec,
  available : Array[String],
  used : Array[String],
) -> Unit {
  for name in pass.leaves() {
    if available.contains(name) && !used.contains(name) {
      used.push(name)
    }
  }
}

///|
/// Dictionaries a configuration actually links, in first-use order.
fn used_bindings(spec : ChainSpec, available : Array[String]) -> Array[String] {
  let used : Array[String] = []
  for pass in spec.normalization {
    note_used(pass, available, used)
  }
  match spec.segmentation {
    Some(pass) => note_used(pass, available, used)
    None => ()
  }
  for pass in spec.conversion {
    note_used(pass, available, used)
  }
  used
}

///|
/// Emit one pass node as a runtime expression, or nothing when no leaf resolves.
fn runtime_node(spec : Spec, available : Array[String]) -> String? {
  match spec {
    Leaf(binding) =>
      if available.contains(binding) {
        Some(
          "@core.Node::leaf(@core.Dict::parse(\"" +
          binding +
          "\", @" +
          binding +
          "." +
          binding +
          "))",
        )
      } else {
        None
      }
    Group(policy, children) => {
      let parts : Array[String] = []
      for child in children {
        match runtime_node(child, available) {
          Some(text) => parts.push(text)
          None => ()
        }
      }
      if parts.is_empty() {
        None
      } else {
        let policy_text = match policy {
          Union => "@core.Union"
          ShortCircuit => "@core.ShortCircuit"
        }
        Some(
          "@core.Node::group(" + policy_text + ", [" + parts.join(", ") + "])",
        )
      }
    }
  }
}

///|
/// Emit a list of passes as runtime expressions.
fn emit_runtime_list(
  out : StringBuilder,
  indent : String,
  specs : Array[Spec],
  available : Array[String],
) -> Unit {
  out.write_string(indent)
  out.write_string("[\n")
  for spec in specs {
    match runtime_node(spec, available) {
      Some(text) => {
        out.write_string(indent)
        out.write_string("  ")
        out.write_string(text)
        out.write_string(",\n")
      }
      None => ()
    }
  }
  out.write_string(indent)
  out.write_string("],\n")
}

///|
/// Emit `config//moon.pkg`.
pub fn emit_config_subpkg(module_path : String, used : Array[String]) -> String {
  let out = StringBuilder()
  out.write_string("// Generated by tools/gen_dict — do not edit by hand.\n")
  out.write_string("//\n")
  out.write_string(
    "// This package builds one configuration and references only the dictionaries it\n",
  )
  out.write_string(
    "// needs, so a program that wants one direction links one direction's worth of\n",
  )
  out.write_string(
    "// data. Use the aggregate `config` package when the name is only known at run\n",
  )
  out.write_string("// time.\n\n")
  out.write_string("import {\n  \"")
  out.write_string(module_path)
  out.write_string("/core\",\n")
  for binding in used {
    out.write_string("  \"")
    out.write_string(module_path)
    out.write_string("/dict/")
    out.write_string(binding)
    out.write_string("\",\n")
  }
  out.write_string("}\n\nformatter(ignore: [\"chain.mbt\"])\n")
  out.to_string()
}

///|
/// Emit `config//chain.mbt`.
pub fn emit_config_chain(
  spec : ChainSpec,
  available : Array[String],
  missing : Array[String],
) -> String {
  let out = StringBuilder()
  out.write_string("// Generated by tools/gen_dict from data/opencc/config/")
  out.write_string(spec.name)
  out.write_string(".json — do not edit by hand.\n\n")
  out.write_string("///| Configuration name as OpenCC spells it.\n")
  out.write_string("pub let name : String = \"")
  out.write_string(spec.name)
  out.write_string("\"\n\n")
  out.write_string(
    "///| Human-readable name from the upstream configuration.\n",
  )
  out.write_string("pub let title : String = \"")
  out.write_string(escape(spec.title))
  out.write_string("\"\n\n")
  out.write_string(
    "///| Dictionaries this configuration references but the snapshot lacks.\n",
  )
  out.write_string("pub let missing : Array[String] = [")
  for name in missing {
    out.write_string("\"")
    out.write_string(name)
    out.write_string("\", ")
  }
  out.write_string("]\n\n")
  out.write_string(
    "///| Build the conversion chain. Only the dictionaries listed in the package\n",
  )
  out.write_string("/// import are linked in.\n")
  out.write_string("pub fn chain() -> @core.Chain {\n")
  out.write_string("  @core.Chain::new(\n    \"")
  out.write_string(spec.name)
  out.write_string("\",\n")
  emit_runtime_list(out, "    ", spec.normalization, available)
  out.write_string("    ")
  match spec.segmentation {
    Some(pass) =>
      match runtime_node(pass, available) {
        Some(text) => {
          out.write_string("Some(")
          out.write_string(text)
          out.write_string(")")
        }
        None => out.write_string("None")
      }
    None => out.write_string("None")
  }
  out.write_string(",\n")
  emit_runtime_list(out, "    ", spec.conversion, available)
  out.write_string("  )\n}\n")
  out.to_string()
}