///|
/// 全部规则的元信息。
pub fn rule_infos() -> Array[RuleInfo] {
  rules()
}

///|
/// 判断规则 id 是否已知。
pub fn is_known_rule(id : String) -> Bool {
  let mut found = false
  for info in rules() {
    if info.id == id {
      found = true
      break
    }
  }
  found
}

///|
/// 复制一条诊断并补上行列信息。
fn with_location(d : Diagnostic, line : Int, column : Int) -> Diagnostic {
  {
    rule: d.rule,
    severity: d.severity,
    message: d.message,
    line,
    column,
    start: d.start,
    end: d.end,
    replacement: d.replacement,
  }
}

///|
/// 为诊断补齐 1 起始的行列号。
fn locate(diags : Array[Diagnostic], lines : Array[SLine]) -> Unit {
  let n_lines = lines.length()
  for i = 0; i < diags.length(); i = i + 1 {
    let d = diags[i]
    let mut line_no = 1
    let mut column = 1
    let mut found = false
    for k = 0; k < n_lines; k = k + 1 {
      let line = lines[k]
      let line_end = line.start + line.chars.length()
      if d.start >= line.start && d.start <= line_end {
        line_no = k + 1
        column = d.start - line.start + 1
        found = true
        break
      }
    }
    if !found && n_lines > 0 {
      line_no = n_lines
      column = lines[n_lines - 1].chars.length() + 1
    }
    diags[i] = with_location(d, line_no, column)
  }
}

///|
/// 按起始位置、结束位置、规则 id 稳定排序诊断。
fn sort_diags(diags : Array[Diagnostic]) -> Unit {
  diags.sort_by(fn(a, b) {
    if a.start != b.start {
      return a.start - b.start
    }
    if a.end != b.end {
      return a.end - b.end
    }
    a.rule.compare(b.rule)
  })
}

///|
/// 对文本做排版检查。
///
/// 返回值按位置排序,`line` / `column` 已填充。
pub fn lint(source : String, config : Config) -> Array[Diagnostic] {
  let scan = scan_source(source, config.markdown)
  let ctx = Ctx::{
    lines: scan.lines,
    config,
    unclosed_fence: scan.unclosed_fence,
  }
  let out : Array[Diagnostic] = []
  for rule in rule_set() {
    if config.is_enabled(rule.info.id, rule.info.enabled_by_default) {
      out.append((rule.check)(ctx))
    }
  }
  locate(out, scan.lines)
  sort_diags(out)
  out
}

///|
/// 是否存在严重级别 >= Warning 的诊断(用于 CLI 退出码)。
pub fn has_problems(diags : Array[Diagnostic]) -> Bool {
  let mut found = false
  for d in diags {
    match d.severity {
      Error | Warning => {
        found = true
        break
      }
      Info => ()
    }
  }
  found
}

///|
/// 对文本执行排版检查。
pub fn lint_with_default_config(source : String) -> Array[Diagnostic] {
  lint(source, Config::new())
}

///|
/// 应用一批互不重叠的诊断修复,返回修复后的文本。
fn apply_edits(chars : Array[Char], edits : Array[Diagnostic]) -> String {
  let sorted = edits.copy()
  sort_diags(sorted)
  let buf = StringBuilder()
  let mut pos = 0
  let mut last_start = -1
  let mut last_end = -1
  for d in sorted {
    if d.start < pos {
      // 与前一个编辑重叠,留到下一轮再处理
      continue
    }
    if d.start == last_start && d.end == last_end {
      // 同一位置的重复插入,跳过
      continue
    }
    match d.replacement {
      None => ()
      Some(repl) => {
        while pos < d.start {
          buf.write_char(chars[pos])
          pos = pos + 1
        }
        buf.write_string(repl)
        pos = d.end
        last_start = d.start
        last_end = d.end
      }
    }
  }
  while pos < chars.length() {
    buf.write_char(chars[pos])
    pos = pos + 1
  }
  buf.to_string()
}

///|
/// 自动修复文本。
///
/// 反复「检查 -> 应用可修复项」直到收敛(最多 8 轮),因此结果是幂等的:
/// 对同一份输入重复调用 `fix`,第二次不会再产生任何改动。
pub fn fix(source : String, config : Config) -> FixOutcome {
  let mut text = source
  let mut applied = 0
  let mut remaining : Array[Diagnostic] = []
  let mut round = 0
  while round < 8 {
    let diags = lint(text, config)
    let edits : Array[Diagnostic] = []
    for d in diags {
      match d.replacement {
        Some(_) => edits.push(d)
        None => ()
      }
    }
    if edits.is_empty() {
      remaining = diags
      break
    }
    let chars = text.iter().to_array()
    text = apply_edits(chars, edits)
    applied = applied + edits.length()
    round = round + 1
    if round >= 8 {
      remaining = lint(text, config)
    }
  }
  { output: text, applied, remaining, }
}