///|
fn hex(text : String) -> String {
  let digits = "0123456789abcdef".to_array()
  let result = StringBuilder()
  for byte in @utf8.encode(text) {
    let n = byte.to_int()
    result.write_char(digits[n >> 4])
    result.write_char(digits[n & 15])
  }
  result.to_string()
}

///|
fn validate_name(name : @ir.Name) -> Unit raise {
  if @ir.Name::parse(name.to_canonical()) != name {
    raise Invalid(
      "moonbit.invalid_name",
      name.to_canonical(),
      "Name segments do not round-trip through canonical spelling",
    )
  }
}

///|
fn validate_path(path : @ir.Path) -> Unit raise {
  if path.to_names().is_empty() {
    raise Invalid("moonbit.invalid_name", "", "Empty path")
  }
  for name in path.to_names() {
    validate_name(name)
  }
}

///|
/// Refuses an FQName whose names do not round-trip, as `value_name` does,
/// without building the hex name.
fn validate_fqname(name : @ir.FQName) -> Unit raise {
  validate_path(name.package_name)
  validate_path(name.module_name)
  validate_name(name.local_name)
}

///|
/// Legacy ABI helper. Read Output.symbols for names allocated in readable mode.
/// Hex encodes validated canonical FQNames without folding case or word boundaries.
pub fn value_name(name : @ir.FQName) -> String raise {
  validate_fqname(name)
  "v_" + hex(name.to_canonical())
}

///|
pub fn value_node(name : @ir.FQName) -> String {
  "value:" + name.to_canonical()
}

///|
fn quote(text : String) -> String {
  Json::string(text).stringify()
}

///|
/// Sort map keys while retaining numeric lexemes from the IR codec.
fn canonical_json(value : Json) -> String {
  match value {
    Object(fields) => {
      let keys = fields.keys().collect()
      keys.sort()
      "{" +
      keys.map(key => quote(key) + ":" + canonical_json(fields[key])).join(",") +
      "}"
    }
    Array(items) => "[" + items.map(canonical_json).join(",") + "]"
    _ => value.stringify()
  }
}