///|
/// 引号类型。
pub enum QuoteKind {
  DoubleQuote
  SingleQuote
} derive(Eq, Debug)

///|
pub extend QuoteKind with Eq::{not_equal, equal}

///|
pub extend QuoteKind with @moonbitlang/core/debug.Debug::{to_repr}

///|
/// 一次引号替换编辑。
pub struct QuoteEdit {
  /// 全文字符偏移(闭端)
  start : Int
  /// 全文字符偏移(开端)
  end : Int
  /// 替换文本
  text : String
  /// 是否为开引号
  opening : Bool
} derive(Eq, Debug)

///|
pub extend QuoteEdit with Eq::{not_equal, equal}

///|
pub extend QuoteEdit with @moonbitlang/core/debug.Debug::{to_repr}

///|
/// 未配对的引号。
pub struct UnpairedQuote {
  /// 全文字符偏移(闭端)
  start : Int
  /// 全文字符偏移(开端)
  end : Int
  kind : QuoteKind
  /// `true` 表示这是一个缺少闭引号的开引号
  opening : Bool
} derive(Eq, Debug)

///|
pub extend UnpairedQuote with Eq::{not_equal, equal}

///|
pub extend UnpairedQuote with @moonbitlang/core/debug.Debug::{to_repr}

///|
/// 一行的引号扫描结果。
pub struct QuoteScan {
  /// 需要替换的引号编辑,按位置升序
  edits : Array[QuoteEdit]
  /// 扫描过程中发现的未配对引号
  unpaired : Array[UnpairedQuote]
} derive(Debug)

///|
pub extend QuoteScan with @moonbitlang/core/debug.Debug::{to_repr}

///|
/// 判断是否为直引号(`"` 或 `'`)。
pub fn is_straight_quote(c : Char) -> Bool {
  c == '"' || c == '\''
}

///|
/// 判断是否为开括号类字符:其后的引号应为开引号。
fn is_open_bracket_char(c : Char) -> Bool {
  match c {
    '('
    | '['
    | '{'
    | '('
    | '['
    | '{'
    | '【'
    | '《'
    | '〈'
    | '「'
    | '『'
    | '〔'
    | '“'
    | '‘' => true
    _ => false
  }
}

///|
/// 判断是否为闭括号 / 句读类字符:其前的引号应为闭引号。
fn is_close_punct_char(c : Char) -> Bool {
  match c {
    ')'
    | ']'
    | '}'
    | ')'
    | ']'
    | '}'
    | '】'
    | '》'
    | '〉'
    | '」'
    | '』'
    | '〕'
    | '”'
    | '’'
    | '。'
    | ','
    | '、'
    | ';'
    | ':'
    | '!'
    | '?'
    | '…'
    | '—' => true
    _ => false
  }
}

///|
/// 向左找最近的非空白字符。
fn prev_non_space_char(chars : Array[Char], i : Int) -> Char? {
  let mut k = i - 1
  while k >= 0 {
    if !is_space_char(chars[k]) {
      return Some(chars[k])
    }
    k = k - 1
  }
  None
}

///|
/// 向右找最近的非空白字符。
fn next_non_space_char(chars : Array[Char], i : Int) -> Char? {
  let mut k = i + 1
  while k < chars.length() {
    if !is_space_char(chars[k]) {
      return Some(chars[k])
    }
    k = k + 1
  }
  None
}

///|
/// 判断位置 `i` 附近是否出现 CJK 文字,用于界定「中文语境」。
///
/// 只有处于中文语境的直引号才会被转换为弯引号,纯英文段落不受影响。
fn has_cjk_nearby(chars : Array[Char], i : Int, radius : Int) -> Bool {
  let mut k = i - radius
  while k <= i + radius {
    if k >= 0 && k < chars.length() && is_cjk_ideograph(chars[k]) {
      return true
    }
    k = k + 1
  }
  false
}

///|
/// 判断该位置的 `'` 是否为英文撇号(如 `don't`)。
///
/// 只有两侧都是 ASCII 字母数字时才视为撇号;中文两侧的单引号仍按引号处理。
fn is_apostrophe(chars : Array[Char], i : Int) -> Bool {
  if chars[i] != '\'' {
    return false
  }
  match (prev_non_space_char(chars, i), next_non_space_char(chars, i)) {
    (Some(p), Some(n)) => is_ascii_alnum(p) && is_ascii_alnum(n)
    _ => false
  }
}

///|
/// 判断引号方向:`true` 为开引号。
///
/// 先看上下文(前面的字符、后面的字符),上下文不明确时退回「交替」状态。
fn classify_quote(chars : Array[Char], i : Int, inside : Bool) -> Bool {
  match prev_non_space_char(chars, i) {
    None => true
    Some(prev) =>
      if is_open_bracket_char(prev) {
        true
      } else {
        match next_non_space_char(chars, i) {
          None => false
          Some(next) => {
            let prev_is_word = is_cjk_ideograph(prev) ||
              is_ascii_alnum(prev) ||
              is_close_punct_char(prev)
            let next_is_boundary = is_cjk_punct(next) ||
              is_ascii_punct(next) ||
              is_close_punct_char(next)
            if prev_is_word && next_is_boundary {
              false
            } else if is_ascii_punct(prev) && !is_straight_quote(prev) {
              true
            } else {
              !inside
            }
          }
        }
      }
  }
}

///|
/// 跨行扫描时需要在行间传递的引号状态。
priv struct QuoteCarry {
  in_double : Bool
  in_single : Bool
  /// 未闭合开引号的绝对偏移,`-1` 表示没有
  double_open : Int
  single_open : Int
}

///|
/// 空状态。
fn QuoteCarry::empty() -> QuoteCarry {
  { in_double: false, in_single: false, double_open: -1, single_open: -1, }
}

///|
/// 当前仍未闭合的开引号。
fn QuoteCarry::dangling(self : QuoteCarry) -> Array[UnpairedQuote] {
  let out : Array[UnpairedQuote] = []
  if self.double_open >= 0 {
    out.push({
      start: self.double_open,
      end: self.double_open + 1,
      kind: DoubleQuote,
      opening: true,
    })
  }
  if self.single_open >= 0 {
    out.push({
      start: self.single_open,
      end: self.single_open + 1,
      kind: SingleQuote,
      opening: true,
    })
  }
  out
}

///|
/// 扫描一行的直引号。
///
/// - `mask[i]` 为 `true` 的位置被跳过(行内代码、URL 等受保护区域);
/// - `base` 为该行首字符在全文中的偏移,用于换算绝对位置;
/// - `carry` 为进入本行时的引号状态,返回值是离开本行的状态。
fn scan_quotes_in_line(
  chars : Array[Char],
  mask : Array[Bool],
  base : Int,
  carry : QuoteCarry,
) -> (QuoteScan, QuoteCarry) {
  let edits : Array[QuoteEdit] = []
  let unpaired : Array[UnpairedQuote] = []
  let n = chars.length()
  let mut in_double = carry.in_double
  let mut in_single = carry.in_single
  let mut double_open = carry.double_open
  let mut single_open = carry.single_open
  let mut i = 0
  while i < n {
    let c = chars[i]
    let skip = i < mask.length() && mask[i]
    if !skip &&
      is_straight_quote(c) &&
      has_cjk_nearby(chars, i, 8) &&
      !is_apostrophe(chars, i) {
      let is_double = c == '"'
      let inside = if is_double { in_double } else { in_single }
      let opening = classify_quote(chars, i, inside)
      if opening {
        let prev_open = if is_double { double_open } else { single_open }
        if prev_open >= 0 {
          unpaired.push({
            start: prev_open,
            end: prev_open + 1,
            kind: if is_double {
              DoubleQuote
            } else {
              SingleQuote
            },
            opening: true,
          })
        }
        if is_double {
          in_double = true
          double_open = base + i
        } else {
          in_single = true
          single_open = base + i
        }
      } else if inside {
        if is_double {
          in_double = false
          double_open = -1
        } else {
          in_single = false
          single_open = -1
        }
      } else {
        unpaired.push({
          start: base + i,
          end: base + i + 1,
          kind: if is_double {
            DoubleQuote
          } else {
            SingleQuote
          },
          opening: false,
        })
      }
      let repl = if opening {
        if is_double {
          "“"
        } else {
          "‘"
        }
      } else if is_double {
        "”"
      } else {
        "’"
      }
      edits.push({ start: base + i, end: base + i + 1, text: repl, opening, })
    }
    i = i + 1
  }
  unpaired.sort_by(fn(a, b) { a.start - b.start })
  ({ edits, unpaired, }, { in_double, in_single, double_open, single_open, })
}

///|
/// 把中文语境中的直引号转换为弯引号。
///
/// 这是纯文本级 API(不做 Markdown 区域识别);库内规则使用掩码版本,
/// 会自动跳过行内代码、URL 等区域。
pub fn convert_straight_quotes(text : String) -> String {
  let raws = split_raw_lines(text)
  let buf = StringBuilder()
  let mut carry = QuoteCarry::empty()
  for raw in raws {
    let chars = raw.chars
    let mask = Array::make(chars.length(), false)
    let (scan, next) = scan_quotes_in_line(chars, mask, 0, carry)
    carry = next
    let mut pos = 0
    for e in scan.edits {
      if e.start < pos {
        continue
      }
      while pos < e.start {
        buf.write_char(chars[pos])
        pos = pos + 1
      }
      buf.write_string(e.text)
      pos = e.end
    }
    while pos < chars.length() {
      buf.write_char(chars[pos])
      pos = pos + 1
    }
    buf.write_string(raw.newline)
  }
  buf.to_string()
}

///|
/// 找出文本中未配对的引号(同时覆盖直引号与弯引号场景)。
pub fn find_unpaired_quotes(text : String) -> Array[UnpairedQuote] {
  let raws = split_raw_lines(text)
  let out : Array[UnpairedQuote] = []
  let mut carry = QuoteCarry::empty()
  let mut base = 0
  for raw in raws {
    let chars = raw.chars
    let mask = Array::make(chars.length(), false)
    let (scan, next) = scan_quotes_in_line(chars, mask, base, carry)
    carry = next
    for u in scan.unpaired {
      out.push(u)
    }
    base = base + chars.length() + raw.newline.length()
  }
  for u in carry.dangling() {
    out.push(u)
  }
  out.sort_by(fn(a, b) { a.start - b.start })
  out
}

///|
/// 单引号 / 双引号的显示名,用于诊断与展示。
pub fn QuoteKind::label(self : QuoteKind) -> String {
  match self {
    DoubleQuote => "双引号"
    SingleQuote => "单引号"
  }
}

///|
/// 规则 `curly-quote`:中文语境中的直引号应改为弯引号。
fn check_curly_quote(ctx : Ctx) -> Array[Diagnostic] {
  let out : Array[Diagnostic] = []
  let mut carry = QuoteCarry::empty()
  for line in ctx.lines {
    if line.code {
      continue
    }
    let (scan, next) = scan_quotes_in_line(
      line.chars,
      line.mask,
      line.start,
      carry,
    )
    carry = next
    for e in scan.edits {
      let kind = if e.opening { "开引号" } else { "闭引号" }
      out.push(
        mk_diag(
          "curly-quote",
          Warning,
          "中文语境应使用弯引号(此处为" + kind + ")",
          e.start,
          e.end,
          Some(e.text),
        ),
      )
    }
  }
  out
}

///|
/// 规则 `quote-pairing`:引号必须成对出现。
fn check_quote_pairing(ctx : Ctx) -> Array[Diagnostic] {
  let out : Array[Diagnostic] = []
  let mut carry = QuoteCarry::empty()
  for line in ctx.lines {
    if line.code {
      continue
    }
    let (scan, next) = scan_quotes_in_line(
      line.chars,
      line.mask,
      line.start,
      carry,
    )
    carry = next
    for u in scan.unpaired {
      out.push(unpaired_diag(u))
    }
  }
  for u in carry.dangling() {
    out.push(unpaired_diag(u))
  }
  out
}

///|
/// 把一条未配对引号转换为诊断。
fn unpaired_diag(u : UnpairedQuote) -> Diagnostic {
  let msg = if u.opening {
    u.kind.label() + "缺少配对的闭引号"
  } else {
    u.kind.label() + "缺少配对的开引号"
  }
  mk_diag("quote-pairing", Warning, msg, u.start, u.end, None)
}