//! Mixture types and utilities for multi-component refrigerants.
//!
//! This module provides types for representing refrigerant mixtures
//! (e.g., R454B = R32/R1234yf) and their thermodynamic properties.

use std::fmt;
use std::hash::{Hash, Hasher};

/// A refrigerant mixture composed of multiple components.
///
/// # Example
///
/// ```
/// use entropyk_fluids::mixture::Mixture;
///
/// let r454b = Mixture::from_mass_fractions(&[
///     ("R32", 0.5),
///     ("R1234yf", 0.5),
/// ]).unwrap();
///
/// let r410a = Mixture::from_mole_fractions(&[
///     ("R32", 0.5),
///     ("R125", 0.5),
/// ]).unwrap();
/// ```
#[derive(Clone, Debug, PartialEq)]
pub struct Mixture {
    /// Components in the mixture (names as used by CoolProp)
    components: Vec<String>,
    /// Fractions (either mass or mole basis, depending on constructor)
    fractions: Vec<f64>,
    /// Whether fractions are mole-based (true) or mass-based (false)
    mole_fractions: bool,
}

impl Mixture {
    /// Create a mixture from mass fractions.
    ///
    /// # Arguments
    /// * `fractions` - Pairs of (fluid name, mass fraction)
    ///
    /// # Errors
    /// Returns an error if fractions don't sum to 1.0 or are invalid
    pub fn from_mass_fractions(fractions: &[(&str, f64)]) -> Result<Self, MixtureError> {
        Self::validate_fractions(fractions)?;
        Ok(Mixture {
            components: fractions
                .iter()
                .map(|(name, _)| (*name).to_string())
                .collect(),
            fractions: fractions.iter().map(|(_, frac)| *frac).collect(),
            mole_fractions: false,
        })
    }

    /// Create a mixture from mole fractions.
    ///
    /// # Arguments
    /// * `fractions` - Pairs of (fluid name, mole fraction)
    ///
    /// # Errors
    /// Returns an error if fractions don't sum to 1.0 or are invalid
    pub fn from_mole_fractions(fractions: &[(&str, f64)]) -> Result<Self, MixtureError> {
        Self::validate_fractions(fractions)?;
        Ok(Mixture {
            components: fractions
                .iter()
                .map(|(name, _)| (*name).to_string())
                .collect(),
            fractions: fractions.iter().map(|(_, frac)| *frac).collect(),
            mole_fractions: true,
        })
    }

    /// Validate that fractions are valid (sum to 1.0, all non-negative)
    fn validate_fractions(fractions: &[(&str, f64)]) -> Result<(), MixtureError> {
        if fractions.is_empty() {
            return Err(MixtureError::InvalidComposition(
                "Mixture must have at least one component".to_string(),
            ));
        }

        let sum: f64 = fractions.iter().map(|(_, frac)| frac).sum();
        if (sum - 1.0).abs() > 1e-6 {
            return Err(MixtureError::InvalidComposition(format!(
                "Fractions must sum to 1.0, got {}",
                sum
            )));
        }

        for (_, frac) in fractions {
            if *frac < 0.0 || *frac > 1.0 {
                return Err(MixtureError::InvalidComposition(format!(
                    "Fraction must be between 0 and 1, got {}",
                    frac
                )));
            }
        }

        Ok(())
    }

    /// Get the components in this mixture.
    pub fn components(&self) -> &[String] {
        &self.components
    }

    /// Get the fractions (mass or mole basis depending on constructor).
    pub fn fractions(&self) -> &[f64] {
        &self.fractions
    }

    /// Check if fractions are mole-based.
    pub fn is_mole_fractions(&self) -> bool {
        self.mole_fractions
    }

    /// Check if fractions are mass-based.
    pub fn is_mass_fractions(&self) -> bool {
        !self.mole_fractions
    }

    /// Convert to CoolProp mixture string format.
    ///
    /// CoolProp format: "R32[0.5]&R125[0.5]" (mole fractions)
    pub fn to_coolprop_string(&self) -> String {
        self.components
            .iter()
            .zip(self.fractions.iter())
            .map(|(name, frac)| format!("{}[{}]", name, frac))
            .collect::<Vec<_>>()
            .join("&")
    }

    /// Get the number of components in this mixture.
    pub fn len(&self) -> usize {
        self.components.len()
    }

    /// Check if this mixture has no components.
    pub fn is_empty(&self) -> bool {
        self.components.is_empty()
    }

    /// Convert mass fractions to mole fractions.
    ///
    /// Requires molar masses for each component.
    /// Uses simplified molar masses for common refrigerants.
    pub fn to_mole_fractions(&self) -> Result<Vec<f64>, MixtureError> {
        if self.mole_fractions {
            return Ok(self.fractions.to_vec());
        }

        let total: f64 = self
            .components
            .iter()
            .zip(self.fractions.iter())
            .map(|(c, frac)| frac / Self::molar_mass(c))
            .sum();

        Ok(self
            .components
            .iter()
            .zip(self.fractions.iter())
            .map(|(c, frac)| (frac / Self::molar_mass(c)) / total)
            .collect())
    }

    /// Get molar mass (g/mol) for common refrigerants.
    fn molar_mass(fluid: &str) -> f64 {
        match fluid.to_uppercase().as_str() {
            // HFC Refrigerants
            "R32" => 52.02,
            "R125" => 120.02,
            "R134A" => 102.03,
            "R143A" => 84.04,
            "R152A" => 66.05,
            "R22" => 86.47,
            "R23" => 70.01,
            "R41" => 34.03,
            "R245FA" => 134.05,
            // HFO Refrigerants
            "R1234YF" => 114.04,
            "R1234ZE" | "R1234ZE(E)" => 114.04,
            "R1233ZD" | "R1233ZD(E)" => 130.50,
            "R1243ZF" => 96.06,
            // Predefined Mixtures (average molar mass)
            "R410A" => 72.58,
            "R404A" => 97.60,
            "R407C" => 86.20,
            "R507A" => 98.86,
            "R513A" => 108.5,
            "R454B" => 83.03,
            "R452B" => 68.5,
            "R454C" => 99.6,
            // Natural Refrigerants
            "R290" | "PROPANE" => 44.10,
            "R600" | "BUTANE" | "N-BUTANE" => 58.12,
            "R600A" | "ISOBUTANE" => 58.12,
            "R1270" | "PROPYLENE" => 42.08,
            "R717" | "AMMONIA" => 17.03,
            "CO2" | "R744" => 44.01,
            // Other Fluids
            "WATER" | "H2O" => 18.02,
            "AIR" => 28.97,
            "NITROGEN" | "N2" => 28.01,
            "OXYGEN" | "O2" => 32.00,
            "ARGON" => 39.95,
            "HELIUM" => 4.00,
            "HYDROGEN" | "H2" => 2.02,
            "METHANE" => 16.04,
            "ETHANE" => 30.07,
            "ETHYLENE" => 28.05,
            "ETHANOL" => 46.07,
            "METHANOL" => 32.04,
            "ACETONE" => 58.08,
            "BENZENE" => 78.11,
            "TOLUENE" => 92.14,
            _ => 50.0, // Default fallback
        }
    }
}

impl Hash for Mixture {
    fn hash<H: Hasher>(&self, state: &mut H) {
        // Use CoolProp string as stable hash representation
        self.to_coolprop_string().hash(state);
    }
}

impl Eq for Mixture {}

impl fmt::Display for Mixture {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let fraction_type = if self.mole_fractions { "mole" } else { "mass" };
        write!(f, "Mixture ({} fractions): ", fraction_type)?;
        for (i, (comp, frac)) in self
            .components
            .iter()
            .zip(self.fractions.iter())
            .enumerate()
        {
            if i > 0 {
                write!(f, ", ")?;
            }
            write!(f, "{}={:.2}", comp, frac)?;
        }
        Ok(())
    }
}

/// Errors that can occur when working with mixtures.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MixtureError {
    /// Invalid mixture composition
    InvalidComposition(String),
    /// Mixture not supported by backend
    MixtureNotSupported(String),
    /// Invalid fraction type
    InvalidFractionType(String),
}

impl fmt::Display for MixtureError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            MixtureError::InvalidComposition(msg) => {
                write!(f, "Invalid mixture composition: {}", msg)
            }
            MixtureError::MixtureNotSupported(msg) => write!(f, "Mixture not supported: {}", msg),
            MixtureError::InvalidFractionType(msg) => write!(f, "Invalid fraction type: {}", msg),
        }
    }
}

impl std::error::Error for MixtureError {}

/// Pre-defined common refrigerant mixtures.
pub mod predefined {
    use super::*;

    /// R454B: R32 (50%) / R1234yf (50%) - mass fractions (Opteon XL41)
    pub fn r454b() -> Mixture {
        Mixture::from_mass_fractions(&[("R32", 0.5), ("R1234yf", 0.5)]).unwrap()
    }

    /// R410A: R32 (50%) / R125 (50%) - mass fractions
    pub fn r410a() -> Mixture {
        Mixture::from_mass_fractions(&[("R32", 0.5), ("R125", 0.5)]).unwrap()
    }

    /// R407C: R32 (23%) / R125 (25%) / R134a (52%) - mass fractions
    pub fn r407c() -> Mixture {
        Mixture::from_mass_fractions(&[("R32", 0.23), ("R125", 0.25), ("R134a", 0.52)]).unwrap()
    }

    /// R404A: R125 (44%) / R143a (52%) / R134a (4%) - mass fractions
    pub fn r404a() -> Mixture {
        Mixture::from_mass_fractions(&[("R125", 0.44), ("R143a", 0.52), ("R134a", 0.04)]).unwrap()
    }

    /// R507A: R125 (50%) / R143a (50%) - mass fractions (azeotropic)
    pub fn r507a() -> Mixture {
        Mixture::from_mass_fractions(&[("R125", 0.5), ("R143a", 0.5)]).unwrap()
    }

    /// R513A: R134a (56%) / R1234yf (44%) - mass fractions (Opteon XP10)
    pub fn r513a() -> Mixture {
        Mixture::from_mass_fractions(&[("R134a", 0.56), ("R1234yf", 0.44)]).unwrap()
    }

    /// R452B: R32 (67%) / R125 (7%) / R1234yf (26%) - mass fractions (Opteon XL55)
    pub fn r452b() -> Mixture {
        Mixture::from_mass_fractions(&[("R32", 0.67), ("R125", 0.07), ("R1234yf", 0.26)]).unwrap()
    }

    /// R454C: R32 (21.5%) / R1234yf (78.5%) - mass fractions (Opteon XL20)
    pub fn r454c() -> Mixture {
        Mixture::from_mass_fractions(&[("R32", 0.215), ("R1234yf", 0.785)]).unwrap()
    }

    /// R455A: R32 (21.5%) / R1234yf (75.5%) / CO2 (3%) - mass fractions
    pub fn r455a() -> Mixture {
        Mixture::from_mass_fractions(&[("R32", 0.215), ("R1234yf", 0.755), ("CO2", 0.03)]).unwrap()
    }

    /// R32/R125 (50/50) mixture - mass fractions
    pub fn r32_r125_5050() -> Mixture {
        Mixture::from_mass_fractions(&[("R32", 0.5), ("R125", 0.5)]).unwrap()
    }
}

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

    #[test]
    fn test_mixture_creation_mass() {
        let mixture = Mixture::from_mass_fractions(&[("R32", 0.5), ("R1234yf", 0.5)]).unwrap();
        assert_eq!(mixture.components().len(), 2);
        assert!(mixture.is_mass_fractions());
    }

    #[test]
    fn test_mixture_creation_mole() {
        let mixture = Mixture::from_mole_fractions(&[("R32", 0.5), ("R125", 0.5)]).unwrap();
        assert_eq!(mixture.components().len(), 2);
        assert!(mixture.is_mole_fractions());
    }

    #[test]
    fn test_coolprop_string() {
        let mixture = Mixture::from_mass_fractions(&[("R32", 0.5), ("R1234yf", 0.5)]).unwrap();
        let cp_string = mixture.to_coolprop_string();
        assert!(cp_string.contains("R32[0.5]"));
        assert!(cp_string.contains("R1234yf[0.5]"));
    }

    #[test]
    fn test_predefined_r454b() {
        let mixture = predefined::r454b();
        assert_eq!(mixture.components().len(), 2);
    }

    #[test]
    fn test_invalid_fractions_sum() {
        let result = Mixture::from_mass_fractions(&[("R32", 0.3), ("R125", 0.5)]);
        assert!(result.is_err());
    }

    #[test]
    fn test_invalid_fraction_negative() {
        let result = Mixture::from_mass_fractions(&[("R32", -0.5), ("R125", 1.5)]);
        assert!(result.is_err());
    }

    #[test]
    fn test_mixture_hash() {
        let m1 = Mixture::from_mass_fractions(&[("R32", 0.5), ("R1234yf", 0.5)]).unwrap();
        let m2 = Mixture::from_mass_fractions(&[("R32", 0.5), ("R1234yf", 0.5)]).unwrap();

        use std::collections::hash_map::DefaultHasher;
        let mut h1 = DefaultHasher::new();
        let mut h2 = DefaultHasher::new();
        m1.hash(&mut h1);
        m2.hash(&mut h2);
        assert_eq!(h1.finish(), h2.finish());
    }

    #[test]
    fn test_mass_to_mole_conversion() {
        // R454B: 50% mass R32, 50% mass R1234yf
        // Molar masses: R32=52.02, R1234yf=114.04
        // Mole fraction R32 = (0.5/52.02) / (0.5/52.02 + 0.5/114.04) ≈ 0.687
        let mixture = Mixture::from_mass_fractions(&[("R32", 0.5), ("R1234yf", 0.5)]).unwrap();
        let mole_fracs = mixture.to_mole_fractions().unwrap();

        // Verify sum = 1.0
        let sum: f64 = mole_fracs.iter().sum();
        assert!((sum - 1.0).abs() < 1e-6);

        // R32 should be ~69% mole fraction (higher due to lower molar mass)
        assert!(mole_fracs[0] > 0.6 && mole_fracs[0] < 0.8);
    }

    #[test]
    fn test_mole_fractions_passthrough() {
        let mixture = Mixture::from_mole_fractions(&[("R32", 0.5), ("R125", 0.5)]).unwrap();
        let mole_fracs = mixture.to_mole_fractions().unwrap();

        assert!((mole_fracs[0] - 0.5).abs() < 1e-6);
        assert!((mole_fracs[1] - 0.5).abs() < 1e-6);
    }
}
