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

///|
/// Adds up the determinants of the lines between consecutive `points`,
/// shifted so the first coordinate is the origin (numerical stability).
/// The first and last segment have determinant zero after shifting and are
/// skipped.
fn sum_line_determinants(points : Array[@type.Coord]) -> Double {
  if points.length() <= 2 {
    return 0.0
  }
  let shift = points[0]
  let init_prev = points[1] - shift
  let acc = (2)
    .until(points.length())
    .fold(init=(init_prev, 0.0), fn(acc, i) {
      let (prev, sum) = acc
      let shifted = points[i] - shift
      let line = @type.Line::Line(prev, shifted)
      (shifted, sum + line.determinant())
    })
  acc.1
}

///|
/// Twice the signed area of a closed ring (`twice_signed_ring_area` in geo).
/// Returns `0.0` if the ring is open or has fewer than 3 coordinates.
fn twice_signed_ring_area(ls : @type.LineString) -> Double {
  let coords = ls.coords()
  if coords.length() < 3 {
    return 0.0
  }
  if coords[0] != coords[coords.length() - 1] {
    return 0.0
  }
  sum_line_determinants(coords)
}

///|
/// Signed area of a single linestring ring (the shoelace formula divided by 2).
fn linestring_signed_area(ls : @type.LineString) -> Double {
  twice_signed_ring_area(ls) / 2.0
}

///|
/// Signed planar area of a `Polygon`. The interior rings reduce the area in
/// magnitude; the sign matches the orientation of the exterior ring.
fn signed_area_of_polygon(p : @type.Polygon) -> Double {
  let exterior_area = linestring_signed_area(p.exterior())
  let is_negative = exterior_area < 0.0
  let area = p
    .interiors()
    .iter()
    .fold(init=exterior_area.abs(), fn(acc, interior) {
      acc - linestring_signed_area(interior).abs()
    })
  if is_negative {
    -area
  } else {
    area
  }
}

///|
fn unsigned_area_of_polygon(p : @type.Polygon) -> Double {
  signed_area_of_polygon(p).abs()
}

///|
fn signed_area_of_multi_polygon(mp : @type.MultiPolygon) -> Double {
  mp
  .polygons()
  .iter()
  .fold(init=0.0, fn(acc, p) { acc + signed_area_of_polygon(p) })
}

///|
fn unsigned_area_of_multi_polygon(mp : @type.MultiPolygon) -> Double {
  mp
  .polygons()
  .iter()
  .fold(init=0.0, fn(acc, p) { acc + unsigned_area_of_polygon(p) })
}

///|
fn signed_area_of_rect(r : @type.Rect) -> Double {
  r.width() * r.height()
}

///|
fn unsigned_area_of_rect(r : @type.Rect) -> Double {
  r.width() * r.height()
}

///|
fn signed_area_of_triangle(t : @type.Triangle) -> Double {
  sum_line_determinants(coords_of_triangle(t)) / 2.0
}

///|
fn unsigned_area_of_triangle(t : @type.Triangle) -> Double {
  signed_area_of_triangle(t).abs()
}

///|
/// Signed planar area of a `Geometry`. Zero-dimensional and one-dimensional
/// geometries (Point, MultiPoint, Line, LineString, MultiLineString) have area
/// zero.
fn signed_area_of_geometry(g : @type.Geometry) -> Double {
  match g {
    @type.Geometry::Polygon(p) => signed_area_of_polygon(p)
    @type.Geometry::MultiPolygon(mp) => signed_area_of_multi_polygon(mp)
    @type.Geometry::Rect(r) => signed_area_of_rect(r)
    @type.Geometry::Triangle(t) => signed_area_of_triangle(t)
    @type.Geometry::GeometryCollection(gc) =>
      gc
      .geometries()
      .iter()
      .fold(init=0.0, fn(acc, g2) { acc + signed_area_of_geometry(g2) })
    _ => 0.0
  }
}

///|
/// Unsigned planar area = `signed_area_of_geometry(g).abs()` for individual
/// geometries; for `GeometryCollection` it sums the unsigned areas of the
/// components.
fn unsigned_area_of_geometry(g : @type.Geometry) -> Double {
  match g {
    @type.Geometry::Polygon(p) => unsigned_area_of_polygon(p)
    @type.Geometry::MultiPolygon(mp) => unsigned_area_of_multi_polygon(mp)
    @type.Geometry::Rect(r) => unsigned_area_of_rect(r)
    @type.Geometry::Triangle(t) => unsigned_area_of_triangle(t)
    @type.Geometry::GeometryCollection(gc) =>
      gc
      .geometries()
      .iter()
      .fold(init=0.0, fn(acc, g2) { acc + unsigned_area_of_geometry(g2) })
    _ => 0.0
  }
}

// MARK: trait HasArea + impls -----------------------------------------------

///|
/// Anything that has a planar area (including signed area for orientation).
pub(open) trait HasArea {
  fn signed_area(Self) -> Double
  fn unsigned_area(Self) -> Double
}

///|
pub impl HasArea for @type.Polygon with fn signed_area(self) {
  signed_area_of_polygon(self)
}

///|
pub impl HasArea for @type.Polygon with fn unsigned_area(self) {
  unsigned_area_of_polygon(self)
}

///|
pub impl HasArea for @type.MultiPolygon with fn signed_area(self) {
  signed_area_of_multi_polygon(self)
}

///|
pub impl HasArea for @type.MultiPolygon with fn unsigned_area(self) {
  unsigned_area_of_multi_polygon(self)
}

///|
pub impl HasArea for @type.Rect with fn signed_area(self) {
  signed_area_of_rect(self)
}

///|
pub impl HasArea for @type.Rect with fn unsigned_area(self) {
  unsigned_area_of_rect(self)
}

///|
pub impl HasArea for @type.Triangle with fn signed_area(self) {
  signed_area_of_triangle(self)
}

///|
pub impl HasArea for @type.Triangle with fn unsigned_area(self) {
  unsigned_area_of_triangle(self)
}

///|
pub impl HasArea for @type.Geometry with fn signed_area(self) {
  signed_area_of_geometry(self)
}

///|
pub impl HasArea for @type.Geometry with fn unsigned_area(self) {
  unsigned_area_of_geometry(self)
}