///|
pub fn to_geojson_point(point : Point) -> Result[String, RouteError] {
  match validate_point(point, 0) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let out = StringBuilder(size_hint=64)
  out.write_string("{\"type\":\"Point\",\"coordinates\":")
  write_point_coordinate(out, point)
  out.write_char('}')
  Ok(out.to_string())
}

///|
pub fn to_geojson_multipoint(
  points : ArrayView[Point],
) -> Result[String, RouteError] {
  match require_points(points) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let out = StringBuilder(size_hint=points.length() * 32)
  out.write_string("{\"type\":\"MultiPoint\",\"coordinates\":")
  write_coordinates(out, points)
  out.write_char('}')
  Ok(out.to_string())
}

///|
pub fn to_geojson_bbox_polygon(bbox : BBox) -> Result[String, RouteError] {
  let polygon = bbox_polygon_points(bbox)
  let out = StringBuilder(size_hint=160)
  out.write_string("{\"type\":\"Polygon\",\"coordinates\":[")
  write_coordinates(out, polygon)
  out.write_string("]}")
  Ok(out.to_string())
}

///|
pub fn to_geojson_bbox_feature(
  bbox : BBox,
  name? : String = "bbox",
) -> Result[String, RouteError] {
  let polygon = bbox_polygon_points(bbox)
  let out = StringBuilder(size_hint=180 + name.length())
  out.write_string("{\"type\":\"Feature\",\"properties\":{\"name\":\"")
  out.write_string(escape_json_string(name))
  out.write_string("\"},\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[")
  write_coordinates(out, polygon)
  out.write_string("]}}")
  Ok(out.to_string())
}

///|
pub fn routes_to_geojson_feature_collection(
  routes : ArrayView[Array[Point]],
  name_prefix? : String = "route",
) -> Result[String, RouteError] {
  if routes.length() == 0 {
    return Err(EmptyRoute)
  }
  let out = StringBuilder(size_hint=routes.length() * 128)
  out.write_string("{\"type\":\"FeatureCollection\",\"features\":[")
  for i, route in routes {
    if i > 0 {
      out.write_char(',')
    }
    match require_points(route) {
      Ok(_) => ()
      Err(err) => return Err(err)
    }
    out.write_string("{\"type\":\"Feature\",\"properties\":{\"name\":\"")
    out.write_string(escape_json_string("\{name_prefix}-\{i + 1}"))
    out.write_string("\",\"point_count\":")
    out.write_string(route.length().to_string())
    out.write_string("},\"geometry\":{\"type\":\"LineString\",\"coordinates\":")
    write_coordinates(out, route)
    out.write_string("}}")
  }
  out.write_string("]}")
  Ok(out.to_string())
}

///|
pub fn samples_to_geojson_feature_collection(
  samples : ArrayView[RouteSample],
  name? : String = "samples",
) -> Result[String, RouteError] {
  if samples.length() == 0 {
    return Err(EmptyRoute)
  }
  let out = StringBuilder(size_hint=samples.length() * 120 + name.length())
  out.write_string("{\"type\":\"FeatureCollection\",\"name\":\"")
  out.write_string(escape_json_string(name))
  out.write_string("\",\"features\":[")
  for i, sample in samples {
    if i > 0 {
      out.write_char(',')
    }
    out.write_string("{\"type\":\"Feature\",\"properties\":{\"distance_m\":")
    out.write_string(round_meters(sample.distance_m).to_string())
    out.write_string(",\"segment_index\":")
    out.write_string(sample.segment_index.to_string())
    out.write_string(",\"fraction\":")
    out.write_string(sample.fraction.to_string())
    out.write_string("},\"geometry\":{\"type\":\"Point\",\"coordinates\":")
    write_point_coordinate(out, sample.point)
    out.write_string("}}")
  }
  out.write_string("]}")
  Ok(out.to_string())
}

///|
pub fn nearest_points_to_geojson_feature_collection(
  points : ArrayView[NearestPoint],
  name? : String = "nearest-points",
) -> Result[String, RouteError] {
  if points.length() == 0 {
    return Err(EmptyRoute)
  }
  let out = StringBuilder(size_hint=points.length() * 140 + name.length())
  out.write_string("{\"type\":\"FeatureCollection\",\"name\":\"")
  out.write_string(escape_json_string(name))
  out.write_string("\",\"features\":[")
  for i, item in points {
    if i > 0 {
      out.write_char(',')
    }
    out.write_string(
      "{\"type\":\"Feature\",\"properties\":{\"distance_to_route_m\":",
    )
    out.write_string(round_meters(item.distance_to_route_m).to_string())
    out.write_string(",\"distance_along_route_m\":")
    out.write_string(round_meters(item.distance_along_route_m).to_string())
    out.write_string(",\"segment_index\":")
    out.write_string(item.segment_index.to_string())
    out.write_string("},\"geometry\":{\"type\":\"Point\",\"coordinates\":")
    write_point_coordinate(out, item.point)
    out.write_string("}}")
  }
  out.write_string("]}")
  Ok(out.to_string())
}

///|
pub fn tiles_to_geojson_feature_collection(
  tiles : ArrayView[Tile],
  name? : String = "tiles",
) -> Result[String, RouteError] {
  if tiles.length() == 0 {
    return Err(EmptyRoute)
  }
  let out = StringBuilder(size_hint=tiles.length() * 200 + name.length())
  out.write_string("{\"type\":\"FeatureCollection\",\"name\":\"")
  out.write_string(escape_json_string(name))
  out.write_string("\",\"features\":[")
  for i, tile in tiles {
    if i > 0 {
      out.write_char(',')
    }
    let bounds = match tile_bounds(tile) {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    out.write_string("{\"type\":\"Feature\",\"properties\":{\"tile\":\"")
    out.write_string(tile.key())
    out.write_string("\"},\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[")
    write_coordinates(out, bbox_polygon_points(bounds.to_bbox()))
    out.write_string("]}}")
  }
  out.write_string("]}")
  Ok(out.to_string())
}

///|
pub fn route_report_geojson(
  route : ArrayView[Point],
  zoom? : Int = 12,
  name? : String = "route",
) -> Result[String, RouteError] {
  match require_points(route) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let summary = match summarize_route(route, zoom~) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let out = StringBuilder(size_hint=route.length() * 64 + 260 + name.length())
  out.write_string("{\"type\":\"Feature\",\"properties\":{\"name\":\"")
  out.write_string(escape_json_string(name))
  out.write_string("\",\"point_count\":")
  out.write_string(summary.point_count.to_string())
  out.write_string(",\"distance_m\":")
  out.write_string(round_meters(summary.distance_m).to_string())
  out.write_string(",\"simplified_point_count\":")
  out.write_string(summary.simplified_point_count.to_string())
  out.write_string(",\"encoded_polyline\":\"")
  out.write_string(escape_json_string(summary.encoded_polyline))
  out.write_string("\",\"tile_count\":")
  out.write_string(summary.tiles.length().to_string())
  out.write_string("},\"geometry\":{\"type\":\"LineString\",\"coordinates\":")
  write_coordinates(out, route)
  out.write_string("}}")
  Ok(out.to_string())
}

///|
pub fn route_debug_bundle_geojson(
  route : ArrayView[Point],
  zoom? : Int = 12,
) -> Result[String, RouteError] {
  match require_points(route) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let samples = match sample_route_by_count(route, 5) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let tiles = match tile_cover(route, zoom) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let bbox = match bounding_box(route) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let out = StringBuilder(size_hint=2048)
  out.write_string("{\"type\":\"FeatureCollection\",\"features\":[")
  let route_feature = match route_report_geojson(route, zoom~, name="route") {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  out.write_string(route_feature)
  out.write_char(',')
  let bbox_feature = match to_geojson_bbox_feature(bbox, name="bbox") {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  out.write_string(bbox_feature)
  for sample in samples {
    out.write_char(',')
    out.write_string(
      "{\"type\":\"Feature\",\"properties\":{\"kind\":\"sample\",\"distance_m\":",
    )
    out.write_string(round_meters(sample.distance_m).to_string())
    out.write_string("},\"geometry\":{\"type\":\"Point\",\"coordinates\":")
    write_point_coordinate(out, sample.point)
    out.write_string("}}")
  }
  for tile in tiles {
    let bounds = match tile_bounds(tile) {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    out.write_char(',')
    out.write_string(
      "{\"type\":\"Feature\",\"properties\":{\"kind\":\"tile\",\"tile\":\"",
    )
    out.write_string(tile.key())
    out.write_string("\"},\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[")
    write_coordinates(out, bbox_polygon_points(bounds.to_bbox()))
    out.write_string("]}}")
  }
  out.write_string("]}")
  Ok(out.to_string())
}

///|
fn bbox_polygon_points(bbox : BBox) -> Array[Point] {
  [
    Point(bbox.min_lat, bbox.min_lon),
    Point(bbox.min_lat, bbox.max_lon),
    Point(bbox.max_lat, bbox.max_lon),
    Point(bbox.max_lat, bbox.min_lon),
    Point(bbox.min_lat, bbox.min_lon),
  ]
}

///|
fn write_point_coordinate(out : StringBuilder, point : Point) -> Unit {
  out.write_char('[')
  out.write_string(point.lon.to_string())
  out.write_char(',')
  out.write_string(point.lat.to_string())
  out.write_char(']')
}