//! # Friedman differential and integral isoconversional methods
//!
//! ## What this module does
//! Implements two Friedman-family **isoconversional** methods for model-free
//! kinetic analysis of TGA data:
//!
//! | Struct | Experiment type | Linearised form |
//! |--------|-----------------|-----------------|
//! | [`DifferentialFriedmanSolver`] / [`DifferentialFriedman`] | Non-isothermal (several heating rates) | `ln(dα/dt)` vs `1/T` |
//! | [`FriedmanIntegralSolver`] / [`FriedmanIntegral`] | Isothermal (several constant temperatures) | `-ln(tα)` vs `1/T` |
//!
//! Both methods are **differential** in spirit (they work directly with
//! measured or interpolated quantities rather than approximating the
//! temperature integral), which makes them more sensitive to noise but
//! free from the approximation errors of OFW/KAS/Starink.
//!
//! ## Pipeline
//! ```text
//! KineticDataView
//!      │
//!      ▼
//! ConversionGridBuilder
//!   ├─ build_nonisothermal()           ← DifferentialFriedman  (non-isothermal)
//!   └─ build_isothermal() ← FriedmanIntegral      (isothermal)
//!      │
//!      ▼
//! ConversionGrid  [n_exp × n_α]
//!      │
//!      ▼
//! DifferentialFriedmanSolver / FriedmanIntegralSolver
//!      │
//!      ▼
//! IsoconversionalResult
//! ```
//!
//! ## Main data structures
//! - [`DifferentialFriedmanSolver`] — stateless solver; reads
//!   `grid.conversion_rate` and `grid.inv_temperature`.
//! - [`FriedmanIntegralSolver`] — stateless solver; reads `grid.time` and
//!   `grid.inv_temperature`.
//! - [`DifferentialFriedman`] / [`FriedmanIntegral`] — zero-size marker
//!   structs that implement `KineticMethod` and own the grid-building step.
//!
//! ## Math
//!
//! ### Differential Friedman (non-isothermal)
//! At each fixed α the reaction rate satisfies:
//! ```text
//! dα/dt = A · f(α) · exp(-Eα/RT)
//! ```
//! Taking the natural log at constant α (so `f(α)` is constant across
//! experiments):
//! ```text
//! ln(dα/dt) = const - (Eα/R) · (1/T)
//! ```
//! A linear regression of `ln(dα/dt)` vs `1/T` across heating rates gives
//! slope = `-Eα/R`, hence `Eα = -slope · R`.
//!
//! ### Integral Friedman (isothermal)
//! For a first-order reaction at constant temperature T the time to reach
//! conversion α is:
//! ```text
//! tα = -ln(1-α) / k(T)  ⇒  ln(tα) = -ln(k₀) + (Eα/R) · (1/T) + const
//! ```
//! Regressing `-ln(tα)` vs `1/T` across isothermal temperatures gives
//! slope = `-Eα/R`, hence `Eα = -slope · R`.
//!
//! ## Non-trivial technique
//! Points with non-positive rate or time are silently skipped before
//! regression (guard against log-of-zero at the tails of the conversion
//! curve).  The regression is performed only when at least 2 valid points
//! remain, so sparse or noisy layers degrade gracefully instead of panicking.

use super::kinetic_regression::linear_regression;
use crate::Kinetics::experimental_kinetics::kinetic_methods::integral_isoconversion::{
    IsoLayerResult, IsoconversionalResult,
};
use crate::Kinetics::experimental_kinetics::kinetic_methods::{
    ConversionGrid, ConversionGridBuilder, GridInterpolation, KineticDataView, KineticMethod,
    KineticRequirements, TGADomainError, require_isothermal,
};
use crate::Kinetics::experimental_kinetics::one_experiment_dataset::ColumnNature;
use ndarray::Array1;
//============================================================================================================
//          DIFFERENTIAL FRIEDMAN
//===========================================================================================================
/// Stateless solver for the differential Friedman method (non-isothermal).
///
/// Operates directly on a pre-built [`ConversionGrid`]; call `solve` to
/// obtain an [`IsoconversionalResult`].
pub struct DifferentialFriedmanSolver;

impl Default for DifferentialFriedmanSolver {
    fn default() -> Self {
        Self
    }
}

impl DifferentialFriedmanSolver {
    /// Regresses `ln(dα/dt)` vs `1/T` at each α layer.
    ///
    /// Points with `rate ≤ 0` are skipped to avoid `ln(0)`.  Layers with
    /// fewer than 2 valid points are omitted from the output.
    pub fn solve(&self, grid: &ConversionGrid) -> Result<IsoconversionalResult, TGADomainError> {
        let r = 8.314462618;

        let n_eta = grid.eta.len();
        let n_exp = grid.inv_temperature.nrows();

        let mut layers = Vec::with_capacity(n_eta);

        for k in 0..n_eta {
            let inv_t = grid.inv_temperature.column(k);
            let rate = grid.conversion_rate.column(k);

            let mut x = Vec::with_capacity(n_exp);
            let mut y = Vec::with_capacity(n_exp);

            for (&it, &r) in inv_t.iter().zip(rate.iter()) {
                if !it.is_finite() || !r.is_finite() || it <= 0.0 || r <= 0.0 {
                    continue;
                }

                x.push(it);
                y.push(r.ln());
            }

            if x.len() < 2 {
                continue;
            }

            let reg = linear_regression(&Array1::from_vec(x), &Array1::from_vec(y));

            let ea = -reg.slope * r;

            layers.push(IsoLayerResult {
                eta: grid.eta[k],

                ea,

                k: None,

                regression: reg,
            });
        }

        Ok(IsoconversionalResult {
            method: "Friedman differential",

            layers,
        })
    }
}

/// `KineticMethod` entry point for the differential Friedman method.
///
/// Builds a non-isothermal `ConversionGrid` (via `build()`) and delegates
/// to [`DifferentialFriedmanSolver`].
pub struct DifferentialFriedman;

impl KineticMethod for DifferentialFriedman {
    type Output = IsoconversionalResult;

    fn name(&self) -> &'static str {
        "Friedman differential"
    }

    fn compute(&self, data: &KineticDataView) -> Result<Self::Output, TGADomainError> {
        let grid = ConversionGridBuilder::new()
            .interpolation(GridInterpolation::Linear)
            .build_nonisothermal(data)?;
        DifferentialFriedmanSolver.solve(&grid)
    }
    fn check_input(&self, data: &KineticDataView) -> Result<(), TGADomainError> {
        require_isothermal(data)
    }

    fn requirements(&self) -> KineticRequirements {
        KineticRequirements {
            min_experiments: 3,
            needs_conversion: true,
            needs_conversion_rate: true,
            needs_temperature: true,
            needs_heating_rate: false,
        }
    }

    fn required_columns_by_nature(&self) -> Vec<ColumnNature> {
        vec![
            ColumnNature::Conversion,
            ColumnNature::Temperature,
            ColumnNature::Time,
            ColumnNature::ConversionRate,
        ]
    }
}
//================================================================================================================================
//              INTEGRAL FRIEDMAN
//=======================================================================================================================
/// Stateless solver for the integral Friedman method (isothermal).
///
/// Operates on a pre-built [`ConversionGrid`] where each row corresponds to
/// one isothermal temperature; call `solve` to obtain an
/// [`IsoconversionalResult`].
pub struct FriedmanIntegralSolver;

impl FriedmanIntegralSolver {
    /// Regresses `-ln(tα)` vs `1/T` at each α layer.
    ///
    /// Points with `time ≤ 0` are skipped.  Layers with fewer than 2 valid
    /// points are omitted from the output.
    pub fn solve(&self, grid: &ConversionGrid) -> Result<IsoconversionalResult, TGADomainError> {
        let r = 8.314462618;

        let n_eta = grid.eta.len();
        let n_exp = grid.inv_temperature.nrows();

        let mut layers = Vec::with_capacity(n_eta);

        for k in 0..n_eta {
            let inv_t = grid.inv_temperature.column(k);
            let t = grid.time.column(k);

            let mut x = Vec::with_capacity(n_exp);
            let mut y = Vec::with_capacity(n_exp);

            for (&it, &time) in inv_t.iter().zip(t.iter()) {
                if !it.is_finite() || !time.is_finite() || it <= 0.0 || time <= 0.0 {
                    continue;
                }

                x.push(it);
                y.push(-time.ln());
            }

            if x.len() < 2 {
                continue;
            }

            let reg = linear_regression(&Array1::from_vec(x), &Array1::from_vec(y));

            let ea = -reg.slope * r;

            layers.push(IsoLayerResult {
                eta: grid.eta[k],

                ea,

                k: None,

                regression: reg,
            });
        }

        Ok(IsoconversionalResult {
            method: "Friedman integral",

            layers,
        })
    }
}

/// `KineticMethod` entry point for the integral Friedman method.
///
/// Builds an isothermal `ConversionGrid` (via `build_isothermal()`, which
/// fills each row with the constant temperature of that experiment) and
/// delegates to [`FriedmanIntegralSolver`].
pub struct FriedmanIntegral;

impl KineticMethod for FriedmanIntegral {
    type Output = IsoconversionalResult;

    fn name(&self) -> &'static str {
        "Friedman integral"
    }

    fn compute(&self, data: &KineticDataView) -> Result<Self::Output, TGADomainError> {
        let grid = ConversionGridBuilder::new()
            .interpolation(GridInterpolation::Linear)
            .build_isothermal(data)?;
        FriedmanIntegralSolver.solve(&grid)
    }

    fn requirements(&self) -> KineticRequirements {
        KineticRequirements {
            min_experiments: 3,
            needs_conversion: true,
            needs_conversion_rate: false,
            needs_temperature: true,
            needs_heating_rate: false,
        }
    }

    fn required_columns_by_nature(&self) -> Vec<ColumnNature> {
        vec![
            ColumnNature::Conversion,
            ColumnNature::Temperature,
            ColumnNature::Time,
            ColumnNature::ConversionRate,
        ]
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::Kinetics::experimental_kinetics::kinetic_methods::integral_isoconversion_tests::tests::simulate_tga_first_order_isothermal;
    use crate::Kinetics::experimental_kinetics::kinetic_methods_tests::tests::{
         build_view_from_cfg_exact_m0,
    };
    use crate::Kinetics::experimental_kinetics::testing_mod::tests_afvanced_config::base_advanced_config_isothermal;
    use std::time::Instant;

    // ── helpers ──────────────────────────────────────────────────────────────

    /// Temperatures used across most tests (K).
    fn iso_temps() -> Vec<f64> {
        vec![520.0, 540.0, 560.0, 580.0, 600.0, 620.0]
    }

    // ── grid-level tests (simulate_tga_first_order_isothermal) ───────────────

    #[test]
    fn friedman_integral_grid_recovers_activation_energy() {
        let e_true = 80_000.0;
        let grid = simulate_tga_first_order_isothermal(
            0.0, // t0 unused for isothermal
            1e5,
            e_true,
            &iso_temps(),
            0.5,
            50_000,
            Some((5..90).map(|x| x as f64 / 100.0).collect()),
        );
        grid.report();
        let result = FriedmanIntegralSolver.solve(&grid).unwrap();
        result.pretty_print_and_assert(0.05, 0.90, 50, Some(0.99));
    }

    #[test]
    fn friedman_integral_grid_recovers_activation_energy2() {
        let e_true = 120_000.0;
        let grid = simulate_tga_first_order_isothermal(
            0.0,
            1e8,
            e_true,
            &[550.0, 575.0, 600.0, 625.0, 650.0, 675.0, 700.0],
            0.3,
            150_000,
            Some((5..90).map(|x| x as f64 / 100.0).collect()),
        );
        grid.report();
        let result = FriedmanIntegralSolver.solve(&grid).unwrap();
        result.pretty_print_and_assert(0.10, 0.90, 50, Some(0.99));
    }

    #[test]
    fn friedman_integral_grid_no_alpha_grid() {
        let now = Instant::now();
        let e_true = 85_000.0;
        let grid =
            simulate_tga_first_order_isothermal(0.0, 1e5, e_true, &iso_temps(), 0.5, 50_000, None);
        println!("grid built in {} ms", now.elapsed().as_millis());
        grid.report();
        let now = Instant::now();
        let result = FriedmanIntegralSolver.solve(&grid).unwrap();
        println!("solver completed in {} ms", now.elapsed().as_millis());
        result.pretty_print_and_assert(0.05, 0.90, 50, Some(0.99));
    }

    #[test]
    fn friedman_integral_grid_wider_temperature_range() {
        let grid = simulate_tga_first_order_isothermal(
            0.0,
            5e6,
            115_000.0,
            &[500.0, 530.0, 560.0, 590.0, 620.0, 650.0, 680.0],
            1.0,
            80_000,
            Some((5..90).map(|x| x as f64 / 100.0).collect()),
        );
        grid.report();
        let result = FriedmanIntegralSolver.solve(&grid).unwrap();
        result.pretty_print_and_assert(0.10, 0.90, 50, Some(0.99));
    }

    // ── full-pipeline tests (build_view_from_cfg_exact_m0) ───────────────────

    #[test]
    fn friedman_integral_compute_with_mock_isothermal_data() {
        let cfg = base_advanced_config_isothermal(1e5, 80_000.0, iso_temps(), 0.1, 10_000);
        let view = build_view_from_cfg_exact_m0(&cfg).unwrap();
        let result = FriedmanIntegral.compute(&view).unwrap();
        assert!(!result.layers.is_empty());
        for layer in &result.layers {
            assert!(layer.ea.is_finite());
            assert!((0.0..=1.0).contains(&layer.regression.r2));
        }
    }

    #[test]
    fn friedman_integral_compute_with_mock_isothermal_data2() {
        let now = Instant::now();
        let cfg = base_advanced_config_isothermal(
            1e6,
            100_000.0,
            vec![520.0, 540.0, 560.0, 580.0, 600.0, 620.0, 640.0, 660.0],
            1.0,
            30_000,
        );
        let view = build_view_from_cfg_exact_m0(&cfg).unwrap();
        println!("view built in {} ms", now.elapsed().as_millis());

        let now = Instant::now();
        let result = FriedmanIntegral.compute(&view).unwrap();
        println!("elapsed: {} ms", now.elapsed().as_millis());
        result.pretty_print_and_assert(0.05, 0.90, 100, Some(0.99));
    }

    #[test]
    fn friedman_integral_compute_with_mock_isothermal_data3() {
        let cfg = base_advanced_config_isothermal(
            1e7,
            150_000.0,
            vec![600.0, 625.0, 650.0, 675.0, 700.0, 725.0],
            0.5,
            50_000,
        );
        let view = build_view_from_cfg_exact_m0(&cfg).unwrap();
        let result = FriedmanIntegral.compute(&view).unwrap();
        result.pretty_print_and_assert(0.05, 0.90, 100, Some(0.99));
    }
}

#[cfg(test)]
mod tests_differential {
    use super::*;
    use crate::Kinetics::experimental_kinetics::kinetic_methods::integral_isoconversion_tests::tests::simulate_tga_first_order2;
    use crate::Kinetics::experimental_kinetics::kinetic_methods_tests::tests::build_view_from_cfg_exact_m0;
    use crate::Kinetics::experimental_kinetics::testing_mod::tests_afvanced_config::base_advanced_config_non_isothermal;
    use std::time::Instant;

    // ── grid-level tests (simulate_tga_first_order2) ─────────────────────────

    #[test]
    fn friedman_differential_grid_recovers_activation_energy() {
        let e_true = 80_000.0;
        let grid = simulate_tga_first_order2(
            500.0,
            1e5,
            e_true,
            &[2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
            0.5,
            50_000,
            Some((5..90).map(|x| x as f64 / 100.0).collect()),
        );
        grid.report();
        let result = DifferentialFriedmanSolver.solve(&grid).unwrap();
        result.pretty_print_and_assert(0.05, 0.90, 50, Some(0.99));
    }

    #[test]
    fn friedman_differential_grid_recovers_activation_energy2() {
        let e_true = 120_000.0;
        let grid = simulate_tga_first_order2(
            600.0,
            1e8,
            e_true,
            &[0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 4.0],
            1.0,
            175_000,
            Some((5..90).map(|x| x as f64 / 100.0).collect()),
        );
        grid.report();
        let result = DifferentialFriedmanSolver.solve(&grid).unwrap();
        result.pretty_print_and_assert(0.05, 0.90, 50, Some(0.99));
    }

    #[test]
    fn friedman_differential_grid_no_alpha_grid() {
        let now = Instant::now();
        let grid = simulate_tga_first_order2(
            500.0,
            1e5,
            85_000.0,
            &[2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
            0.5,
            50_000,
            None,
        );
        println!("grid built in {} ms", now.elapsed().as_millis());
        grid.report();
        let now = Instant::now();
        let result = DifferentialFriedmanSolver.solve(&grid).unwrap();
        println!("solver completed in {} ms", now.elapsed().as_millis());
        result.pretty_print_and_assert(0.05, 0.90, 50, Some(0.99));
    }

    #[test]
    fn friedman_differential_grid_wider_heating_rates() {
        let grid = simulate_tga_first_order2(
            550.0,
            5e7,
            150_000.0,
            &[0.5, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0],
            1.0,
            80_000,
            Some((5..90).map(|x| x as f64 / 100.0).collect()),
        );
        grid.report();
        let result = DifferentialFriedmanSolver.solve(&grid).unwrap();
        result.pretty_print_and_assert(0.05, 0.90, 50, Some(0.99));
    }

    // ── full-pipeline tests (build_view_from_cfg_exact_m0) ───────────────────

    #[test]
    fn friedman_differential_compute_with_mock_non_isothermal_data() {
        let cfg = base_advanced_config_non_isothermal(
            700.0,
            1e5,
            80_000.0,
            0.1,
            10_000,
            vec![0.5, 3.0, 5.0],
        );
        let view = build_view_from_cfg_exact_m0(&cfg).unwrap();
        let result = DifferentialFriedman.compute(&view).unwrap();
        assert!(!result.layers.is_empty());
        for layer in &result.layers {
            let eta = layer.eta;
            if eta > 0.05 && eta < 0.95 {
                assert!(layer.ea.is_finite());
                assert!((0.0..=1.0).contains(&layer.regression.r2));
            }
        }
    }

    #[test]
    fn friedman_differential_compute_with_mock_non_isothermal_data2() {
        let now = Instant::now();
        let cfg = base_advanced_config_non_isothermal(
            420.0,
            1e6,
            100_000.0,
            1.0,
            30_000,
            vec![0.5, 0.75, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],
        );
        let view = build_view_from_cfg_exact_m0(&cfg).unwrap();
        println!("view built in {} ms", now.elapsed().as_millis());

        let now = Instant::now();
        let result = DifferentialFriedman.compute(&view).unwrap();
        println!("elapsed: {} ms", now.elapsed().as_millis());
        result.pretty_print_and_assert(0.05, 0.90, 100, Some(0.99));
    }

    #[test]
    fn friedman_differential_compute_with_mock_non_isothermal_data3() {
        let cfg = base_advanced_config_non_isothermal(
            600.0,
            1e7,
            150_000.0,
            0.5,
            50_000,
            vec![0.5, 1.0, 1.5, 2.0, 2.5, 3.0],
        );
        let view = build_view_from_cfg_exact_m0(&cfg).unwrap();
        let result = DifferentialFriedman.compute(&view).unwrap();
        result.pretty_print_and_assert(0.05, 0.90, 100, Some(0.99));
    }
}
