pnc 0.2.1

A fast, fully offline, terminal-only reverse Polish notation (RPN) calculator
use num_rational::BigRational;
use num_traits::ToPrimitive;
use pnc_engine::Value;

#[derive(Debug, Clone)]
pub enum PlotUnit {
    AllUnitless,
    SingleUnit(pnc_engine::UnitId),
    Temperature,
    Incompatible,
}

#[derive(Debug)]
pub enum PlotSkip {
    NoNumeric,
    Conversion,
}

#[derive(Debug, Clone)]
pub struct Series {
    pub points: Vec<usize>,
    pub label: Option<String>,
}

#[derive(Debug, Clone)]
pub struct PlotCache {
    pub series: Series,
    pub unit_class: PlotUnit,
    pub x_min: usize,
    pub x_max: usize,
    pub y_min_idx: usize,
    pub y_max_idx: usize,
    pub y_min: num_rational::BigRational,
    pub y_max: num_rational::BigRational,
    pub y_min_label: String,
    pub y_max_label: String,
    pub width: u16,
    pub height: u16,
}

pub fn unit_class(units: &[Option<pnc_engine::UnitId>]) -> PlotUnit {
    if units.is_empty() {
        return PlotUnit::Incompatible;
    }
    let mut has_unit = false;
    let mut first_unit: Option<pnc_engine::UnitId> = None;
    let mut all_same = true;
    let mut has_temperature = false;
    let mut has_unknown = false;

    for u in units {
        match u {
            None => {
                if has_unit {
                    return PlotUnit::Incompatible;
                }
            }
            Some(unit) => {
                if !has_unit {
                    first_unit = Some(unit.clone());
                    has_unit = true;
                } else if Some(unit.clone()) != first_unit {
                    all_same = false;
                }
                match unit.dimension() {
                    Some(pnc_engine::Dimension::Temperature) => has_temperature = true,
                    None => has_unknown = true,
                    _ => {}
                }
            }
        }
    }

    if !has_unit {
        return PlotUnit::AllUnitless;
    }

    if all_same {
        return PlotUnit::SingleUnit(first_unit.unwrap());
    }

    if has_temperature && !has_unknown {
        // Check if all are temperature
        let all_temp = units.iter().all(|u| match u {
            Some(unit) => unit.dimension() == Some(pnc_engine::Dimension::Temperature),
            None => false,
        });
        if all_temp {
            return PlotUnit::Temperature;
        }
    }

    PlotUnit::Incompatible
}

pub fn series_from_stack(stack: &[Value]) -> Result<Series, PlotSkip> {
    if stack.is_empty() {
        return Err(PlotSkip::NoNumeric);
    }
    let points: Vec<usize> = (0..stack.len()).collect();
    Ok(Series {
        points,
        label: None,
    })
}

pub fn normalize_value(value: &Value, class: &PlotUnit) -> Result<BigRational, PlotSkip> {
    match value {
        Value::Int(n) => Ok(BigRational::from_integer(n.clone())),
        Value::Rational(r) => Ok(r.clone()),
        Value::Unit { number, unit: _ } => {
            match class {
                PlotUnit::Temperature => {
                    // Normalize to Kelvin
                    pnc_engine::unit::convert(value, pnc_engine::UnitId::Kelvin)
                        .map(|v| match v {
                            Value::Unit { number, .. } => match number.as_ref() {
                                Value::Int(n) => BigRational::from_integer(n.clone()),
                                Value::Rational(r) => r.clone(),
                                _ => BigRational::from_integer(num_bigint::BigInt::from(0)),
                            },
                            Value::Int(n) => BigRational::from_integer(n.clone()),
                            Value::Rational(r) => r.clone(),
                        })
                        .map_err(|_| PlotSkip::Conversion)
                }
                _ => match number.as_ref() {
                    Value::Int(n) => Ok(BigRational::from_integer(n.clone())),
                    Value::Rational(r) => Ok(r.clone()),
                    _ => Err(PlotSkip::NoNumeric),
                },
            }
        }
    }
}

pub fn to_f64_saturated(v: &BigRational) -> Option<f64> {
    let f = v.to_f64()?;
    if f.is_finite() { Some(f) } else { None }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn plot_types_constructible() {
        let s = Series {
            points: vec![0, 1],
            label: Some("test".to_string()),
        };
        assert_eq!(s.points.len(), 2);
        let u = PlotUnit::AllUnitless;
        match u {
            PlotUnit::AllUnitless => {}
            _ => panic!("expected AllUnitless"),
        }
        let u2 = PlotUnit::SingleUnit(pnc_engine::UnitId::Celsius);
        match u2 {
            PlotUnit::SingleUnit(id) => {
                let _ = id.symbol();
            }
            _ => panic!("expected SingleUnit"),
        }
        let skip = PlotSkip::NoNumeric;
        match skip {
            PlotSkip::NoNumeric => {}
            _ => panic!("expected NoNumeric"),
        }
        let skip = PlotSkip::Conversion;
        match skip {
            PlotSkip::Conversion => {}
            _ => {}
        }
        let cache = PlotCache {
            series: s,
            unit_class: PlotUnit::AllUnitless,
            x_min: 0,
            x_max: 1,
            y_min_idx: 0,
            y_max_idx: 1,
            y_min: BigRational::from_integer(num_bigint::BigInt::from(0)),
            y_max: BigRational::from_integer(num_bigint::BigInt::from(1)),
            y_min_label: "0".to_string(),
            y_max_label: "1".to_string(),
            width: 10,
            height: 5,
        };
        assert_eq!(cache.width, 10);
    }

    #[test]
    fn unit_class_basic() {
        let uc = unit_class(&[None, None]);
        match uc {
            PlotUnit::AllUnitless => {}
            _ => panic!("expected AllUnitless"),
        }
    }

    #[test]
    fn series_from_stack_basic() {
        use pnc_engine::Value;
        let stack = vec![Value::Int(1.into())];
        let s = series_from_stack(&stack).unwrap();
        assert_eq!(s.points.len(), 1);
    }

    #[test]
    fn normalize_value_basic() {
        use pnc_engine::Value;
        let v = Value::Int(42.into());
        let r = normalize_value(&v, &PlotUnit::AllUnitless).unwrap();
        assert_eq!(r, BigRational::from_integer(num_bigint::BigInt::from(42)));
    }

    #[test]
    fn to_f64_saturated_basic() {
        let v = BigRational::from_integer(num_bigint::BigInt::from(42));
        assert_eq!(to_f64_saturated(&v), Some(42.0));
    }
}