///|
/// Errors are stable codes with context. No user input is used as an index before validation.
pub suberror PegError {
  InvalidBoard(String)
  InvalidPosition
  InvalidGoal
  InvalidBudget
  InvalidTranscript(Int)
  InvalidMove(Int)
} derive(Debug, Eq)

///|
/// A hole coordinate. x and y are zero based.
pub(all) struct Cell {
  x : Int
  y : Int
} derive(Debug, Eq)

///|
/// Board topology is opaque; returned collections are copies.
pub struct Board {
  width : Int
  height : Int
  cells : Array[Cell]
  mask : UInt64
  jumps : Array[Jump]
  lattice : Lattice
} derive(Debug)

///|
/// An immutable occupancy bitset; indices follow row-major order over holes only.
pub(all) struct Position {
  bits : UInt64
} derive(Debug, Eq, Hash)

///|
fn bit(i : Int) -> UInt64 {
  1UL << i
}

///|
pub fn Board::size(self : Board) -> Int {
  self.cells.length()
}

///|
pub fn Board::dimensions(self : Board) -> (Int, Int) {
  (self.width, self.height)
}

///|
pub fn Board::holes(self : Board) -> Array[Cell] {
  self.cells.copy()
}

///|
pub fn Board::index(self : Board, x : Int, y : Int) -> Int? {
  for i = 0; i < self.cells.length(); i = i + 1 {
    if self.cells[i] == { x, y } {
      return Some(i)
    }
  }
  None
}

///|
pub fn Board::validate(self : Board, pos : Position) -> Unit raise PegError {
  if (pos.bits & self.mask) != pos.bits {
    raise InvalidPosition
  }
}

///|
pub fn Position::count(self : Position) -> Int {
  let mut bits = self.bits
  let mut n = 0
  while bits != 0UL {
    bits = bits & (bits - 1UL)
    n = n + 1
  }
  n
}

///|
/// Rectangular ASCII: o=peg, .=empty hole, #=absent. Optional final LF, CRLF accepted.
pub fn parse_board(
  text : String,
  lattice? : Lattice = Orthogonal,
) -> (Board, Position) raise PegError {
  if text.length() == 0 || text.length() > 200 {
    raise InvalidBoard("size")
  }
  // Only CRLF pairs are line endings; never silently discard stray CR bytes.
  for i = 0; i < text.length(); i = i + 1 {
    if text[i] == '\r' && (i + 1 >= text.length() || text[i + 1] != '\n') {
      raise InvalidBoard("line ending")
    }
  }
  let rows = text
    .split("\n")
    .map(s => s.to_owned().trim_end(chars="\r").to_owned())
    .collect()
  if rows.length() > 1 && rows[rows.length() - 1] == "" {
    ignore(rows.pop())
  }
  if rows.length() == 0 || rows.length() > 8 {
    raise InvalidBoard("height")
  }
  let w = rows[0].length()
  if w == 0 || w > 8 {
    raise InvalidBoard("width")
  }
  let cells : Array[Cell] = []
  let mut bits = 0UL
  let mut mask = 0UL
  for y = 0; y < rows.length(); y = y + 1 {
    if rows[y].length() != w {
      raise InvalidBoard("ragged")
    }
    for x = 0; x < w; x = x + 1 {
      match rows[y][x] {
        'o' | '.' => {
          let b = bit(cells.length())
          mask = mask | b
          if rows[y][x] == 'o' {
            bits = bits | b
          }
          cells.push({ x, y })
        }
        '#' => ()
        _ => raise InvalidBoard("character")
      }
    }
  }
  if cells.length() == 0 {
    raise InvalidBoard("no holes")
  }
  (
    {
      width: w,
      height: rows.length(),
      cells,
      mask,
      jumps: compile_jumps(cells, lattice),
      lattice,
    },
    { bits, },
  )
}

///|
pub fn Board::render(self : Board, pos : Position) -> String raise PegError {
  self.validate(pos)
  let out = StringBuilder::new()
  for y = 0; y < self.height; y = y + 1 {
    if y > 0 {
      out.write_string("\n")
    }
    for x = 0; x < self.width; x = x + 1 {
      match self.index(x, y) {
        None => out.write_string("#")
        Some(i) =>
          out.write_string(if (pos.bits & bit(i)) == 0UL { "." } else { "o" })
      }
    }
  }
  out.to_string()
}