// RFC 5870 `geo:` URIs: a latitude, longitude and optional altitude, plus
// optional uncertainty and coordinate reference system parameters.

///|
/// A validated WGS84 `geo:` URI. Unsupported CRSs are rejected before a
/// coordinate can escape as LatLng. Readonly outside the defining package.
pub struct GeoUri {
  point : LatLng
  altitude : Double?
  uncertainty : Double?
  crs : String?
  raw : String
} derive(Eq)

///|
/// Parse a `geo:` URI. Returns `None` for a bad prefix, a coordinate outside
/// its range, a malformed number/parameter, or an unsupported CRS.
pub fn geouri_parse(input : String) -> GeoUri? {
  let s = trim_ascii(input)
  if s.length() < 5 || ascii_lower(s[0:4].to_owned()) != "geo:" {
    return None
  }
  let body = s[4:].to_owned()
  let sections = split_text(body, ';')
  if sections.length() == 0 {
    return None
  }
  let coords = split_text(sections[0], ',')
  if coords.length() < 2 || coords.length() > 3 {
    return None
  }
  let lat = match parse_decimal(coords[0]) {
    None => return None
    Some(v) => v
  }
  let lng = match parse_decimal(coords[1]) {
    None => return None
    Some(v) => v
  }
  if !geo_number(coords[0], true) ||
    !geo_number(coords[1], true) ||
    lng < -180.0 ||
    lng > 180.0 {
    return None
  }
  let point = match LatLng::new(lat, lng) {
    None => return None
    Some(p) => p
  }
  let mut altitude : Double? = None
  if coords.length() == 3 {
    if !geo_number(coords[2], true) {
      return None
    }
    match parse_decimal(coords[2]) {
      None => return None
      Some(v) => {
        if !is_finite(v) {
          return None
        }
        altitude = Some(v)
      }
    }
  }
  let mut uncertainty : Double? = None
  let mut crs : String? = None
  let mut extensions = false
  for i = 1; i < sections.length(); i = i + 1 {
    let section = sections[i]
    if section == "" {
      return None
    }
    let kv = split_text(section, '=')
    if !geo_label(kv[0]) || kv.length() > 2 {
      return None
    }
    if kv.length() == 2 {
      let key = ascii_lower(kv[0])
      let value = kv[1]
      if key == "u" {
        if extensions || uncertainty is Some(_) || !geo_number(value, false) {
          return None
        }
        match parse_decimal(value) {
          None => return None
          Some(v) => {
            if v < 0.0 || !is_finite(v) || (v == 0.0 && geo_nonzero(value)) {
              return None
            }
            uncertainty = Some(v)
          }
        }
      } else if key == "crs" {
        if extensions ||
          uncertainty is Some(_) ||
          crs is Some(_) ||
          ascii_lower(value) != "wgs84" {
          return None
        }
        crs = Some(value)
      } else {
        if !geo_parameter_value(value) {
          return None
        }
        extensions = true
      }
    } else if ascii_lower(kv[0]) == "u" || ascii_lower(kv[0]) == "crs" {
      return None
    } else {
      extensions = true
    }
  }
  Some({ point, altitude, uncertainty, crs, raw: s, })
}

///|
/// RFC 5870 num/pnum: no leading plus, exponent or surrounding whitespace.
fn geo_number(text : String, signed : Bool) -> Bool {
  let cs = chars_of(text)
  let mut i = if signed && cs.length() > 0 && cs[0] == '-' { 1 } else { 0 }
  let begin = i
  while i < cs.length() && cs[i] >= '0' && cs[i] <= '9' {
    i = i + 1
  }
  if i == begin {
    return false
  }
  if i < cs.length() && cs[i] == '.' {
    i = i + 1
    let fraction = i
    while i < cs.length() && cs[i] >= '0' && cs[i] <= '9' {
      i = i + 1
    }
    if i == fraction {
      return false
    }
  }
  i == cs.length()
}

///|
fn geo_label(text : String) -> Bool {
  if text == "" {
    return false
  }
  for c in text {
    if !((c >= '0' && c <= '9') ||
      (c >= 'A' && c <= 'Z') ||
      (c >= 'a' && c <= 'z') ||
      c == '-') {
      return false
    }
  }
  true
}

///|
/// Preserve the validated URI exactly, including tiny u, explicit zero,
/// coordinate/altitude precision and extension parameters. Reformatting to a
/// fixed number of decimals could move the centre or turn a positive u into 0.
pub fn geouri_format(uri : GeoUri) -> String {
  uri.raw
}

///|
fn geo_parameter_value(text : String) -> Bool {
  if text == "" {
    return false
  }
  let cs = chars_of(text)
  let mut i = 0
  while i < cs.length() {
    let c = cs[i]
    if c == '%' {
      if i + 2 >= cs.length() || !geo_hex(cs[i + 1]) || !geo_hex(cs[i + 2]) {
        return false
      }
      i = i + 3
    } else if (c >= 'a' && c <= 'z') ||
      (c >= 'A' && c <= 'Z') ||
      (c >= '0' && c <= '9') ||
      "-_.!~*'()[]:&+$".contains(char_str(c)) {
      i = i + 1
    } else {
      return false
    }
  }
  true
}

///|
fn geo_hex(c : Char) -> Bool {
  (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
}

///|
fn geo_nonzero(text : String) -> Bool {
  for c in text {
    if c >= '1' && c <= '9' {
      return true
    }
  }
  false
}

///|
/// Split a string on a single-character delimiter, keeping empty fields.
pub fn split_text(text : String, delimiter : Char) -> Array[String] {
  let chars = chars_of(text)
  let out : Array[String] = []
  let mut current : Array[Char] = []
  for ch in chars {
    if ch == delimiter {
      out.push(string_of(current))
      current = []
    } else {
      current.push(ch)
    }
  }
  out.push(string_of(current))
  out
}

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