///|
pub suberror ChessError {
  Invalid(String)
} derive(Debug)

///|
pub struct Board {
  cells : Array[Int]
  red : Bool
} derive(Debug, Eq)

///|
pub(all) struct Move {
  from : Int
  to : Int
} derive(Debug, Eq)

///|
pub fn initial() -> Board {
  parse_fen("rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w") catch {
    _ => abort("internal FEN")
  }
}

///|
pub fn parse_fen(fen : String) -> Board raise ChessError {
  let fields = fen.split(" ").filter(x => !x.is_empty()).collect()
  if fields.length() < 2 || (fields[1] != "w" && fields[1] != "b") {
    raise Invalid("FEN turn")
  }
  let rows = fields[0].split("/").collect()
  if rows.length() != 10 {
    raise Invalid("FEN ranks")
  }
  let cells : Array[Int] = []
  let mut rk = 0
  let mut bk = 0
  for row in rows {
    let start = cells.length()
    for c in row {
      if c >= '1' && c <= '9' {
        for _ in 0..<(c.to_int() - '0'.to_int()) {
          cells.push(0)
        }
      } else {
        let p = match c {
          'K' => 1
          'A' => 2
          'B' | 'E' => 3
          'N' | 'H' => 4
          'R' => 5
          'C' => 6
          'P' => 7
          'k' => -1
          'a' => -2
          'b' | 'e' => -3
          'n' | 'h' => -4
          'r' => -5
          'c' => -6
          'p' => -7
          _ => raise Invalid("FEN piece")
        }
        if p == 1 {
          rk += 1
        }
        if p == -1 {
          bk += 1
        }
        cells.push(p)
      }
    }
    if cells.length() - start != 9 {
      raise Invalid("FEN file count")
    }
  }
  if rk != 1 || bk != 1 {
    raise Invalid("FEN needs both kings")
  }
  { cells, red: fields[1] == "w", }
}

///|
pub fn Board::fen(self : Board) -> String {
  let rows : Array[String] = []
  for y in 0..<10 {
    let mut row = ""
    let mut empty = 0
    for x in 0..<9 {
      let p = self.cells[y * 9 + x]
      if p == 0 {
        empty += 1
      } else {
        if empty > 0 {
          row += empty.to_string()
          empty = 0
        }
        row += match p {
          1 => "K"
          2 => "A"
          3 => "B"
          4 => "N"
          5 => "R"
          6 => "C"
          7 => "P"
          -1 => "k"
          -2 => "a"
          -3 => "b"
          -4 => "n"
          -5 => "r"
          -6 => "c"
          _ => "p"
        }
      }
    }
    if empty > 0 {
      row += empty.to_string()
    }
    rows.push(row)
  }
  rows.join("/") + (if self.red { " w" } else { " b" })
}

///|
fn abs(n : Int) -> Int {
  if n < 0 {
    -n
  } else {
    n
  }
}

///|
fn palace(x : Int, y : Int, red : Bool) -> Bool {
  x >= 3 && x <= 5 && (if red { y >= 7 && y <= 9 } else { y >= 0 && y <= 2 })
}

///|
fn Board::pseudo(self : Board, from : Int, to : Int) -> Bool {
  if from == to {
    return false
  }
  let p = self.cells[from]
  let target = self.cells[to]
  if p == 0 || (target != 0 && (p > 0) == (target > 0)) {
    return false
  }
  let x = from % 9
  let y = from / 9
  let tx = to % 9
  let ty = to / 9
  let dx = tx - x
  let dy = ty - y
  let red = p > 0
  let aligned = dx == 0 || dy == 0
  let mut screen = 0
  if aligned {
    let step = if dx == 0 {
      if dy > 0 {
        9
      } else {
        -9
      }
    } else if dx > 0 {
      1
    } else {
      -1
    }
    let mut i = from + step
    while i != to {
      if self.cells[i] != 0 {
        screen += 1
      }
      i += step
    }
  }
  match abs(p) {
    1 =>
      (palace(tx, ty, red) && abs(dx) + abs(dy) == 1) ||
      (target == -p && dx == 0 && screen == 0)
    2 => palace(tx, ty, red) && abs(dx) == 1 && abs(dy) == 1
    3 =>
      abs(dx) == 2 &&
      abs(dy) == 2 &&
      (if red { ty >= 5 } else { ty <= 4 }) &&
      self.cells[(y + dy / 2) * 9 + x + dx / 2] == 0
    4 =>
      if abs(dx) == 2 && abs(dy) == 1 {
        self.cells[y * 9 + x + dx / 2] == 0
      } else if abs(dx) == 1 && abs(dy) == 2 {
        self.cells[(y + dy / 2) * 9 + x] == 0
      } else {
        false
      }
    5 => aligned && screen == 0
    6 => aligned && screen == (if target == 0 { 0 } else { 1 })
    7 =>
      (dx == 0 && dy == (if red { -1 } else { 1 })) ||
      (dy == 0 && abs(dx) == 1 && (if red { y <= 4 } else { y >= 5 }))
    _ => false
  }
}

///|
pub fn Board::in_check(self : Board, red : Bool) -> Bool {
  let mut king = -1
  for i, p in self.cells {
    if p == (if red { 1 } else { -1 }) {
      king = i
      break
    }
  }
  if king < 0 {
    return true
  }
  for i, p in self.cells {
    if p != 0 && (p > 0) != red && self.pseudo(i, king) {
      return true
    }
  }
  false
}

///|
fn Board::apply(self : Board, m : Move) -> Board {
  let cells = self.cells.copy()
  cells[m.to] = cells[m.from]
  cells[m.from] = 0
  { cells, red: !self.red, }
}

///|
pub fn Board::legal_moves(self : Board) -> Array[Move] {
  let out : Array[Move] = []
  for i, p in self.cells {
    if p == 0 || (p > 0) != self.red {
      continue
    }
    let x = i % 9
    let y = i / 9
    let targets : Array[Int] = []
    // Generate geometrically reachable squares before testing blockers and check.
    if abs(p) == 5 || abs(p) == 6 || abs(p) == 1 {
      for tx in 0..<9 {
        targets.push(y * 9 + tx)
      }
      for ty in 0..<10 {
        if ty != y {
          targets.push(ty * 9 + x)
        }
      }
    } else {
      let offsets = match abs(p) {
        2 => [(1, 1), (1, -1), (-1, 1), (-1, -1)]
        3 => [(2, 2), (2, -2), (-2, 2), (-2, -2)]
        4 =>
          [
            (2, 1),
            (2, -1),
            (-2, 1),
            (-2, -1),
            (1, 2),
            (1, -2),
            (-1, 2),
            (-1, -2),
          ]
        _ => [(0, if p > 0 { -1 } else { 1 }), (1, 0), (-1, 0)]
      }
      for (dx, dy) in offsets {
        let tx = x + dx
        let ty = y + dy
        if tx >= 0 && tx < 9 && ty >= 0 && ty < 10 {
          targets.push(ty * 9 + tx)
        }
      }
    }
    for j in targets {
      if self.pseudo(i, j) {
        let m = Move::{ from: i, to: j, }
        if !self.apply(m).in_check(self.red) {
          out.push(m)
        }
      }
    }
  }
  out
}

///|
pub fn Move::coordinate(self : Move) -> String {
  let files = "abcdefghi".to_array()
  files[self.from % 9].to_string() +
  (9 - self.from / 9).to_string() +
  files[self.to % 9].to_string() +
  (9 - self.to / 9).to_string()
}

///|
pub fn Board::play(self : Board, coordinate : String) -> Board raise ChessError {
  for m in self.legal_moves() {
    if m.coordinate() == coordinate {
      return self.apply(m)
    }
  }
  raise Invalid("illegal move " + coordinate)
}

///|
fn Board::evaluate(self : Board) -> Int {
  let mut score = 0
  for i, p in self.cells {
    let v = match abs(p) {
      1 => 100000
      2 | 3 => 120
      4 => 270
      5 => 600
      6 => 300
      7 => if (p > 0 && i / 9 < 5) || (p < 0 && i / 9 > 4) { 100 } else { 60 }
      _ => 0
    }
    score += if p > 0 { v } else { -v }
  }
  if self.red {
    score
  } else {
    -score
  }
}

///|
pub fn Board::best_move(
  self : Board,
  depth : Int,
  node_limit? : Int = 50000,
) -> Move? raise ChessError {
  if depth < 1 || depth > 64 || node_limit < 1 || node_limit > 1000000 {
    raise Invalid("search bounds")
  }
  let result = self.search(depth, node_limit~)
  if result.stopped {
    raise Invalid("search node budget")
  }
  result.best
}

///|
pub fn Board::perft(self : Board, depth : Int) -> Int raise ChessError {
  if depth < 0 || depth > 4 {
    raise Invalid("perft depth 0..4")
  }
  if depth == 0 {
    return 1
  }
  let mut n = 0
  for m in self.legal_moves() {
    let children = self.apply(m).perft(depth - 1)
    if n > 2147483647 - children {
      raise Invalid("perft count exceeds Int")
    }
    n += children
  }
  n
}