// Ported from georust/geo
// https://github.com/georust/geo/blob/main/geo/src/algorithm/dimensions.rs

///|
/// Geometries can have 0, 1, or 2 dimensions, plus a special `Empty`
/// dimensionality (for empty collections / linestrings).
pub(all) enum Dimensions {
  Empty
  ZeroDimensional
  OneDimensional
  TwoDimensional
} derive(Eq, Debug)

///|
/// Compares dimensions in the natural ordering Empty < ZeroDimensional <
/// OneDimensional < TwoDimensional. Returns the integer rank.
pub fn Dimensions::rank(self : Dimensions) -> Int {
  match self {
    Empty => 0
    ZeroDimensional => 1
    OneDimensional => 2
    TwoDimensional => 3
  }
}

///|
/// Returns whether `a` has strictly less dimensionality than `b`.
pub fn dimensions_less(a : Dimensions, b : Dimensions) -> Bool {
  a.rank() < b.rank()
}

///|
/// Returns the dimension with the higher rank. Used as a fold combinator
/// when computing the max dimensionality across a collection of components.
fn max_dimension(a : Dimensions, b : Dimensions) -> Dimensions {
  if a.rank() >= b.rank() {
    a
  } else {
    b
  }
}

// MARK: trait HasDimensions + impls -----------------------------------------

///|
/// Anything whose dimensionality (Empty / 0D / 1D / 2D) can be computed.
pub(open) trait HasDimensions {
  fn dimensions(Self) -> Dimensions
}

///|
/// A degenerate rectangle of zero width and zero height collapses to
/// `ZeroDimensional`; one of zero width or zero height (but not both) is
/// `OneDimensional`.
pub impl HasDimensions for @type.Rect with fn dimensions(self) {
  let zero_w = self.width() == 0.0
  let zero_h = self.height() == 0.0
  match (zero_w, zero_h) {
    (true, true) => ZeroDimensional
    (true, false) | (false, true) => OneDimensional
    _ => TwoDimensional
  }
}

///|
/// Empty / single-coord linestrings are `Empty`. A linestring whose coords
/// are all identical is `ZeroDimensional`; otherwise `OneDimensional`.
pub impl HasDimensions for @type.LineString with fn dimensions(self) {
  let coords = self.coords()
  if coords.length() == 0 {
    return Empty
  }
  let first = coords[0]
  if coords.iter().any(fn(c) { c != first }) {
    OneDimensional
  } else {
    ZeroDimensional
  }
}

///|
/// `Polygon` is `TwoDimensional` unless its exterior is empty (`Empty`) or
/// all of its exterior coordinates collapse to a single coordinate
/// (`ZeroDimensional`).
pub impl HasDimensions for @type.Geometry with fn dimensions(self) {
  match self {
    @type.Geometry::Point(_) => ZeroDimensional
    @type.Geometry::Line(l) =>
      if l.start() == l.end() {
        ZeroDimensional
      } else {
        OneDimensional
      }
    @type.Geometry::LineString(ls) => HasDimensions::dimensions(ls)
    @type.Geometry::Polygon(p) =>
      match HasDimensions::dimensions(p.exterior()) {
        Empty => Empty
        ZeroDimensional => ZeroDimensional
        _ => TwoDimensional
      }
    @type.Geometry::MultiPoint(mp) =>
      if mp.is_empty() {
        Empty
      } else {
        ZeroDimensional
      }
    @type.Geometry::MultiLineString(mls) =>
      mls
      .line_strings()
      .iter()
      .map(HasDimensions::dimensions)
      .fold(init=Dimensions::Empty, max_dimension)
    @type.Geometry::MultiPolygon(mp) =>
      mp
      .polygons()
      .iter()
      .map(fn(p) { HasDimensions::dimensions(@type.Geometry::Polygon(p)) })
      .fold(init=Dimensions::Empty, max_dimension)
    @type.Geometry::Rect(r) => HasDimensions::dimensions(r)
    @type.Geometry::Triangle(_) => TwoDimensional
    @type.Geometry::GeometryCollection(gc) =>
      gc
      .geometries()
      .iter()
      .map(HasDimensions::dimensions)
      .fold(init=Dimensions::Empty, max_dimension)
  }
}