pnc 0.2.1

A fast, fully offline, terminal-only reverse Polish notation (RPN) calculator
use num_bigint::BigInt;
use num_rational::BigRational;

use pnc_engine::Value;
use pnc_engine::persist::DisplayMode;

pub fn format_value(value: &Value, mode: DisplayMode, precision: u8) -> String {
    match value {
        Value::Int(n) => {
            let r = BigRational::from_integer(n.clone());
            format_rational(&r, mode, precision, None)
        }
        Value::Rational(r) => format_rational(r, mode, precision, None),
        Value::Unit { number, unit } => {
            let r = match number.as_ref() {
                Value::Int(n) => BigRational::from_integer(n.clone()),
                Value::Rational(r) => r.clone(),
                _ => BigRational::from_integer(BigInt::from(0)),
            };
            let formatted = format_rational(&r, mode, precision, None);
            format!("{} {}", formatted, unit)
        }
    }
}

fn format_rational(
    r: &BigRational,
    mode: DisplayMode,
    precision: u8,
    _unit: Option<&str>,
) -> String {
    match mode {
        DisplayMode::Auto => r.to_string_custom(),
        DisplayMode::Fix => format_fix(r, precision),
        DisplayMode::Sci => format_sci(r, precision),
        DisplayMode::Eng => format_eng(r, precision),
    }
}

trait ToStrCustom {
    fn to_string_custom(&self) -> String;
}

impl ToStrCustom for BigRational {
    fn to_string_custom(&self) -> String {
        if self.denom() == &BigInt::from(1) {
            self.numer().to_string()
        } else {
            format!("{}/{}", self.numer(), self.denom())
        }
    }
}

fn round_half_away(r: &BigRational) -> BigInt {
    // Round a BigRational to nearest integer, half away from zero.
    let (n, d) = (r.numer(), r.denom());
    // d is always positive for BigRational.
    let half_d = d / BigInt::from(2);
    let adjusted = if n >= &BigInt::from(0) {
        n + half_d
    } else {
        n - half_d
    };
    adjusted / d
}

fn format_fix(r: &BigRational, precision: u8) -> String {
    if precision == 0 {
        let rounded = round_half_away(r);
        return rounded.to_string();
    }
    let scale = BigInt::from(10).pow(precision as u32);
    let scaled = r * BigRational::from_integer(scale.clone());
    let rounded = round_half_away(&scaled);
    // Format: insert decimal point precision digits from right
    let s = rounded.to_string();
    if s.len() <= precision as usize {
        // Pad with leading zeros
        let padded = format!("{:0>width$}", s, width = precision as usize + 1);
        let (int_part, frac_part) = padded.split_at(padded.len() - precision as usize);
        format!("{}.{}", int_part, frac_part)
    } else {
        let (int_part, frac_part) = s.split_at(s.len() - precision as usize);
        format!("{}.{}", int_part, frac_part)
    }
}

fn format_sci(r: &BigRational, precision: u8) -> String {
    format_scientific(r, precision, false)
}

fn format_eng(r: &BigRational, precision: u8) -> String {
    format_scientific(r, precision, true)
}

fn format_scientific(r: &BigRational, precision: u8, engineering: bool) -> String {
    // Handle zero
    if r == &BigRational::from_integer(BigInt::from(0)) {
        let frac_digits = if precision == 0 {
            0
        } else {
            precision as usize - 1
        };
        let frac = "0".repeat(frac_digits);
        if frac.is_empty() {
            return "0e0".to_string();
        }
        return format!("0.{}e0", frac);
    }

    let negative = r < &BigRational::from_integer(BigInt::from(0));
    let abs_r = if negative { -r.clone() } else { r.clone() };

    // Find exponent
    // We want mantissa in [1, 10) for sci, [1, 1000) for eng
    let (exp, _) = if engineering {
        // exponent multiple of 3
        // log10 of abs_r
        find_eng_exponent(&abs_r)
    } else {
        find_sci_exponent(&abs_r)
    };

    // mantissa = abs_r / 10^exp
    let ten_pow_exp = BigInt::from(10).pow(exp.unsigned_abs());
    let mantissa = if exp >= 0 {
        &abs_r / BigRational::from_integer(ten_pow_exp.clone())
    } else {
        &abs_r * BigRational::from_integer(ten_pow_exp.clone())
    };

    // Round mantissa to `precision` significant digits
    let frac_digits = if precision == 0 {
        0
    } else {
        precision as usize - 1
    };
    let scale = BigInt::from(10).pow(frac_digits as u32);
    let scaled_mantissa = &mantissa * BigRational::from_integer(scale.clone());
    let rounded_scaled = round_half_away(&scaled_mantissa);
    let rounded_mantissa = BigRational::new(rounded_scaled.clone(), scale.clone());

    // Carry overflow: if rounded mantissa >= upper bound, divide and increment exp
    let upper_bound = if engineering {
        BigRational::from_integer(BigInt::from(1000))
    } else {
        BigRational::from_integer(BigInt::from(10))
    };
    let (final_mantissa, final_exp) = if rounded_mantissa >= upper_bound {
        let divisor = if engineering {
            BigRational::from_integer(BigInt::from(1000))
        } else {
            BigRational::from_integer(BigInt::from(10))
        };
        let new_m = rounded_mantissa / divisor;
        let new_e = exp + if engineering { 3 } else { 1 };
        (new_m, new_e)
    } else {
        (rounded_mantissa, exp)
    };

    // Format mantissa
    let mantissa_str = format_mantissa(&final_mantissa, frac_digits);

    // Format exponent
    let exp_str = format!("e{}", final_exp);

    let sign = if negative { "-" } else { "" };
    format!("{}{}{}", sign, mantissa_str, exp_str)
}

fn find_sci_exponent(r: &BigRational) -> (i32, ()) {
    // Find exponent such that r / 10^exp is in [1, 10)
    // exp = floor(log10(r))
    // Use integer comparison.
    if r >= &BigRational::from_integer(BigInt::from(1)) {
        let mut exp = 0i32;
        let mut current = r.clone();
        while current >= BigRational::from_integer(BigInt::from(10)) {
            current /= BigRational::from_integer(BigInt::from(10));
            exp += 1;
        }
        (exp, ())
    } else {
        let mut exp = 0i32;
        let mut current = r.clone();
        while current < BigRational::from_integer(BigInt::from(1))
            && current > BigRational::from_integer(BigInt::from(0))
        {
            current *= BigRational::from_integer(BigInt::from(10));
            exp -= 1;
        }
        (exp, ())
    }
}

fn find_eng_exponent(r: &BigRational) -> (i32, ()) {
    let (sci_exp, _) = find_sci_exponent(r);
    let remainder = sci_exp.rem_euclid(3);
    let eng_exp = sci_exp - remainder;
    (eng_exp, ())
}

fn format_mantissa(m: &BigRational, frac_digits: usize) -> String {
    // Format a mantissa (in [1, 10) or [1, 1000)) with frac_digits decimal places.
    if frac_digits == 0 {
        // Round to integer
        let rounded = round_half_away(m);
        return rounded.to_string();
    }
    let scale = BigInt::from(10).pow(frac_digits as u32);
    let scaled = m * BigRational::from_integer(scale.clone());
    let rounded = round_half_away(&scaled);
    let s = rounded.to_string();
    if s.len() <= frac_digits {
        let padded = format!("{:0>width$}", s, width = frac_digits + 1);
        let int_part = &padded[..1];
        let frac_part = &padded[1..];
        format!("{}.{}", int_part, frac_part)
    } else {
        let int_part = &s[..s.len() - frac_digits];
        let frac_part = &s[s.len() - frac_digits..];
        // Handle carry: int_part might be 2 digits
        if int_part.len() > 1 {
            // This shouldn't happen if we handled carry overflow above, but just in case
            format!("{}.{}", &int_part[..1], &int_part[1..])
        } else {
            format!("{}.{}", int_part, frac_part)
        }
    }
}

pub fn precision_for(settings: &pnc_engine::persist::Settings, mode: DisplayMode) -> u8 {
    match mode {
        DisplayMode::Auto => 0,
        DisplayMode::Fix => settings.fix_decimals,
        DisplayMode::Sci => settings.sci_digits,
        DisplayMode::Eng => settings.eng_digits,
    }
}