///|
/// Regulated verse (近体诗) templates and checker.
///
/// The four-line templates below are the standard 平起/仄起 × 首句入韵/不入韵
/// set. A regulated eight-line poem (律诗) is the four-line template followed by
/// the non-rhyming variant of the same 起式, because line 5 must 粘 line 4.
pub(all) enum ShiForm {
  WuJue
  QiJue
  WuLv
  QiLv
}

///|
pub fn shi_form_label(form : ShiForm) -> String {
  match form {
    WuJue => "五绝"
    QiJue => "七绝"
    WuLv => "五律"
    QiLv => "七律"
  }
}

///|
fn shi_width(form : ShiForm) -> Int32 {
  match form {
    WuJue | WuLv => 5
    QiJue | QiLv => 7
  }
}

///|
fn shi_is_lv(form : ShiForm) -> Bool {
  match form {
    WuJue | QiJue => false
    WuLv | QiLv => true
  }
}

///|
fn wu_ju_patterns() -> Array[Array[String]] {
  [
    // 平起首句入韵
    ["ppzzp", "zzzpp", "zzppz", "ppzzp"],
    // 平起首句不入韵
    ["pppzz", "zzzpp", "zzppz", "ppzzp"],
    // 仄起首句入韵
    ["zzzpp", "ppzzp", "pppzz", "zzzpp"],
    // 仄起首句不入韵
    ["zzppz", "ppzzp", "pppzz", "zzzpp"],
  ]
}

///|
fn qi_ju_patterns() -> Array[Array[String]] {
  [
    // 平起首句入韵
    ["ppzzzpp", "zzppzzp", "zzpppzz", "ppzzzpp"],
    // 平起首句不入韵
    ["ppzzppz", "zzppzzp", "zzpppzz", "ppzzzpp"],
    // 仄起首句入韵
    ["zzppzzp", "ppzzzpp", "ppzzppz", "zzppzzp"],
    // 仄起首句不入韵
    ["zzpppzz", "ppzzzpp", "ppzzppz", "zzppzzp"],
  ]
}

///|
fn variant_label(index : Int32) -> String {
  match index {
    0 => "平起首句入韵"
    1 => "平起首句不入韵"
    2 => "仄起首句入韵"
    _ => "仄起首句不入韵"
  }
}

///|
/// 4 variants, each with 4 or 8 lines of 'p' / 'z' requirement.
fn patterns_for(form : ShiForm) -> Array[Array[String]] {
  let base = match form {
    WuJue | WuLv => wu_ju_patterns()
    QiJue | QiLv => qi_ju_patterns()
  }
  if !shi_is_lv(form) {
    return base
  }
  let out : Array[Array[String]] = []
  for v in 0..<4 {
    // Same 起式, but the second half is the 首句不入韵 template.
    let tail_variant = if v == 0 || v == 1 { 1 } else { 3 }
    let lines : Array[String] = []
    for i in 0..<4 {
      lines.push(base[v][i])
    }
    for i in 0..<4 {
      lines.push(base[tail_variant][i])
    }
    out.push(lines)
  }
  out
}

///|
/// Strict (二四六分明) positions. Positions 1/3/5 are flexible, but the last
/// character of a line is always fixed because it decides rhyme.
fn strict_positions(width : Int32) -> Array[Int32] {
  if width == 5 {
    [1, 3, 4]
  } else {
    [1, 3, 5, 6]
  }
}

///|
fn is_strict(width : Int32, pos : Int32) -> Bool {
  for p in strict_positions(width) {
    if p == pos {
      return true
    }
  }
  false
}

///|
/// Tone candidates for one character, restricted to an expected tone when
/// possible. Returns all distinct tones; an unknown character yields none.
fn tone_candidates(
  lex : Lexicon,
  ch : Char,
  expected : Char,
  restrict : Bool,
) -> Array[Char] {
  let out : Array[Char] = []
  for r in lookup(lex, Pingshui, ch) {
    let code = tone_code(r.tone)
    let ok = if restrict && expected != 'a' { code == expected } else { true }
    if ok {
      let mut seen = false
      for c in out {
        if c == code {
          seen = true
        }
      }
      if !seen {
        out.push(code)
      }
    }
  }
  out
}

///|
priv struct Assignment {
  hard : Int32
  warn : Int32
  tones : Array[Char]
}

///|
fn exception_penalty(tones : Array[Char], pattern : String) -> (Int32, Int32) {
  let n = tones.length()
  let last = pattern[n - 1]
  let mut hard = 0
  let mut warn = 0
  if last == 'p' {
    // 三平调: the three characters before the rhyme must not all be 平.
    if n >= 3 &&
      tones[n - 1] == 'p' &&
      tones[n - 2] == 'p' &&
      tones[n - 3] == 'p' {
      hard = hard + 1
    }
    // 孤平: 平平仄仄平 / 仄仄平平仄仄平 with the safeguard character 仄.
    if n == 5 && pattern == "ppzzp" && tones[0] == 'z' && tones[2] == 'z' {
      hard = hard + 1
    }
    if n == 7 && pattern == "zzppzzp" && tones[2] == 'z' && tones[4] == 'z' {
      hard = hard + 1
    }
    // 三仄尾 is avoided but not a hard fault in most teaching traditions.
  } else if n >= 3 &&
    tones[n - 1] == 'z' &&
    tones[n - 2] == 'z' &&
    tones[n - 3] == 'z' {
    warn = warn + 1
  }
  (hard, warn)
}

///|
fn search_assignment(
  pos : Int32,
  choices : Array[Array[Char]],
  current : Array[Char],
  pattern : String,
) -> Assignment {
  if pos == choices.length() {
    let (hard, warn) = exception_penalty(current, pattern)
    return { hard, warn, tones: current.copy(), }
  }
  let mut best : Assignment = { hard: 99, warn: 99, tones: [], }
  let mut first = true
  for tone in choices[pos] {
    current[pos] = tone
    let candidate = search_assignment(pos + 1, choices, current, pattern)
    if first ||
      candidate.hard < best.hard ||
      (candidate.hard == best.hard && candidate.warn < best.warn) {
      best = candidate
      first = false
    }
  }
  best
}

///|
fn make_issue(
  code : String,
  severity : Severity,
  line : Int32,
  col : Int32,
  message : String,
  expected : String,
  actual : String,
) -> Issue {
  { code, severity, line, col, message, expected, actual, }
}

///|
fn analyze_line(
  lex : Lexicon,
  text : String,
  pattern : String,
  line_no : Int32,
  issues : Array[Issue],
) -> LineView {
  let chars = text.to_array()
  let n = chars.length()
  let width = n
  let choices : Array[Array[Char]] = []
  let unknown_cols : Array[Int32] = []
  for i in 0.. 0 { choices[i][0] } else { '?' })
    }
    fallback
  }

  let bad_cols : Array[Int32] = []
  for i in 0.. 0 {
    if tones[n - 1] == 'p' &&
      n >= 3 &&
      tones[n - 2] == 'p' &&
      tones[n - 3] == 'p' {
      issues.push(
        make_issue(
          "SAN_PING",
          Error,
          line_no,
          n,
          "三平调:韵脚连用三个平声",
          "末三字不全平",
          "平平平",
        ),
      )
    } else {
      issues.push(
        make_issue(
          "GU_PING",
          Error,
          line_no,
          0,
          "孤平:韵句除韵脚外只剩一个平声字",
          "",
          "",
        ),
      )
    }
  }
  if warn > 0 {
    issues.push(
      make_issue(
        "SAN_ZE",
        Warning,
        line_no,
        n,
        "三仄尾:出句末三字全仄,读来拗口(非硬伤)",
        "末三字不全仄",
        "仄仄仄",
      ),
    )
  }
  let actual_sb = StringBuilder()
  for t in tones {
    actual_sb.write_char(t)
  }
  { text, pattern, actual: actual_sb.to_string(), bad_cols, }
}

///|
fn count_mismatches(
  lex : Lexicon,
  lines : Array[String],
  pattern : Array[String],
) -> Int32 {
  let mut score = 0
  for i in 0.. 0
          let unknown = lookup(lex, Pingshui, chars[pos]).length() == 0
          if !ok && !unknown {
            score = score + 1
          }
        }
      }
    }
    // A rhyme line must end in a 平 reading.
    if pattern[i].to_array()[chars.length() - 1] == 'p' {
      let last = chars[chars.length() - 1]
      let ping = lookup(lex, Pingshui, last)
      let mut has_ping = false
      for r in ping {
        if is_ping(r.tone) {
          has_ping = true
        }
      }
      if !has_ping && ping.length() > 0 {
        score = score + 2
      }
    }
  }
  score
}

///|
fn check_rhyme(
  lex : Lexicon,
  lines : Array[String],
  pattern : Array[String],
  issues : Array[Issue],
) -> String {
  let rhyme_lines : Array[Int32] = []
  for i in 0.. group
    None => {
      let first = rhyme_chars[0]
      let first_groups = groups_of(lex, Pingshui, first)
      for k in 1.. 0 {
                gs[0]
              } else {
                "未知"
              },
            ),
          )
        }
      }
      ""
    }
  }
}

///|
/// Check a regulated verse. When `form` is None the shape (5/7 characters,
/// 4/8 lines) decides the form, and the four 起式 are scored to pick the
/// closest template.
pub fn check_shi(lex : Lexicon, text : String, form : ShiForm?) -> Report {
  let lines = poem_lines(text)
  let report = empty_report()
  let width = if lines.length() > 0 { lines[0].to_array().length() } else { 0 }
  let resolved : ShiForm? = match form {
    Some(f) => Some(f)
    None => {
      let n = lines.length()
      let mut uniform = n == 4 || n == 8
      for l in lines {
        if l.to_array().length() != width {
          uniform = false
        }
      }
      if !uniform {
        None
      } else if width == 5 {
        Some(if n == 4 { WuJue } else { WuLv })
      } else if width == 7 {
        Some(if n == 4 { QiJue } else { QiLv })
      } else {
        None
      }
    }
  }
  match resolved {
    None => {
      report.ok = false
      report.form = "未识别"
      report.issues.push(
        make_issue(
          "SHI_SHAPE",
          Error,
          0,
          0,
          "无法识别诗体:需要 4 句或 8 句,且每句 5 或 7 个字",
          "五/七言绝句或律诗",
          lines.length().to_string() + " 句",
        ),
      )
      report
    }
    Some(f) => {
      let expected_width = shi_width(f)
      let expected_lines = if shi_is_lv(f) { 8 } else { 4 }
      if lines.length() != expected_lines || width != expected_width {
        report.ok = false
        report.form = shi_form_label(f)
        report.issues.push(
          make_issue(
            "SHI_SHAPE",
            Error,
            0,
            0,
            "句数或字数与所选诗体不符",
            expected_lines.to_string() +
            " 句,每句 " +
            expected_width.to_string() +
            " 字",
            lines.length().to_string() +
            " 句,每句 " +
            width.to_string() +
            " 字",
          ),
        )
        return report
      }
      let variants = patterns_for(f)
      let mut best = 0
      let mut best_score = 1_000_000
      for v in 0..<4 {
        let score = count_mismatches(lex, lines, variants[v])
        if score < best_score {
          best_score = score
          best = v
        }
      }
      let pattern = variants[best]
      report.form = shi_form_label(f)
      report.template = variant_label(best)
      let issues : Array[Issue] = []
      let views : Array[LineView] = []
      for i in 0.. 0 {
        report.notes.push("押韵:" + rhyme + "(平水韵)")
      }
      report.lines = views
      report.issues = issues
      let mut ok = true
      for issue in issues {
        match issue.severity {
          Error => ok = false
          _ => ()
        }
      }
      report.ok = ok
      report
    }
  }
}