///|
fn validate_finite_coordinate(
  coordinate : Coordinate,
  path : String,
) -> Result[Unit, GeoError] {
  if coordinate.x.is_nan() || coordinate.x.is_inf() {
    Err(InvalidCoordinate(path + " has non-finite x ordinate"))
  } else if coordinate.y.is_nan() || coordinate.y.is_inf() {
    Err(InvalidCoordinate(path + " has non-finite y ordinate"))
  } else if (match coordinate.z {
      Some(value) => value.is_nan() || value.is_inf()
      None => false
    }) {
    Err(InvalidCoordinate(path + " has non-finite z ordinate"))
  } else if (match coordinate.m {
      Some(value) => value.is_nan() || value.is_inf()
      None => false
    }) {
    Err(InvalidCoordinate(path + " has non-finite m ordinate"))
  } else {
    Ok(())
  }
}

///|
fn validate_dimension_summary(
  summary : DimensionSummary,
  path : String,
) -> Result[Unit, GeoError] {
  match summary {
    Mixed =>
      Err(InvalidGeometry(path + " has inconsistent coordinate dimensions"))
    _ => Ok(())
  }
}

///|
fn validate_coordinate_array(
  coordinates : Array[Coordinate],
  path : String,
) -> Result[Unit, GeoError] {
  let mut index = 0
  for coordinate in coordinates {
    match
      validate_finite_coordinate(
        coordinate,
        path + "[" + index.to_string() + "]",
      ) {
      Ok(_) => ()
      Err(err) => return Err(err)
    }
    index += 1
  }
  Ok(())
}

///|
fn has_two_distinct_coordinates(coordinates : Array[Coordinate]) -> Bool {
  for left in 0.. Double {
  let mut area = 0.0
  let end = ring.length() - 1
  for index in 0.. Result[Unit, GeoError] {
  if ring.length() < 4 {
    return Err(
      InvalidBoundary(
        path + " ring must contain at least four coordinates",
        ring_index,
      ),
    )
  }
  if ring[0] != ring[ring.length() - 1] {
    return Err(InvalidBoundary(path + " ring must be closed", ring_index))
  }
  match validate_coordinate_array(ring, path) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  let area = ring_signed_area2(ring)
  if area == 0.0 || area.is_nan() || area.is_inf() {
    return Err(InvalidBoundary(path + " ring has zero area", ring_index))
  }
  Ok(())
}

///|
fn validate_line_string(
  coordinates : Array[Coordinate],
  path : String,
) -> Result[Unit, GeoError] {
  match
    validate_dimension_summary(coordinate_dimension_summary(coordinates), path) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  match validate_coordinate_array(coordinates, path) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  if coordinates.length() == 0 {
    return Ok(())
  }
  if coordinates.length() < 2 {
    return Err(InvalidGeometry(path + " must contain at least two coordinates"))
  }
  if !has_two_distinct_coordinates(coordinates) {
    return Err(InvalidGeometry(path + " must contain distinct coordinates"))
  }
  Ok(())
}

///|
fn validate_polygon(
  rings : Array[Array[Coordinate]],
  path : String,
) -> Result[Unit, GeoError] {
  match validate_dimension_summary(ring_dimension_summary(rings), path) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  if rings.length() == 0 {
    return Ok(())
  }
  for index, ring in rings {
    match validate_ring(ring, index, path) {
      Ok(_) => ()
      Err(err) => return Err(err)
    }
  }
  Ok(())
}

///|
fn validate_geometry_at(
  geometry : Geometry,
  path : String,
) -> Result[Unit, GeoError] {
  match geometry {
    Point(coordinate) => validate_finite_coordinate(coordinate, path)
    EmptyPoint => Ok(())
    LineString(coordinates) => validate_line_string(coordinates, path)
    Polygon(rings) => validate_polygon(rings, path)
    MultiPoint(coordinates) => {
      match
        validate_dimension_summary(
          coordinate_dimension_summary(coordinates),
          path,
        ) {
        Ok(_) => ()
        Err(err) => return Err(err)
      }
      validate_coordinate_array(coordinates, path)
    }
    MultiLineString(lines) => {
      match validate_dimension_summary(ring_dimension_summary(lines), path) {
        Ok(_) => ()
        Err(err) => return Err(err)
      }
      for index, line in lines {
        let line_path = path + ".lines[" + index.to_string() + "]"
        if line.length() == 0 {
          return Err(
            InvalidGeometry(
              line_path + " must contain at least two coordinates",
            ),
          )
        }
        match validate_line_string(line, line_path) {
          Ok(_) => ()
          Err(err) => return Err(err)
        }
      }
      Ok(())
    }
    MultiPolygon(polygons) => {
      match
        validate_dimension_summary(polygon_dimension_summary(polygons), path) {
        Ok(_) => ()
        Err(err) => return Err(err)
      }
      for index, polygon in polygons {
        let polygon_path = path + ".polygons[" + index.to_string() + "]"
        if polygon.length() == 0 {
          return Err(InvalidGeometry(polygon_path + " must not be empty"))
        }
        match validate_polygon(polygon, polygon_path) {
          Ok(_) => ()
          Err(err) => return Err(err)
        }
      }
      Ok(())
    }
    GeometryCollection(geometries) => {
      for index, child in geometries {
        let child_path = path + ".geometries[" + index.to_string() + "]"
        match validate_geometry_at(child, child_path) {
          Ok(_) => ()
          Err(err) => return Err(err)
        }
      }
      Ok(())
    }
  }
}

///|
/// Validates the structural invariants of a geometry.
pub fn validate_geometry(geometry : Geometry) -> Result[Unit, GeoError] {
  validate_geometry_at(geometry, "geometry")
}

///|
fn dedupe_consecutive(coordinates : Array[Coordinate]) -> Array[Coordinate] {
  let result : Array[Coordinate] = []
  for coordinate in coordinates {
    if result.length() == 0 || result[result.length() - 1] != coordinate {
      result.push(coordinate)
    }
  }
  result
}

///|
fn normalize_line(
  coordinates : Array[Coordinate],
  path : String,
) -> Result[Array[Coordinate], GeoError] {
  let normalized = dedupe_consecutive(coordinates)
  match
    validate_dimension_summary(coordinate_dimension_summary(normalized), path) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  match validate_coordinate_array(normalized, path) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  if normalized.length() > 0 && normalized.length() < 2 {
    return Err(
      InvalidGeometry(path + " must contain at least two distinct coordinates"),
    )
  }
  Ok(normalized)
}

///|
fn normalize_ring(
  ring : Array[Coordinate],
  ring_index : Int32,
  path : String,
) -> Result[Array[Coordinate], GeoError] {
  let normalized = dedupe_consecutive(ring)
  if normalized.length() == 0 {
    return Err(InvalidBoundary(path + " ring must not be empty", ring_index))
  }
  let closed = if normalized[0] == normalized[normalized.length() - 1] {
    normalized
  } else {
    let result = normalized.copy()
    result.push(result[0])
    result
  }
  match validate_ring(closed, ring_index, path) {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  Ok(closed)
}

///|
fn normalize_polygon(
  rings : Array[Array[Coordinate]],
  path : String,
) -> Result[Array[Array[Coordinate]], GeoError] {
  if rings.length() == 0 {
    return Ok([])
  }
  let normalized : Array[Array[Coordinate]] = []
  for index, ring in rings {
    let closed = match normalize_ring(ring, index, path) {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    let area = ring_signed_area2(closed)
    let oriented = if (index == 0 && area < 0.0) || (index > 0 && area > 0.0) {
      closed.rev()
    } else {
      closed
    }
    normalized.push(oriented)
  }
  Ok(normalized)
}

///|
fn normalize_geometry_at(
  geometry : Geometry,
  path : String,
) -> Result[Geometry, GeoError] {
  match geometry {
    Point(coordinate) =>
      match validate_finite_coordinate(coordinate, path) {
        Ok(_) => Ok(Point(coordinate))
        Err(err) => Err(err)
      }
    EmptyPoint => Ok(EmptyPoint)
    LineString(coordinates) => {
      let normalized = match normalize_line(coordinates, path) {
        Ok(value) => value
        Err(err) => return Err(err)
      }
      Ok(LineString(normalized))
    }
    Polygon(rings) => {
      let normalized = match normalize_polygon(rings, path) {
        Ok(value) => value
        Err(err) => return Err(err)
      }
      Ok(Polygon(normalized))
    }
    MultiPoint(coordinates) => {
      match
        validate_dimension_summary(
          coordinate_dimension_summary(coordinates),
          path,
        ) {
        Ok(_) => ()
        Err(err) => return Err(err)
      }
      match validate_coordinate_array(coordinates, path) {
        Ok(_) => ()
        Err(err) => return Err(err)
      }
      Ok(MultiPoint(coordinates.copy()))
    }
    MultiLineString(lines) => {
      let normalized : Array[Array[Coordinate]] = []
      for index, line in lines {
        let line_path = path + ".lines[" + index.to_string() + "]"
        let value = match normalize_line(line, line_path) {
          Ok(value) => value
          Err(err) => return Err(err)
        }
        if value.length() == 0 {
          return Err(InvalidGeometry(line_path + " must not be empty"))
        }
        normalized.push(value)
      }
      Ok(MultiLineString(normalized))
    }
    MultiPolygon(polygons) => {
      let normalized : Array[Array[Array[Coordinate]]] = []
      for index, polygon in polygons {
        let polygon_path = path + ".polygons[" + index.to_string() + "]"
        let value = match normalize_polygon(polygon, polygon_path) {
          Ok(value) => value
          Err(err) => return Err(err)
        }
        if value.length() == 0 {
          return Err(InvalidGeometry(polygon_path + " must not be empty"))
        }
        normalized.push(value)
      }
      Ok(MultiPolygon(normalized))
    }
    GeometryCollection(geometries) => {
      let normalized : Array[Geometry] = []
      for index, child in geometries {
        let child_path = path + ".geometries[" + index.to_string() + "]"
        let value = match normalize_geometry_at(child, child_path) {
          Ok(value) => value
          Err(err) => return Err(err)
        }
        normalized.push(value)
      }
      Ok(GeometryCollection(normalized))
    }
  }
}

///|
/// Normalizes ring closure, consecutive duplicates, and ring direction.
pub fn normalize_geometry(geometry : Geometry) -> Result[Geometry, GeoError] {
  let normalized = match normalize_geometry_at(geometry, "geometry") {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  match validate_geometry(normalized) {
    Ok(_) => Ok(normalized)
    Err(err) => Err(err)
  }
}