//! Answer column formatting for CalcPad.
//!
//! Pure-Rust formatting utilities ported from the cross-platform spec in
//! Epic 10-4 (Answer Column Formatting). Supports standard, scientific, and
//! SI notation with configurable decimal places (0--10), thousands separators,
//! and currency symbol placement.
//!
//! This module is platform-agnostic: the same options produce identical output
//! on macOS (SwiftUI), Windows (Iced), and Web (WASM), ensuring the engine is
//! the single source of truth for how numbers appear in the answer column.

use serde::{Deserialize, Serialize};

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

/// Notation style for the answer column.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Notation {
    /// Fixed-point with thousands separators (e.g. `1,234.50`).
    Standard,
    /// Mantissa + exponent (e.g. `1.50e6`).
    Scientific,
    /// SI prefix (e.g. `1.50M`, `3.00k`).
    SI,
}

/// Thousands-separator style.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ThousandsSeparator {
    /// `1,234,567.89`
    Comma,
    /// `1.234.567,89` (European-style; decimal separator becomes `,`).
    Period,
    /// `1 234 567.89`
    Space,
    /// `1234567.89`
    None,
}

/// Where to place a currency symbol relative to the number.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CurrencyPosition {
    /// `$1,234.50`
    Prefix,
    /// `1,234.50€`
    Suffix,
}

/// Complete set of formatting options for the answer column.
///
/// Persisted per-sheet (or globally) and optionally overridden per-line.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FormattingOptions {
    /// Number of decimal places (clamped to 0..=10).
    pub decimal_places: u8,
    /// Notation style.
    pub notation: Notation,
    /// Thousands-separator style.
    pub thousands_separator: ThousandsSeparator,
    /// Currency symbol string (e.g. `"$"`, `"€"`). Empty means no symbol.
    pub currency_symbol: String,
    /// Where to place the currency symbol.
    pub currency_position: CurrencyPosition,
}

/// Per-line override. Every field is optional; `None` inherits from global.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct LineFormatOverride {
    pub decimal_places: Option<u8>,
    pub notation: Option<Notation>,
    pub thousands_separator: Option<ThousandsSeparator>,
    pub currency_symbol: Option<String>,
    pub currency_position: Option<CurrencyPosition>,
}

// ---------------------------------------------------------------------------
// Defaults
// ---------------------------------------------------------------------------

impl Default for FormattingOptions {
    fn default() -> Self {
        Self {
            decimal_places: 2,
            notation: Notation::Standard,
            thousands_separator: ThousandsSeparator::Comma,
            currency_symbol: String::new(),
            currency_position: CurrencyPosition::Prefix,
        }
    }
}

// ---------------------------------------------------------------------------
// Resolution
// ---------------------------------------------------------------------------

/// Merge a per-line override on top of global settings. Missing override
/// fields inherit from the global options.
pub fn resolve_formatting(
    global: &FormattingOptions,
    line_override: Option<&LineFormatOverride>,
) -> FormattingOptions {
    match line_override {
        None => global.clone(),
        Some(ov) => FormattingOptions {
            decimal_places: ov.decimal_places.unwrap_or(global.decimal_places),
            notation: ov.notation.unwrap_or(global.notation),
            thousands_separator: ov.thousands_separator.unwrap_or(global.thousands_separator),
            currency_symbol: ov
                .currency_symbol
                .clone()
                .unwrap_or_else(|| global.currency_symbol.clone()),
            currency_position: ov.currency_position.unwrap_or(global.currency_position),
        },
    }
}

// ---------------------------------------------------------------------------
// SI prefixes
// ---------------------------------------------------------------------------

struct SIPrefix {
    symbol: &'static str,
    exponent: i32,
}

const SI_PREFIXES: &[SIPrefix] = &[
    SIPrefix { symbol: "T", exponent: 12 },
    SIPrefix { symbol: "G", exponent: 9 },
    SIPrefix { symbol: "M", exponent: 6 },
    SIPrefix { symbol: "k", exponent: 3 },
    SIPrefix { symbol: "",  exponent: 0 },
    SIPrefix { symbol: "m", exponent: -3 },
    SIPrefix { symbol: "\u{03bc}", exponent: -6 },  // μ
    SIPrefix { symbol: "n", exponent: -9 },
    SIPrefix { symbol: "p", exponent: -12 },
];

// ---------------------------------------------------------------------------
// Core formatting helpers
// ---------------------------------------------------------------------------

/// Insert thousands separators into an integer-part string.
fn insert_thousands_separator(integer_part: &str, sep: ThousandsSeparator) -> String {
    if sep == ThousandsSeparator::None {
        return integer_part.to_string();
    }

    let sep_char = match sep {
        ThousandsSeparator::Comma => ',',
        ThousandsSeparator::Period => '.',
        ThousandsSeparator::Space => ' ',
        ThousandsSeparator::None => unreachable!(),
    };

    let (negative, digits) = if let Some(rest) = integer_part.strip_prefix('-') {
        (true, rest)
    } else {
        (false, integer_part)
    };

    let mut parts: Vec<&str> = Vec::new();
    let len = digits.len();
    let mut end = len;
    while end > 3 {
        let start = end - 3;
        parts.push(&digits[start..end]);
        end = start;
    }
    parts.push(&digits[..end]);
    parts.reverse();

    let joined = parts.join(&sep_char.to_string());
    if negative {
        format!("-{}", joined)
    } else {
        joined
    }
}

/// Format in standard (fixed-point) notation.
fn format_standard(value: f64, decimal_places: u8, thousands_sep: ThousandsSeparator) -> String {
    let dp = decimal_places as usize;
    let fixed = format!("{:.prec$}", value, prec = dp);

    let (int_part, dec_part) = if let Some(dot_pos) = fixed.find('.') {
        (&fixed[..dot_pos], Some(&fixed[dot_pos + 1..]))
    } else {
        (fixed.as_str(), None)
    };

    let formatted_int = insert_thousands_separator(int_part, thousands_sep);

    if dp == 0 {
        return formatted_int;
    }

    // When using period as thousands separator, the decimal separator is comma.
    let dec_sep = if thousands_sep == ThousandsSeparator::Period {
        ','
    } else {
        '.'
    };

    match dec_part {
        Some(d) => format!("{}{}{}", formatted_int, dec_sep, d),
        None => formatted_int,
    }
}

/// Format in scientific notation (e.g. `1.50e6`).
fn format_scientific(value: f64, decimal_places: u8) -> String {
    let dp = decimal_places as usize;

    if value == 0.0 {
        let mantissa = format!("{:.prec$}", 0.0_f64, prec = dp);
        return format!("{}e0", mantissa);
    }
    if !value.is_finite() {
        return format!("{}", value);
    }

    let exponent = value.abs().log10().floor() as i32;
    let mantissa = value / 10_f64.powi(exponent);
    let mantissa_str = format!("{:.prec$}", mantissa, prec = dp);

    format!("{}e{}", mantissa_str, exponent)
}

/// Format in SI notation (e.g. `1.50M`).
fn format_si(value: f64, decimal_places: u8) -> String {
    let dp = decimal_places as usize;

    if value == 0.0 {
        return format!("{:.prec$}", 0.0_f64, prec = dp);
    }
    if !value.is_finite() {
        return format!("{}", value);
    }

    let abs_value = value.abs();
    let sign = if value < 0.0 { "-" } else { "" };

    for prefix in SI_PREFIXES {
        let threshold = 10_f64.powi(prefix.exponent);
        let is_last = prefix.exponent == SI_PREFIXES.last().unwrap().exponent;
        if abs_value >= threshold || is_last {
            let scaled = abs_value / threshold;
            return format!("{}{:.prec$}{}", sign, scaled, prefix.symbol, prec = dp);
        }
    }

    // Fallback (should not be reached).
    format!("{:.prec$}", value, prec = dp)
}

/// Wrap a formatted number string with currency symbol.
fn apply_currency(formatted: &str, symbol: &str, position: CurrencyPosition) -> String {
    if symbol.is_empty() {
        return formatted.to_string();
    }
    match position {
        CurrencyPosition::Prefix => format!("{}{}", symbol, formatted),
        CurrencyPosition::Suffix => format!("{}{}", formatted, symbol),
    }
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/// Format a numeric value according to the given formatting options.
///
/// This is the main entry point for answer-column number formatting.
///
/// # Examples
///
/// ```
/// use calcpad_engine::formatting::answer_format::*;
///
/// let opts = FormattingOptions::default();
/// assert_eq!(format_number(1234.5, &opts), "1,234.50");
///
/// let sci = FormattingOptions { notation: Notation::Scientific, ..Default::default() };
/// assert_eq!(format_number(1500000.0, &sci), "1.50e6");
/// ```
pub fn format_number(value: f64, options: &FormattingOptions) -> String {
    if value.is_nan() {
        return "NaN".to_string();
    }
    if value.is_infinite() {
        return if value > 0.0 {
            "Infinity".to_string()
        } else {
            "-Infinity".to_string()
        };
    }

    let formatted = match options.notation {
        Notation::Standard => {
            format_standard(value, options.decimal_places, options.thousands_separator)
        }
        Notation::Scientific => format_scientific(value, options.decimal_places),
        Notation::SI => format_si(value, options.decimal_places),
    };

    apply_currency(&formatted, &options.currency_symbol, options.currency_position)
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    fn default_opts() -> FormattingOptions {
        FormattingOptions::default()
    }

    // ── Standard notation ────────────────────────────────────────────

    #[test]
    fn standard_default() {
        assert_eq!(format_number(1234.5, &default_opts()), "1,234.50");
    }

    #[test]
    fn standard_zero_decimal_places() {
        let opts = FormattingOptions { decimal_places: 0, ..default_opts() };
        assert_eq!(format_number(1234.567, &opts), "1,235");
    }

    #[test]
    fn standard_four_decimal_places() {
        let opts = FormattingOptions { decimal_places: 4, ..default_opts() };
        assert_eq!(format_number(1234.5, &opts), "1,234.5000");
    }

    #[test]
    fn standard_ten_decimal_places() {
        let opts = FormattingOptions { decimal_places: 10, ..default_opts() };
        assert_eq!(format_number(3.14, &opts), "3.1400000000");
    }

    #[test]
    fn standard_small_number_no_thousands() {
        assert_eq!(format_number(42.0, &default_opts()), "42.00");
    }

    #[test]
    fn standard_negative() {
        assert_eq!(format_number(-1234.5, &default_opts()), "-1,234.50");
    }

    #[test]
    fn standard_zero() {
        assert_eq!(format_number(0.0, &default_opts()), "0.00");
    }

    // ── Thousands separators ─────────────────────────────────────────

    #[test]
    fn thousands_comma() {
        let opts = FormattingOptions {
            thousands_separator: ThousandsSeparator::Comma,
            ..default_opts()
        };
        assert_eq!(format_number(1234567.89, &opts), "1,234,567.89");
    }

    #[test]
    fn thousands_period() {
        let opts = FormattingOptions {
            thousands_separator: ThousandsSeparator::Period,
            ..default_opts()
        };
        assert_eq!(format_number(1234567.89, &opts), "1.234.567,89");
    }

    #[test]
    fn thousands_space() {
        let opts = FormattingOptions {
            thousands_separator: ThousandsSeparator::Space,
            ..default_opts()
        };
        assert_eq!(format_number(1234567.89, &opts), "1 234 567.89");
    }

    #[test]
    fn thousands_none() {
        let opts = FormattingOptions {
            thousands_separator: ThousandsSeparator::None,
            ..default_opts()
        };
        assert_eq!(format_number(1234567.89, &opts), "1234567.89");
    }

    #[test]
    fn thousands_period_negative() {
        let opts = FormattingOptions {
            thousands_separator: ThousandsSeparator::Period,
            ..default_opts()
        };
        assert_eq!(format_number(-1234.56, &opts), "-1.234,56");
    }

    // ── Scientific notation ──────────────────────────────────────────

    #[test]
    fn scientific_large() {
        let opts = FormattingOptions { notation: Notation::Scientific, ..default_opts() };
        assert_eq!(format_number(1500000.0, &opts), "1.50e6");
    }

    #[test]
    fn scientific_small() {
        let opts = FormattingOptions { notation: Notation::Scientific, ..default_opts() };
        assert_eq!(format_number(0.005, &opts), "5.00e-3");
    }

    #[test]
    fn scientific_42() {
        let opts = FormattingOptions { notation: Notation::Scientific, ..default_opts() };
        assert_eq!(format_number(42.0, &opts), "4.20e1");
    }

    #[test]
    fn scientific_negative() {
        let opts = FormattingOptions { notation: Notation::Scientific, ..default_opts() };
        assert_eq!(format_number(-1500000.0, &opts), "-1.50e6");
    }

    #[test]
    fn scientific_zero() {
        let opts = FormattingOptions { notation: Notation::Scientific, ..default_opts() };
        assert_eq!(format_number(0.0, &opts), "0.00e0");
    }

    #[test]
    fn scientific_one() {
        let opts = FormattingOptions { notation: Notation::Scientific, ..default_opts() };
        assert_eq!(format_number(1.0, &opts), "1.00e0");
    }

    #[test]
    fn scientific_four_dp() {
        let opts = FormattingOptions {
            notation: Notation::Scientific,
            decimal_places: 4,
            ..default_opts()
        };
        assert_eq!(format_number(1500000.0, &opts), "1.5000e6");
    }

    #[test]
    fn scientific_zero_dp() {
        let opts = FormattingOptions {
            notation: Notation::Scientific,
            decimal_places: 0,
            ..default_opts()
        };
        assert_eq!(format_number(1500000.0, &opts), "2e6");
    }

    // ── SI notation ──────────────────────────────────────────────────

    #[test]
    fn si_mega() {
        let opts = FormattingOptions { notation: Notation::SI, ..default_opts() };
        assert_eq!(format_number(1500000.0, &opts), "1.50M");
    }

    #[test]
    fn si_kilo() {
        let opts = FormattingOptions { notation: Notation::SI, ..default_opts() };
        assert_eq!(format_number(1500.0, &opts), "1.50k");
    }

    #[test]
    fn si_giga() {
        let opts = FormattingOptions { notation: Notation::SI, ..default_opts() };
        assert_eq!(format_number(1500000000.0, &opts), "1.50G");
    }

    #[test]
    fn si_tera() {
        let opts = FormattingOptions { notation: Notation::SI, ..default_opts() };
        assert_eq!(format_number(1500000000000.0, &opts), "1.50T");
    }

    #[test]
    fn si_plain_42() {
        let opts = FormattingOptions { notation: Notation::SI, ..default_opts() };
        assert_eq!(format_number(42.0, &opts), "42.00");
    }

    #[test]
    fn si_milli() {
        let opts = FormattingOptions { notation: Notation::SI, ..default_opts() };
        assert_eq!(format_number(0.005, &opts), "5.00m");
    }

    #[test]
    fn si_micro() {
        let opts = FormattingOptions { notation: Notation::SI, ..default_opts() };
        assert_eq!(format_number(0.000005, &opts), "5.00\u{03bc}");
    }

    #[test]
    fn si_negative() {
        let opts = FormattingOptions { notation: Notation::SI, ..default_opts() };
        assert_eq!(format_number(-1500000.0, &opts), "-1.50M");
    }

    #[test]
    fn si_zero() {
        let opts = FormattingOptions { notation: Notation::SI, ..default_opts() };
        assert_eq!(format_number(0.0, &opts), "0.00");
    }

    #[test]
    fn si_zero_dp() {
        let opts = FormattingOptions {
            notation: Notation::SI,
            decimal_places: 0,
            ..default_opts()
        };
        assert_eq!(format_number(1500000.0, &opts), "2M");
    }

    // ── Currency ─────────────────────────────────────────────────────

    #[test]
    fn currency_prefix() {
        let opts = FormattingOptions {
            currency_symbol: "$".to_string(),
            currency_position: CurrencyPosition::Prefix,
            ..default_opts()
        };
        assert_eq!(format_number(1234.5, &opts), "$1,234.50");
    }

    #[test]
    fn currency_suffix() {
        let opts = FormattingOptions {
            currency_symbol: "\u{20ac}".to_string(), // €
            currency_position: CurrencyPosition::Suffix,
            ..default_opts()
        };
        assert_eq!(format_number(1234.5, &opts), "1,234.50\u{20ac}");
    }

    #[test]
    fn currency_empty_means_none() {
        assert_eq!(format_number(1234.5, &default_opts()), "1,234.50");
    }

    #[test]
    fn currency_with_scientific() {
        let opts = FormattingOptions {
            notation: Notation::Scientific,
            currency_symbol: "$".to_string(),
            currency_position: CurrencyPosition::Prefix,
            ..default_opts()
        };
        assert_eq!(format_number(1500000.0, &opts), "$1.50e6");
    }

    #[test]
    fn currency_with_si() {
        let opts = FormattingOptions {
            notation: Notation::SI,
            currency_symbol: "\u{20ac}".to_string(),
            currency_position: CurrencyPosition::Suffix,
            ..default_opts()
        };
        assert_eq!(format_number(1500000.0, &opts), "1.50M\u{20ac}");
    }

    // ── Edge cases ───────────────────────────────────────────────────

    #[test]
    fn nan() {
        assert_eq!(format_number(f64::NAN, &default_opts()), "NaN");
    }

    #[test]
    fn pos_infinity() {
        assert_eq!(format_number(f64::INFINITY, &default_opts()), "Infinity");
    }

    #[test]
    fn neg_infinity() {
        assert_eq!(format_number(f64::NEG_INFINITY, &default_opts()), "-Infinity");
    }

    #[test]
    fn very_large_standard() {
        let opts = FormattingOptions { decimal_places: 0, ..default_opts() };
        assert_eq!(format_number(1e15, &opts), "1,000,000,000,000,000");
    }

    #[test]
    fn very_small_standard() {
        let opts = FormattingOptions { decimal_places: 6, ..default_opts() };
        assert_eq!(format_number(0.000001, &opts), "0.000001");
    }

    // ── resolve_formatting ───────────────────────────────────────────

    #[test]
    fn resolve_no_override() {
        let global = default_opts();
        let result = resolve_formatting(&global, None);
        assert_eq!(result, global);
    }

    #[test]
    fn resolve_partial_override() {
        let global = default_opts();
        let ov = LineFormatOverride {
            notation: Some(Notation::Scientific),
            decimal_places: Some(4),
            ..Default::default()
        };
        let result = resolve_formatting(&global, Some(&ov));
        assert_eq!(result.notation, Notation::Scientific);
        assert_eq!(result.decimal_places, 4);
        // Inherited
        assert_eq!(result.thousands_separator, ThousandsSeparator::Comma);
        assert_eq!(result.currency_symbol, "");
        assert_eq!(result.currency_position, CurrencyPosition::Prefix);
    }

    #[test]
    fn resolve_full_override() {
        let global = default_opts();
        let ov = LineFormatOverride {
            decimal_places: Some(5),
            notation: Some(Notation::SI),
            thousands_separator: Some(ThousandsSeparator::Space),
            currency_symbol: Some("\u{00a3}".to_string()), // £
            currency_position: Some(CurrencyPosition::Suffix),
        };
        let result = resolve_formatting(&global, Some(&ov));
        assert_eq!(result.decimal_places, 5);
        assert_eq!(result.notation, Notation::SI);
        assert_eq!(result.thousands_separator, ThousandsSeparator::Space);
        assert_eq!(result.currency_symbol, "\u{00a3}");
        assert_eq!(result.currency_position, CurrencyPosition::Suffix);
    }

    #[test]
    fn resolve_empty_override_inherits_global() {
        let global = default_opts();
        let ov = LineFormatOverride::default();
        let result = resolve_formatting(&global, Some(&ov));
        assert_eq!(result, global);
    }
}
