// The two value types every codec shares: a validated point and a bounding
// box that can cross the antimeridian.

///|
/// A WGS84 latitude/longitude pair in decimal degrees.
///
/// Values are only produced through a validating constructor, so a `LatLng`
/// always has a latitude in `[-90, 90]` and a finite, normalised longitude.
pub struct LatLng {
  lat : Double
  lng : Double
} derive(Eq)

///|
/// Build a point, or `None` when the latitude is outside `[-90, 90]` or either
/// value is not finite. A longitude outside `[-180, 180)` is wrapped.
pub fn LatLng::new(lat : Double, lng : Double) -> LatLng? {
  if !is_finite(lat) || !is_finite(lng) {
    return None
  }
  if lat < -90.0 || lat > 90.0 {
    return None
  }
  Some({ lat, lng: normalize_lng(lng), })
}

///|
/// The latitude in degrees.
pub fn LatLng::lat(self : LatLng) -> Double {
  self.lat
}

///|
/// The longitude in degrees, in `[-180, 180)`.
pub fn LatLng::lng(self : LatLng) -> Double {
  self.lng
}

///|
/// `lat, lng` with a fixed number of decimals.
pub fn LatLng::to_text(self : LatLng, digits~ : Int32 = 6) -> String {
  format_latlng(self.lat, self.lng, digits)
}

///|
/// A rectangle in degrees. `west > east` means the box crosses the
/// antimeridian; `east == 180` is allowed for a box ending at the date line.
pub struct BoundingBox {
  south : Double
  west : Double
  north : Double
  east : Double
} derive(Eq)

///|
/// Build a box. The latitude bounds must be ordered and inside `[-90, 90]`;
/// longitudes are wrapped into `[-180, 180]`. Returns `None` for non-finite
/// input.
pub fn BoundingBox::new(
  south : Double,
  west : Double,
  north : Double,
  east : Double,
) -> BoundingBox? {
  if !is_finite(south) ||
    !is_finite(west) ||
    !is_finite(north) ||
    !is_finite(east) {
    return None
  }
  if south < -90.0 || north > 90.0 || south > north {
    return None
  }
  let w = if west == 180.0 { 180.0 } else { normalize_lng(west) }
  let e = if east == 180.0 { 180.0 } else { normalize_lng(east) }
  Some({ south, west: w, north, east: e, })
}

///|
pub fn BoundingBox::south(self : BoundingBox) -> Double {
  self.south
}

///|
pub fn BoundingBox::west(self : BoundingBox) -> Double {
  self.west
}

///|
pub fn BoundingBox::north(self : BoundingBox) -> Double {
  self.north
}

///|
pub fn BoundingBox::east(self : BoundingBox) -> Double {
  self.east
}

///|
/// True when the box crosses the antimeridian.
pub fn BoundingBox::crosses_antimeridian(self : BoundingBox) -> Bool {
  self.west > self.east
}

///|
/// Width in degrees, always in `[0, 360]`.
pub fn BoundingBox::width(self : BoundingBox) -> Double {
  let w = if self.west > self.east {
    self.east + 360.0 - self.west
  } else {
    self.east - self.west
  }
  w
}

///|
/// Height in degrees, always in `[0, 180]`.
pub fn BoundingBox::height(self : BoundingBox) -> Double {
  self.north - self.south
}

///|
/// True when the point lies inside the box, antimeridian crossing included.
pub fn BoundingBox::contains(self : BoundingBox, point : LatLng) -> Bool {
  // Edges are inclusive. A tiny tolerance absorbs the difference between a
  // decimal coordinate and the rounded cell edge produced by a codec, without
  // letting a genuinely outside point in (1e-9 degrees is about 0.1 mm).
  let eps = 1.0e-9
  if point.lat() < self.south - eps || point.lat() > self.north + eps {
    return false
  }
  let lng = point.lng()
  if self.west <= self.east {
    lng >= self.west - eps && lng <= self.east + eps
  } else {
    lng >= self.west - eps || lng <= self.east + eps
  }
}

///|
/// The centre of the box. For a box crossing the antimeridian the centre is
/// normalised, so it may read `-179.5` rather than `180.5`.
pub fn BoundingBox::center(self : BoundingBox) -> LatLng? {
  let lat = (self.south + self.north) / 2.0
  let lng = if self.west <= self.east {
    (self.west + self.east) / 2.0
  } else {
    normalize_lng((self.west + self.east + 360.0) / 2.0)
  }
  LatLng::new(lat, lng)
}

///|
/// True when two boxes share any area (touching edges count).
pub fn BoundingBox::intersects(self : BoundingBox, other : BoundingBox) -> Bool {
  if self.north < other.south || other.north < self.south {
    return false
  }
  lng_in_range(other.west, self) ||
  lng_in_range(other.east, self) ||
  lng_in_range(self.west, other) ||
  lng_in_range(self.east, other)
}

///|
/// Whether a longitude falls inside a box's longitude span.
fn lng_in_range(lng : Double, box : BoundingBox) -> Bool {
  if box.west <= box.east {
    lng >= box.west && lng <= box.east
  } else {
    lng >= box.west || lng <= box.east
  }
}

///|
/// `south,west,north,east` with a fixed number of decimals.
pub fn BoundingBox::to_text(self : BoundingBox, digits~ : Int32 = 6) -> String {
  format_fixed(self.south, digits) +
  "," +
  format_fixed(self.west, digits) +
  "," +
  format_fixed(self.north, digits) +
  "," +
  format_fixed(self.east, digits)
}

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

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