///|
pub struct Position {
  x : Int
  y : Int
} derive(Debug, Eq, ToJson)

///|
pub fn Position::new(x~ : Int, y~ : Int) -> Position {
  { x, y }
}

///|
pub(all) enum MoveRule {
  FourDirections
  EightDirections
} derive(Debug, Eq, ToJson)

///|
struct WeightedCell {
  position : Position
  weight : Int
} derive(Debug, Eq, ToJson)

///|
pub struct GridMap {
  width : Int
  height : Int
  obstacles : Array[Position]
  weights : Array[WeightedCell]
} derive(Debug, Eq, ToJson)

///|
pub fn GridMap::new(width~ : Int, height~ : Int) -> GridMap raise {
  if width <= 0 || height <= 0 {
    fail("GridMap dimensions must be positive")
  }
  { width, height, obstacles: [], weights: [] }
}

///|
pub fn GridMap::contains(self : GridMap, position : Position) -> Bool {
  position.x >= 0 &&
  position.y >= 0 &&
  position.x < self.width &&
  position.y < self.height
}

///|
pub fn GridMap::is_obstacle(self : GridMap, position : Position) -> Bool {
  self.obstacles.contains(position)
}

///|
pub fn GridMap::is_walkable(self : GridMap, position : Position) -> Bool {
  self.contains(position) && !self.is_obstacle(position)
}

///|
pub fn GridMap::with_obstacle(
  self : GridMap,
  position : Position,
) -> GridMap raise {
  if !self.contains(position) {
    fail("Obstacle position is outside the grid")
  }
  let next = self.copy()
  if !next.obstacles.contains(position) {
    next.obstacles.push(position)
  }
  next
}

///|
pub fn GridMap::with_weight(
  self : GridMap,
  position : Position,
  weight : Int,
) -> GridMap raise {
  if !self.contains(position) {
    fail("Weighted position is outside the grid")
  }
  if weight < 0 {
    fail("GridMap weights must be non-negative")
  }
  let next = self.copy()
  let existing = next.weights.search_by(fn(cell) { cell.position == position })
  match existing {
    Some(index) => next.weights[index] = { position, weight }
    None => next.weights.push({ position, weight })
  }
  next
}

///|
pub fn GridMap::weight_at(self : GridMap, position : Position) -> Int {
  match self.weights.search_by(fn(cell) { cell.position == position }) {
    Some(index) => self.weights[index].weight
    None => 1
  }
}

///|
pub fn GridMap::neighbors(
  self : GridMap,
  position : Position,
  rule : MoveRule,
) -> Array[Position] {
  let deltas = match rule {
    FourDirections => [(0, -1), (1, 0), (0, 1), (-1, 0)]
    EightDirections =>
      [(0, -1), (1, 0), (0, 1), (-1, 0), (1, -1), (1, 1), (-1, 1), (-1, -1)]
  }
  let out : Array[Position] = []
  for delta in deltas {
    let (dx, dy) = delta
    let next = Position::new(x=position.x + dx, y=position.y + dy)
    if self.is_walkable(next) {
      out.push(next)
    }
  }
  out
}

///|
fn GridMap::copy(self : GridMap) -> GridMap {
  {
    width: self.width,
    height: self.height,
    obstacles: self.obstacles.copy(),
    weights: self.weights.copy(),
  }
}