// Copyright (C) 2026 alto555
// SPDX-License-Identifier: GPL-2.0-only

//! Measurements. Depends on `wave-core` and nothing else.
//!
//! Every measurement here reads through [`Curve::for_each_sample`], never
//! through `reduce`.  That distinction is the whole reason the third data
//! path exists: a rise time computed from bucket min/max is not a rise time,
//! it is a rise time of the envelope, and it would be wrong by exactly as
//! much as the zoom level happens to decimate.  Measurements must see real
//! samples or they must refuse.
//!
//! Everything streams.  A measurement over a 6M-point span accumulates in
//! constant memory rather than materialising the samples.

use wave_core::{Curve, Poll, Span};

/// Summary statistics over a span — the cursor-range readout.
#[derive(Clone, Copy, Debug, Default)]
pub struct Stats {
    pub n: usize,
    pub min: f64,
    pub max: f64,
    pub x_at_min: f64,
    pub x_at_max: f64,
    pub mean: f64,
    pub rms: f64,
    /// Trapezoidal integral over x — charge, energy, or area depending on unit.
    pub area: f64,
    pub x0: f64,
    pub x1: f64,
}

impl Stats {
    pub fn peak_to_peak(&self) -> f64 {
        self.max - self.min
    }
}

/// Time-weighted, not sample-weighted.
///
/// SPICE timesteps are not uniform: a transient run puts a hundred points on
/// an edge and a handful across a quiet microsecond.  A plain sample average
/// would therefore weight the edges by two orders of magnitude too much and
/// report a mean that no oscilloscope would agree with.
pub fn stats(curve: &dyn Curve, span: Span) -> Option<Stats> {
    let mut s = Stats {
        min: f64::INFINITY,
        max: f64::NEG_INFINITY,
        x0: span.x0,
        x1: span.x1,
        ..Default::default()
    };
    let mut prev: Option<(f64, f64)> = None;
    let mut sum_v = 0.0f64;
    let mut sum_v2 = 0.0f64;
    let mut total_dx = 0.0f64;

    let got = curve.for_each_sample(span, &mut |x, v| {
        if !v.is_finite() {
            return;
        }
        s.n += 1;
        if v < s.min {
            s.min = v;
            s.x_at_min = x;
        }
        if v > s.max {
            s.max = v;
            s.x_at_max = x;
        }
        if let Some((px, pv)) = prev {
            let dx = x - px;
            if dx > 0.0 {
                s.area += 0.5 * (v + pv) * dx;
                sum_v += 0.5 * (v + pv) * dx;
                sum_v2 += 0.5 * (v * v + pv * pv) * dx;
                total_dx += dx;
            }
        }
        prev = Some((x, v));
    });
    let n = match got {
        Poll::Ready(n) => n,
        Poll::Pending => return None,
    };
    if n == 0 {
        return None;
    }
    if total_dx > 0.0 {
        s.mean = sum_v / total_dx;
        s.rms = (sum_v2 / total_dx).max(0.0).sqrt();
    } else {
        s.mean = s.min;
        s.rms = s.min.abs();
    }
    Some(s)
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Edge {
    pub x: f64,
    pub rising: bool,
}

/// Schmitt-triggered threshold crossings with linear interpolation.
///
/// Two levels rather than one: a single threshold on a noisy or ringing
/// signal reports a burst of edges where a designer sees one.  A crossing is
/// only emitted after the signal has committed to the opposite band.
pub fn edges(curve: &dyn Curve, span: Span, lo: f64, hi: f64) -> Option<Vec<Edge>> {
    let mut out: Vec<Edge> = Vec::new();
    let mut prev: Option<(f64, f64)> = None;
    // None until the signal has been in one of the bands.
    let mut armed_high: Option<bool> = None;

    let got = curve.for_each_sample(span, &mut |x, v| {
        if !v.is_finite() {
            return;
        }
        if let Some((px, pv)) = prev {
            match armed_high {
                Some(false) if v >= hi => {
                    out.push(Edge {
                        x: interp(px, pv, x, v, hi),
                        rising: true,
                    });
                    armed_high = Some(true);
                }
                Some(true) if v <= lo => {
                    out.push(Edge {
                        x: interp(px, pv, x, v, lo),
                        rising: false,
                    });
                    armed_high = Some(false);
                }
                None => {
                    if v >= hi {
                        armed_high = Some(true);
                    } else if v <= lo {
                        armed_high = Some(false);
                    }
                }
                _ => {}
            }
        } else if v >= hi {
            armed_high = Some(true);
        } else if v <= lo {
            armed_high = Some(false);
        }
        prev = Some((x, v));
    });
    if got.is_pending() {
        return None;
    }
    Some(out)
}

fn interp(x0: f64, y0: f64, x1: f64, y1: f64, target: f64) -> f64 {
    if (y1 - y0).abs() < f64::MIN_POSITIVE {
        x1
    } else {
        x0 + (x1 - x0) * (target - y0) / (y1 - y0)
    }
}

/// Steady low/high levels of a swinging signal.
#[derive(Clone, Copy, Debug)]
pub struct Levels {
    pub low: f64,
    pub high: f64,
}

impl Levels {
    pub fn at(&self, pct: f64) -> f64 {
        self.low + (self.high - self.low) * pct / 100.0
    }
}

/// Levels from the extremes of the span.
///
/// Crude on a signal that overshoots — a proper implementation uses a
/// bimodal histogram so that ringing does not inflate `high` — but honest,
/// and the overshoot measurement below reports exactly that inflation.
pub fn levels(curve: &dyn Curve, span: Span) -> Option<Levels> {
    let s = stats(curve, span)?;
    Some(Levels {
        low: s.min,
        high: s.max,
    })
}

#[derive(Clone, Copy, Debug)]
pub struct Transition {
    /// Time from the low threshold to the high threshold.
    pub duration: f64,
    pub start: f64,
    pub end: f64,
    pub rising: bool,
}

/// First rising (or falling) transition in the span, measured between
/// `lo_pct` and `hi_pct` of the signal's own swing.
pub fn transition(
    curve: &dyn Curve,
    span: Span,
    rising: bool,
    lo_pct: f64,
    hi_pct: f64,
) -> Option<Transition> {
    let lv = levels(curve, span)?;
    if !(lv.high > lv.low) {
        return None;
    }
    let tl = lv.at(lo_pct);
    let th = lv.at(hi_pct);

    let mut prev: Option<(f64, f64)> = None;
    let mut t_lo: Option<f64> = None;
    let mut t_hi: Option<f64> = None;
    let got = curve.for_each_sample(span, &mut |x, v| {
        if t_hi.is_some() || !v.is_finite() {
            return;
        }
        if let Some((px, pv)) = prev {
            if rising {
                if t_lo.is_none() && pv < tl && v >= tl {
                    t_lo = Some(interp(px, pv, x, v, tl));
                }
                if t_lo.is_some() && pv < th && v >= th {
                    t_hi = Some(interp(px, pv, x, v, th));
                }
                // Fell back below before completing: not a transition.
                if t_lo.is_some() && t_hi.is_none() && v < lv.at(0.0) + (tl - lv.low) * 0.5 {
                    t_lo = None;
                }
            } else {
                if t_lo.is_none() && pv > th && v <= th {
                    t_lo = Some(interp(px, pv, x, v, th));
                }
                if t_lo.is_some() && pv > tl && v <= tl {
                    t_hi = Some(interp(px, pv, x, v, tl));
                }
            }
        }
        prev = Some((x, v));
    });
    if got.is_pending() {
        return None;
    }
    match (t_lo, t_hi) {
        (Some(a), Some(b)) if b >= a => Some(Transition {
            duration: b - a,
            start: a,
            end: b,
            rising,
        }),
        _ => None,
    }
}

#[derive(Clone, Copy, Debug)]
pub struct Periodic {
    pub period: f64,
    pub frequency: f64,
    pub duty: f64,
    pub cycles: usize,
}

/// Period, frequency and duty cycle from mid-level crossings.
///
/// Averaged over every complete cycle in the span rather than taken from the
/// first one, so a single jittery edge does not set the answer.
pub fn periodic(curve: &dyn Curve, span: Span) -> Option<Periodic> {
    let lv = levels(curve, span)?;
    periodic_with(curve, span, lv)
}

/// As [`periodic`], but reusing levels the caller already computed.
///
/// `levels` is itself a full pass over the samples, so a caller that has
/// already run [`stats`] would otherwise pay for the same traversal twice —
/// which on a large span is hundreds of milliseconds of frozen UI for nothing.
pub fn periodic_with(curve: &dyn Curve, span: Span, lv: Levels) -> Option<Periodic> {
    if !(lv.high > lv.low) {
        return None;
    }
    let swing = lv.high - lv.low;
    let e = edges(curve, span, lv.at(40.0), lv.at(60.0))?;
    if swing <= 0.0 || e.len() < 3 {
        return None;
    }
    let rises: Vec<f64> = e.iter().filter(|x| x.rising).map(|x| x.x).collect();
    if rises.len() < 2 {
        return None;
    }
    let period = (rises[rises.len() - 1] - rises[0]) / (rises.len() - 1) as f64;

    // High time: from each rise to the next fall.
    let mut high_total = 0.0;
    let mut cycles = 0usize;
    for w in e.windows(2) {
        if w[0].rising && !w[1].rising {
            high_total += w[1].x - w[0].x;
            cycles += 1;
        }
    }
    Some(Periodic {
        period,
        frequency: if period > 0.0 { 1.0 / period } else { 0.0 },
        duty: if cycles > 0 && period > 0.0 {
            100.0 * (high_total / cycles as f64) / period
        } else {
            0.0
        },
        cycles: rises.len() - 1,
    })
}

/// Delay from a's crossing to b's next crossing — `.meas trig ... targ ...`.
pub fn delay(
    a: &dyn Curve,
    b: &dyn Curve,
    span: Span,
    a_thresh: f64,
    a_rising: bool,
    b_thresh: f64,
    b_rising: bool,
) -> Option<f64> {
    let ea = crossings(a, span, a_thresh, a_rising)?;
    let eb = crossings(b, span, b_thresh, b_rising)?;
    let t0 = *ea.first()?;
    let t1 = *eb.iter().find(|t| **t >= t0)?;
    Some(t1 - t0)
}

/// Plain interpolated crossings of one threshold in one direction.
pub fn crossings(curve: &dyn Curve, span: Span, thresh: f64, rising: bool) -> Option<Vec<f64>> {
    let mut out = Vec::new();
    let mut prev: Option<(f64, f64)> = None;
    let got = curve.for_each_sample(span, &mut |x, v| {
        if !v.is_finite() {
            return;
        }
        if let Some((px, pv)) = prev {
            let hit = if rising {
                pv < thresh && v >= thresh
            } else {
                pv > thresh && v <= thresh
            };
            if hit {
                out.push(interp(px, pv, x, v, thresh));
            }
        }
        prev = Some((x, v));
    });
    if got.is_pending() {
        return None;
    }
    Some(out)
}

/// Overshoot past the settled level, as a percentage of the swing.
pub fn overshoot(curve: &dyn Curve, span: Span, settled: f64, swing: f64) -> Option<f64> {
    let s = stats(curve, span)?;
    if swing == 0.0 {
        return None;
    }
    Some(100.0 * (s.max - settled) / swing)
}
