///|
/// Ci (词) checking against the curated 词谱 table.
///
/// The table stores one flat tone pattern per format plus the offsets that must
/// rhyme. Input punctuation and line breaks are ignored for matching, but are
/// preserved for reporting.
fn flatten(lines : Array[String]) -> String {
  let sb = StringBuilder()
  for l in lines {
    sb.write_string(l)
  }
  sb.to_string()
}

///|
fn find_tune_formats(lex : Lexicon, name : String) -> Array[CiTune] {
  let out : Array[CiTune] = []
  for t in lex.tunes {
    if t.name == name {
      out.push(t)
    }
  }
  out
}

///|
/// Tone fit for one ci position. Returns 0 for a match, 1 for a mismatch and
/// 0 for an unknown character (reported separately as a warning).
fn ci_tone_mismatch(lex : Lexicon, ch : Char, expected : Char) -> Int32 {
  if expected == 'a' {
    return 0
  }
  let readings = lookup(lex, Cilin, ch)
  if readings.length() == 0 {
    return 0
  }
  for r in readings {
    let code = tone_code(r.tone)
    if code == expected {
      return 0
    }
  }
  1
}

///|
fn ci_score(lex : Lexicon, flat : String, tune : CiTune) -> Int32 {
  let chars = flat.to_array()
  if chars.length() != tune.pattern.length() {
    let diff = if chars.length() > tune.pattern.length() {
      chars.length() - tune.pattern.length()
    } else {
      tune.pattern.length() - chars.length()
    }
    return 1000 + diff
  }
  let pattern = tune.pattern.to_array()
  let mut score = 0
  for i in 0.. Array[String] {
  let want_ping = expected == 'p'
  let readings = lookup(lex, Cilin, ch)
  let out : Array[String] = []
  for r in readings {
    let tone_ok = is_ping(r.tone) == want_ping
    if tone_ok && r.tone_class != "入" {
      out.push(r.group)
    }
  }
  if out.length() > 0 {
    return out
  }
  // No 上/去 reading: fall back to 入声 (词牌 入声韵).
  for r in readings {
    if r.tone_class == "入" {
      out.push(r.group)
    }
  }
  out
}

///|
priv struct CiMatch {
  tune : CiTune
  score : Int32
}

///|
pub fn identify_ci(
  lex : Lexicon,
  text : String,
) -> Array[(String, Int32, String)] {
  let lines = poem_lines(text)
  let flat = flatten(lines)
  let matches : Array[CiMatch] = []
  for t in lex.tunes {
    matches.push({ tune: t, score: ci_score(lex, flat, t), })
  }
  // Insertion sort: small table, deterministic order.
  let sorted : Array[CiMatch] = []
  for m in matches {
    let mut at = sorted.length()
    let mut i = 0
    while i < sorted.length() {
      if m.score < sorted[i].score {
        at = i
        break
      }
      i = i + 1
    }
    sorted.insert(at, m)
  }
  let out : Array[(String, Int32, String)] = []
  for m in sorted {
    let mut duplicate = false
    for item in out {
      let (name, _, _) = item
      if name == m.tune.name {
        duplicate = true
      }
    }
    if !duplicate {
      out.push((m.tune.name, m.score, m.tune.sketch))
    }
    if out.length() == 5 {
      break
    }
  }
  out
}

///|
fn flat_to_line_col(offsets : Array[Int32], index : Int32) -> (Int32, Int32) {
  let mut line = 0
  let mut i = 0
  while i < offsets.length() {
    if offsets[i] <= index {
      line = i
    }
    i = i + 1
  }
  (line, index - offsets[line] + 1)
}

///|
fn report_ci(
  lex : Lexicon,
  lines : Array[String],
  tune : CiTune,
  exact : Bool,
) -> Report {
  let report = empty_report()
  report.form = tune.name + "(" + tune.sketch + ")"
  report.template = tune.name + "·" + tune.author
  let chars = flatten(lines).to_array()
  let pattern = tune.pattern.to_array()
  let offsets : Array[Int32] = []
  let mut acc = 0
  for l in lines {
    offsets.push(acc)
    acc = acc + l.to_array().length()
  }
  let issues : Array[Issue] = []
  if !exact {
    report.ok = false
    issues.push(
      make_issue(
        "CI_LENGTH",
        Error,
        0,
        0,
        "字数与词谱不符",
        tune.pattern.length().to_string() + " 字",
        chars.length().to_string() + " 字",
      ),
    )
    // Still show what can be compared.
  }
  let views : Array[LineView] = []
  let n = if chars.length() < pattern.length() {
    chars.length()
  } else {
    pattern.length()
  }
  let mut line_start = 0
  for li in 0..= n {
      continue
    }
    let tone = pattern[idx]
    if current.length() > 0 && tone != last_tone {
      segments.push(current)
      current = []
    }
    current.push(idx)
    last_tone = tone
  }
  if current.length() > 0 {
    segments.push(current)
  }
  let rhyme_notes : Array[String] = []
  for seg in segments {
    if seg.length() == 0 {
      continue
    }
    let expected = pattern[seg[0]]
    let seg_chars : Array[Char] = []
    for idx in seg {
      seg_chars.push(chars[idx])
    }
    let common = common_ci_group(lex, seg_chars, expected)
    let common_pingshui = if common == None {
      common_group(lex, Pingshui, seg_chars, Some(expected == 'p'))
    } else {
      None
    }
    match common {
      Some(g) =>
        rhyme_notes.push(
          g + "(" + (if expected == 'p' { "平" } else { "仄" }) + ")",
        )
      None =>
        match common_pingshui {
          Some(g) =>
            rhyme_notes.push(
              g +
              "(平水韵同部,词韵跨部通押;" +
              (if expected == 'p' { "平" } else { "仄" }) +
              ")",
            )
          None =>
            for k in 0.. 0 {
                    gs[0]
                  } else {
                    "未知"
                  },
                  "",
                ),
              )
            }
        }
    }
  }
  for note in rhyme_notes {
    report.notes.push("用韵:" + note)
  }
  report.lines = views
  report.issues = issues
  let mut ok = report.ok
  for issue in issues {
    match issue.severity {
      Error => ok = false
      _ => ()
    }
  }
  report.ok = ok
  report
}

///|
/// Intersect rhyme groups within one ci rhyme segment. 入声 readings are kept
/// apart from 上/去 so a 入声韵 词牌 is not accepted with an 上声 rhyme.
fn common_ci_group(
  lex : Lexicon,
  chars : Array[Char],
  expected : Char,
) -> String? {
  if chars.length() == 0 {
    return None
  }
  let want_ping = expected == 'p'
  let mut candidates = filtered_groups(lex, chars[0], expected)
  let mut i = 1
  while i < chars.length() {
    let next : Array[String] = []
    let gs = filtered_groups(lex, chars[i], expected)
    for g in candidates {
      for h in gs {
        if g == h {
          next.push(g)
          break
        }
      }
    }
    candidates = next
    i = i + 1
  }
  let mut best : String? = None
  for g in candidates {
    match best {
      None => best = Some(g)
      Some(b) => if g < b { best = Some(g) }
    }
  }
  let _ = want_ping
  best
}

///|
pub fn check_ci(lex : Lexicon, text : String, name : String?) -> Report {
  let lines = poem_lines(text)
  let flat = flatten(lines)
  match name {
    None => {
      let ranked = identify_ci(lex, text)
      if ranked.length() == 0 {
        let report = empty_report()
        report.ok = false
        report.form = "未识别"
        report.issues.push(
          make_issue(
            "CI_IDENTIFY",
            Error,
            0,
            0,
            "没有匹配的词牌格式",
            "",
            "",
          ),
        )
        return report
      }
      let (best_name, best_score, _) = ranked[0]
      let formats = find_tune_formats(lex, best_name)
      let mut chosen = formats[0]
      let mut best = 1_000_000
      for t in formats {
        let s = ci_score(lex, flat, t)
        if s < best {
          best = s
          chosen = t
        }
      }
      let report = report_ci(lex, lines, chosen, best < 1000)
      report.notes.push(
        "词牌按格律最接近匹配:" +
        best_name +
        "(差异 " +
        best_score.to_string() +
        ")",
      )
      report
    }
    Some(wanted) => {
      let formats = find_tune_formats(lex, wanted)
      if formats.length() == 0 {
        let report = empty_report()
        report.ok = false
        report.form = wanted
        report.issues.push(
          make_issue(
            "CI_UNKNOWN_TUNE",
            Error,
            0,
            0,
            "内置词谱中没有该词牌",
            wanted,
            "",
          ),
        )
        return report
      }
      let mut chosen = formats[0]
      let mut best = 1_000_000
      for t in formats {
        let s = ci_score(lex, flat, t)
        if s < best {
          best = s
          chosen = t
        }
      }
      report_ci(lex, lines, chosen, best < 1000)
    }
  }
}