///|
/// Coordinate dimensionality supported by geometry interchange formats.
pub(all) enum CoordinateDimension {
  Xy
  Xyz
  Xym
  Xyzm
} derive(Eq, @debug.Debug)

///|
/// A coordinate with optional Z and M ordinates.
pub(all) struct Coordinate {
  x : Double
  y : Double
  z : Double?
  m : Double?
} derive(Eq, @debug.Debug)

///|
pub fn Coordinate::xy(x : Double, y : Double) -> Coordinate {
  { x, y, z: None, m: None, }
}

///|
pub fn Coordinate::xyz(x : Double, y : Double, z : Double) -> Coordinate {
  { x, y, z: Some(z), m: None, }
}

///|
pub fn Coordinate::xym(x : Double, y : Double, m : Double) -> Coordinate {
  { x, y, z: None, m: Some(m), }
}

///|
pub fn Coordinate::xyzm(
  x : Double,
  y : Double,
  z : Double,
  m : Double,
) -> Coordinate {
  { x, y, z: Some(z), m: Some(m), }
}

///|
pub fn Coordinate::dimension(self : Coordinate) -> CoordinateDimension {
  match (self.z, self.m) {
    (None, None) => Xy
    (Some(_), None) => Xyz
    (None, Some(_)) => Xym
    (Some(_), Some(_)) => Xyzm
  }
}

///|
pub fn Coordinate::has_z(self : Coordinate) -> Bool {
  match self.z {
    Some(_) => true
    None => false
  }
}

///|
pub fn Coordinate::has_m(self : Coordinate) -> Bool {
  match self.m {
    Some(_) => true
    None => false
  }
}

///|
pub(all) enum GeometryKind {
  Point
  LineString
  Polygon
  MultiPoint
  MultiLineString
  MultiPolygon
  GeometryCollection
} derive(Eq, @debug.Debug)

///|
/// A closed coordinate bounds box. Empty geometries have no bounds.
pub(all) struct BoundingBox {
  min_x : Double
  min_y : Double
  max_x : Double
  max_y : Double
} derive(Eq, @debug.Debug)

///|
pub fn BoundingBox::width(self : BoundingBox) -> Double {
  self.max_x - self.min_x
}

///|
pub fn BoundingBox::height(self : BoundingBox) -> Double {
  self.max_y - self.min_y
}

///|
/// A closed three-dimensional bounds box.
pub(all) struct BoundingBox3D {
  min_x : Double
  min_y : Double
  min_z : Double
  max_x : Double
  max_y : Double
  max_z : Double
} derive(Eq, @debug.Debug)

///|
pub fn BoundingBox3D::width(self : BoundingBox3D) -> Double {
  self.max_x - self.min_x
}

///|
pub fn BoundingBox3D::height(self : BoundingBox3D) -> Double {
  self.max_y - self.min_y
}

///|
pub fn BoundingBox3D::depth(self : BoundingBox3D) -> Double {
  self.max_z - self.min_z
}

///|
pub fn BoundingBox3D::extent(self : BoundingBox3D) -> (Double, Double, Double) {
  (self.width(), self.height(), self.depth())
}

///|
/// Typed geometry model independent of text and binary encodings.
pub(all) enum Geometry {
  Point(Coordinate)
  EmptyPoint
  LineString(Array[Coordinate])
  Polygon(Array[Array[Coordinate]])
  MultiPoint(Array[Coordinate])
  MultiLineString(Array[Array[Coordinate]])
  MultiPolygon(Array[Array[Array[Coordinate]]])
  GeometryCollection(Array[Geometry])
} derive(Eq, @debug.Debug)

///|
/// A geometry with optional spatial reference system metadata.
pub(all) struct SpatialGeometry {
  srid : Int32?
  geometry : Geometry
} derive(Eq, @debug.Debug)

///|
pub fn SpatialGeometry::new(
  geometry : Geometry,
  srid : Int32?,
) -> Result[SpatialGeometry, GeoError] {
  match srid {
    Some(value) =>
      if value < 0 {
        Err(InvalidSrid(value))
      } else {
        Ok({ srid, geometry, })
      }
    None => Ok({ srid, geometry, })
  }
}

///|
pub fn Geometry::kind(self : Geometry) -> GeometryKind {
  match self {
    Point(_) => Point
    EmptyPoint => Point
    LineString(_) => LineString
    Polygon(_) => Polygon
    MultiPoint(_) => MultiPoint
    MultiLineString(_) => MultiLineString
    MultiPolygon(_) => MultiPolygon
    GeometryCollection(_) => GeometryCollection
  }
}

///|
pub fn Geometry::coordinate_count(self : Geometry) -> Int32 {
  match self {
    Point(_) => 1
    EmptyPoint => 0
    LineString(points) => points.length()
    Polygon(rings) => coordinate_count_rings(rings)
    MultiPoint(points) => points.length()
    MultiLineString(lines) => coordinate_count_lines(lines)
    MultiPolygon(polygons) => coordinate_count_polygons(polygons)
    GeometryCollection(geometries) => {
      let mut count = 0
      for geometry in geometries {
        count += geometry.coordinate_count()
      }
      count
    }
  }
}

///|
fn coordinate_count_rings(rings : Array[Array[Coordinate]]) -> Int32 {
  let mut count = 0
  for ring in rings {
    count += ring.length()
  }
  count
}

///|
fn coordinate_count_lines(lines : Array[Array[Coordinate]]) -> Int32 {
  coordinate_count_rings(lines)
}

///|
fn coordinate_count_polygons(
  polygons : Array[Array[Array[Coordinate]]],
) -> Int32 {
  let mut count = 0
  for polygon in polygons {
    count += coordinate_count_rings(polygon)
  }
  count
}

///|
pub fn Geometry::is_empty(self : Geometry) -> Bool {
  self.coordinate_count() == 0
}

///|
pub fn Geometry::bounds(self : Geometry) -> BoundingBox? {
  match self {
    Point(coordinate) => Some(point_bounds(coordinate))
    EmptyPoint => None
    LineString(points) => bounds_from_coordinates(points)
    Polygon(rings) => bounds_from_rings(rings)
    MultiPoint(points) => bounds_from_coordinates(points)
    MultiLineString(lines) => bounds_from_lines(lines)
    MultiPolygon(polygons) => bounds_from_polygons(polygons)
    GeometryCollection(geometries) => {
      let mut result : BoundingBox? = None
      for geometry in geometries {
        result = merge_bounds(result, geometry.bounds())
      }
      result
    }
  }
}

///|
fn point_bounds(coordinate : Coordinate) -> BoundingBox {
  {
    min_x: coordinate.x,
    min_y: coordinate.y,
    max_x: coordinate.x,
    max_y: coordinate.y,
  }
}

///|
fn extend_bounds(bounds : BoundingBox, coordinate : Coordinate) -> BoundingBox {
  {
    min_x: if coordinate.x < bounds.min_x {
      coordinate.x
    } else {
      bounds.min_x
    },
    min_y: if coordinate.y < bounds.min_y {
      coordinate.y
    } else {
      bounds.min_y
    },
    max_x: if coordinate.x > bounds.max_x {
      coordinate.x
    } else {
      bounds.max_x
    },
    max_y: if coordinate.y > bounds.max_y {
      coordinate.y
    } else {
      bounds.max_y
    },
  }
}

///|
fn bounds_from_coordinates(coordinates : Array[Coordinate]) -> BoundingBox? {
  let mut result : BoundingBox? = None
  for coordinate in coordinates {
    result = match result {
      Some(current) => Some(extend_bounds(current, coordinate))
      None => Some(point_bounds(coordinate))
    }
  }
  result
}

///|
fn bounds_from_rings(rings : Array[Array[Coordinate]]) -> BoundingBox? {
  let mut result : BoundingBox? = None
  for ring in rings {
    result = merge_bounds(result, bounds_from_coordinates(ring))
  }
  result
}

///|
fn bounds_from_lines(lines : Array[Array[Coordinate]]) -> BoundingBox? {
  bounds_from_rings(lines)
}

///|
fn bounds_from_polygons(
  polygons : Array[Array[Array[Coordinate]]],
) -> BoundingBox? {
  let mut result : BoundingBox? = None
  for polygon in polygons {
    result = merge_bounds(result, bounds_from_rings(polygon))
  }
  result
}

///|
fn merge_bounds(left : BoundingBox?, right : BoundingBox?) -> BoundingBox? {
  match (left, right) {
    (None, None) => None
    (Some(value), None) => Some(value)
    (None, Some(value)) => Some(value)
    (Some(a), Some(b)) =>
      Some({
        min_x: if a.min_x < b.min_x {
          a.min_x
        } else {
          b.min_x
        },
        min_y: if a.min_y < b.min_y {
          a.min_y
        } else {
          b.min_y
        },
        max_x: if a.max_x > b.max_x {
          a.max_x
        } else {
          b.max_x
        },
        max_y: if a.max_y > b.max_y {
          a.max_y
        } else {
          b.max_y
        },
      })
  }
}

///|
pub extend CoordinateDimension with Eq::{not_equal, equal}

///|
pub extend CoordinateDimension with @debug.Debug::{to_repr}

///|
pub extend Coordinate with Eq::{not_equal, equal}

///|
pub extend Coordinate with @debug.Debug::{to_repr}

///|
pub extend GeometryKind with Eq::{not_equal, equal}

///|
pub extend GeometryKind with @debug.Debug::{to_repr}

///|
pub extend BoundingBox with Eq::{not_equal, equal}

///|
pub extend BoundingBox with @debug.Debug::{to_repr}

///|
pub extend BoundingBox3D with Eq::{not_equal, equal}

///|
pub extend BoundingBox3D with @debug.Debug::{to_repr}

///|
pub extend Geometry with Eq::{not_equal, equal}

///|
pub extend Geometry with @debug.Debug::{to_repr}

///|
pub extend SpatialGeometry with Eq::{not_equal, equal}

///|
pub extend SpatialGeometry with @debug.Debug::{to_repr}