//! Internal configuration for first-order solving.

use crate::{
    Float,
    dense::InterpolateFn,
    matrix::MatrixStorage,
    methods::{BDF, DOP853, DOPRI5, LSODA, RADAU, RK4, RK23, Tolerance},
};

/// Numerical methods for first-order IVPs.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Method {
    /// Bogacki–Shampine 3(2) adaptive RK
    RK23,
    /// Dormand–Prince 5(4) adaptive RK; in SciPy known as RK45
    DOPRI5,
    /// Dormand–Prince 8(5,3) high-order adaptive RK
    DOP853,
    /// Classic fixed-step RK4
    RK4,
    /// Radau 5th order implicit Runge-Kutta method
    RADAU,
    /// Variable-order (1-5) Backward Differentiation Formula method for stiff problems
    BDF,
    /// LSODA automatic Adams/BDF switching method
    LSODA,
}

impl Method {
    /// Number of dense output coefficients per state variable.
    ///
    /// This determines the memory layout of the `cont` buffer used by each method.
    #[inline]
    pub const fn coeffs_per_state(self) -> usize {
        match self {
            Method::RK4 => 4,
            Method::RK23 => 4,
            Method::DOPRI5 => 5,
            Method::DOP853 => 8,
            Method::RADAU => 4,
            Method::BDF => 7,
            Method::LSODA => 8,
        }
    }

    /// Get the interpolation function for this method.
    ///
    /// Returns a function pointer that can interpolate dense output for any step.
    #[inline]
    pub fn interpolate_fn(self) -> InterpolateFn {
        match self {
            Method::RK4 => RK4::interpolate,
            Method::RK23 => RK23::interpolate,
            Method::DOPRI5 => DOPRI5::interpolate,
            Method::DOP853 => DOP853::interpolate,
            Method::RADAU => RADAU::interpolate,
            Method::BDF => BDF::interpolate,
            Method::LSODA => LSODA::interpolate,
        }
    }
}

impl From<&str> for Method {
    fn from(s: &str) -> Self {
        match s.to_uppercase().as_str() {
            "RK23" => Method::RK23,
            "DOPRI5" | "RK45" => Method::DOPRI5,
            "DOP853" => Method::DOP853,
            "RK4" => Method::RK4,
            "RADAU" | "RADAU5" => Method::RADAU,
            "BDF" | "BDF15" => Method::BDF,
            "LSODA" => Method::LSODA,
            _ => Method::DOPRI5, // Default
        }
    }
}

/// Selects how a first-order solver should obtain Jacobian information.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum JacobianSource {
    /// Use the solver's default behavior.
    ///
    /// For LSODA this means its internal finite-difference Jacobian path.
    /// Other solvers continue to call [`crate::ivp::FirstOrderSystem::jac`].
    Auto,
    /// Call [`crate::ivp::FirstOrderSystem::jac`] for Jacobian information.
    UserProvided,
    /// Force the solver's internal finite-difference Jacobian path when available.
    ///
    /// Solvers without an internal Jacobian approximation may ignore this and
    /// continue to call [`crate::ivp::FirstOrderSystem::jac`].
    InternalFiniteDifference,
}

/// Internal options for first-order solving.
pub(crate) struct FirstOrderConfig {
    /// Integration method. Choose an explicit RK (RK23/DOPRI5/DOP853/RK4) for non‑stiff
    /// problems or an implicit RK (RADAU) for stiff/DAE systems. Default: DOPRI5 (aka RK45).
    pub method: Method,
    /// Relative tolerance for local error control. Accepts scalar or per‑component array/vector
    /// via [`Tolerance`]. The effective scaling for component i is `atol[i] + rtol[i]*|y[i]|`.
    pub rtol: Tolerance,
    /// Absolute tolerance for local error control. Accepts scalar or per‑component array/vector.
    /// Used together with `rtol` to build the error scale `atol + rtol*|y|`.
    pub atol: Tolerance,
    /// Maximum number of solver steps.
    pub max_steps: Option<usize>,
    /// Times at which to return the solution. If `None` the points are selected by the solver.
    pub t_eval: Option<Vec<Float>>,
    /// Initial step size (its sign must match `xend - x0`).
    pub first_step: Option<Float>,
    /// Upper bound on step size.
    pub max_step: Option<Float>,
    /// Lower bound on step size.
    pub min_step: Option<Float>,
    /// Store per‑step interpolants for cheap post‑run evaluation via `Solution::sol`/`sol_many`.
    /// Increases memory usage; recommended if you need values at times other than internal steps.
    pub dense_output: bool,
    /// Preferred storage for the Jacobian `J = ∂f/∂y`. Default: `Full` (dense, writable).
    /// Solvers that don’t use a Jacobian ignore this. For banded storage you must provide
    /// an analytical Jacobian consistent with the chosen layout.
    pub jac_storage: MatrixStorage,
    /// How the solver should obtain Jacobian information.
    pub jacobian_source: JacobianSource,
    /// Preferred storage for the mass matrix `M` in `M y' = f(t,y)`. Default: `Identity`
    /// (implicit I, no allocation). Set to `Full`/`Banded` to provide a non‑trivial mass matrix.
    pub mass_storage: MatrixStorage,
    /// DAE partition: number of index‑1 (differential) variables at the start of the state.
    /// If `nind2`/`nind3` are set and `nind1` is omitted, it is inferred as `n − nind2 − nind3`.
    /// If none are set, all variables are treated as index‑1 (pure ODE).
    pub nind1: Option<usize>,
    /// DAE partition: number of index‑2 algebraic variables following the index‑1 block.
    /// In RADAU error estimation these components are scaled by the current step size `h`.
    pub nind2: Option<usize>,
    /// DAE partition: number of index‑3 algebraic variables following the index‑2 block.
    /// In RADAU error estimation these components are scaled by `h^2`.
    pub nind3: Option<usize>,
}
