///|
/// A latitude/longitude pair in decimal degrees.
pub struct LatLng {
  latitude : Double
  longitude : Double
} derive(Eq, Debug)

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

///|
pub fn LatLng::longitude(self : LatLng) -> Double {
  self.longitude
}

///|
priv struct Vec3(Double, Double, Double)

///|
priv struct CoordIJK(Int, Int, Int)

///|
priv struct FaceIJK(Int, CoordIJK)

///|
priv struct FaceOrientIJK(Int, CoordIJK, Int)

///|
priv enum Overage {
  NoOverage
  NewFace
} derive(Eq)

///|
const TWO_PI : Double = 6.283185307179586476925286766559005768

///|
const DEGREES_PER_RADIAN : Double = 57.295779513082320876798154814105170332

///|
const EPSILON : Double = 1.0e-16

///|
const SQRT3_OVER_2 : Double = 0.866025403784438646763723170752936183

///|
const AP7_ROT_RADS : Double = 0.333473172251832115336090755351601070

///|
const RES0_U_GNOMONIC : Double = 0.38196601125010500003

///|
const INVERSE_SQRT7 : Double = 0.3779644730092272272145165362341800608

///|
let face_center_points : Array[Vec3] = [
  Vec3(0.2199307791404606, 0.6583691780274996, 0.7198475378926182),
  Vec3(-0.2139234834501421, 0.1478171829550703, 0.9656017935214205),
  Vec3(0.1092625278784797, -0.4811951572873210, 0.8697775121287253),
  Vec3(0.7428567301586791, -0.3593941678278028, 0.5648005936517033),
  Vec3(0.8112534709140969, 0.3448953237639384, 0.4721387736413930),
  Vec3(-0.1055498149613921, 0.9794457296411413, 0.1718874610009365),
  Vec3(-0.8075407579970092, 0.1533552485898818, 0.5695261994882688),
  Vec3(-0.2846148069787907, -0.8644080972654206, 0.4144792552473539),
  Vec3(0.7405621473854482, -0.6673299564565524, -0.0789837646326737),
  Vec3(0.8512303986474293, 0.4722343788582681, -0.2289137388687808),
  Vec3(-0.7405621473854481, 0.6673299564565524, 0.0789837646326737),
  Vec3(-0.8512303986474292, -0.4722343788582682, 0.2289137388687808),
  Vec3(0.1055498149613919, -0.9794457296411413, -0.1718874610009365),
  Vec3(0.8075407579970092, -0.1533552485898819, -0.5695261994882688),
  Vec3(0.2846148069787908, 0.8644080972654204, -0.4144792552473539),
  Vec3(-0.7428567301586791, 0.3593941678278027, -0.5648005936517033),
  Vec3(-0.8112534709140971, -0.3448953237639382, -0.4721387736413930),
  Vec3(-0.2199307791404607, -0.6583691780274996, -0.7198475378926182),
  Vec3(0.2139234834501420, -0.1478171829550704, -0.9656017935214205),
  Vec3(-0.1092625278784796, 0.4811951572873210, -0.8697775121287253),
]

///|
let face_axes_azimuth : Array[Double] = [
  5.619958268523939882, 5.760339081714187279, 0.780213654393430055, 0.430469363979999913,
  6.130269123335111400, 2.692877706530642877, 2.982963003477243874, 3.532912002790141181,
  3.494305004259568154, 3.003214169499538391, 5.930472956509811562, 0.138378484090254847,
  0.448714947059150361, 0.158629650112549365, 5.891865957979238535, 2.711123289609793325,
  3.294508837434268316, 3.804819692245439833, 3.664438879055192436, 2.361378999196363184,
]

///|
let base_cell_home : Array[(Int, Int, Int, Int)] = [
  (1, 1, 0, 0),
  (2, 1, 1, 0),
  (1, 0, 0, 0),
  (2, 1, 0, 0),
  (0, 2, 0, 0),
  (1, 1, 1, 0),
  (1, 0, 0, 1),
  (2, 0, 0, 0),
  (0, 1, 0, 0),
  (2, 0, 1, 0),
  (1, 0, 1, 0),
  (1, 0, 1, 1),
  (3, 1, 0, 0),
  (3, 1, 1, 0),
  (11, 2, 0, 0),
  (4, 1, 0, 0),
  (0, 0, 0, 0),
  (6, 0, 1, 0),
  (0, 0, 0, 1),
  (2, 0, 1, 1),
  (7, 0, 0, 1),
  (2, 0, 0, 1),
  (0, 1, 1, 0),
  (6, 0, 0, 1),
  (10, 2, 0, 0),
  (6, 0, 0, 0),
  (3, 0, 0, 0),
  (11, 1, 0, 0),
  (4, 1, 1, 0),
  (3, 0, 1, 0),
  (0, 0, 1, 1),
  (4, 0, 0, 0),
  (5, 0, 1, 0),
  (0, 0, 1, 0),
  (7, 0, 1, 0),
  (11, 1, 1, 0),
  (7, 0, 0, 0),
  (10, 1, 0, 0),
  (12, 2, 0, 0),
  (6, 1, 0, 1),
  (7, 1, 0, 1),
  (4, 0, 0, 1),
  (3, 0, 0, 1),
  (3, 0, 1, 1),
  (4, 0, 1, 0),
  (6, 1, 0, 0),
  (11, 0, 0, 0),
  (8, 0, 0, 1),
  (5, 0, 0, 1),
  (14, 2, 0, 0),
  (5, 0, 0, 0),
  (12, 1, 0, 0),
  (10, 1, 1, 0),
  (4, 0, 1, 1),
  (12, 1, 1, 0),
  (7, 1, 0, 0),
  (11, 0, 1, 0),
  (10, 0, 0, 0),
  (13, 2, 0, 0),
  (10, 0, 0, 1),
  (11, 0, 0, 1),
  (9, 0, 1, 0),
  (8, 0, 1, 0),
  (6, 2, 0, 0),
  (8, 0, 0, 0),
  (9, 0, 0, 1),
  (14, 1, 0, 0),
  (5, 1, 0, 1),
  (16, 0, 1, 1),
  (8, 1, 0, 1),
  (5, 1, 0, 0),
  (12, 0, 0, 0),
  (7, 2, 0, 0),
  (12, 0, 1, 0),
  (10, 0, 1, 0),
  (9, 0, 0, 0),
  (13, 1, 0, 0),
  (16, 0, 0, 1),
  (15, 0, 1, 1),
  (15, 0, 1, 0),
  (16, 0, 1, 0),
  (14, 1, 1, 0),
  (13, 1, 1, 0),
  (5, 2, 0, 0),
  (8, 1, 0, 0),
  (14, 0, 0, 0),
  (9, 1, 0, 1),
  (14, 0, 0, 1),
  (17, 0, 0, 1),
  (12, 0, 0, 1),
  (16, 0, 0, 0),
  (17, 0, 1, 1),
  (15, 0, 0, 1),
  (16, 1, 0, 1),
  (9, 1, 0, 0),
  (15, 0, 0, 0),
  (13, 0, 0, 0),
  (8, 2, 0, 0),
  (13, 0, 1, 0),
  (17, 1, 0, 1),
  (19, 0, 1, 0),
  (14, 0, 1, 0),
  (19, 0, 1, 1),
  (17, 0, 1, 0),
  (13, 0, 0, 1),
  (17, 0, 0, 0),
  (16, 1, 0, 0),
  (9, 2, 0, 0),
  (15, 1, 0, 1),
  (15, 1, 0, 0),
  (18, 0, 1, 1),
  (18, 0, 0, 1),
  (19, 0, 0, 1),
  (17, 1, 0, 0),
  (19, 0, 0, 0),
  (18, 0, 1, 0),
  (18, 1, 0, 1),
  (19, 2, 0, 0),
  (19, 1, 0, 0),
  (18, 0, 0, 0),
  (19, 1, 0, 1),
  (18, 1, 0, 0),
]

///|
let face_neighbors : Array[Array[FaceOrientIJK]] = [
  [
    FaceOrientIJK(0, CoordIJK(0, 0, 0), 0),
    FaceOrientIJK(4, CoordIJK(2, 0, 2), 1),
    FaceOrientIJK(1, CoordIJK(2, 2, 0), 5),
    FaceOrientIJK(5, CoordIJK(0, 2, 2), 3),
  ],
  [
    FaceOrientIJK(1, CoordIJK(0, 0, 0), 0),
    FaceOrientIJK(0, CoordIJK(2, 0, 2), 1),
    FaceOrientIJK(2, CoordIJK(2, 2, 0), 5),
    FaceOrientIJK(6, CoordIJK(0, 2, 2), 3),
  ],
  [
    FaceOrientIJK(2, CoordIJK(0, 0, 0), 0),
    FaceOrientIJK(1, CoordIJK(2, 0, 2), 1),
    FaceOrientIJK(3, CoordIJK(2, 2, 0), 5),
    FaceOrientIJK(7, CoordIJK(0, 2, 2), 3),
  ],
  [
    FaceOrientIJK(3, CoordIJK(0, 0, 0), 0),
    FaceOrientIJK(2, CoordIJK(2, 0, 2), 1),
    FaceOrientIJK(4, CoordIJK(2, 2, 0), 5),
    FaceOrientIJK(8, CoordIJK(0, 2, 2), 3),
  ],
  [
    FaceOrientIJK(4, CoordIJK(0, 0, 0), 0),
    FaceOrientIJK(3, CoordIJK(2, 0, 2), 1),
    FaceOrientIJK(0, CoordIJK(2, 2, 0), 5),
    FaceOrientIJK(9, CoordIJK(0, 2, 2), 3),
  ],
  [
    FaceOrientIJK(5, CoordIJK(0, 0, 0), 0),
    FaceOrientIJK(10, CoordIJK(2, 2, 0), 3),
    FaceOrientIJK(14, CoordIJK(2, 0, 2), 3),
    FaceOrientIJK(0, CoordIJK(0, 2, 2), 3),
  ],
  [
    FaceOrientIJK(6, CoordIJK(0, 0, 0), 0),
    FaceOrientIJK(11, CoordIJK(2, 2, 0), 3),
    FaceOrientIJK(10, CoordIJK(2, 0, 2), 3),
    FaceOrientIJK(1, CoordIJK(0, 2, 2), 3),
  ],
  [
    FaceOrientIJK(7, CoordIJK(0, 0, 0), 0),
    FaceOrientIJK(12, CoordIJK(2, 2, 0), 3),
    FaceOrientIJK(11, CoordIJK(2, 0, 2), 3),
    FaceOrientIJK(2, CoordIJK(0, 2, 2), 3),
  ],
  [
    FaceOrientIJK(8, CoordIJK(0, 0, 0), 0),
    FaceOrientIJK(13, CoordIJK(2, 2, 0), 3),
    FaceOrientIJK(12, CoordIJK(2, 0, 2), 3),
    FaceOrientIJK(3, CoordIJK(0, 2, 2), 3),
  ],
  [
    FaceOrientIJK(9, CoordIJK(0, 0, 0), 0),
    FaceOrientIJK(14, CoordIJK(2, 2, 0), 3),
    FaceOrientIJK(13, CoordIJK(2, 0, 2), 3),
    FaceOrientIJK(4, CoordIJK(0, 2, 2), 3),
  ],
  [
    FaceOrientIJK(10, CoordIJK(0, 0, 0), 0),
    FaceOrientIJK(5, CoordIJK(2, 2, 0), 3),
    FaceOrientIJK(6, CoordIJK(2, 0, 2), 3),
    FaceOrientIJK(15, CoordIJK(0, 2, 2), 3),
  ],
  [
    FaceOrientIJK(11, CoordIJK(0, 0, 0), 0),
    FaceOrientIJK(6, CoordIJK(2, 2, 0), 3),
    FaceOrientIJK(7, CoordIJK(2, 0, 2), 3),
    FaceOrientIJK(16, CoordIJK(0, 2, 2), 3),
  ],
  [
    FaceOrientIJK(12, CoordIJK(0, 0, 0), 0),
    FaceOrientIJK(7, CoordIJK(2, 2, 0), 3),
    FaceOrientIJK(8, CoordIJK(2, 0, 2), 3),
    FaceOrientIJK(17, CoordIJK(0, 2, 2), 3),
  ],
  [
    FaceOrientIJK(13, CoordIJK(0, 0, 0), 0),
    FaceOrientIJK(8, CoordIJK(2, 2, 0), 3),
    FaceOrientIJK(9, CoordIJK(2, 0, 2), 3),
    FaceOrientIJK(18, CoordIJK(0, 2, 2), 3),
  ],
  [
    FaceOrientIJK(14, CoordIJK(0, 0, 0), 0),
    FaceOrientIJK(9, CoordIJK(2, 2, 0), 3),
    FaceOrientIJK(5, CoordIJK(2, 0, 2), 3),
    FaceOrientIJK(19, CoordIJK(0, 2, 2), 3),
  ],
  [
    FaceOrientIJK(15, CoordIJK(0, 0, 0), 0),
    FaceOrientIJK(16, CoordIJK(2, 0, 2), 1),
    FaceOrientIJK(19, CoordIJK(2, 2, 0), 5),
    FaceOrientIJK(10, CoordIJK(0, 2, 2), 3),
  ],
  [
    FaceOrientIJK(16, CoordIJK(0, 0, 0), 0),
    FaceOrientIJK(17, CoordIJK(2, 0, 2), 1),
    FaceOrientIJK(15, CoordIJK(2, 2, 0), 5),
    FaceOrientIJK(11, CoordIJK(0, 2, 2), 3),
  ],
  [
    FaceOrientIJK(17, CoordIJK(0, 0, 0), 0),
    FaceOrientIJK(18, CoordIJK(2, 0, 2), 1),
    FaceOrientIJK(16, CoordIJK(2, 2, 0), 5),
    FaceOrientIJK(12, CoordIJK(0, 2, 2), 3),
  ],
  [
    FaceOrientIJK(18, CoordIJK(0, 0, 0), 0),
    FaceOrientIJK(19, CoordIJK(2, 0, 2), 1),
    FaceOrientIJK(17, CoordIJK(2, 2, 0), 5),
    FaceOrientIJK(13, CoordIJK(0, 2, 2), 3),
  ],
  [
    FaceOrientIJK(19, CoordIJK(0, 0, 0), 0),
    FaceOrientIJK(15, CoordIJK(2, 0, 2), 1),
    FaceOrientIJK(18, CoordIJK(2, 2, 0), 5),
    FaceOrientIJK(14, CoordIJK(0, 2, 2), 3),
  ],
]

///|
let max_dimension : Array[Int] = [
  2, 0, 14, 0, 98, 0, 686, 0, 4802, 0, 33614, 0, 235298, 0, 1647086, 0, 11529602,
]

///|
let unit_scale : Array[Int] = [
  1, 0, 7, 0, 49, 0, 343, 0, 2401, 0, 16807, 0, 117649, 0, 823543, 0, 5764801,
]

///|
fn is_resolution_class_iii(resolution : Int) -> Bool {
  resolution % 2 == 1
}

///|
fn leading_nonzero_digit(value : UInt64) -> Int {
  let mut current = 1
  while current <= resolution_of(value) {
    let digit = digit_of(value, current)
    if digit != 0 {
      return digit
    }
    current += 1
  }
  0
}

///|
fn rotate_digit_60cw(digit : Int) -> Int {
  match digit {
    1 => 3
    3 => 2
    2 => 6
    6 => 4
    4 => 5
    5 => 1
    _ => digit
  }
}

///|
fn set_digit(value : UInt64, resolution : Int, digit : Int) -> UInt64 {
  let shift = (MAX_RESOLUTION - resolution) * 3
  let mask = 7UL << shift
  (value & UInt64::lnot(mask)) | (digit.to_uint64() << shift)
}

///|
fn rotate_index_60cw(value : UInt64) -> UInt64 {
  let mut rotated = value
  let mut current = 1
  while current <= resolution_of(value) {
    rotated = set_digit(
      rotated,
      current,
      rotate_digit_60cw(digit_of(rotated, current)),
    )
    current += 1
  }
  rotated
}

///|
fn coord(i : Int, j : Int, k : Int) -> CoordIJK {
  CoordIJK(i, j, k)
}

///|
fn coord_add(left : CoordIJK, right : CoordIJK) -> CoordIJK {
  CoordIJK(left.0 + right.0, left.1 + right.1, left.2 + right.2)
}

///|
fn coord_sub(left : CoordIJK, right : CoordIJK) -> CoordIJK {
  CoordIJK(left.0 - right.0, left.1 - right.1, left.2 - right.2)
}

///|
fn coord_scale(value : CoordIJK, factor : Int) -> CoordIJK {
  CoordIJK(value.0 * factor, value.1 * factor, value.2 * factor)
}

///|
fn coord_normalize(value : CoordIJK) -> CoordIJK {
  let mut i = value.0
  let mut j = value.1
  let mut k = value.2
  if i < 0 {
    j -= i
    k -= i
    i = 0
  }
  if j < 0 {
    i -= j
    k -= j
    j = 0
  }
  if k < 0 {
    i -= k
    j -= k
    k = 0
  }
  let minimum = Int::min(i, Int::min(j, k))
  if minimum > 0 {
    i -= minimum
    j -= minimum
    k -= minimum
  }
  CoordIJK(i, j, k)
}

///|
fn coord_down_ap7(value : CoordIJK) -> CoordIJK {
  let i_vec = coord_scale(coord(3, 0, 1), value.0)
  let j_vec = coord_scale(coord(1, 3, 0), value.1)
  let k_vec = coord_scale(coord(0, 1, 3), value.2)
  coord_normalize(coord_add(coord_add(i_vec, j_vec), k_vec))
}

///|
fn coord_down_ap7r(value : CoordIJK) -> CoordIJK {
  let i_vec = coord_scale(coord(3, 1, 0), value.0)
  let j_vec = coord_scale(coord(0, 3, 1), value.1)
  let k_vec = coord_scale(coord(1, 0, 3), value.2)
  coord_normalize(coord_add(coord_add(i_vec, j_vec), k_vec))
}

///|
fn coord_up_ap7r(value : CoordIJK) -> CoordIJK {
  let i = value.0 - value.2
  let j = value.1 - value.2
  coord_normalize(
    CoordIJK(
      ((2 * i + j).to_double() / 7.0).round().to_int(),
      ((3 * j - i).to_double() / 7.0).round().to_int(),
      0,
    ),
  )
}

///|
fn coord_rotate_60ccw(value : CoordIJK) -> CoordIJK {
  let i_vec = coord_scale(coord(1, 1, 0), value.0)
  let j_vec = coord_scale(coord(0, 1, 1), value.1)
  let k_vec = coord_scale(coord(1, 0, 1), value.2)
  coord_normalize(coord_add(coord_add(i_vec, j_vec), k_vec))
}

///|
fn coord_rotate_60cw(value : CoordIJK) -> CoordIJK {
  let i_vec = coord_scale(coord(1, 0, 1), value.0)
  let j_vec = coord_scale(coord(1, 1, 0), value.1)
  let k_vec = coord_scale(coord(0, 1, 1), value.2)
  coord_normalize(coord_add(coord_add(i_vec, j_vec), k_vec))
}

///|
fn coord_neighbor(value : CoordIJK, digit : Int) -> CoordIJK {
  let unit = match digit {
    1 => coord(0, 0, 1)
    2 => coord(0, 1, 0)
    3 => coord(0, 1, 1)
    4 => coord(1, 0, 0)
    5 => coord(1, 0, 1)
    6 => coord(1, 1, 0)
    _ => coord(0, 0, 0)
  }
  coord_normalize(coord_add(value, unit))
}

///|
fn home_face_ijk(base_cell : Int) -> FaceIJK {
  let (face, i, j, k) = base_cell_home[base_cell]
  FaceIJK(face, coord(i, j, k))
}

///|
fn adjust_overage_class_ii(
  face_ijk : FaceIJK,
  resolution : Int,
  pentagon_leading_4 : Bool,
) -> (FaceIJK, Overage) {
  let maximum = max_dimension[resolution]
  let current = face_ijk.1
  if current.0 + current.1 + current.2 <= maximum {
    return (face_ijk, NoOverage)
  }
  let mut index = current
  let quadrant = if index.2 > 0 {
    if index.1 > 0 {
      3
    } else {
      if pentagon_leading_4 {
        let origin = coord(maximum, 0, 0)
        index = coord_add(coord_rotate_60cw(coord_sub(index, origin)), origin)
      }
      2
    }
  } else {
    1
  }
  let orientation = face_neighbors[face_ijk.0][quadrant]
  let mut rotated = index
  let mut rotation = 0
  while rotation < orientation.2 {
    rotated = coord_rotate_60ccw(rotated)
    rotation += 1
  }
  let translated = coord_add(
    rotated,
    coord_scale(orientation.1, unit_scale[resolution]),
  )
  (FaceIJK(orientation.0, coord_normalize(translated)), NewFace)
}

///|
fn index_to_face_ijk(value : UInt64) -> FaceIJK {
  let base_cell = base_cell_of(value)
  let pentagon = is_pentagon_base_cell(base_cell)
  let leading = leading_nonzero_digit(value)
  let adjusted = if pentagon && leading == 5 {
    rotate_index_60cw(value)
  } else {
    value
  }
  let home = home_face_ijk(base_cell)
  let resolution = resolution_of(adjusted)
  let mut index = home.1
  let mut current = 1
  while current <= resolution {
    index = if is_resolution_class_iii(current) {
      coord_down_ap7(index)
    } else {
      coord_down_ap7r(index)
    }
    index = coord_neighbor(index, digit_of(adjusted, current))
    current += 1
  }
  let possible_overage = if !pentagon &&
    (resolution == 0 || (home.1.0 == 0 && home.1.1 == 0 && home.1.2 == 0)) {
    false
  } else {
    true
  }
  if !possible_overage {
    return FaceIJK(home.0, index)
  }
  let original = index
  let mut face = home.0
  let mut adjusted_resolution = resolution
  if is_resolution_class_iii(adjusted_resolution) {
    index = coord_down_ap7r(index)
    adjusted_resolution += 1
  }
  let pentagon_leading_4 = pentagon && leading == 4
  let (adjusted_face, overage) = adjust_overage_class_ii(
    FaceIJK(face, index),
    adjusted_resolution,
    pentagon_leading_4,
  )
  face = adjusted_face.0
  index = adjusted_face.1
  if overage != NoOverage {
    if pentagon {
      let mut more = true
      while more {
        let (next_face, next_overage) = adjust_overage_class_ii(
          FaceIJK(face, index),
          adjusted_resolution,
          false,
        )
        face = next_face.0
        index = next_face.1
        more = next_overage != NoOverage
      }
    }
    if adjusted_resolution != resolution {
      index = coord_up_ap7r(index)
    }
  } else if adjusted_resolution != resolution {
    index = original
  }
  FaceIJK(face, index)
}

///|
fn vec3_add(left : Vec3, right : Vec3) -> Vec3 {
  Vec3(left.0 + right.0, left.1 + right.1, left.2 + right.2)
}

///|
fn vec3_scale(value : Vec3, factor : Double) -> Vec3 {
  Vec3(value.0 * factor, value.1 * factor, value.2 * factor)
}

///|
fn vec3_linear_combination(
  left_factor : Double,
  left : Vec3,
  right_factor : Double,
  right : Vec3,
) -> Vec3 {
  vec3_add(vec3_scale(left, left_factor), vec3_scale(right, right_factor))
}

///|
fn vec3_dot(left : Vec3, right : Vec3) -> Double {
  left.0 * right.0 + left.1 * right.1 + left.2 * right.2
}

///|
fn vec3_cross(left : Vec3, right : Vec3) -> Vec3 {
  Vec3(
    left.1 * right.2 - left.2 * right.1,
    left.2 * right.0 - left.0 * right.2,
    left.0 * right.1 - left.1 * right.0,
  )
}

///|
fn vec3_normalize(value : Vec3) -> Vec3 {
  let norm = vec3_dot(value, value).sqrt()
  let factor = if norm > 0.0 { 1.0 / norm } else { 0.0 }
  vec3_scale(value, factor)
}

///|
fn positive_angle(radians : Double) -> Double {
  let mut angle = radians
  if angle < 0.0 {
    angle += TWO_PI
  }
  if angle >= TWO_PI {
    angle -= TWO_PI
  }
  angle
}

///|
fn ijk_to_hex_2d(value : CoordIJK) -> (Double, Double) {
  let i = value.0 - value.2
  let j = value.1 - value.2
  (i.to_double() - 0.5 * j.to_double(), j.to_double() * SQRT3_OVER_2)
}

///|
fn tangent_basis(point : Vec3) -> (Vec3, Vec3) {
  let north_pole = Vec3(0.0, 0.0, 1.0)
  let north = vec3_normalize(
    vec3_linear_combination(
      1.0,
      north_pole,
      -vec3_dot(north_pole, point),
      point,
    ),
  )
  let east = vec3_cross(north, point)
  (north, east)
}

///|
fn hex_2d_to_vec3(x : Double, y : Double, face : Int, resolution : Int) -> Vec3 {
  let mut radius = (x * x + y * y).sqrt()
  if radius < EPSILON {
    return face_center_points[face]
  }
  let mut theta = @math.atan2(y, x)
  let mut current = 0
  while current < resolution {
    radius *= INVERSE_SQRT7
    current += 1
  }
  radius *= RES0_U_GNOMONIC
  radius = @math.atan(radius)
  if is_resolution_class_iii(resolution) {
    theta = positive_angle(theta + AP7_ROT_RADS)
  }
  theta = positive_angle(face_axes_azimuth[face] - theta)
  let center = face_center_points[face]
  let (north, east) = tangent_basis(center)
  let direction = vec3_linear_combination(
    @math.cos(theta),
    north,
    @math.sin(theta),
    east,
  )
  vec3_normalize(
    vec3_linear_combination(
      @math.cos(radius),
      center,
      @math.sin(radius),
      direction,
    ),
  )
}

///|
fn face_ijk_to_vec3(face_ijk : FaceIJK, resolution : Int) -> Vec3 {
  let (x, y) = ijk_to_hex_2d(face_ijk.1)
  hex_2d_to_vec3(x, y, face_ijk.0, resolution)
}

///|
/// Returns the center point of this H3 cell in decimal degrees.
pub fn H3Index::to_lat_lng(self : H3Index) -> LatLng {
  let center = face_ijk_to_vec3(index_to_face_ijk(self.0), self.resolution())
  {
    latitude: @math.asin(center.2) * DEGREES_PER_RADIAN,
    longitude: @math.atan2(center.1, center.0) * DEGREES_PER_RADIAN,
  }
}