//! Polynomial curve models for component performance characterization.
//!
//! This module provides polynomial curve implementations for:
//! - 1D polynomials: Pump curves (Q-H, efficiency), Fan curves
//! - 2D polynomials: Compressor maps based on SST/SDT
//!
//! ## 1D Polynomial (Pump/Fan Curves)
//!
//! ```text
//! y = c0 + c1*x + c2*x² + c3*x³ + ...
//! ```
//!
//! ## 2D Polynomial (Compressor Maps)
//!
//! ```text
//! z = Σ a_ij * x^i * y^j
//! ```
//!
//! Where x = SST (Saturated Suction Temperature)
//! and y = SDT (Saturated Discharge Temperature)

use crate::ComponentError;
use serde::{Deserialize, Serialize};

/// 1D Polynomial curve for component performance modeling.
///
/// Used for pump head curves, fan static pressure curves, and efficiency curves.
///
/// # Example
///
/// ```
/// use entropyk_components::polynomials::Polynomial1D;
///
/// // Pump curve: H = 50 - 0.1*Q - 0.001*Q²
/// let curve = Polynomial1D::new(vec![50.0, -0.1, -0.001]);
///
/// // Evaluate at Q = 100 m³/h
/// let head = curve.evaluate(100.0);
/// assert!(head > 0.0);
/// ```
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Polynomial1D {
    /// Polynomial coefficients [c0, c1, c2, ...] for y = c0 + c1*x + c2*x² + ...
    coefficients: Vec<f64>,
}

impl Polynomial1D {
    /// Creates a new 1D polynomial from coefficients.
    ///
    /// # Arguments
    ///
    /// * `coefficients` - Coefficients [c0, c1, c2, ...] where y = c0 + c1*x + c2*x² + ...
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_components::polynomials::Polynomial1D;
    ///
    /// // Linear: y = 2x + 3
    /// let linear = Polynomial1D::new(vec![3.0, 2.0]);
    ///
    /// // Quadratic: y = 1 + 2x + 3x²
    /// let quadratic = Polynomial1D::new(vec![1.0, 2.0, 3.0]);
    /// ```
    pub fn new(coefficients: Vec<f64>) -> Self {
        Self { coefficients }
    }

    /// Creates a constant polynomial (degree 0).
    pub fn constant(value: f64) -> Self {
        Self::new(vec![value])
    }

    /// Creates a linear polynomial: y = a + b*x.
    pub fn linear(a: f64, b: f64) -> Self {
        Self::new(vec![a, b])
    }

    /// Creates a quadratic polynomial: y = a + b*x + c*x².
    pub fn quadratic(a: f64, b: f64, c: f64) -> Self {
        Self::new(vec![a, b, c])
    }

    /// Creates a cubic polynomial: y = a + b*x + c*x² + d*x³.
    pub fn cubic(a: f64, b: f64, c: f64, d: f64) -> Self {
        Self::new(vec![a, b, c, d])
    }

    /// Evaluates the polynomial at point x using Horner's method.
    ///
    /// # Arguments
    ///
    /// * `x` - The point at which to evaluate
    ///
    /// # Returns
    ///
    /// The polynomial value y = P(x)
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_components::polynomials::Polynomial1D;
    ///
    /// let p = Polynomial1D::quadratic(1.0, 2.0, 3.0);
    /// // y = 1 + 2*2 + 3*4 = 1 + 4 + 12 = 17
    /// assert!((p.evaluate(2.0) - 17.0).abs() < 1e-10);
    /// ```
    pub fn evaluate(&self, x: f64) -> f64 {
        if self.coefficients.is_empty() {
            return 0.0;
        }

        // Horner's method: efficient polynomial evaluation
        self.coefficients
            .iter()
            .rev()
            .fold(0.0, |acc, &c| acc * x + c)
    }

    /// Computes the derivative of the polynomial at point x.
    ///
    /// # Arguments
    ///
    /// * `x` - The point at which to evaluate the derivative
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_components::polynomials::Polynomial1D;
    ///
    /// let p = Polynomial1D::quadratic(1.0, 2.0, 3.0);
    /// // dy/dx = 2 + 6x, at x=2: dy/dx = 2 + 12 = 14
    /// assert!((p.derivative(2.0) - 14.0).abs() < 1e-10);
    /// ```
    pub fn derivative(&self, x: f64) -> f64 {
        if self.coefficients.len() <= 1 {
            return 0.0;
        }

        // Derivative coefficients: [c1, 2*c2, 3*c3, ...]
        let deriv_coeffs: Vec<f64> = self.coefficients[1..]
            .iter()
            .enumerate()
            .map(|(i, &c)| c * (i + 1) as f64)
            .collect();

        Polynomial1D::new(deriv_coeffs).evaluate(x)
    }

    /// Returns the degree of the polynomial.
    pub fn degree(&self) -> usize {
        if self.coefficients.is_empty() {
            0
        } else {
            self.coefficients.len() - 1
        }
    }

    /// Returns a reference to the coefficients.
    pub fn coefficients(&self) -> &[f64] {
        &self.coefficients
    }

    /// Validates that all coefficients are finite (not NaN or infinite).
    pub fn validate(&self) -> Result<(), ComponentError> {
        for (i, &c) in self.coefficients.iter().enumerate() {
            if c.is_nan() {
                return Err(ComponentError::InvalidState(format!(
                    "Coefficient {} is NaN",
                    i
                )));
            }
            if c.is_infinite() {
                return Err(ComponentError::InvalidState(format!(
                    "Coefficient {} is infinite",
                    i
                )));
            }
        }
        Ok(())
    }
}

impl Default for Polynomial1D {
    fn default() -> Self {
        Self::constant(0.0)
    }
}

/// 2D Polynomial for compressor maps based on SST/SDT.
///
/// Models performance as a function of two variables (e.g., SST and SDT):
///
/// ```text
/// z = Σ a_ij * x^i * y^j  for i=0..nx, j=0..ny
/// ```
///
/// # Example
///
/// ```
/// use entropyk_components::polynomials::Polynomial2D;
///
/// // Simple bilinear: z = a00 + a10*x + a01*y + a11*x*y
/// let coeffs = vec![
///     vec![1.0, 0.5],   // a00, a01
///     vec![2.0, 0.1],   // a10, a11
/// ];
/// let poly = Polynomial2D::new(coeffs);
///
/// let z = poly.evaluate(10.0, 20.0);
/// ```
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Polynomial2D {
    /// Coefficient matrix where coeffs[i][j] is the coefficient for x^i * y^j
    /// coeffs[i] contains coefficients for all j values at degree i in x
    coefficients: Vec<Vec<f64>>,
}

impl Polynomial2D {
    /// Creates a new 2D polynomial from coefficient matrix.
    ///
    /// # Arguments
    ///
    /// * `coefficients` - Matrix where coefficients[i][j] is coefficient for x^i * y^j
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_components::polynomials::Polynomial2D;
    ///
    /// // z = 5 + 2*x + 3*y + 0.5*x*y
    /// let coeffs = vec![
    ///     vec![5.0, 3.0],    // Constant and y term
    ///     vec![2.0, 0.5],    // x term and x*y term
    /// ];
    /// let poly = Polynomial2D::new(coeffs);
    /// ```
    pub fn new(coefficients: Vec<Vec<f64>>) -> Self {
        Self { coefficients }
    }

    /// Creates a constant 2D polynomial.
    pub fn constant(value: f64) -> Self {
        Self::new(vec![vec![value]])
    }

    /// Creates a bilinear polynomial: z = a00 + a10*x + a01*y + a11*x*y.
    pub fn bilinear(a00: f64, a10: f64, a01: f64, a11: f64) -> Self {
        Self::new(vec![vec![a00, a01], vec![a10, a11]])
    }

    /// Creates a biquadratic polynomial (degree 2 in both variables).
    ///
    /// z = a00 + a10*x + a01*y + a20*x² + a11*x*y + a02*y²
    pub fn biquadratic(a00: f64, a10: f64, a01: f64, a20: f64, a11: f64, a02: f64) -> Self {
        Self::new(vec![vec![a00, a01, a02], vec![a10, a11], vec![a20]])
    }

    /// Evaluates the polynomial at (x, y).
    ///
    /// # Arguments
    ///
    /// * `x` - First variable (e.g., SST)
    /// * `y` - Second variable (e.g., SDT)
    ///
    /// # Returns
    ///
    /// The polynomial value z = P(x, y)
    pub fn evaluate(&self, x: f64, y: f64) -> f64 {
        let mut result = 0.0;

        for (i, row) in self.coefficients.iter().enumerate() {
            let x_pow = x.powi(i as i32);
            for (j, &coeff) in row.iter().enumerate() {
                result += coeff * x_pow * y.powi(j as i32);
            }
        }

        result
    }

    /// Computes partial derivative with respect to x.
    pub fn partial_x(&self, x: f64, y: f64) -> f64 {
        let mut result = 0.0;

        for (i, row) in self.coefficients.iter().enumerate() {
            if i == 0 {
                continue; // ∂/∂x of constant is 0
            }
            let x_pow = (i as f64) * x.powi((i - 1) as i32);
            for (j, &coeff) in row.iter().enumerate() {
                result += coeff * x_pow * y.powi(j as i32);
            }
        }

        result
    }

    /// Computes partial derivative with respect to y.
    pub fn partial_y(&self, x: f64, y: f64) -> f64 {
        let mut result = 0.0;

        for (i, row) in self.coefficients.iter().enumerate() {
            let x_pow = x.powi(i as i32);
            for (j, &coeff) in row.iter().enumerate() {
                if j == 0 {
                    continue; // ∂/∂y of constant is 0
                }
                result += coeff * x_pow * (j as f64) * y.powi((j - 1) as i32);
            }
        }

        result
    }

    /// Returns the degree in x.
    pub fn degree_x(&self) -> usize {
        if self.coefficients.is_empty() {
            0
        } else {
            self.coefficients.len() - 1
        }
    }

    /// Returns the degree in y.
    pub fn degree_y(&self) -> usize {
        self.coefficients
            .iter()
            .map(|row| if row.is_empty() { 0 } else { row.len() - 1 })
            .max()
            .unwrap_or(0)
    }

    /// Returns a reference to the coefficient matrix.
    pub fn coefficients(&self) -> &[Vec<f64>] {
        &self.coefficients
    }

    /// Validates that all coefficients are finite.
    pub fn validate(&self) -> Result<(), ComponentError> {
        for (i, row) in self.coefficients.iter().enumerate() {
            for (j, &c) in row.iter().enumerate() {
                if c.is_nan() {
                    return Err(ComponentError::InvalidState(format!(
                        "Coefficient [{},{}] is NaN",
                        i, j
                    )));
                }
                if c.is_infinite() {
                    return Err(ComponentError::InvalidState(format!(
                        "Coefficient [{},{}] is infinite",
                        i, j
                    )));
                }
            }
        }
        Ok(())
    }
}

impl Default for Polynomial2D {
    fn default() -> Self {
        Self::constant(0.0)
    }
}

/// Pump/Fan curve set containing head, efficiency, and power polynomials.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PerformanceCurves {
    /// Head/Pressure curve: H = f(Q) where Q is volumetric flow
    pub head_curve: Polynomial1D,
    /// Efficiency curve: η = f(Q)
    pub efficiency_curve: Polynomial1D,
    /// Optional power curve: P = f(Q) - if not provided, calculated from head and efficiency
    pub power_curve: Option<Polynomial1D>,
}

impl PerformanceCurves {
    /// Creates a new performance curve set.
    pub fn new(
        head_curve: Polynomial1D,
        efficiency_curve: Polynomial1D,
        power_curve: Option<Polynomial1D>,
    ) -> Self {
        Self {
            head_curve,
            efficiency_curve,
            power_curve,
        }
    }

    /// Creates a simple pump curve with head and efficiency only.
    pub fn simple(head_curve: Polynomial1D, efficiency_curve: Polynomial1D) -> Self {
        Self::new(head_curve, efficiency_curve, None)
    }

    /// Validates all curves.
    pub fn validate(&self) -> Result<(), ComponentError> {
        self.head_curve.validate()?;
        self.efficiency_curve.validate()?;
        if let Some(ref pc) = self.power_curve {
            pc.validate()?;
        }
        Ok(())
    }
}

impl Default for PerformanceCurves {
    fn default() -> Self {
        Self::simple(Polynomial1D::default(), Polynomial1D::default())
    }
}

/// Affinity laws for variable speed operation.
///
/// When speed changes from N1 to N2:
/// - Q2/Q1 = N2/N1 (flow proportional to speed)
/// - H2/H1 = (N2/N1)² (head proportional to speed squared)
/// - P2/P1 = (N2/N1)³ (power proportional to speed cubed)
pub struct AffinityLaws;

impl AffinityLaws {
    /// Applies affinity laws to scale flow rate.
    ///
    /// # Arguments
    ///
    /// * `flow` - Original flow at speed_ratio = 1.0
    /// * `speed_ratio` - New speed / rated speed (0.0 to 1.0)
    ///
    /// # Returns
    ///
    /// Scaled flow rate
    pub fn scale_flow(flow: f64, speed_ratio: f64) -> f64 {
        flow * speed_ratio
    }

    /// Applies affinity laws to scale head/pressure.
    ///
    /// # Arguments
    ///
    /// * `head` - Original head at speed_ratio = 1.0
    /// * `speed_ratio` - New speed / rated speed (0.0 to 1.0)
    ///
    /// # Returns
    ///
    /// Scaled head
    pub fn scale_head(head: f64, speed_ratio: f64) -> f64 {
        head * speed_ratio * speed_ratio
    }

    /// Applies affinity laws to scale power.
    ///
    /// # Arguments
    ///
    /// * `power` - Original power at speed_ratio = 1.0
    /// * `speed_ratio` - New speed / rated speed (0.0 to 1.0)
    ///
    /// # Returns
    ///
    /// Scaled power
    pub fn scale_power(power: f64, speed_ratio: f64) -> f64 {
        power * speed_ratio * speed_ratio * speed_ratio
    }

    /// Reverse affinity law: find original flow from scaled flow.
    pub fn unscale_flow(scaled_flow: f64, speed_ratio: f64) -> f64 {
        if speed_ratio <= 0.0 {
            return f64::INFINITY;
        }
        scaled_flow / speed_ratio
    }

    /// Reverse affinity law: find original head from scaled head.
    pub fn unscale_head(scaled_head: f64, speed_ratio: f64) -> f64 {
        if speed_ratio <= 0.0 {
            return f64::INFINITY;
        }
        scaled_head / (speed_ratio * speed_ratio)
    }
}

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

    #[test]
    fn test_polynomial_1d_constant() {
        let p = Polynomial1D::constant(5.0);
        assert_relative_eq!(p.evaluate(0.0), 5.0);
        assert_relative_eq!(p.evaluate(100.0), 5.0);
        assert_relative_eq!(p.derivative(0.0), 0.0);
    }

    #[test]
    fn test_polynomial_1d_linear() {
        // y = 3 + 2x
        let p = Polynomial1D::linear(3.0, 2.0);
        assert_relative_eq!(p.evaluate(0.0), 3.0);
        assert_relative_eq!(p.evaluate(1.0), 5.0);
        assert_relative_eq!(p.evaluate(2.0), 7.0);
        assert_relative_eq!(p.derivative(5.0), 2.0);
    }

    #[test]
    fn test_polynomial_1d_quadratic() {
        // y = 1 + 2x + 3x²
        let p = Polynomial1D::quadratic(1.0, 2.0, 3.0);
        assert_relative_eq!(p.evaluate(0.0), 1.0);
        assert_relative_eq!(p.evaluate(1.0), 6.0); // 1 + 2 + 3
        assert_relative_eq!(p.evaluate(2.0), 17.0); // 1 + 4 + 12
        assert_relative_eq!(p.derivative(2.0), 14.0); // 2 + 6*2 = 14
    }

    #[test]
    fn test_polynomial_1d_cubic() {
        // y = 1 + x + x² + x³
        let p = Polynomial1D::cubic(1.0, 1.0, 1.0, 1.0);
        assert_relative_eq!(p.evaluate(0.0), 1.0);
        assert_relative_eq!(p.evaluate(1.0), 4.0);
        assert_relative_eq!(p.evaluate(2.0), 15.0); // 1 + 2 + 4 + 8
    }

    #[test]
    fn test_polynomial_1d_empty() {
        let p = Polynomial1D::new(vec![]);
        assert_relative_eq!(p.evaluate(5.0), 0.0);
    }

    #[test]
    fn test_polynomial_1d_degree() {
        assert_eq!(Polynomial1D::constant(1.0).degree(), 0);
        assert_eq!(Polynomial1D::linear(1.0, 2.0).degree(), 1);
        assert_eq!(Polynomial1D::quadratic(1.0, 2.0, 3.0).degree(), 2);
        assert_eq!(Polynomial1D::cubic(1.0, 2.0, 3.0, 4.0).degree(), 3);
    }

    #[test]
    fn test_polynomial_1d_validate() {
        let valid = Polynomial1D::quadratic(1.0, 2.0, 3.0);
        assert!(valid.validate().is_ok());

        let nan_coeff = Polynomial1D::new(vec![1.0, f64::NAN]);
        assert!(nan_coeff.validate().is_err());

        let inf_coeff = Polynomial1D::new(vec![f64::INFINITY]);
        assert!(inf_coeff.validate().is_err());
    }

    #[test]
    fn test_polynomial_2d_constant() {
        let p = Polynomial2D::constant(5.0);
        assert_relative_eq!(p.evaluate(0.0, 0.0), 5.0);
        assert_relative_eq!(p.evaluate(10.0, 20.0), 5.0);
    }

    #[test]
    fn test_polynomial_2d_bilinear() {
        // z = 1 + 2*x + 3*y + 0.5*x*y
        let p = Polynomial2D::bilinear(1.0, 2.0, 3.0, 0.5);

        // At (0,0): z = 1
        assert_relative_eq!(p.evaluate(0.0, 0.0), 1.0);

        // At (1,0): z = 1 + 2 = 3
        assert_relative_eq!(p.evaluate(1.0, 0.0), 3.0);

        // At (0,1): z = 1 + 3 = 4
        assert_relative_eq!(p.evaluate(0.0, 1.0), 4.0);

        // At (2,3): z = 1 + 4 + 9 + 3 = 17
        assert_relative_eq!(p.evaluate(2.0, 3.0), 17.0);
    }

    #[test]
    fn test_polynomial_2d_biquadratic() {
        let p = Polynomial2D::biquadratic(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);

        // At (0,0): z = 1
        assert_relative_eq!(p.evaluate(0.0, 0.0), 1.0);

        // At (1,1): z = 1 + 2 + 3 + 4 + 5 + 6 = 21
        assert_relative_eq!(p.evaluate(1.0, 1.0), 21.0);
    }

    #[test]
    fn test_polynomial_2d_partial_derivatives() {
        // z = 1 + 2*x + 3*y + 4*x*y
        let p = Polynomial2D::bilinear(1.0, 2.0, 3.0, 4.0);

        // ∂z/∂x = 2 + 4*y
        assert_relative_eq!(p.partial_x(0.0, 0.0), 2.0);
        assert_relative_eq!(p.partial_x(0.0, 1.0), 6.0);

        // ∂z/∂y = 3 + 4*x
        assert_relative_eq!(p.partial_y(0.0, 0.0), 3.0);
        assert_relative_eq!(p.partial_y(1.0, 0.0), 7.0);
    }

    #[test]
    fn test_polynomial_2d_degrees() {
        let p = Polynomial2D::bilinear(1.0, 2.0, 3.0, 4.0);
        assert_eq!(p.degree_x(), 1);
        assert_eq!(p.degree_y(), 1);

        let biq = Polynomial2D::biquadratic(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
        assert_eq!(biq.degree_x(), 2);
        assert_eq!(biq.degree_y(), 2);
    }

    #[test]
    fn test_polynomial_2d_validate() {
        let valid = Polynomial2D::bilinear(1.0, 2.0, 3.0, 4.0);
        assert!(valid.validate().is_ok());

        let nan_coeff = Polynomial2D::new(vec![vec![1.0, f64::NAN]]);
        assert!(nan_coeff.validate().is_err());
    }

    #[test]
    fn test_affinity_laws_flow() {
        let flow = 100.0;

        // At full speed: no change
        assert_relative_eq!(AffinityLaws::scale_flow(flow, 1.0), 100.0);

        // At half speed: half flow
        assert_relative_eq!(AffinityLaws::scale_flow(flow, 0.5), 50.0);

        // At 80% speed: 80% flow
        assert_relative_eq!(AffinityLaws::scale_flow(flow, 0.8), 80.0);
    }

    #[test]
    fn test_affinity_laws_head() {
        let head = 100.0;

        // At full speed: no change
        assert_relative_eq!(AffinityLaws::scale_head(head, 1.0), 100.0);

        // At half speed: 25% head
        assert_relative_eq!(AffinityLaws::scale_head(head, 0.5), 25.0);

        // At 80% speed: 64% head
        assert_relative_eq!(AffinityLaws::scale_head(head, 0.8), 64.0);
    }

    #[test]
    fn test_affinity_laws_power() {
        let power = 1000.0;

        // At full speed: no change
        assert_relative_eq!(AffinityLaws::scale_power(power, 1.0), 1000.0);

        // At half speed: 12.5% power
        assert_relative_eq!(AffinityLaws::scale_power(power, 0.5), 125.0);

        // At 80% speed: 51.2% power
        assert_relative_eq!(AffinityLaws::scale_power(power, 0.8), 512.0);
    }

    #[test]
    fn test_affinity_laws_reverse() {
        let flow = 50.0;
        let head = 25.0;
        let speed = 0.5;

        // Reverse scaling
        assert_relative_eq!(AffinityLaws::unscale_flow(flow, speed), 100.0);
        assert_relative_eq!(AffinityLaws::unscale_head(head, speed), 100.0);
    }

    #[test]
    fn test_performance_curves() {
        // Typical pump curve: H = 50 - 0.1*Q - 0.001*Q²
        let head = Polynomial1D::quadratic(50.0, -0.1, -0.001);
        // Efficiency: η = 0.4 + 0.02*Q - 0.0001*Q²
        let eff = Polynomial1D::quadratic(0.4, 0.02, -0.0001);

        let curves = PerformanceCurves::simple(head.clone(), eff.clone());

        assert!(curves.validate().is_ok());
        assert_relative_eq!(curves.head_curve.evaluate(0.0), 50.0);
        assert_relative_eq!(curves.efficiency_curve.evaluate(0.0), 0.4);
    }

    #[test]
    fn test_pump_curve_realistic() {
        // Realistic pump curve for a small centrifugal pump
        // H (m) = 30 - 0.05*Q - 0.0005*Q², where Q is in m³/h
        let head_curve = Polynomial1D::quadratic(30.0, -0.05, -0.0005);

        // At Q=0 (shut-off): H = 30 m
        assert_relative_eq!(head_curve.evaluate(0.0), 30.0);

        // At Q=100 m³/h: H = 30 - 5 - 5 = 20 m
        assert_relative_eq!(head_curve.evaluate(100.0), 20.0);

        // At Q=200 m³/h: H = 30 - 10 - 20 = 0 m (run-out)
        assert_relative_eq!(head_curve.evaluate(200.0), 0.0, epsilon = 1e-10);
    }
}
