// One value for every supported notation.
//
// A Geohash/Plus Code/Maidenhead string denotes a *cell*: it has an extent,
// a resolution and a centre, and reducing it to a point loses the extent.
// A decimal pair, DMS pair or `geo:` URI denotes a *point*; a `geo:` URI may
// also carry an explicit uncertainty (`u=`). `Location` keeps both the
// representative point and the region, so a conversion can say what it lost
// instead of silently returning a centre.

///|
/// Whether the source notation denotes a cell or a point.
pub(all) enum LocationKind {
  Cell
  Point
} derive(Eq, Debug)

///|
pub impl Show for LocationKind with fn to_string(self : LocationKind) -> String {
  match self {
    Cell => "cell"
    Point => "point"
  }
}

///|
/// A parsed location with its region, resolution and uncertainty.
///
/// Invariants:
/// - `region` always contains `point` geometrically, not necessarily the
///   physical subject when its accuracy is unknown;
/// - `kind == Cell` means `region` is the exact cell, `point` is its centre;
/// - `kind == Point` means the notation itself is a point; when `u=` is given
///   the region is the square envelope of that uncertainty;
/// - `resolution_m` is the longer side of the region in metres (0 when the
///   notation carries no extent);
/// - `uncertainty_m` is an optional coverage bound, never a zero placeholder;
/// - `cell_radius_m` describes grid geometry, independently of input accuracy;
/// - `raw` keeps the text that produced the value, so the original notation is
///   never lost even when the point/region is converted further.
pub struct Location {
  format : LocationFormat
  kind : LocationKind
  point : LatLng
  region : BoundingBox
  resolution_m : Double
  uncertainty_m : Double?
  cell_radius_m : Double?
  area_m2 : Double
  altitude_m : Double?
  crs : String?
  raw : String
} derive(Eq)

///|
/// Build a cell location from its exact bounds. The representative point is
/// the centre; the resolution is the longer side of the cell on the ground,
/// and the uncertainty is a conservative bound from the centre to any corner.
fn cell_location(
  format : LocationFormat,
  raw : String,
  bounds : BoundingBox,
) -> Location? {
  let point = match bounds.center() {
    None => return None
    Some(p) => p
  }
  let width_m = bounds.width() * 111320.0 * @math.cos(to_radians(point.lat()))
  let height_m = bounds.height() * 110574.0
  let resolution = if width_m > height_m { width_m } else { height_m }
  Some({
    format,
    kind: Cell,
    point,
    region: bounds,
    resolution_m: resolution,
    uncertainty_m: Some(cell_radius_bound(bounds)),
    cell_radius_m: Some(cell_radius_bound(bounds)),
    area_m2: width_m * height_m,
    altitude_m: None,
    crs: None,
    raw,
  })
}

///|
/// Build a point location. `uncertainty_m` is the `u=` value from a `geo:`
/// URI, if any; it expands the region to a square envelope around the point.
fn point_location(
  format : LocationFormat,
  raw : String,
  point : LatLng,
  altitude_m : Double?,
  crs : String?,
  uncertainty_m : Double?,
) -> Location? {
  if uncertainty_m is Some(u) && (!is_finite(u) || u < 0.0) {
    return None
  }
  match uncertainty_m {
    Some(u) if u > 0.0 && is_finite(u) => {
      // A sphere with radius below WGS84's minimum curvature radius gives
      // a conservative angular envelope, including at poles and date lines.
      let angle = u / 6335000.0
      let lat_delta = angle / 0.017453292519943295
      let cos_lat = @math.cos(to_radians(point.lat()))
      let touches_pole = point.lat().abs() + lat_delta >= 90.0
      let lng_delta = if touches_pole {
        180.0
      } else {
        @math.asin(@math.sin(angle) / cos_lat) / 0.017453292519943295
      }
      let region = if lat_delta >= 90.0 {
        BoundingBox::new(-90.0, -180.0, 90.0, 180.0)
      } else if lng_delta >= 180.0 {
        BoundingBox::new(
          clamp_lat(point.lat() - lat_delta),
          -180.0,
          clamp_lat(point.lat() + lat_delta),
          180.0,
        )
      } else {
        BoundingBox::new(
          clamp_lat(point.lat() - lat_delta),
          point.lng() - lng_delta,
          clamp_lat(point.lat() + lat_delta),
          point.lng() + lng_delta,
        )
      }
      match region {
        None => return None
        Some(box) =>
          Some({
            format,
            kind: Point,
            point,
            region: box,
            resolution_m: 2.0 * u,
            uncertainty_m: Some(u),
            cell_radius_m: None,
            area_m2: if u > 20000000.0 {
              510100000000000.0
            } else {
              4.0 * u * u
            },
            altitude_m,
            crs,
            raw,
          })
      }
    }
    _ => {
      let region = match
        BoundingBox::new(point.lat(), point.lng(), point.lat(), point.lng()) {
        None => return None
        Some(b) => b
      }
      Some({
        format,
        kind: Point,
        point,
        region,
        resolution_m: 0.0,
        uncertainty_m,
        cell_radius_m: None,
        area_m2: 0.0,
        altitude_m,
        crs,
        raw,
      })
    }
  }
}

///|
/// The representative point (cell centre, or the coordinate itself).
pub fn Location::center(self : Location) -> LatLng {
  self.point
}

///|
/// Notation geometry: the exact cell, a point's u envelope, or its degenerate
/// coordinate. For a physical-position envelope use `uncertainty_bounds`;
/// neither a degenerate coordinate nor a converted cell certifies accuracy.
pub fn Location::bounds(self : Location) -> BoundingBox {
  self.region
}

///|
/// True when the source notation denotes an area rather than a point.
pub fn Location::is_cell(self : Location) -> Bool {
  self.kind == Cell
}

///|
/// True when the source notation denotes a point.
pub fn Location::is_point(self : Location) -> Bool {
  self.kind == Point
}

///|
/// Quantise the representative point and propagate source uncertainty.
/// The target region is grid geometry, not a new measurement. Unknown source
/// accuracy stays None even through repeated conversions; a known bound grows
/// by the centre displacement and covers at least the target cell geometry.
/// `precision` means characters for Geohash, significant digits for Plus Code
/// and characters for Maidenhead.
pub fn Location::to_cell(
  self : Location,
  format : LocationFormat,
  precision : Int32,
) -> Location? {
  let code = match format {
    GeoHash => geohash_encode(self.point, precision)
    PlusCode => pluscode_encode(self.point, code_length=precision)
    Maidenhead => maidenhead_encode(self.point, precision~)
    _ => None
  }
  match code {
    None => None
    Some(text) => {
      let target = match describe_code(text) {
        None => return None
        Some(v) => v
      }
      let uncertainty = match self.uncertainty_m {
        None => None
        Some(u) => {
          let sum = u + center_shift_bound(self.point, target.point)
          let propagated = sum + sum * 1.0e-12 + 1.0e-9
          if !is_finite(propagated) {
            None // No finite bound is representable; never claim zero.
          } else {
            let radius = target.cell_radius_m.unwrap()
            Some(if propagated > radius { propagated } else { radius })
          }
        }
      }
      Some({ ..target, uncertainty_m: uncertainty, })
    }
  }
}

///|
/// Whether a finite coverage bound is known. Derived from the optional value.
pub fn Location::uncertainty_known(self : Location) -> Bool {
  self.uncertainty_m is Some(_)
}

///|
/// Conservative horizontal envelope of the stated/propagated uncertainty.
/// None means no finite envelope is known. Unlike `bounds`, this does not
/// mistake the coordinate or quantised grid cell for the true-position region.
pub fn Location::uncertainty_bounds(self : Location) -> BoundingBox? {
  let u = match self.uncertainty_m {
    None => return None
    Some(v) => v
  }
  match point_location(self.format, self.raw, self.point, None, None, Some(u)) {
    None => None
    Some(loc) => Some(loc.region)
  }
}

///|
/// True when the point lies inside the region.
pub fn Location::contains_point(self : Location, point : LatLng) -> Bool {
  self.region.contains(point)
}

///|
/// True when every point of `other` is inside this region. A cell can contain
/// a point only when the point is inside the cell; a point can contain only a
/// degenerate point at the same coordinate.
pub fn Location::contains(self : Location, other : Location) -> Bool {
  box_contains(self.region, other.region)
}

///|
/// True when the two regions overlap (touching edges count).
pub fn Location::intersects(self : Location, other : Location) -> Bool {
  self.region.intersects(other.region)
}

///|
/// Render a representative point with its known/propagated uncertainty.
/// Unknown accuracy omits u, including after conversion to a grid. A freshly
/// decoded cell carries its geometric covering radius. Rectangle shape is lost.
pub fn Location::to_geouri(self : Location) -> String {
  // Keep all point-URI precision and parameters, including explicit u=0.
  if self.format == GeoUri {
    return self.raw
  }
  let mut out = "geo:" +
    trim_fraction(format_fixed(self.point.lat(), 9)) +
    "," +
    trim_fraction(format_fixed(self.point.lng(), 9))
  match self.altitude_m {
    None => ()
    Some(v) => out = out + "," + trim_fraction(format_fixed(v, 3))
  }
  match self.crs {
    None => ()
    Some(v) => out = out + ";crs=" + v
  }
  if self.uncertainty_m is Some(u) {
    // Round outward and include the displacement from coordinate rounding.
    let radius = u + 0.0002 + u * 1.0e-12
    if is_finite(radius) {
      // Keep fixed-point integer conversion within range, even for huge u.
      let rendered = if radius < 1.0e12 {
        trim_fraction(format_fixed((radius * 1000.0).ceil() / 1000.0, 3))
      } else {
        decimal_notation(radius)
      }
      out = out + ";u=" + rendered
    }
  }
  out
}

///|
/// Conservative WGS84 surface-distance bound along a straight path in angular
/// coordinates. 111700 m/degree bounds both meridional and equatorial scales.
/// This is a covering bound, not an ellipsoidal geodesic measurement.
fn cell_radius_bound(box : BoundingBox) -> Double {
  let near_lat = if box.south() <= 0.0 && box.north() >= 0.0 {
    0.0
  } else if box.south().abs() < box.north().abs() {
    box.south().abs()
  } else {
    box.north().abs()
  }
  let h = box.height() / 2.0
  let w = box.width() / 2.0 * @math.cos(to_radians(near_lat))
  111700.0 * (h * h + w * w).sqrt()
}

///|
/// An explicit conversion record retains source geometry, text and accuracy.
/// `uncertainty_bound_m` bounds the source position around the target centre;
/// None means the accuracy is unknown or no finite bound is representable.
pub struct LocationConversion {
  source : Location
  target : Location
  center_shift_bound_m : Double
  uncertainty_bound_m : Double?
}

///|
pub fn Location::convert_to_cell(
  self : Location,
  format : LocationFormat,
  precision : Int32,
) -> LocationConversion? {
  let target = match self.to_cell(format, precision) {
    None => return None
    Some(v) => v
  }
  let shift = center_shift_bound(self.point, target.point)
  Some({
    source: self,
    target,
    center_shift_bound_m: shift,
    uncertainty_bound_m: target.uncertainty_m,
  })
}

///|
fn center_shift_bound(source : LatLng, target : LatLng) -> Double {
  let lat = source.lat() - target.lat()
  let lng = lng_delta(source.lng(), target.lng())
  111700.0 * (lat * lat + lng * lng).sqrt()
}

///|
/// Whether `outer` contains every point of `inner`, antimeridian included.
fn box_contains(outer : BoundingBox, inner : BoundingBox) -> Bool {
  let eps = 1.0e-9
  if inner.south() < outer.south() - eps || inner.north() > outer.north() + eps {
    return false
  }
  let ow = outer.west()
  let oe = outer.east()
  let iw = inner.west()
  let ie = inner.east()
  let outer_cross = ow > oe
  let inner_cross = iw > ie
  if !inner_cross {
    if !outer_cross {
      iw >= ow - eps && ie <= oe + eps
    } else {
      (iw >= ow - eps && ie <= 180.0 + eps) ||
      (iw >= -180.0 - eps && ie <= oe + eps)
    }
  } else if !outer_cross {
    // A non-crossing outer box can contain a crossing inner box only if it
    // spans the whole longitude range.
    ow <= -180.0 + eps && oe >= 180.0 - eps
  } else {
    iw >= ow - eps && ie <= oe + eps
  }
}

///|
/// Describe a Geohash, Plus Code or Maidenhead locator as a `Location`.
pub fn describe_code(text : String) -> Location? {
  let s = trim_ascii(text)
  match pluscode_bounds(s) {
    Some(b) => return cell_location(PlusCode, s, b)
    None => ()
  }
  match maidenhead_bounds(s) {
    Some(b) => return cell_location(Maidenhead, s, b)
    None => ()
  }
  match geohash_bounds(s) {
    Some(b) => return cell_location(GeoHash, s, b)
    None => ()
  }
  None
}

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

///|
pub extend LocationKind with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend LocationKind with Show::{output, to_string}

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