///|
/// Chinese numeral for 1..9 in Red's notation.
fn red_num(n : Int) -> String {
  match n {
    1 => "一"
    2 => "二"
    3 => "三"
    4 => "四"
    5 => "五"
    6 => "六"
    7 => "七"
    8 => "八"
    9 => "九"
    _ => "?"
  }
}

///|
/// Characters used for a piece in Chinese notation.
fn piece_char(piece : Piece) -> String {
  match piece.kind {
    King => if piece.side.is_red() { "帅" } else { "将" }
    Advisor => if piece.side.is_red() { "仕" } else { "士" }
    Elephant => if piece.side.is_red() { "相" } else { "象" }
    Horse => "马"
    Rook => "车"
    Cannon => "炮"
    Pawn => if piece.side.is_red() { "兵" } else { "卒" }
  }
}

///|
/// Map a notation character to a piece kind (accepts simplified and
/// traditional variants used by common software).
pub fn char_to_piece_kind(ch : Char) -> Kind? {
  match ch {
    '帅' | '将' | '將' => Some(King)
    '仕' | '士' => Some(Advisor)
    '相' | '象' => Some(Elephant)
    '马' | '馬' | '傌' => Some(Horse)
    '车' | '車' | '俥' => Some(Rook)
    '炮' | '砲' | '包' => Some(Cannon)
    '兵' | '卒' => Some(Pawn)
    _ => None
  }
}

///|
/// File number in the side's own notation. Red counts 一..九 from its
/// right (file i is 一); Black counts 1..9 from its right (file a is 1).
fn file_num(file : Int, side : Side) -> Int {
  if side.is_red() {
    9 - file
  } else {
    file + 1
  }
}

///|
/// Render a file/step number in the side's numeral system.
fn num_string(n : Int, side : Side) -> String {
  if side.is_red() {
    red_num(n)
  } else {
    n.to_string()
  }
}

///|
/// Kind-aware same-file disambiguation applies to rook, horse,
/// cannon and pawn only.
fn needs_disambiguation(kind : Kind) -> Bool {
  match kind {
    Rook | Cannon | Horse | Pawn => true
    King | Advisor | Elephant => false
  }
}

///|
/// How many same-side pieces of `kind` share `file` (including the
/// piece at `from` itself).
fn same_file_count(board : Board, from : Pos, piece : Piece) -> Int {
  let mut n = 0
  for rank in 0..<10 {
    match board.get(Pos::new(from.file, rank)) {
      Some(p) => if p.side == piece.side && p.kind == piece.kind { n = n + 1 }
      None => ()
    }
  }
  n
}

///|
/// Prefix (前/中/后) for a piece among its same-file twins. Front
/// means closest to the enemy. For more than three pieces the middle
/// ones all render as 中 (a documented approximation for degenerate
/// positions with 4+ stacked pawns).
fn same_file_prefix(board : Board, from : Pos, piece : Piece) -> String {
  let mut ahead = 0
  let mut behind = 0
  for rank in 0..<10 {
    match board.get(Pos::new(from.file, rank)) {
      Some(p) =>
        if p.side == piece.side && p.kind == piece.kind {
          if piece.side.is_red() {
            if rank > from.rank {
              ahead = ahead + 1
            } else if rank < from.rank {
              behind = behind + 1
            }
          } else if rank < from.rank {
            ahead = ahead + 1
          } else if rank > from.rank {
            behind = behind + 1
          }
        }
      None => ()
    }
  }
  if ahead == 0 {
    "前"
  } else if behind == 0 {
    "后"
  } else {
    "中"
  }
}

///|
/// Render a move in Chinese file notation (中文纵线记谱法), e.g.
/// `炮二平五`, `马8进7`, `前车进一`. The board is required for
/// disambiguation. Works for both the side to move and (for analysis
/// tooling) the opposite side, as long as the moving piece belongs to
/// `board.side_to_move` in the common case.
pub fn Board::move_to_chinese(self : Board, mv : Move) -> String {
  let sb = StringBuilder()
  match self.get(mv.from) {
    Some(piece) => {
      let side = piece.side
      let kind = piece.kind
      let ambiguous = needs_disambiguation(kind) &&
        same_file_count(self, mv.from, piece) >= 2
      if ambiguous {
        sb.write_string(same_file_prefix(self, mv.from, piece))
        sb.write_string(piece_char(piece))
      } else {
        sb.write_string(piece_char(piece))
        sb.write_string(num_string(file_num(mv.from.file, side), side))
      }
      if mv.to.file == mv.from.file {
        // Straight move: 进/退 + number of steps.
        let dr = mv.to.rank - mv.from.rank
        if dr == 0 {
          sb.write_string("平")
        } else {
          let forward = if side.is_red() { dr > 0 } else { dr < 0 }
          sb.write_string(if forward { "进" } else { "退" })
          let steps = if dr < 0 { -dr } else { dr }
          sb.write_string(num_string(steps, side))
        }
      } else if mv.to.rank == mv.from.rank {
        // Sideways: 平 + destination file.
        sb.write_string("平")
        sb.write_string(num_string(file_num(mv.to.file, side), side))
      } else {
        // Diagonal movers (horse/advisor/elephant): 进/退 + destination file.
        let dr = mv.to.rank - mv.from.rank
        let forward = if side.is_red() { dr > 0 } else { dr < 0 }
        sb.write_string(if forward { "进" } else { "退" })
        sb.write_string(num_string(file_num(mv.to.file, side), side))
      }
      sb.to_string()
    }
    None => ""
  }
}

///|
/// Errors from coordinate parsing.
pub enum NotationError {
  /// The notation does not match any legal move of the side to move.
  IllegalOrUnknown(String)
  /// The ICCS coordinate string is malformed (expected 4 chars).
  BadIccs(String)
} derive(Eq, Show, Debug)

///|
/// Interpret a Chinese notation string against the current position.
/// The implementation generates the notation of every legal move and
/// selects the exact match, which guarantees round-trip consistency.
pub fn Board::move_from_chinese(
  self : Board,
  notation : String,
) -> Result[Move, NotationError] {
  for mv in self.legal_moves() {
    if self.move_to_chinese(mv) == notation {
      return Ok(mv)
    }
  }
  Err(IllegalOrUnknown(notation))
}

///|
/// Render a move in ICCS coordinate notation, e.g. `h2e2`.
/// Files are `a`..`i` (a on Red's left), ranks `0`..`9`
/// (0 is Red's back rank).
pub fn Board::move_to_iccs(self : Board, mv : Move) -> String {
  let sb = StringBuilder()
  sb.write_char(('a'.to_int() + mv.from.file).unsafe_to_char())
  sb.write_char(('0'.to_int() + mv.from.rank).unsafe_to_char())
  sb.write_char(('a'.to_int() + mv.to.file).unsafe_to_char())
  sb.write_char(('0'.to_int() + mv.to.rank).unsafe_to_char())
  sb.to_string()
}

///|
/// Parse an ICCS coordinate string into a move, validating that it is
/// legal in the current position.
pub fn Board::move_from_iccs(
  self : Board,
  iccs : String,
) -> Result[Move, NotationError] {
  if iccs.length() != 4 {
    return Err(BadIccs(iccs))
  }
  let chars : Array[Char] = []
  for ch in iccs {
    chars.push(ch)
  }
  let f1 = chars[0].to_int() - 'a'.to_int()
  let r1 = chars[1].to_int() - '0'.to_int()
  let f2 = chars[2].to_int() - 'a'.to_int()
  let r2 = chars[3].to_int() - '0'.to_int()
  if f1 < 0 ||
    f1 > 8 ||
    f2 < 0 ||
    f2 > 8 ||
    r1 < 0 ||
    r1 > 9 ||
    r2 < 0 ||
    r2 > 9 {
    return Err(BadIccs(iccs))
  }
  let mv = Move::new(Pos::new(f1, r1), Pos::new(f2, r2))
  for legal in self.legal_moves() {
    if legal == mv {
      return Ok(mv)
    }
  }
  Err(IllegalOrUnknown(iccs))
}