///|
priv struct GeoJsonParser {
  limits : GeoLimits
  mut coordinate_count : Int32
  mut geometry_count : Int32
}

///|
fn GeoJsonParser::new(limits : GeoLimits) -> GeoJsonParser {
  { limits, coordinate_count: 0, geometry_count: 0, }
}

///|
fn GeoJsonParser::begin_geometry(
  self : GeoJsonParser,
  depth : Int32,
) -> Result[Unit, GeoError] {
  match self.limits.check_depth(depth) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  self.geometry_count += 1
  self.limits.check_geometries(self.geometry_count)
}

///|
fn GeoJsonParser::record_coordinate(
  self : GeoJsonParser,
) -> Result[Unit, GeoError] {
  self.coordinate_count += 1
  self.limits.check_coordinates(self.coordinate_count)
}

///|
fn geojson_error(message : String) -> GeoError {
  InvalidGeoJson(message, 0)
}

///|
fn geojson_field(
  object : Map[String, Json],
  key : String,
  context : String,
) -> Result[Json, GeoError] {
  match object.get(key) {
    Some(value) => Ok(value)
    None => Err(geojson_error(context + " is missing '" + key + "'"))
  }
}

///|
fn geojson_object(
  value : Json,
  context : String,
) -> Result[Map[String, Json], GeoError] {
  match value {
    Object(object) => Ok(object)
    _ => Err(geojson_error(context + " must be an object"))
  }
}

///|
fn geojson_array(
  value : Json,
  context : String,
) -> Result[Array[Json], GeoError] {
  match value {
    Array(values) => Ok(values)
    _ => Err(geojson_error(context + " must be an array"))
  }
}

///|
fn geojson_string(value : Json, context : String) -> Result[String, GeoError] {
  match value {
    String(text) => Ok(text)
    _ => Err(geojson_error(context + " must be a string"))
  }
}

///|
fn geojson_number(value : Json, context : String) -> Result[Double, GeoError] {
  match value {
    Number(number, ..) =>
      if number.is_nan() || number.is_inf() {
        Err(geojson_error(context + " must be finite"))
      } else {
        Ok(number)
      }
    _ => Err(geojson_error(context + " must be a number"))
  }
}

///|
fn geojson_type(object : Map[String, Json]) -> Result[String, GeoError] {
  let value = match object.get("type") {
    Some(value) => value
    None => return Err(geojson_error("GeoJSON object is missing 'type'"))
  }
  geojson_string(value, "GeoJSON 'type'")
}

///|
fn validate_geojson_dimensions(geometry : Geometry) -> Result[Unit, GeoError] {
  match geometry_dimension_summary(geometry) {
    Mixed => Err(geojson_error("inconsistent coordinate dimensions"))
    _ => Ok(())
  }
}

///|
fn GeoJsonParser::parse_position(
  self : GeoJsonParser,
  value : Json,
  context : String,
) -> Result[Coordinate, GeoError] {
  let values = match geojson_array(value, context) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let coordinate = if values.length() == 2 {
    let x = match geojson_number(values[0], context + "[0]") {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    let y = match geojson_number(values[1], context + "[1]") {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    Coordinate::xy(x, y)
  } else if values.length() == 3 {
    let x = match geojson_number(values[0], context + "[0]") {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    let y = match geojson_number(values[1], context + "[1]") {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    let z = match geojson_number(values[2], context + "[2]") {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    Coordinate::xyz(x, y, z)
  } else {
    return Err(geojson_error(context + " must contain two or three ordinates"))
  }
  match self.record_coordinate() {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  Ok(coordinate)
}

///|
fn GeoJsonParser::parse_positions(
  self : GeoJsonParser,
  value : Json,
  context : String,
) -> Result[Array[Coordinate], GeoError] {
  let values = match geojson_array(value, context) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let coordinates : Array[Coordinate] = []
  for index, position in values {
    let coordinate = match
      self.parse_position(position, context + "[" + index.to_string() + "]") {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    coordinates.push(coordinate)
  }
  Ok(coordinates)
}

///|
fn GeoJsonParser::parse_rings(
  self : GeoJsonParser,
  value : Json,
  context : String,
) -> Result[Array[Array[Coordinate]], GeoError] {
  let values = match geojson_array(value, context) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let rings : Array[Array[Coordinate]] = []
  for index, ring_value in values {
    let ring = match
      self.parse_positions(ring_value, context + "[" + index.to_string() + "]") {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    if ring.length() == 0 {
      return Err(geojson_error("polygon ring must not be empty"))
    }
    rings.push(ring)
  }
  Ok(rings)
}

///|
fn GeoJsonParser::parse_lines(
  self : GeoJsonParser,
  value : Json,
  context : String,
) -> Result[Array[Array[Coordinate]], GeoError] {
  let values = match geojson_array(value, context) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let lines : Array[Array[Coordinate]] = []
  for index, line_value in values {
    let line = match
      self.parse_positions(line_value, context + "[" + index.to_string() + "]") {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    if line.length() == 0 {
      return Err(geojson_error("multiline component must not be empty"))
    }
    lines.push(line)
  }
  Ok(lines)
}

///|
fn GeoJsonParser::parse_polygons(
  self : GeoJsonParser,
  value : Json,
  context : String,
) -> Result[Array[Array[Array[Coordinate]]], GeoError] {
  let values = match geojson_array(value, context) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let polygons : Array[Array[Array[Coordinate]]] = []
  for index, polygon_value in values {
    let polygon = match
      self.parse_rings(polygon_value, context + "[" + index.to_string() + "]") {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    if polygon.length() == 0 {
      return Err(geojson_error("multipolygon component must not be empty"))
    }
    polygons.push(polygon)
  }
  Ok(polygons)
}

///|
fn GeoJsonParser::parse_point(
  self : GeoJsonParser,
  coordinates : Json,
) -> Result[Geometry, GeoError] {
  let values = match geojson_array(coordinates, "Point.coordinates") {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  if values.length() == 0 {
    return Ok(EmptyPoint)
  }
  let coordinate = match self.parse_position(coordinates, "Point.coordinates") {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  Ok(Point(coordinate))
}

///|
fn GeoJsonParser::parse_geometry_object(
  self : GeoJsonParser,
  value : Json,
  depth : Int32,
) -> Result[Geometry, GeoError] {
  let object = match geojson_object(value, "geometry") {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let kind = match geojson_type(object) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  match self.begin_geometry(depth) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let geometry = if kind == "Point" {
    let coordinates = match geojson_field(object, "coordinates", "Point") {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    self.parse_point(coordinates)
  } else if kind == "LineString" {
    let coordinates = match geojson_field(object, "coordinates", "LineString") {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    match self.parse_positions(coordinates, "LineString.coordinates") {
      Ok(value) => Ok(LineString(value))
      Err(err) => Err(err)
    }
  } else if kind == "Polygon" {
    let coordinates = match geojson_field(object, "coordinates", "Polygon") {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    match self.parse_rings(coordinates, "Polygon.coordinates") {
      Ok(value) => Ok(Polygon(value))
      Err(err) => Err(err)
    }
  } else if kind == "MultiPoint" {
    let coordinates = match geojson_field(object, "coordinates", "MultiPoint") {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    match self.parse_positions(coordinates, "MultiPoint.coordinates") {
      Ok(value) => Ok(MultiPoint(value))
      Err(err) => Err(err)
    }
  } else if kind == "MultiLineString" {
    let coordinates = match
      geojson_field(object, "coordinates", "MultiLineString") {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    match self.parse_lines(coordinates, "MultiLineString.coordinates") {
      Ok(value) => Ok(MultiLineString(value))
      Err(err) => Err(err)
    }
  } else if kind == "MultiPolygon" {
    let coordinates = match
      geojson_field(object, "coordinates", "MultiPolygon") {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    match self.parse_polygons(coordinates, "MultiPolygon.coordinates") {
      Ok(value) => Ok(MultiPolygon(value))
      Err(err) => Err(err)
    }
  } else if kind == "GeometryCollection" {
    let geometries = match
      geojson_field(object, "geometries", "GeometryCollection") {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    let values = match
      geojson_array(geometries, "GeometryCollection.geometries") {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    let children : Array[Geometry] = []
    for child in values {
      let geometry = match self.parse_geometry_object(child, depth + 1) {
        Ok(value) => value
        Err(err) => return Err(err)
      }
      children.push(geometry)
    }
    Ok(GeometryCollection(children))
  } else {
    Err(geojson_error("unsupported GeoJSON type '" + kind + "'"))
  }
  let geometry = match geometry {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  if kind == "GeometryCollection" {
    Ok(geometry)
  } else {
    match validate_geojson_dimensions(geometry) {
      Ok(_) => Ok(geometry)
      Err(err) => Err(err)
    }
  }
}

///|
fn GeoJsonParser::parse_feature_geometry(
  self : GeoJsonParser,
  feature : Json,
  depth : Int32,
) -> Result[Geometry?, GeoError] {
  let object = match geojson_object(feature, "Feature") {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let kind = match geojson_type(object) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  if kind != "Feature" {
    return Err(geojson_error("expected a GeoJSON Feature"))
  }
  let geometry = match geojson_field(object, "geometry", "Feature") {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  match geometry {
    Null => Ok(None)
    _ => {
      let parsed = match self.parse_geometry_object(geometry, depth) {
        Ok(value) => value
        Err(err) => return Err(err)
      }
      Ok(Some(parsed))
    }
  }
}

///|
fn GeoJsonParser::parse_feature_collection(
  self : GeoJsonParser,
  value : Json,
  depth : Int32,
) -> Result[Geometry, GeoError] {
  let object = match geojson_object(value, "FeatureCollection") {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  match self.begin_geometry(depth) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let features = match geojson_field(object, "features", "FeatureCollection") {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let values = match geojson_array(features, "FeatureCollection.features") {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let geometries : Array[Geometry] = []
  for feature in values {
    let geometry = match self.parse_feature_geometry(feature, depth + 1) {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    match geometry {
      Some(value) => geometries.push(value)
      None => ()
    }
  }
  Ok(GeometryCollection(geometries))
}

///|
pub fn parse_geojson(
  source : StringView,
  limits : GeoLimits,
) -> Result[Geometry, GeoError] {
  match limits.check_text(source.length()) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let value = @json.parse(source) catch {
    err => return Err(geojson_error("invalid JSON: " + err.to_string()))
  }
  let object = match geojson_object(value, "GeoJSON root") {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let kind = match geojson_type(object) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  let parser = GeoJsonParser::new(limits)
  if kind == "Feature" {
    match parser.parse_feature_geometry(value, 1) {
      Ok(Some(geometry)) => Ok(geometry)
      Ok(None) => Ok(GeometryCollection([]))
      Err(err) => Err(err)
    }
  } else if kind == "FeatureCollection" {
    parser.parse_feature_collection(value, 1)
  } else {
    parser.parse_geometry_object(value, 1)
  }
}