///|
/// The maximum grid disk radius accepted by this implementation.
pub const MAX_GRID_DISK_RADIUS : Int = 64

///|
fn geometric_neighbor(index : H3Index, direction : Int) -> H3Index? {
  let original = index_to_face_ijk(index.raw())
  let resolution = index.resolution()
  let moved = coord_neighbor(original.1, direction)
  let mut adjusted_resolution = resolution
  let mut current = moved
  if is_resolution_class_iii(adjusted_resolution) {
    current = coord_down_ap7r(current)
    adjusted_resolution += 1
  }
  let (adjusted, overage) = adjust_overage_class_ii(
    FaceIJK(original.0, current),
    adjusted_resolution,
    false,
  )
  let selected = if overage == NoOverage {
    moved
  } else if adjusted_resolution != resolution {
    coord_up_ap7r(adjusted.1)
  } else {
    adjusted.1
  }
  let face = if overage == NoOverage { original.0 } else { adjusted.0 }
  match face_ijk_to_h3(FaceIJK(face, selected), resolution) {
    Ok(value) =>
      match H3Index::from_u64(value) {
        Ok(cell) => Some(cell)
        Err(_) => None
      }
    Err(_) => None
  }
}

///|
/// Returns the neighboring cell in an H3 index direction from 1 through 6.
///
/// Pentagons have one unavailable direction. In that case,
/// `UnavailableNeighbor` is returned.
pub fn H3Index::neighbor(
  self : H3Index,
  direction : Int,
) -> Result[H3Index, H3Error] {
  if direction < 1 || direction > 6 {
    return Err(InvalidDirection(direction))
  }
  match geometric_neighbor(self, direction) {
    Some(neighbor) => Ok(neighbor)
    None => Err(UnavailableNeighbor(direction))
  }
}

///|
/// Returns whether two cells share an edge.
pub fn H3Index::is_neighbor_of(self : H3Index, other : H3Index) -> Bool {
  if self.resolution() != other.resolution() {
    return false
  }
  let mut direction = 1
  while direction <= 6 {
    match geometric_neighbor(self, direction) {
      Some(neighbor) => if neighbor == other { return true }
      None => ()
    }
    direction += 1
  }
  false
}

///|
/// Returns the direction from this cell to `other`, when they are neighbors.
pub fn H3Index::direction_to(self : H3Index, other : H3Index) -> Int? {
  if self.resolution() != other.resolution() {
    return None
  }
  let mut direction = 1
  while direction <= 6 {
    match geometric_neighbor(self, direction) {
      Some(neighbor) => if neighbor == other { return Some(direction) }
      None => ()
    }
    direction += 1
  }
  None
}

///|
fn H3Index::grid_disk_entries(
  self : H3Index,
  radius : Int,
) -> Result[Array[(H3Index, Int)], H3Error] {
  if radius < 0 || radius > MAX_GRID_DISK_RADIUS {
    return Err(InvalidGridDiskRadius(radius))
  }
  let visited : @hashmap.HashMap[H3Index, Unit] = @hashmap.HashMap([])
  let result : Array[(H3Index, Int)] = []
  let mut frontier : Array[H3Index] = [self]
  visited.set(self, ())
  let mut distance = 0
  while distance <= radius {
    for cell in frontier {
      result.push((cell, distance))
    }
    if distance == radius {
      break
    }
    let next : Array[H3Index] = []
    for cell in frontier {
      for direction in 1..<=6 {
        match geometric_neighbor(cell, direction) {
          Some(neighbor) =>
            if !visited.contains(neighbor) {
              visited.set(neighbor, ())
              next.push(neighbor)
            }
          None => ()
        }
      }
    }
    frontier = next
    distance += 1
  }
  Ok(result)
}

///|
/// Returns the origin, its direct neighbors, and all cells within `radius`
/// grid steps.
///
/// The result does not contain duplicates and is ordered by increasing grid
/// distance from the origin.
pub fn H3Index::grid_disk(
  self : H3Index,
  radius : Int,
) -> Result[Array[H3Index], H3Error] {
  let entries = match self.grid_disk_entries(radius) {
    Ok(entries) => entries
    Err(error) => return Err(error)
  }
  let result : Array[H3Index] = []
  for entry in entries {
    result.push(entry.0)
  }
  Ok(result)
}

///|
/// Returns each cell within `radius` together with its grid distance.
pub fn H3Index::grid_disk_distances(
  self : H3Index,
  radius : Int,
) -> Result[Array[(H3Index, Int)], H3Error] {
  self.grid_disk_entries(radius)
}

///|
/// Returns only the cells exactly `radius` grid steps from the origin.
pub fn H3Index::grid_ring(
  self : H3Index,
  radius : Int,
) -> Result[Array[H3Index], H3Error] {
  let entries = match self.grid_disk_entries(radius) {
    Ok(entries) => entries
    Err(error) => return Err(error)
  }
  let result : Array[H3Index] = []
  for entry in entries {
    if entry.1 == radius {
      result.push(entry.0)
    }
  }
  Ok(result)
}

///|
/// Returns the exact grid distance to another cell within the supported search
/// radius.
pub fn H3Index::grid_distance_to(
  self : H3Index,
  other : H3Index,
) -> Result[Int, H3Error] {
  if self.resolution() != other.resolution() {
    return Err(ResolutionMismatch(other.resolution(), self.resolution()))
  }
  if self == other {
    return Ok(0)
  }
  let entries = match self.grid_disk_entries(MAX_GRID_DISK_RADIUS) {
    Ok(entries) => entries
    Err(error) => return Err(error)
  }
  for entry in entries {
    if entry.0 == other {
      return Ok(entry.1)
    }
  }
  Err(GridDistanceOutOfRange)
}