///|
const MAX_MERCATOR_LAT : Double = 85.05112878

///|
pub fn point_to_tile(point : Point, zoom : Int) -> Result[Tile, RouteError] {
  match validate_zoom(zoom) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  match validate_point(point, 0) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let tile_count = 1 << zoom
  let lat = point.lat.clamp(min=-MAX_MERCATOR_LAT, max=MAX_MERCATOR_LAT)
  let lat_rad = deg_to_rad(lat)
  let raw_x = ((point.lon + 180.0) / 360.0 * tile_count.to_double())
    .floor()
    .to_int()
  let mercator = @math.ln(@math.tan(lat_rad) + 1.0 / @math.cos(lat_rad))
  let raw_y = ((1.0 - mercator / @math.PI) / 2.0 * tile_count.to_double())
    .floor()
    .to_int()
  Ok({
    z: zoom,
    x: raw_x.clamp(min=0, max=tile_count - 1),
    y: raw_y.clamp(min=0, max=tile_count - 1),
  })
}

///|
pub fn tile_cover(
  points : ArrayView[Point],
  zoom : Int,
) -> Result[Array[Tile], RouteError] {
  match validate_zoom(zoom) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  match require_points(points) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let tiles = Array::new()
  if points.length() == 1 {
    match point_to_tile(points[0], zoom) {
      Ok(tile) => push_unique_tile(tiles, tile)
      Err(err) => return Err(err)
    }
    return Ok(tiles)
  }
  for i in 1.. ()
      Err(err) => return Err(err)
    }
  }
  Ok(tiles)
}

///|
pub fn tiles_to_string(tiles : ArrayView[Tile]) -> String {
  let parts = Array::new(capacity=tiles.length())
  for tile in tiles {
    parts.push(tile.key())
  }
  parts.join(", ")
}

///|
fn add_segment_tiles(
  out : Array[Tile],
  a : Point,
  b : Point,
  zoom : Int,
) -> Result[Unit, RouteError] {
  let start_tile = match point_to_tile(a, zoom) {
    Ok(tile) => tile
    Err(err) => return Err(err)
  }
  let end_tile = match point_to_tile(b, zoom) {
    Ok(tile) => tile
    Err(err) => return Err(err)
  }
  let steps = max_int(
    (end_tile.x - start_tile.x).abs(),
    (end_tile.y - start_tile.y).abs(),
  )
  if steps == 0 {
    push_unique_tile(out, start_tile)
    return Ok(())
  }
  let mut step = 0
  while step <= steps {
    let t = step.to_double() / steps.to_double()
    // Follow the shortest longitude arc so antimeridian routes stay local.
    let point = interpolate_point_unchecked(a, b, t)
    match point_to_tile(point, zoom) {
      Ok(tile) => push_unique_tile(out, tile)
      Err(err) => return Err(err)
    }
    step = step + 1
  }
  Ok(())
}