///|
/// Why a move was rejected.
pub(all) enum PlayError {
  OutOfBounds
  Occupied
  /// The move fills the player's last own liberty without capturing.
  Suicide
  /// Immediate recapture of a single ko stone.
  KoViolation
  /// The move would recreate an earlier whole-board position
  /// (positional superko, Chinese rules).
  SuperkoViolation
  GameOver
} derive(Eq, Debug)

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

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

///|
pub fn PlayError::message(self : PlayError) -> String {
  match self {
    OutOfBounds => "move is out of bounds"
    Occupied => "point is occupied"
    Suicide => "move is suicide"
    KoViolation => "immediate ko recapture is forbidden"
    SuperkoViolation => "move would repeat a previous board position (superko)"
    GameOver => "the game is already over"
  }
}

///|
struct Snapshot {
  board : Board
  turn : Stone
  caps_black : Int
  caps_white : Int
  ko_point : Option[Int]
  passes : Int
  over : Bool
  /// The position key inserted into the history by this move ("" for a
  /// pass, which inserts nothing). [undo] removes exactly this key.
  history_key : String
}

///|
/// A Go game under Chinese rules (area scoring, positional superko).
/// Black moves first on an empty board; handicap games place stones with
/// [Game::setup_stone] and start with White to play.
pub struct Game {
  priv mut board : Board
  priv mut turn : Stone
  priv mut caps_black : Int
  priv mut caps_white : Int
  priv mut ko_point : Option[Int]
  /// Sorted position keys of every position reached so far (including the
  /// initial one), used for positional-superko detection.
  priv history : Array[String]
  priv mut passes : Int
  priv mut over : Bool
  priv mut komi : Double
  priv undo_stack : Array[Snapshot]
}

///|
/// Creates a new empty game. `komi` is added to White's score (7.5 is the
/// usual Chinese-rules value).
pub fn Game::new(size : Int, komi : Double) -> Game {
  let board = Board::new(size)
  let g : Game = {
    board,
    turn: Black,
    caps_black: 0,
    caps_white: 0,
    ko_point: None,
    history: [],
    passes: 0,
    over: false,
    komi,
    undo_stack: [],
  }
  g.push_history()
  g
}

///|
fn Game::push_history(self : Game) -> Unit {
  let key = self.board.position_key()
  let pos = binary_search(self.history, key)
  self.history.insert(pos, key)
}

///|
/// Sorted-array binary search: returns the insertion index of `key`.
fn binary_search(arr : Array[String], key : String) -> Int {
  let mut lo = 0
  let mut hi = arr.length()
  while lo < hi {
    let mid = (lo + hi) / 2
    if arr[mid] < key {
      lo = mid + 1
    } else {
      hi = mid
    }
  }
  lo
}

///|
fn Game::snapshot(self : Game, history_key : String) -> Snapshot {
  let s : Snapshot = {
    board: self.board.copy(),
    turn: self.turn,
    caps_black: self.caps_black,
    caps_white: self.caps_white,
    ko_point: self.ko_point,
    passes: self.passes,
    over: self.over,
    history_key,
  }
  s
}

///|
pub fn Game::size(self : Game) -> Int {
  self.board.size
}

///|
pub fn Game::komi(self : Game) -> Double {
  self.komi
}

///|
pub fn Game::set_komi(self : Game, komi : Double) -> Unit {
  self.komi = komi
}

///|
/// The color to move next.
pub fn Game::turn(self : Game) -> Stone {
  self.turn
}

///|
/// A defensive copy of the current board.
pub fn Game::board(self : Game) -> Board {
  self.board.copy()
}

///|
/// Stones captured by (black, white).
pub fn Game::captures(self : Game) -> (Int, Int) {
  (self.caps_black, self.caps_white)
}

///|
pub fn Game::consecutive_passes(self : Game) -> Int {
  self.passes
}

///|
pub fn Game::is_over(self : Game) -> Bool {
  self.over
}

///|
/// The forbidden recapture point after a ko-shaped capture, if any.
pub fn Game::ko_point(self : Game) -> Option[(Int, Int)] {
  match self.ko_point {
    Some(i) => Some((self.board.row_of(i), self.board.col_of(i)))
    None => None
  }
}

///|
/// Places (or clears) a stone without turn handling — used for handicap
/// stones and SGF setup properties. Resets the superko history to the
/// resulting position. Illegal points are silently ignored.
pub fn Game::setup_stone(
  self : Game,
  row : Int,
  col : Int,
  stone : Stone,
) -> Unit {
  if self.board.in_bounds(row, col) {
    self.board.set(row, col, stone)
    self.history.clear()
    self.push_history()
  }
}

///|
/// Replaces the superko history with the given position keys (sorted
/// automatically). Used when loading a position from SGF/GTP where the
/// earlier positions of the game are unknown.
pub fn Game::set_history(self : Game, keys : Array[String]) -> Unit {
  self.history.clear()
  for k in keys {
    let pos = binary_search(self.history, k)
    self.history.insert(pos, k)
  }
}

///|
/// Declares (or revokes) the game-over state. Two consecutive passes end a
/// game automatically; this is provided for adjudication tools.
pub fn Game::set_over(self : Game, over : Bool) -> Unit {
  self.over = over
}

///|
/// Plays a stone of color `player` at (row, col). Out-of-turn coordinates
/// are accepted (the mover defines the color), matching GTP semantics.
/// Returns the number of captured stones.
pub fn Game::play_stone(
  self : Game,
  player : Stone,
  row : Int,
  col : Int,
) -> Result[Int, PlayError] {
  if player is Empty {
    return Err(Occupied)
  }
  if self.over {
    return Err(GameOver)
  }
  if !(self.board.in_bounds(row, col)) {
    return Err(OutOfBounds)
  }
  let i = self.board.idx(row, col)
  if !(self.cells_get(i) is Empty) {
    return Err(Occupied)
  }
  if self.ko_point == Some(i) {
    return Err(KoViolation)
  }
  let nb = self.board.copy()
  nb.set_at(i, player)
  let opp = player.opponent()
  let mut captured = 0
  let mut captured_one : Option[Int] = None
  let removed : Array[Bool] = Array::make(nb.size * nb.size, false)
  for n in nb.neighbors(i) {
    if nb.get_at(n) == opp && !removed[n] {
      let grp = nb.group_at(n)
      if grp.liberties.length() == 0 {
        for s in grp.stones {
          nb.set_at(s, Empty)
          removed[s] = true
          captured += 1
          captured_one = Some(s)
        }
      }
    }
  }
  let own = nb.group_at(i)
  if own.liberties.length() == 0 && captured == 0 {
    return Err(Suicide)
  }
  // positional superko: reject recreation of any earlier position
  let key = nb.position_key()
  let pos = binary_search(self.history, key)
  if pos < self.history.length() && self.history[pos] == key {
    return Err(SuperkoViolation)
  }
  // simple ko: single stone captured, single-stone group with one liberty
  let new_ko : Option[Int] = if captured == 1 &&
    own.stones.length() == 1 &&
    own.liberties.length() == 1 {
    captured_one
  } else {
    None
  }
  // commit
  self.undo_stack.push(self.snapshot(key))
  self.board = nb
  match player {
    Black => self.caps_black += captured
    White => self.caps_white += captured
    Empty => ()
  }
  self.ko_point = new_ko
  self.history.insert(pos, key)
  self.turn = player.opponent()
  self.passes = 0
  Ok(captured)
}

///|
fn Game::cells_get(self : Game, i : Int) -> Stone {
  self.board.get_at(i)
}

///|
/// Plays a stone for the side to move.
pub fn Game::play(self : Game, row : Int, col : Int) -> Result[Int, PlayError] {
  self.play_stone(self.turn, row, col)
}

///|
/// The given color passes. Two consecutive passes end the game.
pub fn Game::play_pass(self : Game, player : Stone) -> Unit {
  if self.over {
    return
  }
  self.undo_stack.push(self.snapshot(""))
  self.ko_point = None
  self.passes += 1
  if self.passes >= 2 {
    self.over = true
  }
  self.turn = player.opponent()
}

///|
/// Undoes the last move or pass. Returns false when nothing to undo.
pub fn Game::undo(self : Game) -> Bool {
  match self.undo_stack.pop() {
    Some(s) => {
      self.board = s.board
      self.turn = s.turn
      self.caps_black = s.caps_black
      self.caps_white = s.caps_white
      self.ko_point = s.ko_point
      self.passes = s.passes
      self.over = s.over
      if s.history_key.length() > 0 {
        let p = binary_search(self.history, s.history_key)
        if p < self.history.length() && self.history[p] == s.history_key {
          let _ = self.history.remove(p)
        }
      }
      true
    }
    None => false
  }
}

///|
/// Area score (Chinese rules): stones on the board plus empty regions
/// enclosed by a single color. Neutral points (dame) count for nobody.
/// Returns (black, white) where white already includes komi.
pub fn Game::score_area(self : Game) -> (Double, Double) {
  let size = self.board.size
  let mut black_stones = 0
  let mut white_stones = 0
  let visited : Array[Bool] = Array::make(size * size, false)
  let mut terr_black = 0
  let mut terr_white = 0
  for i in 0..<(size * size) {
    match self.board.get_at(i) {
      Black => black_stones += 1
      White => white_stones += 1
      Empty =>
        if !visited[i] {
          // flood fill this empty region
          let mut sees_black = false
          let mut sees_white = false
          let mut count = 0
          let stack : Array[Int] = [i]
          visited[i] = true
          while stack.length() > 0 {
            let cur = stack.pop().unwrap()
            count += 1
            for n in self.board.neighbors(cur) {
              match self.board.get_at(n) {
                Black => sees_black = true
                White => sees_white = true
                Empty =>
                  if !visited[n] {
                    visited[n] = true
                    stack.push(n)
                  }
              }
            }
          }
          if sees_black && !sees_white {
            terr_black += count
          } else if sees_white && !sees_black {
            terr_white += count
          }
        }
    }
  }
  ((black_stones + terr_black).to_double(), (white_stones + terr_white).to_double() + self.komi)
}

///|
/// Result string in GTP/SGF style: `B+2.5`, `W+0.5` or `0` (jigo).
/// Returns `void` when the game has not ended with two passes.
pub fn Game::final_score(self : Game) -> String {
  if !(self.over) {
    return "void"
  }
  let (b, w) = self.score_area()
  let diff = b - w
  if diff > 0.0 {
    "B+\{diff}"
  } else if diff < 0.0 {
    let ad = 0.0 - diff
    "W+\{ad}"
  } else {
    "0"
  }
}

///|
/// All points where `player` may legally play right now (ko, superko and
/// suicide excluded). Used by GTP `genmove` and by test tooling.
pub fn Game::legal_points(self : Game, player : Stone) -> Array[(Int, Int)] {
  let acc : Array[(Int, Int)] = []
  if player is Empty || self.over {
    return acc
  }
  let size = self.board.size
  for i in 0..<(size * size) {
    if self.cells_get(i) is Empty && self.ko_point != Some(i) {
      let r = self.board.row_of(i)
      let c = self.board.col_of(i)
      if self.play_stone(player, r, c) is Ok(_) {
        acc.push((r, c))
        let _ = self.undo()
      }
    }
  }
  acc
}