use crate::utilities::data_loader::{source_type, Candles};
use crate::utilities::enums::Kernel;
use crate::utilities::helpers::{
    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
    make_uninit_matrix,
};
#[cfg(feature = "python")]
use crate::utilities::kernel_validation::validate_kernel;
#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
use core::arch::x86_64::*;
#[cfg(not(target_arch = "wasm32"))]
use rayon::prelude::*;
use std::error::Error;
use std::f64::consts::PI;
use std::mem::MaybeUninit;
use thiserror::Error;

#[cfg(feature = "python")]
use numpy::PyUntypedArrayMethods;
#[cfg(feature = "python")]
use numpy::{IntoPyArray, PyArray1};
#[cfg(feature = "python")]
use pyo3::exceptions::PyValueError;
#[cfg(feature = "python")]
use pyo3::prelude::*;
#[cfg(feature = "python")]
use pyo3::types::PyDict;

#[cfg(all(feature = "python", feature = "cuda"))]
use crate::cuda::cuda_available;
#[cfg(all(feature = "python", feature = "cuda"))]
use crate::cuda::moving_averages::CudaEhlersITrend;
#[cfg(all(feature = "python", feature = "cuda"))]
use crate::cuda::moving_averages::DeviceArrayF32;
#[cfg(all(feature = "python", feature = "cuda"))]
use cust::memory::DeviceBuffer;
#[cfg(all(feature = "python", feature = "cuda"))]
use std::sync::Arc;

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
use serde::{Deserialize, Serialize};
#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
use wasm_bindgen::prelude::*;

impl<'a> AsRef<[f64]> for EhlersITrendInput<'a> {
    fn as_ref(&self) -> &[f64] {
        match &self.data {
            EhlersITrendData::Candles { candles, source } => source_type(candles, source),
            EhlersITrendData::Slice(slice) => slice,
        }
    }
}

#[derive(Debug, Clone)]
pub enum EhlersITrendData<'a> {
    Candles {
        candles: &'a Candles,
        source: &'a str,
    },
    Slice(&'a [f64]),
}

#[derive(Debug, Clone)]
pub struct EhlersITrendOutput {
    pub values: Vec<f64>,
}

#[derive(Debug, Clone)]
#[cfg_attr(
    all(target_arch = "wasm32", feature = "wasm"),
    derive(Serialize, Deserialize)
)]
pub struct EhlersITrendParams {
    pub warmup_bars: Option<usize>,
    pub max_dc_period: Option<usize>,
}
impl Default for EhlersITrendParams {
    fn default() -> Self {
        Self {
            warmup_bars: Some(12),
            max_dc_period: Some(50),
        }
    }
}

#[derive(Debug, Clone)]
pub struct EhlersITrendInput<'a> {
    pub data: EhlersITrendData<'a>,
    pub params: EhlersITrendParams,
}
impl<'a> EhlersITrendInput<'a> {
    #[inline(always)]
    pub fn from_candles(c: &'a Candles, s: &'a str, p: EhlersITrendParams) -> Self {
        Self {
            data: EhlersITrendData::Candles {
                candles: c,
                source: s,
            },
            params: p,
        }
    }
    #[inline(always)]
    pub fn from_slice(sl: &'a [f64], p: EhlersITrendParams) -> Self {
        Self {
            data: EhlersITrendData::Slice(sl),
            params: p,
        }
    }
    #[inline(always)]
    pub fn with_default_candles(c: &'a Candles) -> Self {
        Self::from_candles(c, "close", EhlersITrendParams::default())
    }
    #[inline(always)]
    pub fn get_warmup_bars(&self) -> usize {
        self.params.warmup_bars.unwrap_or(12)
    }
    #[inline(always)]
    pub fn get_max_dc_period(&self) -> usize {
        self.params.max_dc_period.unwrap_or(50)
    }
}

#[derive(Debug, Error)]
pub enum EhlersITrendError {
    #[error("ehlers_itrend: Input data is empty.")]
    EmptyInputData,
    #[error("ehlers_itrend: All values are NaN.")]
    AllValuesNaN,
    #[error("ehlers_itrend: Not enough data for warmup. warmup_bars={warmup_bars} but data length={length}")]
    NotEnoughDataForWarmup { warmup_bars: usize, length: usize },

    #[error("ehlers_itrend: Not enough valid data: needed={needed}, valid={valid}")]
    NotEnoughValidData { needed: usize, valid: usize },
    #[error("ehlers_itrend: Invalid warmup_bars: {warmup_bars}")]
    InvalidWarmupBars { warmup_bars: usize },
    #[error("ehlers_itrend: Invalid max_dc_period: {max_dc}")]
    InvalidMaxDcPeriod { max_dc: usize },

    #[allow(dead_code)]
    #[error("ehlers_itrend: Output length mismatch. expected={expected}, got={got}")]
    InvalidOutputLen { expected: usize, got: usize },
    #[error("ehlers_itrend: Invalid batch kernel")]
    InvalidBatchKernel,
    #[error("ehlers_itrend: Invalid batch range")]
    InvalidBatchRange,

    #[error("ehlers_itrend: Output length mismatch. expected={expected}, got={got}")]
    OutputLengthMismatch { expected: usize, got: usize },
    #[error("ehlers_itrend: Invalid range: start={start}, end={end}, step={step}")]
    InvalidRange {
        start: usize,
        end: usize,
        step: usize,
    },
    #[error("ehlers_itrend: invalid kernel for batch: {0:?}")]
    InvalidKernelForBatch(Kernel),
    #[error("ehlers_itrend: arithmetic overflow while computing {context}")]
    SizeOverflow { context: &'static str },
}

#[inline]
pub fn ehlers_itrend(input: &EhlersITrendInput) -> Result<EhlersITrendOutput, EhlersITrendError> {
    ehlers_itrend_with_kernel(input, Kernel::Auto)
}

#[inline(always)]
fn ehlers_itrend_prepare<'a>(
    input: &'a EhlersITrendInput,
    kernel: Kernel,
) -> Result<(&'a [f64], usize, usize, usize, usize, Kernel), EhlersITrendError> {
    let data: &[f64] = match &input.data {
        EhlersITrendData::Candles { candles, source } => source_type(candles, source),
        EhlersITrendData::Slice(sl) => sl,
    };

    let len = data.len();
    if len == 0 {
        return Err(EhlersITrendError::EmptyInputData);
    }
    let first = data
        .iter()
        .position(|x| !x.is_nan())
        .ok_or(EhlersITrendError::AllValuesNaN)?;
    let warmup_bars = input.get_warmup_bars();
    let max_dc = input.get_max_dc_period();
    if warmup_bars == 0 {
        return Err(EhlersITrendError::InvalidWarmupBars { warmup_bars });
    }
    if max_dc == 0 {
        return Err(EhlersITrendError::InvalidMaxDcPeriod { max_dc });
    }

    if len - first < warmup_bars {
        return Err(EhlersITrendError::NotEnoughDataForWarmup {
            warmup_bars,
            length: len - first,
        });
    }

    let chosen = match kernel {
        Kernel::Auto => detect_best_kernel(),
        other => other,
    };

    let warm = first + warmup_bars;

    Ok((data, warmup_bars, max_dc, first, warm, chosen))
}

#[inline(always)]
fn warm_index(first: usize, warmup_bars: usize) -> usize {
    first + warmup_bars
}

#[inline(always)]
pub fn ehlers_itrend_scalar_tail(
    src: &[f64],
    warmup_bars: usize,
    max_dc: usize,
    first_valid: usize,
    warm: usize,
    out: &mut [f64],
) {
    debug_assert_eq!(src.len(), out.len());

    let length = src.len();
    let mut fir_buf = [0.0; 7];
    let mut det_buf = [0.0; 7];
    let mut i1_buf = [0.0; 7];
    let mut q1_buf = [0.0; 7];
    let (mut prev_i2, mut prev_q2) = (0.0, 0.0);
    let (mut prev_re, mut prev_im) = (0.0, 0.0);
    let (mut prev_mesa, mut prev_smooth) = (0.0, 0.0);
    let mut sum_ring = vec![0.0; max_dc];
    let mut sum_idx = 0usize;
    let (mut prev_it1, mut prev_it2, mut prev_it3) = (0.0, 0.0, 0.0);
    let mut ring_ptr = 0usize;

    #[inline(always)]
    fn ring_get(buf: &[f64; 7], center: usize, off: usize) -> f64 {
        let mut idx = center + 7 - off;
        if idx >= 7 {
            idx -= 7;
        }
        buf[idx]
    }

    for i in 0..length {
        let x0 = src[i];
        let x1 = if i >= 1 { src[i - 1] } else { 0.0 };
        let x2 = if i >= 2 { src[i - 2] } else { 0.0 };
        let x3 = if i >= 3 { src[i - 3] } else { 0.0 };

        let fir_val = (4.0 * x0 + 3.0 * x1 + 2.0 * x2 + x3) / 10.0;
        fir_buf[ring_ptr] = fir_val;

        let fir_0 = ring_get(&fir_buf, ring_ptr, 0);
        let fir_2 = ring_get(&fir_buf, ring_ptr, 2);
        let fir_4 = ring_get(&fir_buf, ring_ptr, 4);
        let fir_6 = ring_get(&fir_buf, ring_ptr, 6);

        let h_in = 0.0962 * fir_0 + 0.5769 * fir_2 - 0.5769 * fir_4 - 0.0962 * fir_6;
        let period_mult = 0.075 * prev_mesa + 0.54;
        let det_val = h_in * period_mult;
        det_buf[ring_ptr] = det_val;

        let i1_val = ring_get(&det_buf, ring_ptr, 3);
        i1_buf[ring_ptr] = i1_val;

        let det_0 = ring_get(&det_buf, ring_ptr, 0);
        let det_2 = ring_get(&det_buf, ring_ptr, 2);
        let det_4 = ring_get(&det_buf, ring_ptr, 4);
        let det_6 = ring_get(&det_buf, ring_ptr, 6);
        let h_in_q1 = 0.0962 * det_0 + 0.5769 * det_2 - 0.5769 * det_4 - 0.0962 * det_6;
        let q1_val = h_in_q1 * period_mult;
        q1_buf[ring_ptr] = q1_val;

        let i1_0 = ring_get(&i1_buf, ring_ptr, 0);
        let i1_2 = ring_get(&i1_buf, ring_ptr, 2);
        let i1_4 = ring_get(&i1_buf, ring_ptr, 4);
        let i1_6 = ring_get(&i1_buf, ring_ptr, 6);
        let j_i_val = (0.0962 * i1_0 + 0.5769 * i1_2 - 0.5769 * i1_4 - 0.0962 * i1_6) * period_mult;

        let q1_0 = ring_get(&q1_buf, ring_ptr, 0);
        let q1_2 = ring_get(&q1_buf, ring_ptr, 2);
        let q1_4 = ring_get(&q1_buf, ring_ptr, 4);
        let q1_6 = ring_get(&q1_buf, ring_ptr, 6);
        let j_q_val = (0.0962 * q1_0 + 0.5769 * q1_2 - 0.5769 * q1_4 - 0.0962 * q1_6) * period_mult;

        let mut i2_cur = 0.2 * (i1_val - j_q_val) + 0.8 * prev_i2;
        let mut q2_cur = 0.2 * (q1_val + j_i_val) + 0.8 * prev_q2;

        let re_val = i2_cur * prev_i2 + q2_cur * prev_q2;
        let im_val = i2_cur * prev_q2 - q2_cur * prev_i2;
        prev_i2 = i2_cur;
        prev_q2 = q2_cur;

        let re_smooth = 0.2 * re_val + 0.8 * prev_re;
        let im_smooth = 0.2 * im_val + 0.8 * prev_im;
        prev_re = re_smooth;
        prev_im = im_smooth;

        let mut new_mesa = 0.0;
        if re_smooth != 0.0 && im_smooth != 0.0 {
            let angle = im_smooth.atan2(re_smooth);
            if angle != 0.0 {
                new_mesa = (2.0 * core::f64::consts::PI) / angle;
            }
        }
        let up_lim = 1.5 * prev_mesa;
        if new_mesa > up_lim {
            new_mesa = up_lim;
        }
        let low_lim = 0.67 * prev_mesa;
        if new_mesa < low_lim {
            new_mesa = low_lim;
        }
        new_mesa = new_mesa.clamp(6.0, 50.0);
        let final_mesa = 0.2 * new_mesa + 0.8 * prev_mesa;
        prev_mesa = final_mesa;
        let sp_val = 0.33 * final_mesa + 0.67 * prev_smooth;
        prev_smooth = sp_val;

        let mut dcp = (sp_val + 0.5).floor() as usize;
        dcp = dcp.clamp(1, max_dc);

        sum_ring[sum_idx] = x0;
        sum_idx += 1;
        if sum_idx == max_dc {
            sum_idx = 0;
        }
        let mut sum_src = 0.0;
        let mut idx2 = sum_idx;
        for _ in 0..dcp {
            idx2 = if idx2 == 0 { max_dc - 1 } else { idx2 - 1 };
            sum_src += sum_ring[idx2];
        }
        let it_val = sum_src / dcp as f64;

        let eit_val = if i < warmup_bars {
            x0
        } else {
            (4.0 * it_val + 3.0 * prev_it1 + 2.0 * prev_it2 + prev_it3) / 10.0
        };

        prev_it3 = prev_it2;
        prev_it2 = prev_it1;
        prev_it1 = it_val;

        if i >= warm {
            out[i] = eit_val;
        }
        ring_ptr += 1;
        if ring_ptr == 7 {
            ring_ptr = 0;
        }
    }
}

#[inline(always)]
fn ehlers_itrend_compute_into(
    data: &[f64],
    warmup_bars: usize,
    max_dc: usize,
    first: usize,
    chosen: Kernel,
    out: &mut [f64],
) {
    let warm = warm_index(first, warmup_bars);
    match chosen {
        Kernel::Scalar | Kernel::ScalarBatch => {
            ehlers_itrend_scalar_tail(data, warmup_bars, max_dc, first, warm, out)
        }
        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
        Kernel::Avx2 | Kernel::Avx2Batch | Kernel::Avx512 | Kernel::Avx512Batch => {
            ehlers_itrend_scalar_tail(data, warmup_bars, max_dc, first, warm, out)
        }
        _ => unreachable!(),
    }
}

pub fn ehlers_itrend_with_kernel(
    input: &EhlersITrendInput,
    kernel: Kernel,
) -> Result<EhlersITrendOutput, EhlersITrendError> {
    let (data, warmup_bars, max_dc, first, warm, chosen) = ehlers_itrend_prepare(input, kernel)?;
    let mut out = alloc_with_nan_prefix(data.len(), warm);
    ehlers_itrend_compute_into(data, warmup_bars, max_dc, first, chosen, &mut out);
    Ok(EhlersITrendOutput { values: out })
}

#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
pub fn ehlers_itrend_into(
    input: &EhlersITrendInput,
    out: &mut [f64],
) -> Result<(), EhlersITrendError> {
    ehlers_itrend_into_slice(out, input, Kernel::Auto)
}

pub fn ehlers_itrend_into_slice(
    dst: &mut [f64],
    input: &EhlersITrendInput,
    kern: Kernel,
) -> Result<(), EhlersITrendError> {
    let (data, warmup_bars, max_dc, first, warm, chosen) = ehlers_itrend_prepare(input, kern)?;
    if dst.len() != data.len() {
        return Err(EhlersITrendError::OutputLengthMismatch {
            expected: data.len(),
            got: dst.len(),
        });
    }

    for v in &mut dst[..warm] {
        *v = f64::from_bits(0x7ff8_0000_0000_0000);
    }
    ehlers_itrend_compute_into(data, warmup_bars, max_dc, first, chosen, dst);
    Ok(())
}

#[inline]
pub fn ehlers_itrend_scalar(
    src: &[f64],
    warmup_bars: usize,
    max_dc: usize,
    first_valid: usize,
    out: &mut [f64],
) {
    debug_assert_eq!(src.len(), out.len());
    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
    {
        if std::is_x86_feature_detected!("fma") {
            unsafe { ehlers_itrend_unsafe_scalar(src, warmup_bars, max_dc, out) }
        } else {
            ehlers_itrend_safe_scalar(src, warmup_bars, max_dc, out)
        }
    }
    #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
    {
        ehlers_itrend_safe_scalar(src, warmup_bars, max_dc, out)
    }
}

#[inline(always)]
fn ehlers_itrend_safe_scalar(src: &[f64], warmup_bars: usize, max_dc: usize, out: &mut [f64]) {
    let length = src.len();
    let mut fir_buf = [0.0; 7];
    let mut det_buf = [0.0; 7];
    let mut i1_buf = [0.0; 7];
    let mut q1_buf = [0.0; 7];
    let mut prev_i2 = 0.0;
    let mut prev_q2 = 0.0;
    let mut prev_re = 0.0;
    let mut prev_im = 0.0;
    let mut prev_mesa = 0.0;
    let mut prev_smooth = 0.0;
    let mut sum_ring = vec![0.0; max_dc];
    let mut sum_idx = 0_usize;
    let mut prev_it1 = 0.0;
    let mut prev_it2 = 0.0;
    let mut prev_it3 = 0.0;
    let mut ring_ptr = 0_usize;
    for i in 0..length {
        let x0 = src[i];
        let x1 = if i >= 1 { src[i - 1] } else { 0.0 };
        let x2 = if i >= 2 { src[i - 2] } else { 0.0 };
        let x3 = if i >= 3 { src[i - 3] } else { 0.0 };
        let fir_val = (4.0 * x0 + 3.0 * x1 + 2.0 * x2 + x3) / 10.0;
        fir_buf[ring_ptr] = fir_val;

        #[inline(always)]
        fn get_ring(buf: &[f64; 7], center: usize, offset: usize) -> f64 {
            buf[(7 + center - offset) % 7]
        }
        let fir_0 = get_ring(&fir_buf, ring_ptr, 0);
        let fir_2 = get_ring(&fir_buf, ring_ptr, 2);
        let fir_4 = get_ring(&fir_buf, ring_ptr, 4);
        let fir_6 = get_ring(&fir_buf, ring_ptr, 6);

        let h_in = 0.0962 * fir_0 + 0.5769 * fir_2 - 0.5769 * fir_4 - 0.0962 * fir_6;
        let period_mult = 0.075 * prev_mesa + 0.54;
        let det_val = h_in * period_mult;
        det_buf[ring_ptr] = det_val;

        let i1_val = get_ring(&det_buf, ring_ptr, 3);
        i1_buf[ring_ptr] = i1_val;

        let det_0 = get_ring(&det_buf, ring_ptr, 0);
        let det_2 = get_ring(&det_buf, ring_ptr, 2);
        let det_4 = get_ring(&det_buf, ring_ptr, 4);
        let det_6 = get_ring(&det_buf, ring_ptr, 6);
        let h_in_q1 = 0.0962 * det_0 + 0.5769 * det_2 - 0.5769 * det_4 - 0.0962 * det_6;
        let q1_val = h_in_q1 * period_mult;
        q1_buf[ring_ptr] = q1_val;

        let i1_0 = get_ring(&i1_buf, ring_ptr, 0);
        let i1_2 = get_ring(&i1_buf, ring_ptr, 2);
        let i1_4 = get_ring(&i1_buf, ring_ptr, 4);
        let i1_6 = get_ring(&i1_buf, ring_ptr, 6);
        let j_i_val = (0.0962 * i1_0 + 0.5769 * i1_2 - 0.5769 * i1_4 - 0.0962 * i1_6) * period_mult;

        let q1_0 = get_ring(&q1_buf, ring_ptr, 0);
        let q1_2 = get_ring(&q1_buf, ring_ptr, 2);
        let q1_4 = get_ring(&q1_buf, ring_ptr, 4);
        let q1_6 = get_ring(&q1_buf, ring_ptr, 6);
        let j_q_val = (0.0962 * q1_0 + 0.5769 * q1_2 - 0.5769 * q1_4 - 0.0962 * q1_6) * period_mult;

        let mut i2_cur = i1_val - j_q_val;
        let mut q2_cur = q1_val + j_i_val;
        i2_cur = 0.2 * i2_cur + 0.8 * prev_i2;
        q2_cur = 0.2 * q2_cur + 0.8 * prev_q2;

        let re_val = i2_cur * prev_i2 + q2_cur * prev_q2;
        let im_val = i2_cur * prev_q2 - q2_cur * prev_i2;
        prev_i2 = i2_cur;
        prev_q2 = q2_cur;

        let re_smooth = 0.2 * re_val + 0.8 * prev_re;
        let im_smooth = 0.2 * im_val + 0.8 * prev_im;
        prev_re = re_smooth;
        prev_im = im_smooth;

        let mut new_mesa = 0.0;
        if re_smooth != 0.0 && im_smooth != 0.0 {
            let angle = im_smooth.atan2(re_smooth);
            if angle != 0.0 {
                new_mesa = (2.0 * PI) / angle;
            }
        }
        let up_lim = 1.5 * prev_mesa;
        if new_mesa > up_lim {
            new_mesa = up_lim;
        }
        let low_lim = 0.67 * prev_mesa;
        if new_mesa < low_lim {
            new_mesa = low_lim;
        }
        new_mesa = new_mesa.clamp(6.0, 50.0);
        let final_mesa = 0.2 * new_mesa + 0.8 * prev_mesa;
        prev_mesa = final_mesa;
        let sp_val = 0.33 * final_mesa + 0.67 * prev_smooth;
        prev_smooth = sp_val;
        let mut dcp = (sp_val + 0.5).floor() as i32;
        if dcp < 1 {
            dcp = 1;
        }
        if dcp as usize > max_dc {
            dcp = max_dc as i32;
        }

        sum_ring[sum_idx] = x0;
        sum_idx = (sum_idx + 1) % max_dc;
        let mut sum_src = 0.0;
        let mut idx2 = sum_idx;
        for _ in 0..dcp {
            idx2 = if idx2 == 0 { max_dc - 1 } else { idx2 - 1 };
            sum_src += sum_ring[idx2];
        }
        let it_val = sum_src / dcp as f64;

        let eit_val = if i < warmup_bars {
            x0
        } else {
            (4.0 * it_val + 3.0 * prev_it1 + 2.0 * prev_it2 + prev_it3) / 10.0
        };
        prev_it3 = prev_it2;
        prev_it2 = prev_it1;
        prev_it1 = it_val;

        out[i] = eit_val;

        ring_ptr = (ring_ptr + 1) % 7;
    }
}

#[inline(always)]
unsafe fn r7(buf: &[f64; 7], p: usize, off: usize) -> f64 {
    let idx = if p >= off { p - off } else { p + 7 - off };
    *buf.get_unchecked(idx)
}

#[inline(always)]
pub unsafe fn ehlers_itrend_unsafe_scalar(
    src: &[f64],
    warmup: usize,
    max_dc: usize,
    out: &mut [f64],
) {
    debug_assert_eq!(src.len(), out.len());
    let len = src.len();

    let mut fir = [0.0; 7];
    let mut det = [0.0; 7];
    let mut i1 = [0.0; 7];
    let mut q1 = [0.0; 7];

    let mut sum: Vec<f64> = vec![0.0; max_dc];

    let (mut i2p, mut q2p, mut rep, mut imp) = (0.0, 0.0, 0.0, 0.0);
    let (mut mesa_p, mut sm_p) = (0.0, 0.0);
    let (mut it1p, mut it2p, mut it3p) = (0.0, 0.0, 0.0);

    const C0: f64 = 0.0962;
    const C1: f64 = 0.5769;
    const DIV10: f64 = 0.1;
    const TWO_PI: f64 = core::f64::consts::PI * 2.0;

    let mut rp = 0;
    let mut sp = 0;

    for (idx, &x0) in src.iter().enumerate() {
        let x1 = if idx >= 1 {
            *src.get_unchecked(idx - 1)
        } else {
            0.0
        };
        let x2 = if idx >= 2 {
            *src.get_unchecked(idx - 2)
        } else {
            0.0
        };
        let x3 = if idx >= 3 {
            *src.get_unchecked(idx - 3)
        } else {
            0.0
        };
        let fir_val = (4.0 * x0 + 3.0 * x1 + 2.0 * x2 + x3) * DIV10;
        *fir.get_unchecked_mut(rp) = fir_val;

        let hp =
            C0 * (r7(&fir, rp, 0) - r7(&fir, rp, 6)) + C1 * (r7(&fir, rp, 2) - r7(&fir, rp, 4));
        let period_mult = 0.075 * mesa_p + 0.54;
        let det_val = hp * period_mult;
        *det.get_unchecked_mut(rp) = det_val;

        let i1v = r7(&det, rp, 3);
        let q1v = (C0 * (r7(&det, rp, 0) - r7(&det, rp, 6))
            + C1 * (r7(&det, rp, 2) - r7(&det, rp, 4)))
            * period_mult;
        *i1.get_unchecked_mut(rp) = i1v;
        *q1.get_unchecked_mut(rp) = q1v;

        let j_i = (C0 * (r7(&i1, rp, 0) - r7(&i1, rp, 6)) + C1 * (r7(&i1, rp, 2) - r7(&i1, rp, 4)))
            * period_mult;
        let j_q = (C0 * (r7(&q1, rp, 0) - r7(&q1, rp, 6)) + C1 * (r7(&q1, rp, 2) - r7(&q1, rp, 4)))
            * period_mult;

        let mut i2 = 0.2 * (i1v - j_q) + 0.8 * i2p;
        let mut q2 = 0.2 * (q1v + j_i) + 0.8 * q2p;

        let re = 0.2 * (i2 * i2p + q2 * q2p) + 0.8 * rep;
        let im = 0.2 * (i2 * q2p - q2 * i2p) + 0.8 * imp;
        i2p = i2;
        q2p = q2;
        rep = re;
        imp = im;

        let mut mesa = if re != 0.0 && im != 0.0 {
            TWO_PI / im.atan2(re)
        } else {
            0.0
        };
        mesa = mesa.clamp(0.67 * mesa_p, 1.5 * mesa_p).clamp(6.0, 50.0);
        let mesa_f = 0.2 * mesa + 0.8 * mesa_p;
        mesa_p = mesa_f;

        let sp_v = 0.33 * mesa_f + 0.67 * sm_p;
        sm_p = sp_v;

        let dcp = sp_v.round().clamp(1.0, max_dc as f64) as usize;

        *sum.get_unchecked_mut(sp) = x0;
        sp += 1;
        if sp == max_dc {
            sp = 0;
        }

        let mut acc = 0.0;
        let mut j = sp;
        for _ in 0..dcp {
            j = if j == 0 { max_dc - 1 } else { j - 1 };
            acc += *sum.get_unchecked(j);
        }
        let it = acc / dcp as f64;

        out[idx] = if idx < warmup {
            x0
        } else {
            (4.0 * it + 3.0 * it1p + 2.0 * it2p + it3p) * DIV10
        };

        it3p = it2p;
        it2p = it1p;
        it1p = it;

        rp += 1;
        if rp == 7 {
            rp = 0;
        }
    }
}
#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
pub fn ehlers_itrend_avx2(
    data: &[f64],
    warmup_bars: usize,
    max_dc: usize,
    first_valid: usize,
    out: &mut [f64],
) {
    ehlers_itrend_scalar(data, warmup_bars, max_dc, first_valid, out);
}
#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
#[inline(always)]
pub fn ehlers_itrend_avx512(
    data: &[f64],
    warmup_bars: usize,
    max_dc: usize,
    first_valid: usize,
    out: &mut [f64],
) {
    ehlers_itrend_scalar(data, warmup_bars, max_dc, first_valid, out);
}

#[derive(Copy, Clone, Debug)]
pub struct EhlersITrendBuilder {
    warmup_bars: Option<usize>,
    max_dc_period: Option<usize>,
    kernel: Kernel,
}
impl Default for EhlersITrendBuilder {
    fn default() -> Self {
        Self {
            warmup_bars: None,
            max_dc_period: None,
            kernel: Kernel::Auto,
        }
    }
}
impl EhlersITrendBuilder {
    #[inline(always)]
    pub fn new() -> Self {
        Self::default()
    }
    #[inline(always)]
    pub fn warmup_bars(mut self, n: usize) -> Self {
        self.warmup_bars = Some(n);
        self
    }
    #[inline(always)]
    pub fn max_dc_period(mut self, n: usize) -> Self {
        self.max_dc_period = Some(n);
        self
    }
    #[inline(always)]
    pub fn kernel(mut self, k: Kernel) -> Self {
        self.kernel = k;
        self
    }
    #[inline(always)]
    pub fn apply(self, c: &Candles) -> Result<EhlersITrendOutput, EhlersITrendError> {
        let p = EhlersITrendParams {
            warmup_bars: self.warmup_bars,
            max_dc_period: self.max_dc_period,
        };
        let i = EhlersITrendInput::from_candles(c, "close", p);
        ehlers_itrend_with_kernel(&i, self.kernel)
    }
    #[inline(always)]
    pub fn apply_slice(self, d: &[f64]) -> Result<EhlersITrendOutput, EhlersITrendError> {
        let p = EhlersITrendParams {
            warmup_bars: self.warmup_bars,
            max_dc_period: self.max_dc_period,
        };
        let i = EhlersITrendInput::from_slice(d, p);
        ehlers_itrend_with_kernel(&i, self.kernel)
    }
    #[inline(always)]
    pub fn into_stream(self) -> Result<EhlersITrendStream, EhlersITrendError> {
        let p = EhlersITrendParams {
            warmup_bars: self.warmup_bars,
            max_dc_period: self.max_dc_period,
        };
        EhlersITrendStream::try_new(p)
    }
}

#[derive(Debug, Clone)]
pub struct EhlersITrendStream {
    warmup_bars: usize,
    max_dc: usize,
    fir_buf: [f64; 7],
    det_buf: [f64; 7],
    i1_buf: [f64; 7],
    q1_buf: [f64; 7],
    prev_i2: f64,
    prev_q2: f64,
    prev_re: f64,
    prev_im: f64,
    prev_mesa: f64,
    prev_smooth: f64,
    sum_ring: Vec<f64>,
    sum_idx: usize,

    cum_ring: Vec<f64>,
    cum_idx: usize,

    wma_hist: [f64; 3],
    prev_it1: f64,
    prev_it2: f64,
    prev_it3: f64,
    ring_ptr: usize,
    bar: usize,
}
impl EhlersITrendStream {
    pub fn try_new(params: EhlersITrendParams) -> Result<Self, EhlersITrendError> {
        let warmup_bars = params.warmup_bars.unwrap_or(12);
        let max_dc = params.max_dc_period.unwrap_or(50);
        if warmup_bars == 0 {
            return Err(EhlersITrendError::InvalidWarmupBars { warmup_bars });
        }
        if max_dc == 0 {
            return Err(EhlersITrendError::InvalidMaxDcPeriod { max_dc });
        }
        Ok(Self {
            warmup_bars,
            max_dc,
            fir_buf: [0.0; 7],
            det_buf: [0.0; 7],
            i1_buf: [0.0; 7],
            q1_buf: [0.0; 7],
            prev_i2: 0.0,
            prev_q2: 0.0,
            prev_re: 0.0,
            prev_im: 0.0,
            prev_mesa: 0.0,
            prev_smooth: 0.0,
            sum_ring: vec![0.0; max_dc],
            sum_idx: 0,
            cum_ring: vec![0.0; max_dc + 1],
            cum_idx: 0,
            wma_hist: [0.0; 3],
            prev_it1: 0.0,
            prev_it2: 0.0,
            prev_it3: 0.0,
            ring_ptr: 0,
            bar: 0,
        })
    }

    #[inline(always)]
    pub fn update(&mut self, x0: f64) -> Option<f64> {
        #[inline(always)]
        fn r7(buf: &[f64; 7], p: usize, off: usize) -> f64 {
            let mut idx = p + 7 - off;
            if idx >= 7 {
                idx -= 7;
            }
            buf[idx]
        }
        const C0: f64 = 0.0962;
        const C1: f64 = 0.5769;
        const DIV10: f64 = 0.1;

        let fir_val =
            (4.0 * x0 + 3.0 * self.wma_hist[0] + 2.0 * self.wma_hist[1] + self.wma_hist[2]) * DIV10;

        self.wma_hist[2] = self.wma_hist[1];
        self.wma_hist[1] = self.wma_hist[0];
        self.wma_hist[0] = x0;

        self.sum_ring[self.sum_idx] = x0;
        self.sum_idx += 1;
        if self.sum_idx == self.max_dc {
            self.sum_idx = 0;
        }

        self.fir_buf[self.ring_ptr] = fir_val;

        let fir_0 = r7(&self.fir_buf, self.ring_ptr, 0);
        let fir_2 = r7(&self.fir_buf, self.ring_ptr, 2);
        let fir_4 = r7(&self.fir_buf, self.ring_ptr, 4);
        let fir_6 = r7(&self.fir_buf, self.ring_ptr, 6);

        let h_in = C0 * (fir_0 - fir_6) + C1 * (fir_2 - fir_4);
        let period_mult = 0.075 * self.prev_mesa + 0.54;

        let det_val = h_in * period_mult;
        self.det_buf[self.ring_ptr] = det_val;

        let i1_val = r7(&self.det_buf, self.ring_ptr, 3);
        self.i1_buf[self.ring_ptr] = i1_val;

        let det_0 = r7(&self.det_buf, self.ring_ptr, 0);
        let det_2 = r7(&self.det_buf, self.ring_ptr, 2);
        let det_4 = r7(&self.det_buf, self.ring_ptr, 4);
        let det_6 = r7(&self.det_buf, self.ring_ptr, 6);
        let h_in_q1 = C0 * (det_0 - det_6) + C1 * (det_2 - det_4);
        let q1_val = h_in_q1 * period_mult;
        self.q1_buf[self.ring_ptr] = q1_val;

        let i1_0 = r7(&self.i1_buf, self.ring_ptr, 0);
        let i1_2 = r7(&self.i1_buf, self.ring_ptr, 2);
        let i1_4 = r7(&self.i1_buf, self.ring_ptr, 4);
        let i1_6 = r7(&self.i1_buf, self.ring_ptr, 6);
        let j_i_val = (C0 * (i1_0 - i1_6) + C1 * (i1_2 - i1_4)) * period_mult;

        let q1_0 = r7(&self.q1_buf, self.ring_ptr, 0);
        let q1_2 = r7(&self.q1_buf, self.ring_ptr, 2);
        let q1_4 = r7(&self.q1_buf, self.ring_ptr, 4);
        let q1_6 = r7(&self.q1_buf, self.ring_ptr, 6);
        let j_q_val = (C0 * (q1_0 - q1_6) + C1 * (q1_2 - q1_4)) * period_mult;

        let mut i2_cur = 0.2 * (i1_val - j_q_val) + 0.8 * self.prev_i2;
        let mut q2_cur = 0.2 * (q1_val + j_i_val) + 0.8 * self.prev_q2;

        let re_val = i2_cur * self.prev_i2 + q2_cur * self.prev_q2;
        let im_val = i2_cur * self.prev_q2 - q2_cur * self.prev_i2;
        self.prev_i2 = i2_cur;
        self.prev_q2 = q2_cur;

        let re_smooth = 0.2 * re_val + 0.8 * self.prev_re;
        let im_smooth = 0.2 * im_val + 0.8 * self.prev_im;
        self.prev_re = re_smooth;
        self.prev_im = im_smooth;

        let mut new_mesa = if re_smooth != 0.0 && im_smooth != 0.0 {
            let angle = im_smooth.atan2(re_smooth);
            if angle != 0.0 {
                (2.0 * core::f64::consts::PI) / angle
            } else {
                0.0
            }
        } else {
            0.0
        };

        let up_lim = 1.5 * self.prev_mesa;
        if new_mesa > up_lim {
            new_mesa = up_lim;
        }
        let low_lim = 0.67 * self.prev_mesa;
        if new_mesa < low_lim {
            new_mesa = low_lim;
        }
        new_mesa = new_mesa.clamp(6.0, 50.0);

        let final_mesa = 0.2 * new_mesa + 0.8 * self.prev_mesa;
        self.prev_mesa = final_mesa;
        let sp_val = 0.33 * final_mesa + 0.67 * self.prev_smooth;
        self.prev_smooth = sp_val;

        let mut dcp_i = (sp_val + 0.5).floor() as i32;
        if dcp_i < 1 {
            dcp_i = 1;
        }
        if dcp_i as usize > self.max_dc {
            dcp_i = self.max_dc as i32;
        }
        let dcp = dcp_i as usize;

        let mut sum_src = 0.0;
        let mut idx2 = self.sum_idx;
        for _ in 0..dcp {
            idx2 = if idx2 == 0 { self.max_dc - 1 } else { idx2 - 1 };
            sum_src += self.sum_ring[idx2];
        }
        let it_val = sum_src / dcp as f64;

        let eit_val = if self.bar < self.warmup_bars {
            x0
        } else {
            (4.0 * it_val + 3.0 * self.prev_it1 + 2.0 * self.prev_it2 + self.prev_it3) * DIV10
        };

        self.prev_it3 = self.prev_it2;
        self.prev_it2 = self.prev_it1;
        self.prev_it1 = it_val;

        self.ring_ptr = (self.ring_ptr + 1) % 7;

        let result = if self.bar < self.warmup_bars {
            None
        } else {
            Some(eit_val)
        };
        self.bar += 1;
        result
    }
}

#[derive(Clone, Debug)]
pub struct EhlersITrendBatchRange {
    pub warmup_bars: (usize, usize, usize),
    pub max_dc_period: (usize, usize, usize),
}
impl Default for EhlersITrendBatchRange {
    fn default() -> Self {
        Self {
            warmup_bars: (12, 12, 0),
            max_dc_period: (50, 299, 1),
        }
    }
}
#[derive(Clone, Debug, Default)]
pub struct EhlersITrendBatchBuilder {
    range: EhlersITrendBatchRange,
    kernel: Kernel,
}
impl EhlersITrendBatchBuilder {
    pub fn new() -> Self {
        Self::default()
    }
    pub fn kernel(mut self, k: Kernel) -> Self {
        self.kernel = k;
        self
    }
    #[inline]
    pub fn warmup_range(mut self, start: usize, end: usize, step: usize) -> Self {
        self.range.warmup_bars = (start, end, step);
        self
    }
    #[inline]
    pub fn warmup_static(mut self, w: usize) -> Self {
        self.range.warmup_bars = (w, w, 0);
        self
    }
    #[inline]
    pub fn max_dc_range(mut self, start: usize, end: usize, step: usize) -> Self {
        self.range.max_dc_period = (start, end, step);
        self
    }
    #[inline]
    pub fn max_dc_static(mut self, m: usize) -> Self {
        self.range.max_dc_period = (m, m, 0);
        self
    }
    pub fn apply_slice(self, data: &[f64]) -> Result<EhlersITrendBatchOutput, EhlersITrendError> {
        ehlers_itrend_batch_with_kernel(data, &self.range, self.kernel)
    }
    pub fn apply_candles(
        self,
        c: &Candles,
        src: &str,
    ) -> Result<EhlersITrendBatchOutput, EhlersITrendError> {
        let slice = source_type(c, src);
        self.apply_slice(slice)
    }
    pub fn with_default_candles(c: &Candles) -> Result<EhlersITrendBatchOutput, EhlersITrendError> {
        Self::new().kernel(Kernel::Auto).apply_candles(c, "close")
    }
}

pub fn ehlers_itrend_batch_with_kernel(
    data: &[f64],
    sweep: &EhlersITrendBatchRange,
    kernel: Kernel,
) -> Result<EhlersITrendBatchOutput, EhlersITrendError> {
    let kernel = match kernel {
        Kernel::Auto => detect_best_batch_kernel(),
        other if other.is_batch() => other,
        other => return Err(EhlersITrendError::InvalidKernelForBatch(other)),
    };

    let simd = match kernel {
        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
        Kernel::Avx512Batch => Kernel::Avx512,
        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
        Kernel::Avx2Batch => Kernel::Avx2,
        Kernel::ScalarBatch => Kernel::Scalar,
        _ => unreachable!(),
    };

    ehlers_itrend_batch_par_slice(data, sweep, simd)
}

#[inline(always)]
fn ehlers_itrend_batch_inner(
    data: &[f64],
    sweep: &EhlersITrendBatchRange,
    kern: Kernel,
    parallel: bool,
) -> Result<EhlersITrendBatchOutput, EhlersITrendError> {
    let combos = expand_grid(sweep)?;
    if combos.is_empty() {
        return Err(EhlersITrendError::InvalidBatchRange);
    }
    let rows = combos.len();
    let cols = data.len();

    rows.checked_mul(cols)
        .ok_or(EhlersITrendError::SizeOverflow {
            context: "rows*cols in batch output matrix",
        })?;

    let mut buf_mu = make_uninit_matrix(rows, cols);

    let first = data
        .iter()
        .position(|x| !x.is_nan())
        .ok_or(EhlersITrendError::AllValuesNaN)?;
    let warm: Vec<usize> = combos
        .iter()
        .map(|c| first + c.warmup_bars.unwrap())
        .collect();

    init_matrix_prefixes(&mut buf_mu, cols, &warm);

    let mut buf_guard = std::mem::ManuallyDrop::new(buf_mu);
    let out: &mut [f64] = unsafe {
        core::slice::from_raw_parts_mut(buf_guard.as_mut_ptr() as *mut f64, buf_guard.len())
    };

    let result_combos = ehlers_itrend_batch_inner_into(data, sweep, kern, parallel, out)?;

    let values = unsafe {
        Vec::from_raw_parts(
            buf_guard.as_mut_ptr() as *mut f64,
            buf_guard.len(),
            buf_guard.capacity(),
        )
    };

    Ok(EhlersITrendBatchOutput {
        values,
        combos: result_combos,
        rows,
        cols,
    })
}

#[inline(always)]
fn ehlers_itrend_batch_inner_into(
    data: &[f64],
    sweep: &EhlersITrendBatchRange,
    kern: Kernel,
    parallel: bool,
    out: &mut [f64],
) -> Result<Vec<EhlersITrendParams>, EhlersITrendError> {
    let combos = expand_grid(sweep)?;
    if combos.is_empty() {
        return Err(EhlersITrendError::InvalidBatchRange);
    }
    let first = data
        .iter()
        .position(|x| !x.is_nan())
        .ok_or(EhlersITrendError::AllValuesNaN)?;
    let max_warmup = combos.iter().map(|c| c.warmup_bars.unwrap()).max().unwrap();
    if data.len() - first < max_warmup {
        return Err(EhlersITrendError::NotEnoughDataForWarmup {
            warmup_bars: max_warmup,
            length: data.len() - first,
        });
    }

    let rows = combos.len();
    let cols = data.len();
    rows.checked_mul(cols)
        .ok_or(EhlersITrendError::SizeOverflow {
            context: "rows*cols in batch output matrix (into)",
        })?;
    debug_assert_eq!(out.len(), rows * cols);

    let raw = unsafe {
        core::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut MaybeUninit<f64>, out.len())
    };

    #[inline(always)]
    fn precompute_wma4(src: &[f64]) -> Vec<f64> {
        let n = src.len();
        let mut fir = Vec::with_capacity(n);
        for i in 0..n {
            let x0 = src[i];
            let x1 = if i >= 1 { src[i - 1] } else { 0.0 };
            let x2 = if i >= 2 { src[i - 2] } else { 0.0 };
            let x3 = if i >= 3 { src[i - 3] } else { 0.0 };
            fir.push((4.0 * x0 + 3.0 * x1 + 2.0 * x2 + x3) / 10.0);
        }
        fir
    }

    let fir_series = precompute_wma4(data);

    let do_row = |row: usize, dst_mu: &mut [MaybeUninit<f64>]| unsafe {
        let p = &combos[row];
        let warmup = p.warmup_bars.unwrap();
        let max_dc = p.max_dc_period.unwrap();
        let dst = core::slice::from_raw_parts_mut(dst_mu.as_mut_ptr() as *mut f64, dst_mu.len());
        ehlers_itrend_row_scalar_tail_with_fir(data, &fir_series, warmup, max_dc, first, dst);
    };

    if parallel {
        #[cfg(not(target_arch = "wasm32"))]
        {
            raw.par_chunks_mut(cols)
                .enumerate()
                .for_each(|(row, slice)| do_row(row, slice));
        }
        #[cfg(target_arch = "wasm32")]
        {
            for (row, slice) in raw.chunks_mut(cols).enumerate() {
                do_row(row, slice);
            }
        }
    } else {
        for (row, slice) in raw.chunks_mut(cols).enumerate() {
            do_row(row, slice);
        }
    }

    Ok(combos)
}

#[derive(Clone, Debug)]
pub struct EhlersITrendBatchOutput {
    pub values: Vec<f64>,
    pub combos: Vec<EhlersITrendParams>,
    pub rows: usize,
    pub cols: usize,
}
impl EhlersITrendBatchOutput {
    pub fn row_for_params(&self, p: &EhlersITrendParams) -> Option<usize> {
        self.combos.iter().position(|c| {
            c.warmup_bars.unwrap_or(12) == p.warmup_bars.unwrap_or(12)
                && c.max_dc_period.unwrap_or(50) == p.max_dc_period.unwrap_or(50)
        })
    }
    pub fn values_for(&self, p: &EhlersITrendParams) -> Option<&[f64]> {
        self.row_for_params(p).map(|row| {
            let start = row * self.cols;
            &self.values[start..start + self.cols]
        })
    }
}

#[inline(always)]
fn expand_grid(r: &EhlersITrendBatchRange) -> Result<Vec<EhlersITrendParams>, EhlersITrendError> {
    fn axis((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, EhlersITrendError> {
        if step == 0 {
            return if start == end {
                Ok(vec![start])
            } else {
                Err(EhlersITrendError::InvalidRange { start, end, step })
            };
        }
        let mut out = Vec::new();
        if start <= end {
            let mut x = start;
            loop {
                out.push(x);
                match x.checked_add(step) {
                    Some(n) if n > x && n <= end => x = n,
                    _ => break,
                }
            }
        } else {
            let mut x = start;
            loop {
                out.push(x);
                match x.checked_sub(step) {
                    Some(n) if n < x && n >= end => x = n,
                    _ => break,
                }
            }
        }
        if out.is_empty() {
            return Err(EhlersITrendError::InvalidRange { start, end, step });
        }
        Ok(out)
    }
    let warmups = axis(r.warmup_bars)?;
    let max_dcs = axis(r.max_dc_period)?;
    let cap = warmups
        .len()
        .checked_mul(max_dcs.len())
        .ok_or(EhlersITrendError::SizeOverflow {
            context: "expand_grid capacity warmups.len()*max_dcs.len()",
        })?;
    let mut out = Vec::with_capacity(cap);
    for &w in &warmups {
        for &m in &max_dcs {
            out.push(EhlersITrendParams {
                warmup_bars: Some(w),
                max_dc_period: Some(m),
            });
        }
    }
    Ok(out)
}

#[inline(always)]
pub fn ehlers_itrend_per_slice(
    data: &[f64],
    sweep: &EhlersITrendBatchRange,
    kern: Kernel,
) -> Result<EhlersITrendBatchOutput, EhlersITrendError> {
    ehlers_itrend_batch_inner(data, sweep, kern, false)
}

#[inline(always)]
pub fn ehlers_itrend_batch_par_slice(
    data: &[f64],
    sweep: &EhlersITrendBatchRange,
    kern: Kernel,
) -> Result<EhlersITrendBatchOutput, EhlersITrendError> {
    ehlers_itrend_batch_inner(data, sweep, kern, true)
}

#[inline(always)]
pub fn ehlers_itrend_row_scalar_tail(
    data: &[f64],
    warmup_bars: usize,
    max_dc: usize,
    first: usize,
    out: &mut [f64],
) {
    let warm = warm_index(first, warmup_bars);
    ehlers_itrend_scalar_tail(data, warmup_bars, max_dc, first, warm, out);
}

#[inline(always)]
fn ehlers_itrend_row_scalar_tail_with_fir(
    data: &[f64],
    fir: &[f64],
    warmup_bars: usize,
    max_dc: usize,
    first: usize,
    out: &mut [f64],
) {
    debug_assert_eq!(data.len(), fir.len());
    debug_assert_eq!(data.len(), out.len());

    let warm = warm_index(first, warmup_bars);

    let length = data.len();
    let mut fir_buf = [0.0; 7];
    let mut det_buf = [0.0; 7];
    let mut i1_buf = [0.0; 7];
    let mut q1_buf = [0.0; 7];
    let (mut prev_i2, mut prev_q2) = (0.0, 0.0);
    let (mut prev_re, mut prev_im) = (0.0, 0.0);
    let (mut prev_mesa, mut prev_smooth) = (0.0, 0.0);
    let mut sum_ring = vec![0.0; max_dc];
    let mut sum_idx = 0usize;
    let (mut prev_it1, mut prev_it2, mut prev_it3) = (0.0, 0.0, 0.0);
    let mut ring_ptr = 0usize;

    #[inline(always)]
    fn ring_get(buf: &[f64; 7], center: usize, off: usize) -> f64 {
        let mut idx = center + 7 - off;
        if idx >= 7 {
            idx -= 7;
        }
        buf[idx]
    }

    for i in 0..length {
        let x0 = data[i];

        let fir_val = fir[i];
        fir_buf[ring_ptr] = fir_val;

        let fir_0 = ring_get(&fir_buf, ring_ptr, 0);
        let fir_2 = ring_get(&fir_buf, ring_ptr, 2);
        let fir_4 = ring_get(&fir_buf, ring_ptr, 4);
        let fir_6 = ring_get(&fir_buf, ring_ptr, 6);

        let h_in = 0.0962 * fir_0 + 0.5769 * fir_2 - 0.5769 * fir_4 - 0.0962 * fir_6;
        let period_mult = 0.075 * prev_mesa + 0.54;
        let det_val = h_in * period_mult;
        det_buf[ring_ptr] = det_val;

        let i1_val = ring_get(&det_buf, ring_ptr, 3);
        i1_buf[ring_ptr] = i1_val;

        let det_0 = ring_get(&det_buf, ring_ptr, 0);
        let det_2 = ring_get(&det_buf, ring_ptr, 2);
        let det_4 = ring_get(&det_buf, ring_ptr, 4);
        let det_6 = ring_get(&det_buf, ring_ptr, 6);
        let h_in_q1 = 0.0962 * det_0 + 0.5769 * det_2 - 0.5769 * det_4 - 0.0962 * det_6;
        let q1_val = h_in_q1 * period_mult;
        q1_buf[ring_ptr] = q1_val;

        let i1_0 = ring_get(&i1_buf, ring_ptr, 0);
        let i1_2 = ring_get(&i1_buf, ring_ptr, 2);
        let i1_4 = ring_get(&i1_buf, ring_ptr, 4);
        let i1_6 = ring_get(&i1_buf, ring_ptr, 6);
        let j_i_val = (0.0962 * i1_0 + 0.5769 * i1_2 - 0.5769 * i1_4 - 0.0962 * i1_6) * period_mult;

        let q1_0 = ring_get(&q1_buf, ring_ptr, 0);
        let q1_2 = ring_get(&q1_buf, ring_ptr, 2);
        let q1_4 = ring_get(&q1_buf, ring_ptr, 4);
        let q1_6 = ring_get(&q1_buf, ring_ptr, 6);
        let j_q_val = (0.0962 * q1_0 + 0.5769 * q1_2 - 0.5769 * q1_4 - 0.0962 * q1_6) * period_mult;

        let mut i2_cur = i1_val - j_q_val;
        let mut q2_cur = q1_val + j_i_val;
        i2_cur = 0.2 * i2_cur + 0.8 * prev_i2;
        q2_cur = 0.2 * q2_cur + 0.8 * prev_q2;

        let re_val = i2_cur * prev_i2 + q2_cur * prev_q2;
        let im_val = i2_cur * prev_q2 - q2_cur * prev_i2;
        prev_i2 = i2_cur;
        prev_q2 = q2_cur;

        let re_smooth = 0.2 * re_val + 0.8 * prev_re;
        let im_smooth = 0.2 * im_val + 0.8 * prev_im;
        prev_re = re_smooth;
        prev_im = im_smooth;

        let mut new_mesa = 0.0;
        if re_smooth != 0.0 && im_smooth != 0.0 {
            let angle = im_smooth.atan2(re_smooth);
            if angle != 0.0 {
                new_mesa = (2.0 * core::f64::consts::PI) / angle;
            }
        }
        let up_lim = 1.5 * prev_mesa;
        if new_mesa > up_lim {
            new_mesa = up_lim;
        }
        let low_lim = 0.67 * prev_mesa;
        if new_mesa < low_lim {
            new_mesa = low_lim;
        }
        new_mesa = new_mesa.clamp(6.0, 50.0);
        let final_mesa = 0.2 * new_mesa + 0.8 * prev_mesa;
        prev_mesa = final_mesa;
        let sp_val = 0.33 * final_mesa + 0.67 * prev_smooth;
        prev_smooth = sp_val;

        let mut dcp = (sp_val + 0.5).floor() as usize;
        if dcp == 0 {
            dcp = 1;
        } else if dcp > max_dc {
            dcp = max_dc;
        }

        sum_ring[sum_idx] = x0;
        sum_idx += 1;
        if sum_idx == max_dc {
            sum_idx = 0;
        }
        let mut sum_src = 0.0;
        let mut idx2 = sum_idx;
        for _ in 0..dcp {
            idx2 = if idx2 == 0 { max_dc - 1 } else { idx2 - 1 };
            sum_src += sum_ring[idx2];
        }
        let it_val = sum_src / dcp as f64;

        let eit_val = if i < warmup_bars {
            x0
        } else {
            (4.0 * it_val + 3.0 * prev_it1 + 2.0 * prev_it2 + prev_it3) / 10.0
        };

        prev_it3 = prev_it2;
        prev_it2 = prev_it1;
        prev_it1 = it_val;

        if i >= warm {
            out[i] = eit_val;
        }

        ring_ptr += 1;
        if ring_ptr == 7 {
            ring_ptr = 0;
        }
    }
}

#[cfg(feature = "python")]
#[pyfunction(name = "ehlers_itrend")]
#[pyo3(signature = (data, warmup_bars, max_dc_period, kernel=None))]
pub fn ehlers_itrend_py<'py>(
    py: Python<'py>,
    data: numpy::PyReadonlyArray1<'py, f64>,
    warmup_bars: usize,
    max_dc_period: usize,
    kernel: Option<&str>,
) -> PyResult<Bound<'py, numpy::PyArray1<f64>>> {
    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
    let kern = validate_kernel(kernel, false)?;
    let params = EhlersITrendParams {
        warmup_bars: Some(warmup_bars),
        max_dc_period: Some(max_dc_period),
    };

    let result_vec: Vec<f64> = if let Ok(slice_in) = data.as_slice() {
        let input = EhlersITrendInput::from_slice(slice_in, params);
        py.allow_threads(|| ehlers_itrend_with_kernel(&input, kern).map(|o| o.values))
            .map_err(|e| PyValueError::new_err(e.to_string()))?
    } else {
        let owned = data.as_array().to_owned();
        let slice_in = owned.as_slice().expect("owned array should be contiguous");
        let input = EhlersITrendInput::from_slice(slice_in, params);
        py.allow_threads(|| ehlers_itrend_with_kernel(&input, kern).map(|o| o.values))
            .map_err(|e| PyValueError::new_err(e.to_string()))?
    };
    Ok(result_vec.into_pyarray(py))
}

#[cfg(feature = "python")]
#[pyfunction(name = "ehlers_itrend_batch")]
#[pyo3(signature = (data, warmup_range, max_dc_range, kernel=None))]
pub fn ehlers_itrend_batch_py<'py>(
    py: Python<'py>,
    data: numpy::PyReadonlyArray1<'py, f64>,
    warmup_range: (usize, usize, usize),
    max_dc_range: (usize, usize, usize),
    kernel: Option<&str>,
) -> PyResult<Bound<'py, PyDict>> {
    use numpy::{PyArray1, PyArrayMethods};
    let slice_in = data.as_slice()?;
    let sweep = EhlersITrendBatchRange {
        warmup_bars: warmup_range,
        max_dc_period: max_dc_range,
    };
    let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
    let rows = combos.len();
    let cols = slice_in.len();
    let total = rows
        .checked_mul(cols)
        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
    let out_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
    let slice_out = unsafe { out_arr.as_slice_mut()? };

    let kern = validate_kernel(kernel, true)?;
    py.allow_threads(|| {
        let simd = match kern {
            Kernel::Auto => detect_best_batch_kernel(),
            k => k,
        };

        let simd = match simd {
            Kernel::ScalarBatch => Kernel::Scalar,
            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
            Kernel::Avx2Batch => Kernel::Avx2,
            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
            Kernel::Avx512Batch => Kernel::Avx512,
            _ => Kernel::Scalar,
        };
        ehlers_itrend_batch_inner_into(slice_in, &sweep, simd, true, slice_out)
    })
    .map_err(|e| PyValueError::new_err(e.to_string()))?;

    let dict = PyDict::new(py);
    dict.set_item("values", out_arr.reshape((rows, cols))?)?;
    dict.set_item(
        "warmups",
        combos
            .iter()
            .map(|p| p.warmup_bars.unwrap() as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    dict.set_item(
        "max_dcs",
        combos
            .iter()
            .map(|p| p.max_dc_period.unwrap() as u64)
            .collect::<Vec<_>>()
            .into_pyarray(py),
    )?;
    Ok(dict)
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "ehlers_itrend_cuda_batch_dev")]
#[pyo3(signature = (data_f32, warmup_range=(12, 12, 0), max_dc_range=(50, 50, 0), device_id=0))]
pub fn ehlers_itrend_cuda_batch_dev_py(
    py: Python<'_>,
    data_f32: numpy::PyReadonlyArray1<'_, f32>,
    warmup_range: (usize, usize, usize),
    max_dc_range: (usize, usize, usize),
    device_id: usize,
) -> PyResult<DeviceArrayF32ITrendPy> {
    if !cuda_available() {
        return Err(PyValueError::new_err("CUDA not available"));
    }

    let slice_in = data_f32.as_slice()?;
    let sweep = EhlersITrendBatchRange {
        warmup_bars: warmup_range,
        max_dc_period: max_dc_range,
    };

    let cuda = Arc::new(
        CudaEhlersITrend::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?,
    );
    let inner = py.allow_threads(|| -> PyResult<_> {
        cuda.ehlers_itrend_batch_dev(slice_in, &sweep)
            .map_err(|e| PyValueError::new_err(e.to_string()))
    })?;

    Ok(DeviceArrayF32ITrendPy { inner, guard: cuda })
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyfunction(name = "ehlers_itrend_cuda_many_series_one_param_dev")]
#[pyo3(signature = (data_tm_f32, warmup_bars=12, max_dc_period=50, device_id=0))]
pub fn ehlers_itrend_cuda_many_series_one_param_dev_py(
    py: Python<'_>,
    data_tm_f32: numpy::PyReadonlyArray2<'_, f32>,
    warmup_bars: usize,
    max_dc_period: usize,
    device_id: usize,
) -> PyResult<DeviceArrayF32ITrendPy> {
    if !cuda_available() {
        return Err(PyValueError::new_err("CUDA not available"));
    }
    if warmup_bars == 0 {
        return Err(PyValueError::new_err("warmup_bars must be positive"));
    }
    if max_dc_period == 0 {
        return Err(PyValueError::new_err("max_dc_period must be positive"));
    }

    let flat = data_tm_f32.as_slice()?;
    let shape = data_tm_f32.shape();
    if shape.len() != 2 {
        return Err(PyValueError::new_err("time-major matrix must be 2D"));
    }
    let series_len = shape[0];
    let num_series = shape[1];
    let params = EhlersITrendParams {
        warmup_bars: Some(warmup_bars),
        max_dc_period: Some(max_dc_period),
    };

    let cuda = Arc::new(
        CudaEhlersITrend::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?,
    );
    let inner = py.allow_threads(|| -> PyResult<_> {
        cuda.ehlers_itrend_many_series_one_param_time_major_dev(
            flat, num_series, series_len, &params,
        )
        .map_err(|e| PyValueError::new_err(e.to_string()))
    })?;
    Ok(DeviceArrayF32ITrendPy { inner, guard: cuda })
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pyclass(module = "ta_indicators.cuda", unsendable)]
pub struct DeviceArrayF32ITrendPy {
    pub(crate) inner: DeviceArrayF32,
    pub(crate) guard: Arc<CudaEhlersITrend>,
}

#[cfg(all(feature = "python", feature = "cuda"))]
#[pymethods]
impl DeviceArrayF32ITrendPy {
    #[getter]
    fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
        let d = PyDict::new(py);

        d.set_item("shape", (self.inner.rows, self.inner.cols))?;

        d.set_item("typestr", "<f4")?;

        d.set_item(
            "strides",
            (
                self.inner.cols * std::mem::size_of::<f32>(),
                std::mem::size_of::<f32>(),
            ),
        )?;

        d.set_item("data", (self.inner.device_ptr() as usize, false))?;

        d.set_item("version", 3)?;
        Ok(d)
    }

    fn __dlpack_device__(&self) -> (i32, i32) {
        (2, self.guard.device_id() as i32)
    }

    #[pyo3(signature = (stream=None, max_version=None, dl_device=None, copy=None))]
    fn __dlpack__<'py>(
        &mut self,
        py: Python<'py>,
        stream: Option<PyObject>,
        max_version: Option<PyObject>,
        dl_device: Option<PyObject>,
        copy: Option<PyObject>,
    ) -> PyResult<PyObject> {
        use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;

        let (kdl, alloc_dev) = self.__dlpack_device__();
        if let Some(dev_obj) = dl_device.as_ref() {
            if let Ok((dev_ty, dev_id)) = dev_obj.extract::<(i32, i32)>(py) {
                if dev_ty != kdl || dev_id != alloc_dev {
                    let wants_copy = copy
                        .as_ref()
                        .and_then(|c| c.extract::<bool>(py).ok())
                        .unwrap_or(false);
                    if wants_copy {
                        return Err(PyValueError::new_err(
                            "device copy not implemented for __dlpack__",
                        ));
                    } else {
                        return Err(PyValueError::new_err("dl_device mismatch for __dlpack__"));
                    }
                }
            }
        }
        let _ = stream;

        let dummy =
            DeviceBuffer::from_slice(&[]).map_err(|e| PyValueError::new_err(e.to_string()))?;
        let inner = std::mem::replace(
            &mut self.inner,
            DeviceArrayF32 {
                buf: dummy,
                rows: 0,
                cols: 0,
            },
        );

        let rows = inner.rows;
        let cols = inner.cols;
        let buf = inner.buf;

        let max_version_bound = max_version.map(|obj| obj.into_bound(py));

        export_f32_cuda_dlpack_2d(py, buf, rows, cols, alloc_dev, max_version_bound)
    }
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn ehlers_itrend_js(
    data: &[f64],
    warmup_bars: usize,
    max_dc_period: usize,
) -> Result<Vec<f64>, JsValue> {
    let input = EhlersITrendInput::from_slice(
        data,
        EhlersITrendParams {
            warmup_bars: Some(warmup_bars),
            max_dc_period: Some(max_dc_period),
        },
    );
    let mut out = vec![0.0; data.len()];
    ehlers_itrend_into_slice(&mut out, &input, detect_best_kernel())
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    Ok(out)
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn ehlers_itrend_alloc(len: usize) -> *mut f64 {
    let mut v = Vec::<f64>::with_capacity(len);
    let p = v.as_mut_ptr();
    core::mem::forget(v);
    p
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn ehlers_itrend_free(ptr: *mut f64, len: usize) {
    unsafe {
        let _ = Vec::from_raw_parts(ptr, len, len);
    }
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen]
pub fn ehlers_itrend_into(
    in_ptr: *const f64,
    out_ptr: *mut f64,
    len: usize,
    warmup_bars: usize,
    max_dc_period: usize,
) -> Result<(), JsValue> {
    if in_ptr.is_null() || out_ptr.is_null() {
        return Err(JsValue::from_str("null pointer"));
    }
    unsafe {
        let data = core::slice::from_raw_parts(in_ptr, len);
        let params = EhlersITrendParams {
            warmup_bars: Some(warmup_bars),
            max_dc_period: Some(max_dc_period),
        };
        let input = EhlersITrendInput::from_slice(data, params);
        if core::ptr::eq(in_ptr, out_ptr) {
            let mut tmp = vec![0.0; len];
            ehlers_itrend_into_slice(&mut tmp, &input, detect_best_kernel())
                .map_err(|e| JsValue::from_str(&e.to_string()))?;
            core::slice::from_raw_parts_mut(out_ptr, len).copy_from_slice(&tmp);
        } else {
            let out = core::slice::from_raw_parts_mut(out_ptr, len);
            ehlers_itrend_into_slice(out, &input, detect_best_kernel())
                .map_err(|e| JsValue::from_str(&e.to_string()))?;
        }
    }
    Ok(())
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[derive(Serialize, Deserialize)]
pub struct EhlersBatchConfig {
    pub warmup_range: (usize, usize, usize),
    pub max_dc_range: (usize, usize, usize),
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[derive(Serialize, Deserialize)]
pub struct EhlersITrendBatchJsOutput {
    pub values: Vec<f64>,
    pub combos: Vec<EhlersITrendParams>,
    pub rows: usize,
    pub cols: usize,
}

#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
#[wasm_bindgen(js_name = ehlers_itrend_batch)]
pub fn ehlers_itrend_batch_unified_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
    let cfg: EhlersBatchConfig = serde_wasm_bindgen::from_value(config)
        .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
    let sweep = EhlersITrendBatchRange {
        warmup_bars: cfg.warmup_range,
        max_dc_period: cfg.max_dc_range,
    };
    let out = ehlers_itrend_batch_inner(data, &sweep, detect_best_kernel(), false)
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    let js = EhlersITrendBatchJsOutput {
        values: out.values,
        combos: out.combos,
        rows: out.rows,
        cols: out.cols,
    };
    serde_wasm_bindgen::to_value(&js)
        .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
}

#[cfg(feature = "python")]
#[pyclass(name = "EhlersITrendStream")]
pub struct EhlersITrendStreamPy {
    stream: EhlersITrendStream,
}

#[cfg(feature = "python")]
#[pymethods]
impl EhlersITrendStreamPy {
    #[new]
    fn new(warmup_bars: Option<usize>, max_dc_period: Option<usize>) -> PyResult<Self> {
        let params = EhlersITrendParams {
            warmup_bars,
            max_dc_period,
        };
        let stream = EhlersITrendStream::try_new(params)
            .map_err(|e| PyValueError::new_err(e.to_string()))?;
        Ok(EhlersITrendStreamPy { stream })
    }

    fn update(&mut self, value: f64) -> Option<f64> {
        self.stream.update(value)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::skip_if_unsupported;
    use crate::utilities::data_loader::read_candles_from_csv;

    #[test]
    fn test_ehlers_itrend_into_matches_api() -> Result<(), Box<dyn Error>> {
        let n = 256usize;
        let data: Vec<f64> = (0..n)
            .map(|i| (i as f64) * 0.01 + ((i as f64) * 0.1).sin())
            .collect();

        let input = EhlersITrendInput::from_slice(&data, EhlersITrendParams::default());

        let baseline = ehlers_itrend(&input)?.values;

        let mut out = vec![0.0; n];
        #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
        {
            ehlers_itrend_into(&input, &mut out)?;
        }
        #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
        {
            ehlers_itrend_into_slice(&mut out, &input, Kernel::Auto)?;
        }

        assert_eq!(baseline.len(), out.len());

        fn equal(a: f64, b: f64) -> bool {
            (a.is_nan() && b.is_nan()) || a == b || (a - b).abs() <= 1e-12
        }

        for i in 0..n {
            assert!(
                equal(baseline[i], out[i]),
                "Mismatch at {}: baseline={} out={}",
                i,
                baseline[i],
                out[i]
            );
        }

        Ok(())
    }

    fn check_itrend_partial_params(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file_path)?;
        let default_params = EhlersITrendParams {
            warmup_bars: None,
            max_dc_period: None,
        };
        let input = EhlersITrendInput::from_candles(&candles, "close", default_params);
        let output = ehlers_itrend_with_kernel(&input, kernel)?;
        assert_eq!(output.values.len(), candles.close.len());
        Ok(())
    }

    fn check_itrend_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file_path)?;
        let input = EhlersITrendInput::with_default_candles(&candles);
        let result = ehlers_itrend_with_kernel(&input, kernel)?;

        let expected_last_five = [59638.12, 59497.26, 59431.08, 59391.23, 59372.19];
        let start = result.values.len().saturating_sub(5);
        for (i, &val) in result.values[start..].iter().enumerate() {
            let diff = (val - expected_last_five[i]).abs();
            assert!(
                diff < 1e-1,
                "[{}] EIT {:?} mismatch at idx {}: got {}, expected {}",
                test_name,
                kernel,
                i,
                val,
                expected_last_five[i]
            );
        }
        Ok(())
    }

    fn check_itrend_default_candles(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file_path)?;
        let input = EhlersITrendInput::with_default_candles(&candles);
        match input.data {
            EhlersITrendData::Candles { source, .. } => assert_eq!(source, "close"),
            _ => panic!("Expected EhlersITrendData::Candles"),
        }
        let output = ehlers_itrend_with_kernel(&input, kernel)?;
        assert_eq!(output.values.len(), candles.close.len());
        Ok(())
    }

    fn check_itrend_no_data(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let input_data = [];
        let params = EhlersITrendParams {
            warmup_bars: Some(12),
            max_dc_period: Some(50),
        };
        let input = EhlersITrendInput::from_slice(&input_data, params);
        let res = ehlers_itrend_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] EIT should fail with empty data",
            test_name
        );
        Ok(())
    }

    fn check_itrend_all_nan_data(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let data = [f64::NAN, f64::NAN, f64::NAN];
        let params = EhlersITrendParams {
            warmup_bars: Some(12),
            max_dc_period: Some(50),
        };
        let input = EhlersITrendInput::from_slice(&data, params);
        let res = ehlers_itrend_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] EIT should fail with all-NaN data",
            test_name
        );
        Ok(())
    }

    fn check_itrend_small_data_for_warmup(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let data = [42.0; 5];
        let params = EhlersITrendParams {
            warmup_bars: Some(12),
            max_dc_period: Some(50),
        };
        let input = EhlersITrendInput::from_slice(&data, params);
        let res = ehlers_itrend_with_kernel(&input, kernel);
        assert!(
            res.is_err(),
            "[{}] EIT should fail if warmup_bars >= data length",
            test_name
        );
        Ok(())
    }

    fn check_itrend_very_small_dataset(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let data = [42.0, 43.0];
        let params = EhlersITrendParams {
            warmup_bars: Some(1),
            max_dc_period: Some(50),
        };
        let input = EhlersITrendInput::from_slice(&data, params);
        let result = ehlers_itrend_with_kernel(&input, kernel)?;
        assert_eq!(result.values.len(), data.len());
        Ok(())
    }

    fn check_itrend_reinput(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file_path)?;
        let first_params = EhlersITrendParams {
            warmup_bars: Some(12),
            max_dc_period: Some(50),
        };
        let first_input = EhlersITrendInput::from_candles(&candles, "close", first_params);
        let first_result = ehlers_itrend_with_kernel(&first_input, kernel)?;
        let second_params = EhlersITrendParams {
            warmup_bars: Some(6),
            max_dc_period: Some(25),
        };
        let second_input = EhlersITrendInput::from_slice(&first_result.values, second_params);
        let second_result = ehlers_itrend_with_kernel(&second_input, kernel)?;
        assert_eq!(second_result.values.len(), first_result.values.len());
        if second_result.values.len() > 240 {
            for i in 240..second_result.values.len() {
                assert!(
                    !second_result.values[i].is_nan(),
                    "[{}] NaN found at index {} in EIT result",
                    test_name,
                    i
                );
            }
        }
        Ok(())
    }

    fn check_itrend_nan_handling(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file_path)?;
        let input = EhlersITrendInput::from_candles(
            &candles,
            "close",
            EhlersITrendParams {
                warmup_bars: Some(12),
                max_dc_period: Some(50),
            },
        );
        let result = ehlers_itrend_with_kernel(&input, kernel)?;
        assert_eq!(result.values.len(), candles.close.len());
        if result.values.len() > 240 {
            for i in 240..result.values.len() {
                assert!(
                    !result.values[i].is_nan(),
                    "[{}] NaN found at index {} in EIT result",
                    test_name,
                    i
                );
            }
        }
        Ok(())
    }

    fn check_itrend_streaming(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file_path)?;
        let warmup_bars = 12;
        let max_dc = 50;
        let input = EhlersITrendInput::from_candles(
            &candles,
            "close",
            EhlersITrendParams {
                warmup_bars: Some(warmup_bars),
                max_dc_period: Some(max_dc),
            },
        );
        let batch_output = ehlers_itrend_with_kernel(&input, kernel)?.values;
        let mut stream = EhlersITrendStream::try_new(EhlersITrendParams {
            warmup_bars: Some(warmup_bars),
            max_dc_period: Some(max_dc),
        })?;
        let mut stream_values = Vec::with_capacity(candles.close.len());
        for &price in &candles.close {
            match stream.update(price) {
                Some(val) => stream_values.push(val),
                None => stream_values.push(f64::NAN),
            }
        }
        assert_eq!(batch_output.len(), stream_values.len());
        for (i, (&b, &s)) in batch_output.iter().zip(stream_values.iter()).enumerate() {
            if b.is_nan() && s.is_nan() {
                continue;
            }
            let diff = (b - s).abs();
            assert!(
                diff < 1e-7,
                "[{}] EIT streaming f64 mismatch at idx {}: batch={}, stream={}, diff={}",
                test_name,
                i,
                b,
                s,
                diff
            );
        }
        Ok(())
    }

    fn check_itrend_zero_warmup(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let data = [1.0, 2.0, 3.0];
        let params = EhlersITrendParams {
            warmup_bars: Some(0),
            max_dc_period: Some(10),
        };
        let input = EhlersITrendInput::from_slice(&data, params);
        let res = ehlers_itrend_with_kernel(&input, kernel);
        assert!(matches!(
            res,
            Err(EhlersITrendError::InvalidWarmupBars { .. })
        ));
        Ok(())
    }

    fn check_itrend_invalid_max_dc(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);
        let data = [1.0, 2.0, 3.0];
        let params = EhlersITrendParams {
            warmup_bars: Some(1),
            max_dc_period: Some(0),
        };
        let input = EhlersITrendInput::from_slice(&data, params);
        let res = ehlers_itrend_with_kernel(&input, kernel);
        assert!(matches!(
            res,
            Err(EhlersITrendError::InvalidMaxDcPeriod { .. })
        ));
        Ok(())
    }

    #[cfg(debug_assertions)]
    fn check_itrend_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test_name);

        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let candles = read_candles_from_csv(file_path)?;

        let test_warmup_bars = vec![1, 5, 12, 20, 30];
        let test_max_dc_periods = vec![35, 40, 50, 60];
        let test_sources = vec!["open", "high", "low", "close", "hl2", "hlc3", "ohlc4"];

        for warmup in &test_warmup_bars {
            for max_dc in &test_max_dc_periods {
                if *warmup >= *max_dc {
                    continue;
                }

                for source in &test_sources {
                    let input = EhlersITrendInput::from_candles(
                        &candles,
                        source,
                        EhlersITrendParams {
                            warmup_bars: Some(*warmup),
                            max_dc_period: Some(*max_dc),
                        },
                    );
                    let output = ehlers_itrend_with_kernel(&input, kernel)?;

                    for (i, &val) in output.values.iter().enumerate() {
                        if val.is_nan() {
                            continue;
                        }

                        let bits = val.to_bits();

                        if bits == 0x11111111_11111111 {
                            panic!(
                                "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} with warmup={}, max_dc={}, source={}",
                                test_name, val, bits, i, warmup, max_dc, source
                            );
                        }

                        if bits == 0x22222222_22222222 {
                            panic!(
                                "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} with warmup={}, max_dc={}, source={}",
                                test_name, val, bits, i, warmup, max_dc, source
                            );
                        }

                        if bits == 0x33333333_33333333 {
                            panic!(
                                "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} with warmup={}, max_dc={}, source={}",
                                test_name, val, bits, i, warmup, max_dc, source
                            );
                        }
                    }
                }
            }
        }

        Ok(())
    }

    #[cfg(not(debug_assertions))]
    fn check_itrend_no_poison(_test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
        Ok(())
    }

    #[allow(clippy::float_cmp)]
    fn check_itrend_property(
        test_name: &str,
        kernel: Kernel,
    ) -> Result<(), Box<dyn std::error::Error>> {
        use proptest::prelude::*;
        skip_if_unsupported!(kernel, test_name);

        let strat = (4usize..=15).prop_flat_map(|warmup| {
            ((warmup + 1)..=64).prop_flat_map(move |max_dc| {
                (
                    prop::collection::vec(
                        (-1e6f64..1e6f64).prop_filter("finite", |x| x.is_finite()),
                        (warmup + max_dc + 4)..400,
                    ),
                    Just(warmup),
                    Just(max_dc),
                    (-1e3f64..1e3f64).prop_filter("a≠0", |a| a.is_finite() && *a != 0.0),
                    -1e3f64..1e3f64,
                )
            })
        });

        proptest::test_runner::TestRunner::default()
            .run(&strat, |(data, warmup, max_dc, a, b)| {
                let params = EhlersITrendParams {
                    warmup_bars: Some(warmup),
                    max_dc_period: Some(max_dc),
                };
                let input = EhlersITrendInput::from_slice(&data, params.clone());

                let fast = ehlers_itrend_with_kernel(&input, kernel);
                let slow = ehlers_itrend_with_kernel(&input, Kernel::Scalar);

                match (fast, slow) {
                    (Err(e1), Err(e2))
                        if std::mem::discriminant(&e1) == std::mem::discriminant(&e2) =>
                    {
                        return Ok(())
                    }
                    (Err(e1), Err(e2)) => {
                        prop_assert!(false, "different errors: fast={:?} slow={:?}", e1, e2)
                    }

                    (Err(e), Ok(_)) => {
                        prop_assert!(false, "fast errored {e:?} but scalar succeeded")
                    }
                    (Ok(_), Err(e)) => {
                        prop_assert!(false, "scalar errored {e:?} but fast succeeded")
                    }

                    (Ok(fast), Ok(reference)) => {
                        let EhlersITrendOutput { values: out } = fast;
                        let EhlersITrendOutput { values: rref } = reference;

                        let mut stream = EhlersITrendStream::try_new(params.clone()).unwrap();
                        let mut s_out = Vec::with_capacity(data.len());
                        for &v in &data {
                            s_out.push(stream.update(v).unwrap_or(f64::NAN));
                        }

                        let transformed: Vec<f64> = data.iter().map(|x| a * x + b).collect();
                        let t_out =
                            ehlers_itrend(&EhlersITrendInput::from_slice(&transformed, params))?
                                .values;

                        let first = data.iter().position(|x| !x.is_nan()).unwrap();
                        let warm = first + warmup;
                        for i in warm..data.len() {
                            let y = out[i];
                            let yr = rref[i];
                            let ys = s_out[i];
                            let yt = t_out[i];

                            let start = (i + 1).saturating_sub(max_dc);
                            let look = &data[start..=i];
                            prop_assert!(
                                y.is_nan() || y.is_finite(),
                                "iTrend output at index {} is not finite: {}",
                                i,
                                y
                            );

                            if warmup == 1 && y.is_finite() {
                                prop_assert!((y - data[i]).abs() <= f64::EPSILON);
                            }

                            if look.iter().all(|v| *v == look[0]) {
                                prop_assert!((y - look[0]).abs() <= 1e-7);
                            }

                            let affine_start = warm + max_dc;
                            if i >= affine_start {
                                let expected = a * y + b;
                                let diff = (yt - expected).abs();
                                let tol = 1e-7_f64.max(expected.abs() * 1e-7);
                                let ulp = yt.to_bits().abs_diff(expected.to_bits());
                                prop_assert!(
                                    diff <= tol || ulp <= 8,
                                    "idx {i}: affine mismatch diff={diff:e}  ULP={ulp}"
                                );
                            }

                            let ulp = y.to_bits().abs_diff(yr.to_bits());
                            prop_assert!(
                                (y - yr).abs() <= 1e-7 || ulp <= 4,
                                "idx {i}: fast={y} ref={yr} ULP={ulp}"
                            );

                            prop_assert!(
                                (y - ys).abs() <= 1e-7 || (y.is_nan() && ys.is_nan()),
                                "idx {i}: stream mismatch"
                            );
                        }

                        for j in first..warm {
                            prop_assert!(
                                out[j].is_nan(),
                                "warm-up NaN expected at idx {j}: got {}",
                                out[j]
                            );
                        }
                    }
                }

                Ok(())
            })
            .unwrap();

        assert!(ehlers_itrend(&EhlersITrendInput::from_slice(
            &[],
            EhlersITrendParams::default()
        ))
        .is_err());
        assert!(ehlers_itrend(&EhlersITrendInput::from_slice(
            &[f64::NAN; 12],
            EhlersITrendParams::default()
        ))
        .is_err());
        assert!(ehlers_itrend(&EhlersITrendInput::from_slice(
            &[1.0; 5],
            EhlersITrendParams {
                warmup_bars: Some(8),
                max_dc_period: Some(50)
            }
        ))
        .is_err());
        assert!(ehlers_itrend(&EhlersITrendInput::from_slice(
            &[1.0; 5],
            EhlersITrendParams {
                warmup_bars: Some(0),
                max_dc_period: Some(10)
            }
        ))
        .is_err());

        Ok(())
    }

    macro_rules! generate_all_itrend_tests {
        ($($test_fn:ident),*) => {
            paste::paste! {
                $(
                    #[test]
                    fn [<$test_fn _scalar_f64>]() {
                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
                    }
                    #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
                    #[test]
                    fn [<$test_fn _avx2_f64>]() {
                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
                    }
                    #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
                    #[test]
                    fn [<$test_fn _avx512_f64>]() {
                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
                    }
                )*
            }
        }
    }

    generate_all_itrend_tests!(
        check_itrend_partial_params,
        check_itrend_accuracy,
        check_itrend_default_candles,
        check_itrend_no_data,
        check_itrend_all_nan_data,
        check_itrend_small_data_for_warmup,
        check_itrend_very_small_dataset,
        check_itrend_reinput,
        check_itrend_nan_handling,
        check_itrend_streaming,
        check_itrend_zero_warmup,
        check_itrend_invalid_max_dc,
        check_itrend_property,
        check_itrend_no_poison
    );

    fn check_batch_default_row(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test);
        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let c = read_candles_from_csv(file)?;
        let output = EhlersITrendBatchBuilder::new()
            .kernel(kernel)
            .apply_candles(&c, "close")?;
        let def = EhlersITrendParams::default();
        let row = output.values_for(&def).expect("default row missing");
        assert_eq!(row.len(), c.close.len());
        let expected = [59638.12, 59497.26, 59431.08, 59391.23, 59372.19];
        let start = row.len() - 5;
        for (i, &v) in row[start..].iter().enumerate() {
            assert!(
                (v - expected[i]).abs() < 1e-1,
                "[{test}] default-row mismatch at idx {i}: {v} vs {expected:?}"
            );
        }
        Ok(())
    }

    fn check_batch_invalid_kernel(test: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
        let data = [1.0, 2.0, 3.0];
        let sweep = EhlersITrendBatchRange::default();
        let res = ehlers_itrend_batch_with_kernel(&data, &sweep, Kernel::Scalar);
        assert!(matches!(
            res,
            Err(EhlersITrendError::InvalidKernelForBatch(_))
        ));
        Ok(())
    }

    fn check_batch_invalid_range(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test);
        let data = [1.0, 2.0, 3.0, 4.0];

        let sweep = EhlersITrendBatchRange {
            warmup_bars: (5, 1, 0),
            max_dc_period: (10, 10, 0),
        };
        let res = ehlers_itrend_batch_with_kernel(&data, &sweep, kernel);
        assert!(matches!(res, Err(EhlersITrendError::InvalidRange { .. })));
        Ok(())
    }

    #[cfg(debug_assertions)]
    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
        skip_if_unsupported!(kernel, test);

        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
        let c = read_candles_from_csv(file)?;

        let test_sources = vec!["open", "high", "low", "close", "hl2", "hlc3", "ohlc4"];

        for source in &test_sources {
            let output = EhlersITrendBatchBuilder::new()
                .kernel(kernel)
                .warmup_range(5, 30, 5)
                .max_dc_range(35, 60, 5)
                .apply_candles(&c, source)?;

            for (idx, &val) in output.values.iter().enumerate() {
                if val.is_nan() {
                    continue;
                }

                let bits = val.to_bits();
                let row = idx / output.cols;
                let col = idx % output.cols;

                if bits == 0x11111111_11111111 {
                    panic!(
                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at row {} col {} (flat index {}) with source={}",
                        test, val, bits, row, col, idx, source
                    );
                }

                if bits == 0x22222222_22222222 {
                    panic!(
                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at row {} col {} (flat index {}) with source={}",
                        test, val, bits, row, col, idx, source
                    );
                }

                if bits == 0x33333333_33333333 {
                    panic!(
                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at row {} col {} (flat index {}) with source={}",
                        test, val, bits, row, col, idx, source
                    );
                }
            }
        }

        let edge_case_configs = vec![
            (5, 10, 1, 15, 50, 5),
            (20, 30, 5, 40, 60, 10),
            (5, 15, 2, 20, 40, 3),
        ];

        for (warmup_start, warmup_end, warmup_step, max_dc_start, max_dc_end, max_dc_step) in
            edge_case_configs
        {
            let output = EhlersITrendBatchBuilder::new()
                .kernel(kernel)
                .warmup_range(warmup_start, warmup_end, warmup_step)
                .max_dc_range(max_dc_start, max_dc_end, max_dc_step)
                .apply_candles(&c, "close")?;

            for (idx, &val) in output.values.iter().enumerate() {
                if val.is_nan() {
                    continue;
                }

                let bits = val.to_bits();
                let row = idx / output.cols;
                let col = idx % output.cols;

                if bits == 0x11111111_11111111
                    || bits == 0x22222222_22222222
                    || bits == 0x33333333_33333333
                {
                    panic!(
                        "[{}] Found poison value {} (0x{:016X}) at row {} col {} with warmup_range({},{},{}) and max_dc_range({},{},{})",
                        test, val, bits, row, col, warmup_start, warmup_end, warmup_step, max_dc_start, max_dc_end, max_dc_step
                    );
                }
            }
        }

        Ok(())
    }

    #[cfg(not(debug_assertions))]
    fn check_batch_no_poison(_test: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
        Ok(())
    }

    macro_rules! gen_batch_tests {
        ($fn_name:ident) => {
            paste::paste! {
                #[test]
                fn [<$fn_name _scalar>]() {
                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
                }
                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
                #[test]
                fn [<$fn_name _avx2>]() {
                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
                }
                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
                #[test]
                fn [<$fn_name _avx512>]() {
                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
                }
                #[test]
                fn [<$fn_name _auto_detect>]() {
                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
                }
            }
        };
    }
    gen_batch_tests!(check_batch_default_row);
    gen_batch_tests!(check_batch_invalid_kernel);
    gen_batch_tests!(check_batch_invalid_range);
    gen_batch_tests!(check_batch_no_poison);
}
