// Open Location Code (Plus Codes), per the published OLC specification.
//
// The first ten significant characters encode five latitude/longitude pairs in
// base 20. Characters eleven to fifteen refine the cell with a 5x4 grid. The
// implementation works in the integer domain the specification defines, so
// encode/decode are exact rather than accumulating floating-point error.

///|
/// The OLC alphabet. `0`, `+` and the vowels are deliberately absent.
pub const PLUSCODE_ALPHABET : String = "23456789CFGHJMPQRVWX"

///|
/// The character that separates the area code from the local code.
pub const PLUSCODE_SEPARATOR : Char = '+'

///|
/// The padding character used by area-only codes.
pub const PLUSCODE_PADDING : Char = '0'

///|
/// The precision of the latitude/longitude pair section (20^3).
const OLC_PAIR_PRECISION : Int64 = 8000L

///|
/// The latitude scale that makes a coordinate an integer (20^3 * 5^5).
const OLC_FINAL_LAT_PRECISION : Int64 = 25000000L

///|
/// The longitude scale that makes a coordinate an integer (20^3 * 4^5).
const OLC_FINAL_LNG_PRECISION : Int64 = 8192000L

///|
/// Latitude offset in integer units (90 degrees).
const OLC_LAT_OFFSET : Int64 = 2250000000L

///|
/// Longitude offset in integer units (180 degrees).
const OLC_LNG_OFFSET : Int64 = 1474560000L

///|
/// The number of longitude integer units in a full turn.
const OLC_LNG_RANGE : Int64 = 2949120000L

///|
/// The first pair's place value (20^4).
const OLC_PAIR_FIRST_PLACE : Int64 = 160000L

///|
/// The resolution of each pair in degrees.
let olc_pair_resolutions : Array[Double] = [20.0, 1.0, 0.05, 0.0025, 0.000125]

///|
/// Encode a point as a Plus Code. `code_length` counts significant characters
/// and excludes the separator; 10 (about 13.5 m) is the default, 11 to 15 add
/// grid refinement, and 2/4/6/8 are the area-only lengths.
pub fn pluscode_encode(point : LatLng, code_length? : Int = 10) -> String? {
  let length = if code_length < 2 {
    return None
  } else if code_length < 10 && code_length % 2 == 1 {
    return None
  } else if code_length > 15 {
    15
  } else {
    code_length
  }
  let alphabet = chars_of(PLUSCODE_ALPHABET)
  let mut lat_val = olc_lat_integer(point.lat())
  let mut lng_val = olc_lng_integer(point.lng())
  let reversed : Array[Char] = []
  if length > 10 {
    for _ in 0..<5 {
      let lat_digit = (lat_val % 5L).to_int()
      let lng_digit = (lng_val % 4L).to_int()
      reversed.push(alphabet[lat_digit * 4 + lng_digit])
      lat_val = lat_val / 5L
      lng_val = lng_val / 4L
    }
  } else {
    lat_val = lat_val / 3125L
    lng_val = lng_val / 1024L
  }
  for _ in 0..<5 {
    let lng_digit = (lng_val % 20L).to_int()
    let lat_digit = (lat_val % 20L).to_int()
    reversed.push(alphabet[lng_digit])
    reversed.push(alphabet[lat_digit])
    lat_val = lat_val / 20L
    lng_val = lng_val / 20L
  }
  let chars : Array[Char] = []
  let mut i = reversed.length()
  while i > 0 {
    i = i - 1
    chars.push(reversed[i])
  }
  // Build the full 15-character form with the separator at position 8.
  let buf = StringBuilder()
  let mut k = 0
  while k < chars.length() {
    if k == 8 {
      buf.write_char(PLUSCODE_SEPARATOR)
    }
    buf.write_char(chars[k])
    k = k + 1
  }
  if k == 8 {
    buf.write_char(PLUSCODE_SEPARATOR)
  }
  let full = buf.to_string()
  if length >= 8 {
    Some(full[0:length + 1].to_owned())
  } else {
    Some(full[0:length].to_owned() + "0".repeat(8 - length) + "+")
  }
}

///|
/// Turn a latitude into the OLC integer grid, clamping to the legal range.
fn olc_lat_integer(lat : Double) -> Int64 {
  let limit = OLC_LAT_OFFSET * 2L
  let mut v = (lat * OLC_FINAL_LAT_PRECISION.to_double()).floor().to_int64() +
    OLC_LAT_OFFSET
  if v < 0L {
    v = 0L
  } else if v >= limit {
    v = limit - 1L
  }
  v
}

///|
/// Turn a longitude into the OLC integer grid, wrapping around the date line.
fn olc_lng_integer(lng : Double) -> Int64 {
  let mut v = (lng * OLC_FINAL_LNG_PRECISION.to_double()).floor().to_int64() +
    OLC_LNG_OFFSET
  v = (v % OLC_LNG_RANGE + OLC_LNG_RANGE) % OLC_LNG_RANGE
  v
}

///|
/// True when the string is a syntactically valid OLC, either full or short.
pub fn pluscode_is_valid(code : String) -> Bool {
  let s = ascii_upper(trim_ascii(code))
  let chars = chars_of(s)
  if chars.length() < 2 {
    return false
  }
  let mut separators = 0
  let mut sep = -1
  for i = 0; i < chars.length(); i = i + 1 {
    if chars[i] == PLUSCODE_SEPARATOR {
      separators = separators + 1
      sep = i
    }
  }
  if separators != 1 || sep % 2 == 1 || sep > 8 {
    return false
  }
  let mut padding = -1
  for i = 0; i < chars.length(); i = i + 1 {
    if chars[i] == PLUSCODE_PADDING && padding == -1 {
      padding = i
    }
  }
  if padding != -1 {
    if sep < 8 {
      return false
    }
    if padding == 0 {
      return false
    }
    let mut last = padding
    while last < chars.length() && chars[last] == PLUSCODE_PADDING {
      last = last + 1
    }
    if (last - padding) % 2 == 1 {
      return false
    }
    if last != sep {
      return false
    }
    if chars[chars.length() - 1] != PLUSCODE_SEPARATOR {
      return false
    }
  }
  if chars.length() - sep - 1 == 1 {
    return false
  }
  let alphabet = chars_of(PLUSCODE_ALPHABET)
  for ch in chars {
    if ch == PLUSCODE_SEPARATOR || ch == PLUSCODE_PADDING {
      continue
    }
    if index_of(alphabet, ch) < 0 {
      return false
    }
  }
  true
}

///|
/// True when the string is a valid short code (the separator comes before
/// position eight).
pub fn pluscode_is_short(code : String) -> Bool {
  if !pluscode_is_valid(code) {
    return false
  }
  let chars = chars_of(ascii_upper(trim_ascii(code)))
  let sep = index_of(chars, PLUSCODE_SEPARATOR)
  sep >= 0 && sep < 8
}

///|
/// True when the string is a valid full code: valid, not short, and with the
/// first pair inside the latitude/longitude ranges.
pub fn pluscode_is_full(code : String) -> Bool {
  if !pluscode_is_valid(code) || pluscode_is_short(code) {
    return false
  }
  let chars = chars_of(ascii_upper(trim_ascii(code)))
  let alphabet = chars_of(PLUSCODE_ALPHABET)
  let first_lat = index_of(alphabet, chars[0])
  let first_lng = index_of(alphabet, chars[1])
  if first_lat < 0 || first_lng < 0 {
    return false
  }
  first_lat * 20 < 180 && first_lng * 20 < 360
}

///|
/// The number of significant characters in a full code (separator and padding
/// removed), capped at 15.
pub fn pluscode_code_length(code : String) -> Int {
  let chars = chars_of(ascii_upper(trim_ascii(code)))
  let mut n = 0
  for ch in chars {
    if ch != PLUSCODE_SEPARATOR && ch != PLUSCODE_PADDING {
      n = n + 1
    }
  }
  if n > 15 {
    15
  } else {
    n
  }
}

///|
/// The bounding box a full Plus Code denotes.
pub fn pluscode_bounds(code : String) -> BoundingBox? {
  if !pluscode_is_full(code) {
    return None
  }
  let alphabet = chars_of(PLUSCODE_ALPHABET)
  let raw = chars_of(ascii_upper(trim_ascii(code)))
  let mut chars : Array[Char] = []
  for ch in raw {
    if ch != PLUSCODE_SEPARATOR && ch != PLUSCODE_PADDING {
      chars.push(ch)
    }
  }
  if chars.length() > 15 {
    let cut : Array[Char] = []
    for i = 0; i < 15; i = i + 1 {
      cut.push(chars[i])
    }
    chars = cut
  }
  let digits = if chars.length() < 10 { chars.length() } else { 10 }
  let mut normal_lat = -90L * OLC_PAIR_PRECISION
  let mut normal_lng = -180L * OLC_PAIR_PRECISION
  let mut place = OLC_PAIR_FIRST_PLACE
  let mut i = 0
  while i < digits {
    normal_lat = normal_lat + index_of(alphabet, chars[i]).to_int64() * place
    normal_lng = normal_lng +
      index_of(alphabet, chars[i + 1]).to_int64() * place
    if i < digits - 2 {
      place = place / 20L
    }
    i = i + 2
  }
  let mut lat_precision = place.to_double() / OLC_PAIR_PRECISION.to_double()
  let mut lng_precision = lat_precision
  let mut grid_lat = 0L
  let mut grid_lng = 0L
  if chars.length() > 10 {
    // The place values always start at the finest pair (5^4 and 4^4) and
    // divide once per grid character, regardless of how many are present.
    let mut row_place = 625L
    let mut col_place = 256L
    let mut j = 10
    while j < chars.length() {
      let value = index_of(alphabet, chars[j])
      let row = value / 4
      let col = value % 4
      grid_lat = grid_lat + row.to_int64() * row_place
      grid_lng = grid_lng + col.to_int64() * col_place
      if j < chars.length() - 1 {
        row_place = row_place / 5L
        col_place = col_place / 4L
      }
      j = j + 1
    }
    lat_precision = row_place.to_double() / OLC_FINAL_LAT_PRECISION.to_double()
    lng_precision = col_place.to_double() / OLC_FINAL_LNG_PRECISION.to_double()
  }
  let lat = round_to(
    normal_lat.to_double() / OLC_PAIR_PRECISION.to_double() +
    grid_lat.to_double() / OLC_FINAL_LAT_PRECISION.to_double(),
    14,
  )
  let lng = round_to(
    normal_lng.to_double() / OLC_PAIR_PRECISION.to_double() +
    grid_lng.to_double() / OLC_FINAL_LNG_PRECISION.to_double(),
    14,
  )
  BoundingBox::new(lat, lng, lat + lat_precision, lng + lng_precision)
}

///|
/// The centre of a full Plus Code.
pub fn pluscode_decode(code : String) -> LatLng? {
  match pluscode_bounds(code) {
    None => None
    Some(b) => b.center()
  }
}

///|
/// The cell immediately `dx` cells east and `dy` cells north, at the same
/// precision. At a pole the latitude is clamped, so a northward move from the
/// top row can repeat the same cell.
pub fn pluscode_adjacent(code : String, dx : Int, dy : Int) -> String? {
  let bounds = match pluscode_bounds(code) {
    None => return None
    Some(b) => b
  }
  let center = match bounds.center() {
    None => return None
    Some(c) => c
  }
  let lat = clamp_lat(center.lat() + bounds.height() * dy.to_double())
  let lng = normalize_lng(center.lng() + bounds.width() * dx.to_double())
  let point = match LatLng::new(lat, lng) {
    None => return None
    Some(p) => p
  }
  pluscode_encode(point, code_length=pluscode_code_length(code))
}

///|
/// The eight neighbouring cells, in the same order as `geohash_neighbors`.
pub fn pluscode_neighbors(code : String) -> Array[String]? {
  if pluscode_bounds(code) is None {
    return None
  }
  let out : Array[String] = []
  for dy in [-1, 0, 1] {
    for dx in [-1, 0, 1] {
      if dx != 0 || dy != 0 {
        match pluscode_adjacent(code, dx, dy) {
          Some(n) => out.push(n)
          None => return None
        }
      }
    }
  }
  if out.length() != 8 {
    return None
  }
  Some(out)
}

///|
/// Remove as many leading characters as the reference point safely allows.
/// Returns the original code when the reference is not close enough, and
/// `None` when the input is not a full, unpadded code of at least six digits.
pub fn pluscode_shorten(code : String, reference : LatLng) -> String? {
  if !pluscode_is_full(code) {
    return None
  }
  let s = ascii_upper(trim_ascii(code))
  if s.contains("0") {
    return None
  }
  let length = pluscode_code_length(s)
  if length < 6 {
    return None
  }
  let center = match pluscode_decode(s) {
    None => return None
    Some(p) => p
  }
  let range = max_double(
    (center.lat() - reference.lat()).abs(),
    lng_delta(center.lng(), reference.lng()).abs(),
  )
  let mut i = 3
  while i > 0 {
    if range < olc_pair_resolutions[i] * 0.3 {
      return Some(s[(i + 1) * 2:].to_owned())
    }
    i = i - 1
  }
  Some(s)
}

///|
/// Recover a full code from a short code, choosing the candidate nearest to
/// `reference`. A full code is returned normalised.
pub fn pluscode_recover(short : String, reference : LatLng) -> String? {
  let s = ascii_upper(trim_ascii(short))
  if pluscode_is_full(s) {
    return Some(s)
  }
  if !pluscode_is_short(s) {
    return None
  }
  let chars = chars_of(s)
  let sep = index_of(chars, PLUSCODE_SEPARATOR)
  let padding_length = 8 - sep
  let resolution = pow_double(20.0, 2.0 - padding_length.to_double() / 2.0)
  let half = resolution / 2.0
  let prefix = match pluscode_encode(reference, code_length=10) {
    None => return None
    Some(full) => full[0:padding_length].to_owned()
  }
  let combined = prefix + s
  let center = match pluscode_decode(combined) {
    None => return None
    Some(p) => p
  }
  let mut lat = center.lat()
  let mut lng = center.lng()
  if reference.lat() + half < lat && lat - resolution >= -90.0 {
    lat = lat - resolution
  } else if reference.lat() - half > lat && lat + resolution <= 90.0 {
    lat = lat + resolution
  }
  if lng_delta(reference.lng(), lng) > half {
    lng = normalize_lng(lng - resolution)
  } else if lng_delta(lng, reference.lng()) > half {
    lng = normalize_lng(lng + resolution)
  }
  match LatLng::new(lat, lng) {
    None => None
    Some(adjusted) =>
      pluscode_encode(adjusted, code_length=pluscode_code_length(combined))
  }
}

///|
fn pow_double(base : Double, exponent : Double) -> Double {
  @math.pow(base, exponent)
}

///|
fn max_double(a : Double, b : Double) -> Double {
  if a > b {
    a
  } else {
    b
  }
}