Skip to main content

native/sidereon_nif/src/antex.rs

//! Rustler boundary for ANTEX calibration products.
//!
//! This module is glue only: Elixir passes text or struct-shaped rows, the
//! `sidereon-core` crate parses/selects/interpolates, and the NIF encodes
//! the unchanged Sidereon public struct payloads.

use rustler::{Encoder, Env, Error, NifResult, ResourceArc, Term};
use sidereon_core::antex::{
    Antenna, AntennaKind, Antex, AntexDateTime, Frequency, PcvGrid, PcvSample,
};
use std::collections::BTreeMap;

/// Resource handle holding the parsed ANTEX product across NIF calls, so the
/// serializer can re-emit the full multi-interval product the lossy per-id row
/// view does not carry. Mirrors the SP3/RINEX-OBS parse-to-handle pattern.
pub struct AntexResource {
    pub antex: Antex,
}

#[rustler::resource_impl]
impl rustler::Resource for AntexResource {}

type Vec3 = (f64, f64, f64);
type DateTuple = (i32, i32, i32);
type TimeTuple = (i32, i32, i32, i32);
type DateTimeTuple = (DateTuple, TimeTuple);
type PcvSampleTerm = (String, Option<f64>, f64, f64);
type FrequencyTerm = (String, Vec3, Vec<PcvSampleTerm>);
type AntennaTerm = (
    (String, String, String, String),
    (f64, f64, f64, f64),
    (Option<String>, Option<DateTimeTuple>, Option<DateTimeTuple>),
    Vec<FrequencyTerm>,
);

mod atoms {
    rustler::atoms! {
        ok,
        error,
        not_found,
        unknown_frequency
    }
}

#[rustler::nif(schedule = "DirtyCpu")]
fn antex_parse<'a>(env: Env<'a>, text: String) -> Term<'a> {
    match Antex::parse(&text) {
        Ok(antex) => {
            let rows = encode_antennas(env, antex.antennas.values());
            let handle = ResourceArc::new(AntexResource { antex });
            (atoms::ok(), rows, handle).encode(env)
        }
        Err(err) => (atoms::error(), err.to_string()).encode(env),
    }
}

/// Serialize the held ANTEX product back to ANTEX 1.4 text. Pure delegation to
/// `Antex::encode`; no formatting lives here.
#[rustler::nif(schedule = "DirtyCpu")]
fn antex_encode(handle: ResourceArc<AntexResource>) -> String {
    handle.antex.encode()
}

#[rustler::nif]
fn antex_satellite_antenna<'a>(
    env: Env<'a>,
    antennas: Vec<AntennaTerm>,
    prn: String,
    datetime: DateTimeTuple,
) -> NifResult<Term<'a>> {
    let antennas = decode_antennas_map(antennas)?;
    let epoch = decode_datetime(datetime)?;
    let prn = prn.trim();
    // Mirror the core `Antex::satellite_antenna` selection: the satellite block
    // whose serial matches the PRN and whose validity interval covers the epoch.
    let found = antennas.values().find(|antenna| {
        antenna.kind == AntennaKind::Satellite
            && antenna.serial.trim() == prn
            && antenna.valid_at(epoch)
    });
    match found {
        Some(antenna) => Ok((atoms::ok(), encode_antenna(env, antenna)).encode(env)),
        None => Ok((atoms::error(), atoms::not_found()).encode(env)),
    }
}

#[rustler::nif]
fn antex_pco<'a>(env: Env<'a>, antenna: AntennaTerm, frequency: String) -> NifResult<Term<'a>> {
    let antenna = decode_antenna(antenna)?;
    match antenna.pco(&frequency) {
        Ok(pco) => Ok((atoms::ok(), array_to_vec3(pco)).encode(env)),
        Err(_) => Ok((atoms::error(), atoms::unknown_frequency()).encode(env)),
    }
}

#[rustler::nif]
fn antex_pcv<'a>(
    env: Env<'a>,
    antenna: AntennaTerm,
    frequency: String,
    zenith_deg: f64,
    azimuth_deg: Option<f64>,
) -> NifResult<Term<'a>> {
    let antenna = decode_antenna(antenna)?;
    // The Sidereon public PCV contract clamps to the antenna grid rather than
    // rejecting out-of-range zeniths. The hardened core `pcv` now refuses any
    // zenith outside [zenith_start_deg, zenith_end_deg], so clamp a finite
    // zenith into that grid before delegating: at the grid boundary the core's
    // own linear interpolation returns the boundary sample value, reproducing
    // the clamp exactly. Non-finite zeniths fall through to the core rejection.
    let zenith_deg = if zenith_deg.is_finite() {
        zenith_deg
            .max(antenna.zenith_start_deg)
            .min(antenna.zenith_end_deg)
    } else {
        zenith_deg
    };
    match antenna.pcv(&frequency, zenith_deg, azimuth_deg) {
        Ok(value_m) => Ok((atoms::ok(), value_m).encode(env)),
        Err(_) => Ok((atoms::error(), atoms::unknown_frequency()).encode(env)),
    }
}

fn encode_antennas<'a, 'b>(
    env: Env<'a>,
    antennas: impl Iterator<Item = &'b Antenna>,
) -> Vec<Term<'a>> {
    antennas
        .map(|antenna| encode_antenna(env, antenna))
        .collect()
}

fn encode_antenna<'a>(env: Env<'a>, antenna: &Antenna) -> Term<'a> {
    (
        (
            antenna.id.clone(),
            kind_string(antenna.kind),
            antenna.antenna_type.clone(),
            antenna.serial.clone(),
        ),
        (
            antenna.dazi_deg,
            antenna.zenith_start_deg,
            antenna.zenith_end_deg,
            antenna.zenith_step_deg,
        ),
        (
            antenna.sinex_code.clone(),
            antenna.valid_from.map(encode_datetime),
            antenna.valid_until.map(encode_datetime),
        ),
        encode_frequencies(antenna.frequencies.values()),
    )
        .encode(env)
}

fn encode_frequencies<'a>(frequencies: impl Iterator<Item = &'a Frequency>) -> Vec<FrequencyTerm> {
    frequencies
        .map(|frequency| {
            (
                frequency.frequency.clone(),
                array_to_vec3(frequency.pco_m),
                frequency
                    .pcv_samples
                    .iter()
                    .map(|sample| {
                        (
                            grid_string(sample.grid),
                            sample.azimuth_deg,
                            sample.zenith_deg,
                            sample.value_m,
                        )
                    })
                    .collect(),
            )
        })
        .collect()
}

fn decode_antennas_map(antennas: Vec<AntennaTerm>) -> NifResult<BTreeMap<String, Antenna>> {
    antennas
        .into_iter()
        .map(decode_antenna)
        .map(|result| result.map(|antenna| (antenna.id.clone(), antenna)))
        .collect::<NifResult<BTreeMap<_, _>>>()
}

fn decode_antenna(term: AntennaTerm) -> NifResult<Antenna> {
    let (
        (id, kind, antenna_type, serial),
        (dazi_deg, zenith_start_deg, zenith_end_deg, zenith_step_deg),
        (sinex_code, valid_from, valid_until),
        frequencies,
    ) = term;

    let frequencies = frequencies
        .into_iter()
        .map(|(frequency, pco, samples)| {
            let pcv_samples = samples
                .into_iter()
                .map(|(grid, azimuth_deg, zenith_deg, value_m)| PcvSample {
                    grid: decode_grid(&grid),
                    azimuth_deg,
                    zenith_deg,
                    value_m,
                })
                .collect();
            let freq = Frequency {
                frequency,
                pco_m: vec3_to_array(pco),
                pcv_samples,
            };
            (freq.frequency.clone(), freq)
        })
        .collect();

    Ok(Antenna {
        id,
        kind: decode_kind(&kind),
        antenna_type,
        serial,
        dazi_deg,
        zenith_start_deg,
        zenith_end_deg,
        zenith_step_deg,
        sinex_code,
        valid_from: valid_from.map(decode_datetime).transpose()?,
        valid_until: valid_until.map(decode_datetime).transpose()?,
        frequencies,
    })
}

fn decode_datetime(datetime: DateTimeTuple) -> NifResult<AntexDateTime> {
    let ((year, month, day), (hour, minute, second, _microsecond)) = datetime;
    let month = u8::try_from(month).map_err(|_| Error::Term(Box::new("bad month")))?;
    let day = u8::try_from(day).map_err(|_| Error::Term(Box::new("bad day")))?;
    let hour = u8::try_from(hour).map_err(|_| Error::Term(Box::new("bad hour")))?;
    let minute = u8::try_from(minute).map_err(|_| Error::Term(Box::new("bad minute")))?;
    let second = u8::try_from(second).map_err(|_| Error::Term(Box::new("bad second")))?;
    AntexDateTime::new(year, month, day, hour, minute, second)
        .map_err(|err| Error::Term(Box::new(err.to_string())))
}

fn encode_datetime(datetime: AntexDateTime) -> DateTimeTuple {
    (
        (
            datetime.year,
            i32::from(datetime.month),
            i32::from(datetime.day),
        ),
        (
            i32::from(datetime.hour),
            i32::from(datetime.minute),
            i32::from(datetime.second),
            0,
        ),
    )
}

fn kind_string(kind: AntennaKind) -> String {
    match kind {
        AntennaKind::Receiver => "receiver".to_string(),
        AntennaKind::Satellite => "satellite".to_string(),
    }
}

fn decode_kind(kind: &str) -> AntennaKind {
    if kind == "satellite" {
        AntennaKind::Satellite
    } else {
        AntennaKind::Receiver
    }
}

fn grid_string(grid: PcvGrid) -> String {
    match grid {
        PcvGrid::NoAzimuth => "noazi".to_string(),
        PcvGrid::Azimuth => "azi".to_string(),
    }
}

fn decode_grid(grid: &str) -> PcvGrid {
    if grid == "azi" {
        PcvGrid::Azimuth
    } else {
        PcvGrid::NoAzimuth
    }
}

fn vec3_to_array(vec: Vec3) -> [f64; 3] {
    [vec.0, vec.1, vec.2]
}

fn array_to_vec3(array: [f64; 3]) -> Vec3 {
    (array[0], array[1], array[2])
}