//! Interpolation for 2D property tables.
//!
//! Provides both bilinear (C0) and bicubic-Hermite (C1) interpolation.
//!
//! [`bicubic_interpolate`] is the preferred entry point: it is C1-continuous
//! (continuous value AND first derivative across cell boundaries), which yields
//! smoother property derivatives (e.g. finite-difference cp) and a
//! better-conditioned solver Jacobian. It uses centered-difference tangents at
//! grid nodes (a non-uniform Catmull-Rom / cubic-Hermite scheme) and degrades
//! gracefully to one-sided tangents at the grid edges. On a 2-point grid it
//! reduces exactly to linear interpolation, so bilinear behavior is preserved
//! in the degenerate case.

use std::cmp::Ordering;

/// Resolves the base cell index for a query `x` in an ascending `grid`.
///
/// Returns the index `i` such that `grid[i] <= x <= grid[i+1]`, or `None` if
/// `x` is outside the grid or the grid is degenerate.
#[inline]
fn locate_cell(grid: &[f64], x: f64) -> Option<usize> {
    let n = grid.len();
    if n < 2 || !x.is_finite() {
        return None;
    }
    match grid.binary_search_by(|v| v.partial_cmp(&x).unwrap_or(Ordering::Equal)) {
        Ok(i) => {
            if i >= n - 1 {
                // Exactly on the last node: use the last cell.
                Some(n - 2)
            } else {
                Some(i)
            }
        }
        Err(i) => {
            if i == 0 || i >= n {
                None
            } else {
                Some(i - 1)
            }
        }
    }
}

/// Evaluates a 1D cubic Hermite segment on `[x0, x1]` using secant tangents.
///
/// `xm1`/`vm1` and `x2`/`v2` are the outer stencil points used to build
/// centered-difference tangents at `x0` and `x1`. When a neighbor is absent
/// (`has_left`/`has_right` false), a one-sided secant tangent is used, which
/// makes the scheme reduce to linear interpolation when both are absent.
#[inline]
#[allow(clippy::too_many_arguments)]
fn cubic_hermite_1d(
    xm1: f64,
    x0: f64,
    x1: f64,
    x2: f64,
    vm1: f64,
    v0: f64,
    v1: f64,
    v2: f64,
    x: f64,
    has_left: bool,
    has_right: bool,
) -> f64 {
    let h = x1 - x0;
    if h <= 0.0 {
        return v0;
    }
    let s = ((x - x0) / h).clamp(0.0, 1.0);
    let secant = (v1 - v0) / h;
    let m0 = if has_left && (x1 - xm1) > 0.0 {
        (v1 - vm1) / (x1 - xm1)
    } else {
        secant
    };
    let m1 = if has_right && (x2 - x0) > 0.0 {
        (v2 - v0) / (x2 - x0)
    } else {
        secant
    };
    let s2 = s * s;
    let s3 = s2 * s;
    let h00 = 2.0 * s3 - 3.0 * s2 + 1.0;
    let h10 = s3 - 2.0 * s2 + s;
    let h01 = -2.0 * s3 + 3.0 * s2;
    let h11 = s3 - s2;
    h00 * v0 + h10 * h * m0 + h01 * v1 + h11 * h * m1
}

/// Performs bicubic-Hermite interpolation on a 2D grid.
///
/// C1-continuous drop-in replacement for [`bilinear_interpolate`] with the same
/// signature and bounds semantics. Uses a 4x4 stencil around the query cell
/// (clamped at the boundaries) and centered-difference tangents. Reduces to
/// linear interpolation on 2-point grids.
///
/// # Arguments
/// * `p_grid` - Pressure grid (must be sorted ascending)
/// * `t_grid` - Temperature grid (must be sorted ascending)
/// * `values` - 2D array [p_idx][t_idx], row-major
/// * `p` - Query pressure (Pa)
/// * `t` - Query temperature (K)
#[inline]
pub fn bicubic_interpolate(
    p_grid: &[f64],
    t_grid: &[f64],
    values: &[f64],
    p: f64,
    t: f64,
) -> Option<f64> {
    let n_p = p_grid.len();
    let n_t = t_grid.len();

    if n_p < 2 || n_t < 2 || values.len() != n_p * n_t {
        return None;
    }

    let p_idx = locate_cell(p_grid, p)?;
    let t_idx = locate_cell(t_grid, t)?;

    // Pressure-direction stencil indices (clamped at boundaries).
    let p_has_left = p_idx >= 1;
    let p_has_right = p_idx + 2 <= n_p - 1;
    let pim1 = if p_has_left { p_idx - 1 } else { p_idx };
    let pi2 = if p_has_right { p_idx + 2 } else { p_idx + 1 };
    let p_stencil = [pim1, p_idx, p_idx + 1, pi2];

    // Temperature-direction stencil indices (clamped at boundaries).
    let t_has_left = t_idx >= 1;
    let t_has_right = t_idx + 2 <= n_t - 1;
    let tim1 = if t_has_left { t_idx - 1 } else { t_idx };
    let ti2 = if t_has_right { t_idx + 2 } else { t_idx + 1 };

    // Interpolate along temperature for each of the four pressure rows.
    let mut rows = [0.0f64; 4];
    for (k, &pi) in p_stencil.iter().enumerate() {
        let base = pi * n_t;
        rows[k] = cubic_hermite_1d(
            t_grid[tim1],
            t_grid[t_idx],
            t_grid[t_idx + 1],
            t_grid[ti2],
            values[base + tim1],
            values[base + t_idx],
            values[base + t_idx + 1],
            values[base + ti2],
            t,
            t_has_left,
            t_has_right,
        );
    }

    // Interpolate the four intermediate results along pressure.
    Some(cubic_hermite_1d(
        p_grid[pim1],
        p_grid[p_idx],
        p_grid[p_idx + 1],
        p_grid[pi2],
        rows[0],
        rows[1],
        rows[2],
        rows[3],
        p,
        p_has_left,
        p_has_right,
    ))
}

/// Performs bilinear interpolation on a 2D grid.
///
/// C0-continuous (value-continuous only). Prefer [`bicubic_interpolate`] for
/// solver use where a smooth first derivative matters. Kept for the degenerate
/// 2-point case and as a robust fallback.
///
/// Given a rectangular grid with values at (p_idx, t_idx), interpolates
/// the value at (p, t) where p and t are in the grid's coordinate space.
/// Returns None if (p, t) is outside the grid bounds.
///
/// # Arguments
/// * `p_grid` - Pressure grid (must be sorted ascending)
/// * `t_grid` - Temperature grid (must be sorted ascending)
/// * `values` - 2D array [p_idx][t_idx], row-major
/// * `p` - Query pressure (Pa)
/// * `t` - Query temperature (K)
#[inline]
#[allow(dead_code)] // Retained as a robust C0 fallback; used by tests.
pub fn bilinear_interpolate(
    p_grid: &[f64],
    t_grid: &[f64],
    values: &[f64],
    p: f64,
    t: f64,
) -> Option<f64> {
    let n_p = p_grid.len();
    let n_t = t_grid.len();

    if n_p < 2 || n_t < 2 || values.len() != n_p * n_t {
        return None;
    }

    // Reject NaN to avoid panic in binary_search_by (Zero-Panic Policy)
    if !p.is_finite() || !t.is_finite() {
        return None;
    }

    // Find P indices (p_grid must be ascending)
    let p_idx = match p_grid.binary_search_by(|x| x.partial_cmp(&p).unwrap_or(Ordering::Equal)) {
        Ok(i) => {
            if i >= n_p - 1 {
                return None;
            }
            i
        }
        Err(i) => {
            if i == 0 || i >= n_p {
                return None;
            }
            i - 1
        }
    };

    // Find T indices
    let t_idx = match t_grid.binary_search_by(|x| x.partial_cmp(&t).unwrap_or(Ordering::Equal)) {
        Ok(i) => {
            if i >= n_t - 1 {
                return None;
            }
            i
        }
        Err(i) => {
            if i == 0 || i >= n_t {
                return None;
            }
            i - 1
        }
    };

    let p0 = p_grid[p_idx];
    let p1 = p_grid[p_idx + 1];
    let t0 = t_grid[t_idx];
    let t1 = t_grid[t_idx + 1];

    let dp = p1 - p0;
    let dt = t1 - t0;

    if dp <= 0.0 || dt <= 0.0 {
        return None;
    }

    let fp = (p - p0) / dp;
    let ft = (t - t0) / dt;

    // Clamp to [0,1] for edge cases
    let fp = fp.clamp(0.0, 1.0);
    let ft = ft.clamp(0.0, 1.0);

    let v00 = values[p_idx * n_t + t_idx];
    let v01 = values[p_idx * n_t + t_idx + 1];
    let v10 = values[(p_idx + 1) * n_t + t_idx];
    let v11 = values[(p_idx + 1) * n_t + t_idx + 1];

    let v0 = v00 * (1.0 - ft) + v01 * ft;
    let v1 = v10 * (1.0 - ft) + v11 * ft;

    Some(v0 * (1.0 - fp) + v1 * fp)
}

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

    #[test]
    fn test_bilinear_inside() {
        let p = [100000.0, 200000.0, 300000.0];
        let t = [250.0, 300.0, 350.0];
        let v = [
            1.0, 2.0, 3.0, // p=100k
            4.0, 5.0, 6.0, // p=200k
            7.0, 8.0, 9.0, // p=300k
        ];

        let result = bilinear_interpolate(&p, &t, &v, 200000.0, 300.0);
        assert!(result.is_some());
        assert!((result.unwrap() - 5.0).abs() < 1e-10);
    }

    #[test]
    fn test_bilinear_interpolated() {
        let p = [0.0, 1.0];
        let t = [0.0, 1.0];
        let v = [0.0, 1.0, 1.0, 2.0]; // v(0,0)=0, v(0,1)=1, v(1,0)=1, v(1,1)=2

        let result = bilinear_interpolate(&p, &t, &v, 0.5, 0.5);
        assert!(result.is_some());
        // At center: (0+1+1+2)/4 = 1.0
        assert!((result.unwrap() - 1.0).abs() < 1e-10);
    }

    #[test]
    fn test_bilinear_out_of_bounds() {
        let p = [100000.0, 200000.0];
        let t = [250.0, 300.0];
        let v = [1.0, 2.0, 3.0, 4.0];

        assert!(bilinear_interpolate(&p, &t, &v, 50000.0, 300.0).is_none());
        assert!(bilinear_interpolate(&p, &t, &v, 300000.0, 300.0).is_none());
    }

    #[test]
    fn test_bilinear_nan_rejected() {
        let p = [100000.0, 200000.0];
        let t = [250.0, 300.0];
        let v = [1.0, 2.0, 3.0, 4.0];

        assert!(bilinear_interpolate(&p, &t, &v, f64::NAN, 300.0).is_none());
        assert!(bilinear_interpolate(&p, &t, &v, 150000.0, f64::NAN).is_none());
        assert!(bilinear_interpolate(&p, &t, &v, f64::INFINITY, 300.0).is_none());
    }

    #[test]
    fn test_bicubic_at_grid_nodes_is_exact() {
        // Bicubic must reproduce node values exactly.
        let p = [1.0, 2.0, 3.0, 4.0];
        let t = [10.0, 20.0, 30.0, 40.0];
        let mut v = vec![0.0; 16];
        for (i, &pi) in p.iter().enumerate() {
            for (j, &tj) in t.iter().enumerate() {
                v[i * 4 + j] = pi * pi + 0.5 * tj - 3.0 * pi * tj;
            }
        }
        for (i, &pi) in p.iter().enumerate() {
            for (j, &tj) in t.iter().enumerate() {
                let got = bicubic_interpolate(&p, &t, &v, pi, tj).unwrap();
                assert!(
                    (got - v[i * 4 + j]).abs() < 1e-9,
                    "node ({pi},{tj}) got {got} expected {}",
                    v[i * 4 + j]
                );
            }
        }
    }

    #[test]
    fn test_bicubic_reduces_to_linear_on_2pt_grid() {
        // On a 2x2 grid there are no interior neighbors, so bicubic must match
        // bilinear exactly.
        let p = [0.0, 1.0];
        let t = [0.0, 1.0];
        let v = [0.0, 1.0, 1.0, 2.0];
        for &(qp, qt) in &[(0.25, 0.75), (0.5, 0.5), (0.1, 0.9)] {
            let lin = bilinear_interpolate(&p, &t, &v, qp, qt).unwrap();
            let cub = bicubic_interpolate(&p, &t, &v, qp, qt).unwrap();
            assert!((lin - cub).abs() < 1e-12, "({qp},{qt}) lin={lin} cub={cub}");
        }
    }

    #[test]
    fn test_bicubic_more_accurate_on_cubic_function() {
        // For a smooth cubic surface, bicubic-Hermite is far more accurate than
        // bilinear at a cell interior with genuine neighbors.
        let f = |p: f64, t: f64| p * p * p + 2.0 * t * t * t - p * t;
        let p: Vec<f64> = (0..6).map(|i| i as f64).collect();
        let t: Vec<f64> = (0..6).map(|j| j as f64).collect();
        let mut v = vec![0.0; 36];
        for (i, &pi) in p.iter().enumerate() {
            for (j, &tj) in t.iter().enumerate() {
                v[i * 6 + j] = f(pi, tj);
            }
        }
        let (qp, qt) = (2.5, 3.5);
        let exact = f(qp, qt);
        let e_lin = (bilinear_interpolate(&p, &t, &v, qp, qt).unwrap() - exact).abs();
        let e_cub = (bicubic_interpolate(&p, &t, &v, qp, qt).unwrap() - exact).abs();
        assert!(
            e_cub < e_lin * 0.2,
            "bicubic error {e_cub} not << bilinear error {e_lin}"
        );
    }

    #[test]
    fn test_bicubic_out_of_bounds_and_nan() {
        let p = [1.0, 2.0, 3.0];
        let t = [1.0, 2.0, 3.0];
        let v: Vec<f64> = (0..9).map(|x| x as f64).collect();
        assert!(bicubic_interpolate(&p, &t, &v, 0.5, 2.0).is_none());
        assert!(bicubic_interpolate(&p, &t, &v, 4.0, 2.0).is_none());
        assert!(bicubic_interpolate(&p, &t, &v, f64::NAN, 2.0).is_none());
        assert!(bicubic_interpolate(&p, &t, &v, 2.0, f64::INFINITY).is_none());
    }
}
