//! Critical point damping for thermodynamic properties.
//!
//! This module provides functionality to detect near-critical regions and apply
//! C1-continuous damping to prevent NaN values in derivative properties (Cp, Cv, etc.)
//! that diverge near the critical point.

use crate::types::{CriticalPoint, FluidId, FluidState, Property};

/// Parameters for critical point damping.
#[derive(Debug, Clone)]
pub struct DampingParams {
    /// Reduced temperature threshold (default: 0.05 = 5%)
    pub reduced_temp_threshold: f64,
    /// Reduced pressure threshold (default: 0.05 = 5%)
    pub reduced_pressure_threshold: f64,
    /// Smoothness parameter for sigmoid transition (default: 0.01)
    pub smoothness: f64,
    /// Maximum allowed Cp value in J/(kg·K) (default: 1e6)
    pub cp_max: f64,
    /// Maximum allowed Cv value in J/(kg·K) (default: 1e6)
    pub cv_max: f64,
    /// Maximum allowed derivative value (default: 1e10)
    pub derivative_max: f64,
}

impl Default for DampingParams {
    fn default() -> Self {
        DampingParams {
            reduced_temp_threshold: 0.05,
            reduced_pressure_threshold: 0.05,
            smoothness: 0.01,
            cp_max: 1e6,
            cv_max: 1e6,
            derivative_max: 1e10,
        }
    }
}

/// Extracts pressure and temperature from a FluidState.
/// Returns None if state cannot be converted to (P, T).
pub fn state_to_pt(state: &FluidState) -> Option<(f64, f64)> {
    match state {
        FluidState::PressureTemperature(p, t) => Some((p.to_pascals(), t.to_kelvin())),
        FluidState::PressureEnthalpy(_, _) => None,
        FluidState::PressureEntropy(_, _) => None,
        FluidState::PressureQuality(_, _) => None,
        FluidState::PressureTemperatureMixture(p, t, _) => Some((p.to_pascals(), t.to_kelvin())),
        FluidState::PressureEnthalpyMixture(_, _, _) => None,
        FluidState::PressureQualityMixture(_, _, _) => None,
    }
}

/// Calculate reduced coordinates (Tr, Pr) from absolute values and critical point.
///
/// - Tr = T / Tc
/// - Pr = P / Pc
pub fn reduced_coordinates(
    temperature_kelvin: f64,
    pressure_pascals: f64,
    cp: &CriticalPoint,
) -> (f64, f64) {
    let tr = temperature_kelvin / cp.temperature_kelvin();
    let pr = pressure_pascals / cp.pressure_pascals();
    (tr, pr)
}

/// Calculate the Euclidean distance from the critical point in reduced coordinates.
///
/// Distance = sqrt((Tr - 1)^2 + (Pr - 1)^2)
pub fn reduced_distance(temperature_kelvin: f64, pressure_pascals: f64, cp: &CriticalPoint) -> f64 {
    let (tr, pr) = reduced_coordinates(temperature_kelvin, pressure_pascals, cp);
    ((tr - 1.0).powi(2) + (pr - 1.0).powi(2)).sqrt()
}

/// Check if a state is within the near-critical region.
///
/// A state is "near critical" if:
/// |Tr - 1| < threshold AND |Pr - 1| < threshold
pub fn near_critical_point(
    temperature_kelvin: f64,
    pressure_pascals: f64,
    cp: &CriticalPoint,
    threshold: f64,
) -> bool {
    let (tr, pr) = reduced_coordinates(temperature_kelvin, pressure_pascals, cp);
    (tr - 1.0).abs() < threshold && (pr - 1.0).abs() < threshold
}

/// C1-continuous sigmoid blend factor.
///
/// Blend factor α: 0 = far from critical (use raw), 1 = at critical (use damped).
/// C1-continuous: α and dα/d(distance) are continuous.
///
/// - distance < threshold => near critical => α → 1
/// - distance > threshold + width => far => α → 0
pub fn sigmoid_blend(distance: f64, threshold: f64, width: f64) -> f64 {
    // α = 0.5 * (1 + tanh((threshold - distance) / width))
    // At distance = 0 (critical): α ≈ 1
    // At distance = threshold: α = 0.5
    // At distance >> threshold: α → 0
    let x = (threshold - distance) / width;
    0.5 * (1.0 + x.tanh())
}

/// Derivative of sigmoid blend factor with respect to distance.
///
/// This is used to ensure C1 continuity when applying damping.
pub fn sigmoid_blend_derivative(distance: f64, threshold: f64, width: f64) -> f64 {
    // derivative of 0.5 * (1 + tanh((threshold - distance) / width)) with respect to distance
    // = 0.5 * sech^2((threshold - distance) / width) * (-1 / width)
    // = -0.5 * sech^2(x) / width where x = (threshold - distance) / width
    let x = (threshold - distance) / width;
    let sech = 1.0 / x.cosh();
    -0.5 * sech * sech / width
}

/// Apply damping to a property value.
///
/// Returns the damped value using sigmoid blending between raw and capped values.
pub fn damp_property(value: f64, max_value: f64, blend_factor: f64) -> f64 {
    let capped = value.abs().min(max_value) * value.signum();
    blend_factor * capped + (1.0 - blend_factor) * value
}

/// Apply damping to derivative properties that may diverge near critical point.
///
/// Properties like Cp, Cv, and (∂ρ/∂P)_T can diverge near the critical point.
/// This function applies a smooth cap to prevent NaN values.
pub fn damp_derivative(value: f64, params: &DampingParams) -> f64 {
    let blend = sigmoid_blend(0.0, params.reduced_temp_threshold, params.smoothness);
    damp_property(value, params.derivative_max, blend)
}

/// Check if a property should be damped.
///
/// Derivative properties (Cp, Cv, etc.) may diverge near critical point.
pub fn should_damp_property(property: Property) -> bool {
    matches!(
        property,
        Property::Cp | Property::Cv | Property::SpeedOfSound | Property::Density
    )
}

/// DampingState holds runtime state for damping calculations.
#[derive(Debug, Clone)]
pub struct DampingState {
    /// Whether damping is active for the current query
    pub is_damping: bool,
    /// The blend factor (0 = no damping, 1 = full damping)
    pub blend_factor: f64,
    /// Distance from critical point
    pub distance: f64,
}

impl DampingState {
    /// Create a new DampingState with no damping
    pub fn none() -> Self {
        DampingState {
            is_damping: false,
            blend_factor: 0.0,
            distance: f64::MAX,
        }
    }
}

/// Calculate damping state for a given fluid and state.
pub fn calculate_damping_state(
    _fluid: &FluidId,
    state: &FluidState,
    cp: &CriticalPoint,
    params: &DampingParams,
) -> DampingState {
    let (p, t) = match state_to_pt(state) {
        Some(v) => v,
        None => return DampingState::none(),
    };

    let distance = reduced_distance(t, p, cp);
    let is_near = near_critical_point(t, p, cp, params.reduced_temp_threshold);

    if !is_near {
        return DampingState::none();
    }

    let blend_factor = sigmoid_blend(distance, params.reduced_temp_threshold, params.smoothness);

    DampingState {
        is_damping: true,
        blend_factor,
        distance,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::FluidState;
    use entropyk_core::{Pressure, Temperature};

    fn make_co2_critical_point() -> CriticalPoint {
        CriticalPoint::new(
            Temperature::from_kelvin(304.13),
            Pressure::from_pascals(7.3773e6),
            467.6,
        )
    }

    #[test]
    fn test_reduced_coordinates() {
        let cp = make_co2_critical_point();

        // At critical point: Tr = 1, Pr = 1
        let (tr, pr) = reduced_coordinates(304.13, 7.3773e6, &cp);
        assert!((tr - 1.0).abs() < 1e-10);
        assert!((pr - 1.0).abs() < 1e-10);

        // At 5% above critical
        let (tr, pr) = reduced_coordinates(319.3365, 7.746165e6, &cp);
        assert!((tr - 1.05).abs() < 1e-6);
        assert!((pr - 1.05).abs() < 1e-6);
    }

    #[test]
    fn test_reduced_distance_at_critical() {
        let cp = make_co2_critical_point();

        // At critical point, distance should be 0
        let dist = reduced_distance(304.13, 7.3773e6, &cp);
        assert!(dist.abs() < 1e-10);
    }

    #[test]
    fn test_near_critical_point_true() {
        let cp = make_co2_critical_point();

        // At critical point
        assert!(near_critical_point(304.13, 7.3773e6, &cp, 0.05));

        // 5% from critical
        let t = 304.13 * 1.03;
        let p = 7.3773e6 * 1.03;
        assert!(near_critical_point(t, p, &cp, 0.05));
    }

    #[test]
    fn test_near_critical_point_false() {
        let cp = make_co2_critical_point();

        // Far from critical (room temperature, 1 bar)
        assert!(!near_critical_point(298.15, 1e5, &cp, 0.05));

        // Outside 5% threshold
        let t = 304.13 * 1.10;
        let p = 7.3773e6 * 1.10;
        assert!(!near_critical_point(t, p, &cp, 0.05));
    }

    #[test]
    fn test_sigmoid_blend_at_critical() {
        let threshold = 0.05;
        let width = 0.01;

        // At critical point (distance = 0), blend should be ~1
        let blend = sigmoid_blend(0.0, threshold, width);
        assert!(
            blend > 0.99,
            "Expected blend > 0.99 at critical point, got {}",
            blend
        );

        // At boundary (distance = threshold), blend should be 0.5
        let blend = sigmoid_blend(threshold, threshold, width);
        assert!(
            (blend - 0.5).abs() < 0.001,
            "Expected blend ~0.5 at boundary"
        );

        // Far from critical (distance > threshold + width), blend should be ~0
        let blend = sigmoid_blend(threshold + width * 10.0, threshold, width);
        assert!(blend < 0.001);
    }

    #[test]
    fn test_sigmoid_blend_derivative() {
        let threshold = 0.05;
        let width = 0.01;

        // Derivative should be negative (blend decreases as distance increases)
        let deriv = sigmoid_blend_derivative(0.0, threshold, width);
        assert!(deriv < 0.0, "Expected negative derivative");

        // Derivative should be small (near zero) far from critical
        let deriv = sigmoid_blend_derivative(threshold + width * 10.0, threshold, width);
        assert!(deriv.abs() < 1e-6);
    }

    #[test]
    fn test_sigmoid_c1_continuous() {
        let threshold = 0.05;
        let width = 0.01;

        // Check C1 continuity: finite difference should match analytical derivative
        let eps = 1e-6;
        for distance in [0.0, 0.02, 0.04, 0.06, 0.08] {
            let deriv_analytical = sigmoid_blend_derivative(distance, threshold, width);
            let deriv_numerical = (sigmoid_blend(distance + eps, threshold, width)
                - sigmoid_blend(distance - eps, threshold, width))
                / (2.0 * eps);

            assert!(
                (deriv_analytical - deriv_numerical).abs() < 1e-4,
                "C1 continuity failed at distance {}: analytical={}, numerical={}",
                distance,
                deriv_analytical,
                deriv_numerical
            );
        }
    }

    #[test]
    fn test_damp_property() {
        // Large value should be capped
        let damped = damp_property(1e8, 1e6, 1.0);
        assert!(damped.abs() < 1e6 + 1.0);

        // Small value should remain unchanged
        let damped = damp_property(1000.0, 1e6, 1.0);
        assert!((damped - 1000.0).abs() < 1.0);

        // Partial blend
        let damped = damp_property(1e8, 1e6, 0.5);
        assert!(damped > 1e6 && damped < 1e8);
    }

    #[test]
    fn test_state_to_pt() {
        let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));

        let (p, t) = state_to_pt(&state).unwrap();
        assert!((p - 1e5).abs() < 1.0);
        assert!((t - 298.15).abs() < 1.0);

        // Enthalpy state should return None
        let state = FluidState::from_ph(
            Pressure::from_bar(1.0),
            entropyk_core::Enthalpy::from_kilojoules_per_kg(400.0),
        );
        assert!(state_to_pt(&state).is_none());
    }

    #[test]
    fn test_should_damp_property() {
        assert!(should_damp_property(Property::Cp));
        assert!(should_damp_property(Property::Cv));
        assert!(should_damp_property(Property::Density));
        assert!(should_damp_property(Property::SpeedOfSound));

        assert!(!should_damp_property(Property::Enthalpy));
        assert!(!should_damp_property(Property::Entropy));
        assert!(!should_damp_property(Property::Pressure));
        assert!(!should_damp_property(Property::Temperature));
    }

    #[test]
    fn test_calculate_damping_state_near_critical() {
        let cp = make_co2_critical_point();
        let params = DampingParams::default();

        // At critical point
        let state = FluidState::from_pt(
            Pressure::from_pascals(7.3773e6),
            Temperature::from_kelvin(304.13),
        );
        let fluid = FluidId::new("CO2");

        let damping = calculate_damping_state(&fluid, &state, &cp, &params);
        assert!(damping.is_damping);
        assert!(damping.blend_factor > 0.9);
    }

    #[test]
    fn test_calculate_damping_state_far_from_critical() {
        let cp = make_co2_critical_point();
        let params = DampingParams::default();

        // Room temperature, 1 bar - far from critical
        let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
        let fluid = FluidId::new("CO2");

        let damping = calculate_damping_state(&fluid, &state, &cp, &params);
        assert!(!damping.is_damping);
    }

    #[test]
    fn test_damping_region_boundary_smooth_transition() {
        let cp = make_co2_critical_point();
        let params = DampingParams::default();

        // 4.9% from critical - inside region
        let t_near = 304.13 * (1.0 + 0.049);
        let p_near = 7.3773e6 * (1.0 + 0.049);
        let state_near = FluidState::from_pt(
            Pressure::from_pascals(p_near),
            Temperature::from_kelvin(t_near),
        );
        let damping_near = calculate_damping_state(&FluidId::new("CO2"), &state_near, &cp, &params);

        // 5.1% from critical - outside region
        let t_far = 304.13 * (1.0 + 0.051);
        let p_far = 7.3773e6 * (1.0 + 0.051);
        let state_far = FluidState::from_pt(
            Pressure::from_pascals(p_far),
            Temperature::from_kelvin(t_far),
        );
        let damping_far = calculate_damping_state(&FluidId::new("CO2"), &state_far, &cp, &params);

        // Should transition smoothly
        assert!(damping_near.is_damping, "4.9% should be in damping region");
        assert!(
            !damping_far.is_damping,
            "5.1% should be outside damping region"
        );
    }

    #[test]
    fn test_damping_transition_is_smooth() {
        let cp = make_co2_critical_point();
        let params = DampingParams::default();

        // Test at various distances around the boundary
        let distances = [0.03, 0.04, 0.045, 0.05, 0.055, 0.06];
        let mut previous_blend = 1.0;

        for d in distances {
            let t = 304.13 * (1.0 + d);
            let p = 7.3773e6 * (1.0 + d);
            let state = FluidState::from_pt(Pressure::from_pascals(p), Temperature::from_kelvin(t));
            let damping = calculate_damping_state(&FluidId::new("CO2"), &state, &cp, &params);

            let blend = damping.blend_factor;
            // Blend should decrease smoothly (no sudden jumps)
            assert!(
                blend <= previous_blend + 0.1,
                "Blend should decrease smoothly: prev={}, curr={}",
                previous_blend,
                blend
            );
            previous_blend = blend;
        }
    }
}
