///|
pub struct Geodetic {
  latitude_rad : Double
  longitude_rad : Double
  altitude_km : Double
} derive(Debug, Eq)

///|
pub struct Topocentric {
  east_km : Double
  north_km : Double
  up_km : Double
} derive(Debug, Eq)

///|
pub struct LookAngle {
  azimuth_rad : Double
  elevation_rad : Double
  range_km : Double
} derive(Debug, Eq)

///|
pub fn Geodetic::new(
  latitude_rad : Double,
  longitude_rad : Double,
  altitude_km : Double,
) -> Geodetic {
  { latitude_rad, longitude_rad: normalize_angle(longitude_rad), altitude_km }
}

///|
pub fn geodetic_to_ecef(site : Geodetic) -> Vec3 {
  let r = earth_radius_km + site.altitude_km
  let cos_lat = @math.cos(site.latitude_rad)
  Vec3::new(
    r * cos_lat * @math.cos(site.longitude_rad),
    r * cos_lat * @math.sin(site.longitude_rad),
    r * @math.sin(site.latitude_rad),
  )
}

///|
pub fn ecef_to_geodetic_spherical(pos : Vec3) -> Geodetic {
  let r = pos.norm()
  let lat = @math.asin(clamp(pos.z / r, -1.0, 1.0))
  let lon = normalize_angle(@math.atan2(pos.y, pos.x))
  { latitude_rad: lat, longitude_rad: lon, altitude_km: r - earth_radius_km }
}

///|
pub fn rotate_z(v : Vec3, angle_rad : Double) -> Vec3 {
  let c = @math.cos(angle_rad)
  let s = @math.sin(angle_rad)
  Vec3::new(c * v.x - s * v.y, s * v.x + c * v.y, v.z)
}

///|
pub fn eci_to_ecef(epoch : Epoch, position_eci_km : Vec3) -> Vec3 {
  rotate_z(position_eci_km, -gmst(epoch))
}

///|
pub fn ecef_to_eci(epoch : Epoch, position_ecef_km : Vec3) -> Vec3 {
  rotate_z(position_ecef_km, gmst(epoch))
}

///|
pub fn topocentric(site : Geodetic, satellite_ecef_km : Vec3) -> Topocentric {
  let site_ecef = geodetic_to_ecef(site)
  let rho = satellite_ecef_km.sub(site_ecef)
  let sin_lat = @math.sin(site.latitude_rad)
  let cos_lat = @math.cos(site.latitude_rad)
  let sin_lon = @math.sin(site.longitude_rad)
  let cos_lon = @math.cos(site.longitude_rad)
  {
    east_km: -sin_lon * rho.x + cos_lon * rho.y,
    north_km: -sin_lat * cos_lon * rho.x -
    sin_lat * sin_lon * rho.y +
    cos_lat * rho.z,
    up_km: cos_lat * cos_lon * rho.x +
    cos_lat * sin_lon * rho.y +
    sin_lat * rho.z,
  }
}

///|
pub fn look_angle(site : Geodetic, satellite_ecef_km : Vec3) -> LookAngle {
  let topo = topocentric(site, satellite_ecef_km)
  let range = (topo.east_km * topo.east_km +
  topo.north_km * topo.north_km +
  topo.up_km * topo.up_km).sqrt()
  let az = normalize_angle(@math.atan2(topo.east_km, topo.north_km))
  let el = @math.asin(clamp(topo.up_km / range, -1.0, 1.0))
  { azimuth_rad: az, elevation_rad: el, range_km: range }
}

///|
pub fn look_angle_from_eci(
  site : Geodetic,
  epoch : Epoch,
  satellite_eci_km : Vec3,
) -> LookAngle {
  look_angle(site, eci_to_ecef(epoch, satellite_eci_km))
}