///|
pub(all) enum PointLocation {
  Outside
  Boundary
  Inside
} derive(Eq, @debug.Debug)

///|
fn predicate_abs(value : Double) -> Double {
  if value < 0.0 {
    -value
  } else {
    value
  }
}

///|
fn predicate_epsilon(tolerance : Double) -> Double {
  predicate_abs(tolerance)
}

///|
fn predicate_equal_xy(
  left : Coordinate,
  right : Coordinate,
  epsilon : Double,
) -> Bool {
  predicate_abs(left.x - right.x) <= epsilon &&
  predicate_abs(left.y - right.y) <= epsilon
}

///|
fn predicate_cross(
  start : Coordinate,
  first : Coordinate,
  second : Coordinate,
) -> Double {
  (first.x - start.x) * (second.y - start.y) -
  (first.y - start.y) * (second.x - start.x)
}

///|
fn predicate_between(
  value : Double,
  low : Double,
  high : Double,
  epsilon : Double,
) -> Bool {
  let min_value = if low < high { low } else { high }
  let max_value = if low > high { low } else { high }
  value >= min_value - epsilon && value <= max_value + epsilon
}

///|
fn predicate_point_on_segment(
  point : Coordinate,
  start : Coordinate,
  end : Coordinate,
  epsilon : Double,
) -> Bool {
  if predicate_abs(predicate_cross(start, end, point)) > epsilon {
    return false
  }
  predicate_between(point.x, start.x, end.x, epsilon) &&
  predicate_between(point.y, start.y, end.y, epsilon)
}

///|
fn predicate_ring_location(
  point : Coordinate,
  ring : Array[Coordinate],
  epsilon : Double,
) -> PointLocation {
  let mut inside = false
  let end = ring.length() - 1
  for index in 0.. point.y) != (finish.y > point.y)
    if crosses {
      let intersection_x = start.x +
        (point.y - start.y) * (finish.x - start.x) / (finish.y - start.y)
      if point.x < intersection_x {
        inside = !inside
      }
    }
  }
  if inside {
    PointLocation::Inside
  } else {
    PointLocation::Outside
  }
}

///|
fn predicate_line_location(
  point : Coordinate,
  line : Array[Coordinate],
  epsilon : Double,
) -> PointLocation {
  if line.length() == 0 {
    return PointLocation::Outside
  }
  let closed = predicate_equal_xy(line[0], line[line.length() - 1], epsilon)
  let end = line.length() - 1
  let mut boundary = false
  for index in 0.. PointLocation {
  if rings.length() == 0 {
    return PointLocation::Outside
  }
  match predicate_ring_location(point, rings[0], epsilon) {
    PointLocation::Outside => PointLocation::Outside
    PointLocation::Boundary => PointLocation::Boundary
    PointLocation::Inside => {
      for index in 1.. return PointLocation::Outside
          PointLocation::Boundary => return PointLocation::Boundary
          PointLocation::Outside => ()
        }
      }
      PointLocation::Inside
    }
  }
}

///|
fn predicate_merge_location(
  current : PointLocation,
  next : PointLocation,
) -> PointLocation {
  if current == PointLocation::Inside || next == PointLocation::Inside {
    PointLocation::Inside
  } else if current == PointLocation::Boundary ||
    next == PointLocation::Boundary {
    PointLocation::Boundary
  } else {
    PointLocation::Outside
  }
}

///|
fn predicate_geometry_location(
  geometry : Geometry,
  point : Coordinate,
  epsilon : Double,
) -> PointLocation {
  match geometry {
    Point(coordinate) =>
      if predicate_equal_xy(point, coordinate, epsilon) {
        PointLocation::Inside
      } else {
        PointLocation::Outside
      }
    EmptyPoint => PointLocation::Outside
    LineString(line) => predicate_line_location(point, line, epsilon)
    Polygon(rings) => predicate_polygon_location(point, rings, epsilon)
    MultiPoint(points) => {
      let mut result = PointLocation::Outside
      for coordinate in points {
        if predicate_equal_xy(point, coordinate, epsilon) {
          result = PointLocation::Inside
          break
        }
      }
      result
    }
    MultiLineString(lines) => {
      let mut result = PointLocation::Outside
      for line in lines {
        result = predicate_merge_location(
          result,
          predicate_line_location(point, line, epsilon),
        )
      }
      result
    }
    MultiPolygon(polygons) => {
      let mut result = PointLocation::Outside
      for polygon in polygons {
        result = predicate_merge_location(
          result,
          predicate_polygon_location(point, polygon, epsilon),
        )
      }
      result
    }
    GeometryCollection(geometries) => {
      let mut result = PointLocation::Outside
      for child in geometries {
        result = predicate_merge_location(
          result,
          predicate_geometry_location(child, point, epsilon),
        )
      }
      result
    }
  }
}

///|
pub fn Geometry::locate_point(
  self : Geometry,
  point : Coordinate,
  tolerance~ : Double = 1.0e-12,
) -> PointLocation {
  predicate_geometry_location(self, point, predicate_epsilon(tolerance))
}

///|
pub fn Geometry::contains_point(
  self : Geometry,
  point : Coordinate,
  tolerance~ : Double = 1.0e-12,
) -> Bool {
  self.locate_point(point, tolerance~) == PointLocation::Inside
}

///|
pub fn Geometry::covers_point(
  self : Geometry,
  point : Coordinate,
  tolerance~ : Double = 1.0e-12,
) -> Bool {
  let location = self.locate_point(point, tolerance~)
  location == PointLocation::Inside || location == PointLocation::Boundary
}

///|
pub fn BoundingBox::contains_xy(
  self : BoundingBox,
  x : Double,
  y : Double,
) -> Bool {
  x >= self.min_x && x <= self.max_x && y >= self.min_y && y <= self.max_y
}

///|
pub fn BoundingBox::intersects(self : BoundingBox, other : BoundingBox) -> Bool {
  self.min_x <= other.max_x &&
  self.max_x >= other.min_x &&
  self.min_y <= other.max_y &&
  self.max_y >= other.min_y
}

///|
pub fn BoundingBox3D::intersects(
  self : BoundingBox3D,
  other : BoundingBox3D,
) -> Bool {
  self.min_x <= other.max_x &&
  self.max_x >= other.min_x &&
  self.min_y <= other.max_y &&
  self.max_y >= other.min_y &&
  self.min_z <= other.max_z &&
  self.max_z >= other.min_z
}

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

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