///|
pub(all) struct SourceLocation {
  scalar_offset : Int
  utf16_offset : Int
  line : Int
  column : Int
  utf16_column : Int
} derive(Eq, Debug)

///|
pub(all) struct SourceRange {
  start : SourceLocation
  end : SourceLocation
} derive(Eq, Debug)

///|
pub fn locate_source(source : String, scalar_offset : Int) -> SourceLocation {
  let mut offset = 0
  let mut utf16 = 0
  let mut line = 1
  let mut column = 1
  let mut utf16_column = 1
  for c in source {
    if offset >= scalar_offset {
      break
    }
    offset += 1
    utf16 += c.utf16_len()
    if c == '\n' {
      line += 1
      column = 1
      utf16_column = 1
    } else {
      column += 1
      utf16_column += c.utf16_len()
    }
  }
  { scalar_offset: offset, utf16_offset: utf16, line, column, utf16_column }
}

///|
pub fn Diagnostic::source_range(
  self : Diagnostic,
  source : String,
) -> SourceRange {
  let start = locate_source(source, self.span.start)
  let end = locate_source(
    source,
    if self.span.end < self.span.start {
      self.span.start
    } else {
      self.span.end
    },
  )
  { start, end }
}

///|
fn terminal_safe(text : String) -> String {
  let out = StringBuilder()
  for c in text {
    if c.is_control() {
      out.write_char('�')
    } else {
      out.write_char(c)
    }
  }
  out.to_string()
}

///|
pub fn diagnostic_hint(code : String) -> String {
  match code {
    "lex.string" | "lex.escape" =>
      "补齐双引号;Sieve 中反斜杠引用下一个字符,不把反斜杠 n 当换行。"
    "lex.text" =>
      "text: 后换行,并用单独的点号行终止;正文行开头的点需要再写一个点。"
    "lex.comment" => "检查 /* 注释是否有对应的 */。"
    "lex.number" =>
      "数量支持 K/M/G,结果必须在 0 到 2147483647 之间。"
    "parse.string_list" =>
      "使用非空字符串列表,例如 [\"from\", \"to\"],不要加尾随逗号。"
    "parse.expected" =>
      "检查分号、括号、花括号和列表分隔逗号。"
    "parse.orphan_branch" => "elsif/else 必须紧跟同一个 if 结构。"
    "check.require" => "在脚本开头加入对应的 require 声明。"
    "check.require_order" =>
      "把 require 放到顶层所有执行语句之前。"
    "check.capability" =>
      "调用 supported_capabilities() 查看当前实现支持的扩展;不支持的扩展会拒绝而不是静默跳过。"
    "check.arity" | "check.single" | "check.string" =>
      "对照命令签名检查参数数量,以及单个字符串和字符串列表的区别。"
    "check.tag_order" => "把所有标签及其值放在位置参数之前。"
    "check.conflicting_tags" | "check.duplicate_tag" =>
      "每组选项只保留一种,例如 :is 与 :contains 不能同时出现。"
    "check.numeric_match" =>
      "数值比较器只支持相等或关系比较,不支持通配符和子串查找。"
    "message.header_value" =>
      "由 MIME/邮件解析器先展开折叠行并解码头字段,再传入规范化值。"
    "message.envelope" | "runtime.redirect" =>
      "传入裸邮箱地址,不要包含显示名、多个地址或换行。"
    "limit.comparisons" =>
      "缩短通配符和输入,或在受信任边界内合理调整 comparisons;不要禁用预算。"
    "limit.depth" => "减少规则或注释嵌套层数。"
    "limit.expansion" =>
      "减少变量拼接后的长度;变量替换不是递归展开。"
    "limit.actions" =>
      "减少单封邮件产生的不同目的地;不会返回半份投递计划。"
    _ =>
      "错误带有稳定 code 和源码范围;调用方应按 code 分类,而不是匹配展示文本。"
  }
}

///|
pub fn Diagnostic::render(
  self : Diagnostic,
  source : String,
  filename? : String = "