///|
pub fn tile_bounds(tile : Tile) -> Result[TileBounds, RouteError] {
  match validate_tile(tile) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let n = (1 << tile.z).to_double()
  let west = tile.x.to_double() / n * 360.0 - 180.0
  let east = (tile.x + 1).to_double() / n * 360.0 - 180.0
  let north = tile_y_to_lat(tile.y, n)
  let south = tile_y_to_lat(tile.y + 1, n)
  Ok(TileBounds(tile, west, south, east, north))
}

///|
pub fn tile_center(tile : Tile) -> Result[Point, RouteError] {
  let bounds = match tile_bounds(tile) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  Ok(
    Point(
      (bounds.south + bounds.north) / 2.0,
      (bounds.west + bounds.east) / 2.0,
    ),
  )
}

///|
pub fn tile_range_for_bbox(
  bbox : BBox,
  zoom : Int,
) -> Result[TileRange, RouteError] {
  match validate_zoom(zoom) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let sw = match point_to_tile(Point(bbox.min_lat, bbox.min_lon), zoom) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let ne = match point_to_tile(Point(bbox.max_lat, bbox.max_lon), zoom) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  Ok(
    TileRange(
      zoom,
      min_int(sw.x, ne.x),
      min_int(sw.y, ne.y),
      max_int(sw.x, ne.x),
      max_int(sw.y, ne.y),
    ),
  )
}

///|
pub fn tiles_for_bbox(
  bbox : BBox,
  zoom : Int,
) -> Result[Array[Tile], RouteError] {
  let range = match tile_range_for_bbox(bbox, zoom) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  tiles_for_range(range)
}

///|
pub fn tiles_for_range(range : TileRange) -> Result[Array[Tile], RouteError] {
  match validate_zoom(range.z) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let limit = 1 << range.z
  if range.min_x < 0 ||
    range.min_y < 0 ||
    range.max_x >= limit ||
    range.max_y >= limit ||
    range.max_x < range.min_x ||
    range.max_y < range.min_y {
    return Err(
      InvalidTileCoordinate(tile=Tile(range.z, range.min_x, range.min_y)),
    )
  }
  let out = Array::new()
  let mut y = range.min_y
  while y <= range.max_y {
    let mut x = range.min_x
    while x <= range.max_x {
      out.push(Tile(range.z, x, y))
      x = x + 1
    }
    y = y + 1
  }
  Ok(out)
}

///|
pub fn tile_count_for_range(range : TileRange) -> Result[Int, RouteError] {
  match tiles_for_range(range) {
    Ok(value) => Ok(value.length())
    Err(err) => Err(err)
  }
}

///|
pub fn tile_parent(tile : Tile) -> Result[Tile, RouteError] {
  match validate_tile(tile) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  if tile.z == 0 {
    Ok(tile)
  } else {
    Ok(Tile(tile.z - 1, tile.x / 2, tile.y / 2))
  }
}

///|
pub fn tile_children(tile : Tile) -> Result[Array[Tile], RouteError] {
  match validate_tile(tile) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let out = Array::new(capacity=4)
  let z = tile.z + 1
  if z > 22 {
    return Err(InvalidZoom(zoom=z))
  }
  out.push(Tile(z, tile.x * 2, tile.y * 2))
  out.push(Tile(z, tile.x * 2 + 1, tile.y * 2))
  out.push(Tile(z, tile.x * 2, tile.y * 2 + 1))
  out.push(Tile(z, tile.x * 2 + 1, tile.y * 2 + 1))
  Ok(out)
}

///|
pub fn tile_neighbors(tile : Tile) -> Result[Array[Tile], RouteError] {
  match validate_tile(tile) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let out = Array::new()
  let limit = 1 << tile.z
  let mut dy = -1
  while dy <= 1 {
    let mut dx = -1
    while dx <= 1 {
      if dx == 0 && dy == 0 {
        ()
      } else {
        let x = tile.x + dx
        let y = tile.y + dy
        if x >= 0 && x < limit && y >= 0 && y < limit {
          out.push(Tile(tile.z, x, y))
        }
      }
      dx = dx + 1
    }
    dy = dy + 1
  }
  Ok(out)
}

///|
pub fn expand_tiles_with_neighbors(
  tiles : ArrayView[Tile],
) -> Result[Array[Tile], RouteError] {
  let out = Array::new()
  for tile in tiles {
    match validate_tile(tile) {
      Ok(_) => push_unique_tile(out, tile)
      Err(err) => return Err(err)
    }
    let neighbors = match tile_neighbors(tile) {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    for neighbor in neighbors {
      push_unique_tile(out, neighbor)
    }
  }
  Ok(out)
}

///|
pub fn tile_quadkey(tile : Tile) -> Result[String, RouteError] {
  match validate_tile(tile) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  if tile.z == 0 {
    return Ok("")
  }
  let out = StringBuilder(size_hint=tile.z)
  let mut mask = 1 << (tile.z - 1)
  while mask > 0 {
    let mut digit = 0
    if (tile.x & mask) != 0 {
      digit = digit + 1
    }
    if (tile.y & mask) != 0 {
      digit = digit + 2
    }
    out.write_string(digit.to_string())
    mask = mask / 2
  }
  Ok(out.to_string())
}

///|
pub fn quadkey_to_tile(quadkey : String) -> Result[Tile, RouteError] {
  let z = quadkey.length()
  match validate_zoom(z) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  if z == 0 {
    return Ok(Tile(0, 0, 0))
  }
  let mut x = 0
  let mut y = 0
  let mut mask = 1 << (z - 1)
  for i in 0.. ()
      '1' => x = x + mask
      '2' => y = y + mask
      '3' => {
        x = x + mask
        y = y + mask
      }
      _ =>
        return Err(
          MalformedPolyline(pos=i, reason="quadkey digit must be 0, 1, 2, or 3"),
        )
    }
    mask = mask / 2
  }
  Ok(Tile(z, x, y))
}

///|
pub fn route_tile_range(
  points : ArrayView[Point],
  zoom : Int,
) -> Result[TileRange, RouteError] {
  let bbox = match bounding_box(points) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  tile_range_for_bbox(bbox, zoom)
}

///|
pub fn route_tile_bounds(
  points : ArrayView[Point],
  zoom : Int,
) -> Result[BBox, RouteError] {
  let tiles = match tile_cover(points, zoom) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  if tiles.length() == 0 {
    return Err(EmptyRoute)
  }
  let first = match tile_bounds(tiles[0]) {
    Ok(value) => value.to_bbox()
    Err(err) => return Err(err)
  }
  let mut out = first
  for i in 1.. out = out.merge(bounds.to_bbox())
      Err(err) => return Err(err)
    }
  }
  Ok(out)
}

///|
pub fn tile_range_to_string(range : TileRange) -> String {
  range.to_string()
}

///|
fn validate_tile(tile : Tile) -> Result[Unit, RouteError] {
  match validate_zoom(tile.z) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let limit = 1 << tile.z
  if tile.x >= 0 && tile.x < limit && tile.y >= 0 && tile.y < limit {
    Ok(())
  } else {
    Err(InvalidTileCoordinate(tile~))
  }
}

///|
fn tile_y_to_lat(y : Int, n : Double) -> Double {
  let mercator = @math.PI * (1.0 - 2.0 * y.to_double() / n)
  rad_to_deg(@math.atan(sinh_double(mercator)))
}

///|
fn sinh_double(value : Double) -> Double {
  (@math.exp(value) - @math.exp(-value)) / 2.0
}