//! Hot-path benchmarks for `cheby`.
//!
//! Covers, in order: scalar evaluation, derivative evaluation (raw + typed),
//! fixed-size fitting, adaptive fitting, minimax (Remez), piecewise lookup
//! and evaluation, Clenshaw-Curtis quadrature, Gauss-Chebyshev quadrature,
//! and spectral differentiation matrix construction.

use std::hint::black_box;

use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use qtty::{Kilometer, Second};

fn evaluate_f64(c: &mut Criterion) {
    let coeffs = [1.0, 0.5, -0.25, 0.125, -0.0625, 0.03125, 0.0, 0.0];
    c.bench_function("evaluate_f64", |b| {
        b.iter(|| cheby::evaluate(black_box(&coeffs), black_box(0.25)))
    });
}

fn evaluate_quantity(c: &mut Criterion) {
    let coeffs = [
        Kilometer::new(1.0),
        Kilometer::new(0.5),
        Kilometer::new(-0.25),
        Kilometer::new(0.125),
    ];
    c.bench_function("evaluate_quantity", |b| {
        b.iter(|| cheby::evaluate(black_box(&coeffs), black_box(0.25)))
    });
}

fn evaluate_derivative_f64(c: &mut Criterion) {
    let coeffs = [1.0, 0.5, -0.25, 0.125, -0.0625, 0.03125, 0.0, 0.0];
    c.bench_function("evaluate_derivative_f64", |b| {
        b.iter(|| cheby::evaluate_derivative(black_box(&coeffs), black_box(0.25)))
    });
}

fn evaluate_derivative_quantity(c: &mut Criterion) {
    let domain = cheby::Domain::new(Second::new(0.0), Second::new(1.0));
    let series = cheby::ChebySeriesOn::new(
        domain,
        cheby::ChebySeries::new([Kilometer::new(1.0), Kilometer::new(0.5)]),
    );
    c.bench_function("evaluate_derivative_quantity", |b| {
        b.iter(|| {
            series
                .evaluate_derivative(black_box(Second::new(0.25)))
                .unwrap()
        })
    });
}

fn fit_fixed(c: &mut Criterion) {
    c.bench_function("fit_fixed_n16_sin", |b| {
        b.iter(|| cheby::approx::fit::fit_from_fn::<f64, 16>(|x| black_box(x).sin()))
    });
}

fn fit_quantity(c: &mut Criterion) {
    c.bench_function("fit_quantity_n16", |b| {
        b.iter(|| {
            cheby::approx::fit::fit_from_fn::<Kilometer, 16>(|x| Kilometer::new(black_box(x).sin()))
        })
    });
}

fn adaptive_fit(c: &mut Criterion) {
    c.bench_function("adaptive_fit_sin", |b| {
        b.iter(|| {
            cheby::approx::adaptive::AdaptiveFit::new(cheby::Domain::new(-1.0, 1.0), |x| {
                f64::sin(x)
            })
            .max_degree(32)
            .build::<f64>()
            .unwrap()
        })
    });
}

fn minimax_remez(c: &mut Criterion) {
    c.bench_function("minimax_remez_exp_deg7", |b| {
        b.iter(|| {
            cheby::approx::minimax::remez(
                cheby::Domain::new(-1.0, 1.0),
                7,
                |x: f64| black_box(x).exp(),
                cheby::approx::minimax::RemezOptions::default(),
            )
            .unwrap()
        })
    });
}

fn piecewise_lookup(c: &mut Criterion) {
    let table: cheby::ChebySegmentTable<f64, f64, 8> =
        cheby::ChebySegmentTable::from_fn(|x| x.sin(), 0.0, 8.0, 1.0);
    c.bench_function("piecewise_lookup", |b| {
        b.iter(|| table.get_segment(black_box(4.25)))
    });
}

fn piecewise_evaluate(c: &mut Criterion) {
    let table: cheby::ChebySegmentTable<f64, f64, 8> =
        cheby::ChebySegmentTable::from_fn(|x| x.sin(), 0.0, 8.0, 1.0);
    c.bench_function("piecewise_evaluate", |b| {
        b.iter(|| table.evaluate(black_box(4.25)).unwrap())
    });
}

fn adaptive_piecewise_lookup(c: &mut Criterion) {
    let table: cheby::piecewise::AdaptiveSegmentTable<f64, f64, 16> =
        cheby::piecewise::AdaptiveSegmentTable::from_fn(
            cheby::Domain::new(0.0, 8.0),
            |x: f64| x.sin(),
            1e-10,
            8,
        )
        .unwrap();
    c.bench_function("adaptive_piecewise_lookup", |b| {
        b.iter(|| table.evaluate(black_box(4.25)).unwrap())
    });
}

fn clenshaw_curtis_integrate(c: &mut Criterion) {
    c.bench_function("clenshaw_curtis_integrate_n32", |b| {
        b.iter(|| {
            cheby::quadrature::clenshaw_curtis::integrate::<f64, f64, 32>(
                cheby::Domain::new(0.0, 1.0),
                |x| black_box(x).exp(),
            )
        })
    });
}

fn clenshaw_curtis_cached(c: &mut Criterion) {
    let rule = cheby::quadrature::clenshaw_curtis::ClenshawCurtisRule::<32>::new();
    c.bench_function("clenshaw_curtis_cached_n32", |b| {
        b.iter(|| rule.integrate::<f64, f64>(cheby::Domain::new(0.0, 1.0), |x| black_box(x).exp()))
    });
}

fn definite_integral_one_shot(c: &mut Criterion) {
    let domain = cheby::Domain::new(0.0_f64, core::f64::consts::PI);
    let series = cheby::approx::fit::fit_from_fn_on::<f64, f64, 17>(domain, f64::sin);
    c.bench_function("definite_integral_one_shot_n17", |b| {
        b.iter(|| series.evaluate_integral_from_start(black_box(1.5)).unwrap())
    });
}

fn definite_integral_cached(c: &mut Criterion) {
    let domain = cheby::Domain::new(0.0_f64, core::f64::consts::PI);
    let series = cheby::approx::fit::fit_from_fn_on::<f64, f64, 17>(domain, f64::sin);
    let cached = series.definite_integral_from_start();
    c.bench_function("definite_integral_cached_n17", |b| {
        b.iter(|| cached.evaluate(black_box(1.5)).unwrap())
    });
}

fn gauss_chebyshev(c: &mut Criterion) {
    c.bench_function("gauss_chebyshev_weighted_n32", |b| {
        b.iter(|| {
            cheby::quadrature::gauss_chebyshev::integrate_weighted::<f64, 32>(|x| {
                black_box(x).cos()
            })
        })
    });
}

fn spectral_diff_matrix(c: &mut Criterion) {
    c.bench_function("spectral_diff_matrix_n16", |b| {
        b.iter(|| cheby::spectral::chebyshev_differentiation_matrix(black_box(16)))
    });
}

fn fit_dyn(c: &mut Criterion) {
    let mut group = c.benchmark_group("fit_dyn_from_fn_sin");
    for degree in [4usize, 8, 12, 16, 24, 32] {
        group.bench_with_input(BenchmarkId::new("deg", degree), &degree, |b, &degree| {
            b.iter(|| cheby::fit_dyn_from_fn(degree, |x| black_box(x).sin()).unwrap())
        });
    }
    group.finish();
}

fn evaluate_dyn(c: &mut Criterion) {
    let series = cheby::fit_dyn_from_fn(16, f64::sin).unwrap();
    c.bench_function("evaluate_dyn_n17", |b| {
        b.iter(|| black_box(series.evaluate(black_box(0.37))))
    });
}

fn roots_known_quadratic(c: &mut Criterion) {
    let series = cheby::fit_dyn_from_fn(12, |x| x * x - 0.25).unwrap();
    c.bench_function("roots_quadratic_deg12", |b| {
        b.iter(|| black_box(series.roots()))
    });
}

fn roots_by_degree(c: &mut Criterion) {
    let mut group = c.benchmark_group("roots_product");
    for degree in [4usize, 8, 12, 16, 24, 32] {
        let series = cheby::fit_dyn_from_fn(degree, |x| {
            (1..=degree).fold(x, |acc, k| acc * (x - (k as f64 * 0.07 - 0.5)))
        })
        .unwrap();
        group.bench_with_input(BenchmarkId::new("deg", degree), &series, |b, series| {
            b.iter(|| black_box(series.roots()))
        });
    }
    group.finish();
}

criterion_group!(
    benches,
    evaluate_f64,
    evaluate_quantity,
    evaluate_derivative_f64,
    evaluate_derivative_quantity,
    fit_fixed,
    fit_quantity,
    fit_dyn,
    evaluate_dyn,
    roots_known_quadratic,
    roots_by_degree,
    adaptive_fit,
    minimax_remez,
    piecewise_lookup,
    piecewise_evaluate,
    adaptive_piecewise_lookup,
    clenshaw_curtis_integrate,
    clenshaw_curtis_cached,
    definite_integral_one_shot,
    definite_integral_cached,
    gauss_chebyshev,
    spectral_diff_matrix
);
criterion_main!(benches);
