///|
/// WGS84 route point. Latitude and longitude are stored in degrees.
pub struct Point {
  lat : Double
  lon : Double
} derive(Eq)

///|
/// Latitude/longitude bounding box in WGS84 degrees.
pub struct BBox {
  min_lat : Double
  min_lon : Double
  max_lat : Double
  max_lon : Double
}

///|
/// Slippy-map tile coordinate in the Web Mercator tiling scheme.
pub struct Tile {
  z : Int
  x : Int
  y : Int
} derive(Eq)

///|
/// Compact summary for one route and one tile zoom level.
pub struct RouteSummary {
  point_count : Int
  simplified_point_count : Int
  distance_m : Double
  bbox : BBox
  encoded_polyline : String
  tiles : Array[Tile]
  zoom : Int
}

///|
/// Per-segment route metrics, useful for route tables and diagnostics.
pub struct SegmentMetric {
  index : Int
  start : Point
  finish : Point
  distance_m : Double
  bearing_deg : Double
}

///|
/// Distribution summary for route segment lengths.
pub struct SegmentSpread {
  segment_count : Int
  total_distance_m : Double
  shortest_segment_m : Double
  longest_segment_m : Double
  average_segment_m : Double
  standard_deviation_m : Double
  coefficient_of_variation : Double
  longest_segment_index : Int
}

///|
/// Cumulative distance marker at a concrete point on a route.
pub struct RouteMarker {
  index : Int
  point : Point
  distance_m : Double
}

///|
/// Sample generated from a route at a requested distance.
pub struct RouteSample {
  point : Point
  distance_m : Double
  segment_index : Int
  fraction : Double
}

///|
/// Result of cutting a route between two distances.
pub struct RouteSlice {
  start_m : Double
  end_m : Double
  distance_m : Double
  points : Array[Point]
}

///|
/// Nearest-point projection result for snapping a location onto a route.
pub struct NearestPoint {
  point : Point
  distance_to_route_m : Double
  distance_along_route_m : Double
  segment_index : Int
  fraction : Double
}

///|
/// Aggregate route statistics for dashboards and validation records.
pub struct RouteStats {
  point_count : Int
  segment_count : Int
  distance_m : Double
  direct_distance_m : Double
  sinuosity : Double
  min_segment_m : Double
  max_segment_m : Double
  average_segment_m : Double
  bbox : BBox
  center : Point
}

///|
/// Geographic bounds of one Web Mercator tile.
pub struct TileBounds {
  tile : Tile
  west : Double
  south : Double
  east : Double
  north : Double
}

///|
/// Inclusive tile coordinate range for a bounding box at one zoom level.
pub struct TileRange {
  z : Int
  min_x : Int
  min_y : Int
  max_x : Int
  max_y : Int
}

///|
/// Public error type shared by route codecs, metrics, simplification, and tiles.
pub(all) enum RouteError {
  EmptyRoute
  InvalidCoordinate(index~ : Int, lat~ : Double, lon~ : Double)
  InvalidPrecision(precision~ : Int)
  InvalidZoom(zoom~ : Int)
  InvalidTolerance(tolerance_m~ : Double)
  InvalidDistance(distance_m~ : Double)
  InvalidFraction(fraction~ : Double)
  InvalidSegmentLength(length_m~ : Double)
  InvalidSampleCount(count~ : Int)
  InvalidTileCoordinate(tile~ : Tile)
  MalformedPolyline(pos~ : Int, reason~ : String)
} derive(Eq)

///|
pub fn Point::Point(lat : Double, lon : Double) -> Point {
  { lat, lon }
}

///|
pub fn BBox::BBox(
  min_lat : Double,
  min_lon : Double,
  max_lat : Double,
  max_lon : Double,
) -> BBox {
  { min_lat, min_lon, max_lat, max_lon }
}

///|
pub fn Tile::Tile(z : Int, x : Int, y : Int) -> Tile {
  { z, x, y }
}

///|
pub fn SegmentMetric::SegmentMetric(
  index : Int,
  start : Point,
  finish : Point,
  distance_m : Double,
  bearing_deg : Double,
) -> SegmentMetric {
  { index, start, finish, distance_m, bearing_deg }
}

///|
pub fn SegmentSpread::SegmentSpread(
  segment_count : Int,
  total_distance_m : Double,
  shortest_segment_m : Double,
  longest_segment_m : Double,
  average_segment_m : Double,
  standard_deviation_m : Double,
  coefficient_of_variation : Double,
  longest_segment_index : Int,
) -> SegmentSpread {
  {
    segment_count,
    total_distance_m,
    shortest_segment_m,
    longest_segment_m,
    average_segment_m,
    standard_deviation_m,
    coefficient_of_variation,
    longest_segment_index,
  }
}

///|
pub fn RouteMarker::RouteMarker(
  index : Int,
  point : Point,
  distance_m : Double,
) -> RouteMarker {
  { index, point, distance_m }
}

///|
pub fn RouteSample::RouteSample(
  point : Point,
  distance_m : Double,
  segment_index : Int,
  fraction : Double,
) -> RouteSample {
  { point, distance_m, segment_index, fraction }
}

///|
pub fn RouteSlice::RouteSlice(
  start_m : Double,
  end_m : Double,
  distance_m : Double,
  points : Array[Point],
) -> RouteSlice {
  { start_m, end_m, distance_m, points }
}

///|
pub fn NearestPoint::NearestPoint(
  point : Point,
  distance_to_route_m : Double,
  distance_along_route_m : Double,
  segment_index : Int,
  fraction : Double,
) -> NearestPoint {
  {
    point,
    distance_to_route_m,
    distance_along_route_m,
    segment_index,
    fraction,
  }
}

///|
pub fn RouteStats::RouteStats(
  point_count : Int,
  segment_count : Int,
  distance_m : Double,
  direct_distance_m : Double,
  sinuosity : Double,
  min_segment_m : Double,
  max_segment_m : Double,
  average_segment_m : Double,
  bbox : BBox,
  center : Point,
) -> RouteStats {
  {
    point_count,
    segment_count,
    distance_m,
    direct_distance_m,
    sinuosity,
    min_segment_m,
    max_segment_m,
    average_segment_m,
    bbox,
    center,
  }
}

///|
pub fn TileBounds::TileBounds(
  tile : Tile,
  west : Double,
  south : Double,
  east : Double,
  north : Double,
) -> TileBounds {
  { tile, west, south, east, north }
}

///|
pub fn TileRange::TileRange(
  z : Int,
  min_x : Int,
  min_y : Int,
  max_x : Int,
  max_y : Int,
) -> TileRange {
  { z, min_x, min_y, max_x, max_y }
}

///|
pub fn Point::is_valid(self : Point) -> Bool {
  is_valid_lat(self.lat) && is_valid_lon(self.lon)
}

///|
pub fn Point::to_string(self : Point) -> String {
  "(\{self.lat}, \{self.lon})"
}

///|
pub fn BBox::to_string(self : BBox) -> String {
  "\{self.min_lat},\{self.min_lon}..\{self.max_lat},\{self.max_lon}"
}

///|
pub fn Tile::key(self : Tile) -> String {
  "\{self.z}/\{self.x}/\{self.y}"
}

///|
pub fn Tile::to_string(self : Tile) -> String {
  self.key()
}

///|
pub fn SegmentMetric::to_string(self : SegmentMetric) -> String {
  "\{self.index}: \{round_meters(self.distance_m)}m @ \{round_meters(self.bearing_deg)}deg"
}

///|
pub fn SegmentSpread::to_string(self : SegmentSpread) -> String {
  "segments=\{self.segment_count}, avg=\{round_meters(self.average_segment_m)}m, stddev=\{round_meters(self.standard_deviation_m)}m, cv=\{self.coefficient_of_variation}"
}

///|
pub fn RouteMarker::to_string(self : RouteMarker) -> String {
  "\{self.index}@\{round_meters(self.distance_m)}m \{self.point.to_string()}"
}

///|
pub fn RouteSample::to_string(self : RouteSample) -> String {
  "\{round_meters(self.distance_m)}m on segment \{self.segment_index}:\{self.fraction} \{self.point.to_string()}"
}

///|
pub fn RouteSlice::to_string(self : RouteSlice) -> String {
  "\{round_meters(self.start_m)}..\{round_meters(self.end_m)}m, points=\{self.points.length()}"
}

///|
pub fn NearestPoint::to_string(self : NearestPoint) -> String {
  "segment=\{self.segment_index}, along=\{round_meters(self.distance_along_route_m)}m, off=\{round_meters(self.distance_to_route_m)}m"
}

///|
pub fn TileRange::to_string(self : TileRange) -> String {
  "\{self.z}/\{self.min_x},\{self.min_y}..\{self.max_x},\{self.max_y}"
}

///|
pub fn TileBounds::to_bbox(self : TileBounds) -> BBox {
  BBox(self.south, self.west, self.north, self.east)
}

///|
pub fn RouteError::message(self : RouteError) -> String {
  match self {
    EmptyRoute => "route is empty"
    InvalidCoordinate(index~, lat~, lon~) =>
      "invalid coordinate at index \{index}: lat=\{lat}, lon=\{lon}"
    InvalidPrecision(precision~) =>
      "polyline precision must be between 0 and 7, got \{precision}"
    InvalidZoom(zoom~) => "tile zoom must be between 0 and 22, got \{zoom}"
    InvalidTolerance(tolerance_m~) =>
      "simplify tolerance must be finite and non-negative, got \{tolerance_m}"
    InvalidDistance(distance_m~) =>
      "distance must be finite and non-negative, got \{distance_m}"
    InvalidFraction(fraction~) =>
      "fraction must be finite and between 0 and 1, got \{fraction}"
    InvalidSegmentLength(length_m~) =>
      "segment length must be finite and positive, got \{length_m}"
    InvalidSampleCount(count~) => "sample count must be positive, got \{count}"
    InvalidTileCoordinate(tile~) =>
      "tile coordinate is outside zoom range: \{tile.to_string()}"
    MalformedPolyline(pos~, reason~) =>
      "malformed encoded polyline at offset \{pos}: \{reason}"
  }
}

///|
pub fn round_meters(value : Double) -> Int {
  value.round().to_int()
}

///|
pub fn meters_to_kilometers(value : Double) -> Double {
  value / 1000.0
}

///|
pub fn kilometers_to_meters(value : Double) -> Double {
  value * 1000.0
}

///|
pub fn meters_to_miles(value : Double) -> Double {
  value / 1609.344
}

///|
pub fn miles_to_meters(value : Double) -> Double {
  value * 1609.344
}

///|
pub fn degrees_to_radians(value : Double) -> Double {
  deg_to_rad(value)
}

///|
pub fn radians_to_degrees(value : Double) -> Double {
  value * 180.0 / @math.PI
}

///|
pub fn normalize_bearing_degrees(value : Double) -> Double {
  let mut out = value
  while out < 0.0 {
    out = out + 360.0
  }
  while out >= 360.0 {
    out = out - 360.0
  }
  out
}

///|
pub fn bearing_delta_degrees(from : Double, to : Double) -> Double {
  let raw = normalize_bearing_degrees(to) - normalize_bearing_degrees(from)
  if raw > 180.0 {
    raw - 360.0
  } else if raw < -180.0 {
    raw + 360.0
  } else {
    raw
  }
}

///|
pub fn compass_direction(bearing_deg : Double) -> String {
  let bearing = normalize_bearing_degrees(bearing_deg)
  if bearing < 22.5 || bearing >= 337.5 {
    "N"
  } else if bearing < 67.5 {
    "NE"
  } else if bearing < 112.5 {
    "E"
  } else if bearing < 157.5 {
    "SE"
  } else if bearing < 202.5 {
    "S"
  } else if bearing < 247.5 {
    "SW"
  } else if bearing < 292.5 {
    "W"
  } else {
    "NW"
  }
}

///|
fn is_valid_lat(value : Double) -> Bool {
  !value.is_nan() && !value.is_inf() && value >= -90.0 && value <= 90.0
}

///|
fn is_valid_lon(value : Double) -> Bool {
  !value.is_nan() && !value.is_inf() && value >= -180.0 && value <= 180.0
}

///|
fn validate_point(point : Point, index : Int) -> Result[Unit, RouteError] {
  if point.is_valid() {
    Ok(())
  } else {
    Err(InvalidCoordinate(index~, lat=point.lat, lon=point.lon))
  }
}

///|
fn validate_points(points : ArrayView[Point]) -> Result[Unit, RouteError] {
  for i, point in points {
    match validate_point(point, i) {
      Ok(_) => ()
      Err(err) => return Err(err)
    }
  }
  Ok(())
}

///|
fn require_points(points : ArrayView[Point]) -> Result[Unit, RouteError] {
  if points.length() == 0 {
    Err(EmptyRoute)
  } else {
    validate_points(points)
  }
}

///|
fn validate_precision(precision : Int) -> Result[Unit, RouteError] {
  if precision >= 0 && precision <= 7 {
    Ok(())
  } else {
    Err(InvalidPrecision(precision~))
  }
}

///|
fn validate_zoom(zoom : Int) -> Result[Unit, RouteError] {
  if zoom >= 0 && zoom <= 22 {
    Ok(())
  } else {
    Err(InvalidZoom(zoom~))
  }
}

///|
fn validate_distance(distance_m : Double) -> Result[Unit, RouteError] {
  if is_finite_non_negative(distance_m) {
    Ok(())
  } else {
    Err(InvalidDistance(distance_m~))
  }
}

///|
fn validate_positive_distance(distance_m : Double) -> Result[Unit, RouteError] {
  if !distance_m.is_nan() && !distance_m.is_inf() && distance_m > 0.0 {
    Ok(())
  } else {
    Err(InvalidSegmentLength(length_m=distance_m))
  }
}

///|
fn validate_fraction(fraction : Double) -> Result[Unit, RouteError] {
  if !fraction.is_nan() &&
    !fraction.is_inf() &&
    fraction >= 0.0 &&
    fraction <= 1.0 {
    Ok(())
  } else {
    Err(InvalidFraction(fraction~))
  }
}

///|
fn validate_sample_count(count : Int) -> Result[Unit, RouteError] {
  if count > 0 {
    Ok(())
  } else {
    Err(InvalidSampleCount(count~))
  }
}

///|
fn pow10_int(exp : Int) -> Int {
  let mut out = 1
  for _ in 0.. Double {
  value * @math.PI / 180.0
}

///|
fn rad_to_deg(value : Double) -> Double {
  value * 180.0 / @math.PI
}

///|
fn is_finite_non_negative(value : Double) -> Bool {
  !value.is_nan() && !value.is_inf() && value >= 0.0
}

///|
fn abs_double(value : Double) -> Double {
  if value < 0.0 {
    -value
  } else {
    value
  }
}

///|
fn min_double(left : Double, right : Double) -> Double {
  if left < right {
    left
  } else {
    right
  }
}

///|
fn max_double(left : Double, right : Double) -> Double {
  if left > right {
    left
  } else {
    right
  }
}

///|
fn clamp_double(value : Double, min : Double, max : Double) -> Double {
  if value < min {
    min
  } else if value > max {
    max
  } else {
    value
  }
}

///|
fn lerp_double(start : Double, finish : Double, fraction : Double) -> Double {
  start + (finish - start) * fraction
}

///|
fn normalize_longitude_delta(delta : Double) -> Double {
  let mut out = delta
  while out > 180.0 {
    out = out - 360.0
  }
  while out < -180.0 {
    out = out + 360.0
  }
  out
}

///|
fn interpolate_point_unchecked(
  a : Point,
  b : Point,
  fraction : Double,
) -> Point {
  let lat = lerp_double(a.lat, b.lat, fraction)
  let lon_delta = normalize_longitude_delta(b.lon - a.lon)
  let mut lon = a.lon + lon_delta * fraction
  while lon > 180.0 {
    lon = lon - 360.0
  }
  while lon < -180.0 {
    lon = lon + 360.0
  }
  Point(lat, lon)
}

///|
fn points_equal(a : Point, b : Point) -> Bool {
  a.lat == b.lat && a.lon == b.lon
}

///|
fn tile_contains(tiles : Array[Tile], tile : Tile) -> Bool {
  for item in tiles {
    if item == tile {
      return true
    }
  }
  false
}

///|
fn push_unique_tile(tiles : Array[Tile], tile : Tile) -> Unit {
  if !tile_contains(tiles, tile) {
    tiles.push(tile)
  }
}

///|
fn max_int(left : Int, right : Int) -> Int {
  if left > right {
    left
  } else {
    right
  }
}

///|
fn min_int(left : Int, right : Int) -> Int {
  if left < right {
    left
  } else {
    right
  }
}