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

//! Exact storage for complex (`.ac`) data.
//!
//! A different strategy from the transient path, chosen by the file itself,
//! because the two regimes are genuinely different problems:
//!
//! | | transient | AC |
//! |---|---|---|
//! | size | gigabytes | a few hundred KB to a few MB |
//! | what you plot | the stored scalar | one of *several* projections |
//!
//! That second row is the decisive one. A complex sample backs a magnitude
//! curve, a phase curve, a dB curve and a real/imag pair *at the same time*.
//! Collapsing it to `hypot(re, im)` when the index is built — which is what
//! this code did in its first version — throws phase away permanently and
//! leaves the viewer unable to draw a Bode plot.
//!
//! Reducing first is also mathematically wrong for these projections. The
//! min/max pyramid is lossless only for monotone functions of the sample;
//! `|v|` and `20log10|v|` are not, so `min(|v|) != |min(v)|` whenever the
//! bucket straddles zero. Computing the component from exact samples and
//! reducing afterwards is correct by construction.
//!
//! Both arguments point the same way, and the small file size makes it free.

use std::sync::Arc;

use wave_core::{Component, Curve, CurveId, Poll, Reduced, Span, Unit, ValueKind, XScale};
use wave_io::WaveReader;

/// Refuse to hold more than this exactly. An `.ac` sweep this large is
/// pathological, but failing loudly beats silently exhausting memory.
pub const MAX_EXACT_BYTES: usize = 512 << 20;

pub struct ComplexStore {
    n_points: usize,
    n_vars: usize,
    x_var: usize,
    /// Row-major, re/im interleaved: `data[row * 2 * n_vars + var * 2 (+1)]`.
    data: Vec<f64>,
    /// Per variable, phase in degrees accumulated across the sweep.
    /// Precomputed because unwrapping is a global operation — it cannot be
    /// derived from a windowed view without changing at the window edge.
    unwrapped: Vec<Vec<f64>>,
    x_scale: XScale,
}

impl ComplexStore {
    pub fn build(reader: &Arc<dyn WaveReader>) -> std::io::Result<ComplexStore> {
        let n_points = reader.n_points();
        let n_vars = reader.n_vars();
        let rl = reader.row_len();
        let bytes = n_points.saturating_mul(rl).saturating_mul(8);
        if bytes > MAX_EXACT_BYTES {
            return Err(std::io::Error::new(
                std::io::ErrorKind::Other,
                format!(
                    "complex plot needs {} MB held exactly, over the {} MB limit",
                    bytes >> 20,
                    MAX_EXACT_BYTES >> 20
                ),
            ));
        }

        let mut data = vec![0f64; n_points * rl];
        let block = 8192usize;
        let mut row = 0usize;
        while row < n_points {
            let want = block.min(n_points - row);
            let got = reader.read_rows(row, want, &mut data[row * rl..])?;
            if got == 0 {
                break;
            }
            row += got;
        }

        let mut unwrapped = Vec::with_capacity(n_vars);
        for v in 0..n_vars {
            let mut phases = Vec::with_capacity(n_points);
            for r in 0..n_points {
                let re = data[r * rl + v * 2];
                let im = data[r * rl + v * 2 + 1];
                phases.push(im.atan2(re).to_degrees());
            }
            unwrapped.push(unwrap_degrees(&phases));
        }

        Ok(ComplexStore {
            n_points,
            n_vars,
            x_var: 0,
            data,
            unwrapped,
            x_scale: reader.x_scale(),
        })
    }

    pub fn n_points(&self) -> usize {
        self.n_points
    }
    pub fn n_vars(&self) -> usize {
        self.n_vars
    }
    pub fn x_scale(&self) -> XScale {
        self.x_scale
    }
    pub fn size_bytes(&self) -> usize {
        self.data.len() * 8 + self.unwrapped.iter().map(|u| u.len() * 8).sum::<usize>()
    }

    #[inline]
    pub fn x(&self, row: usize) -> f64 {
        self.data[row * self.n_vars * 2 + self.x_var * 2]
    }

    #[inline]
    pub fn value(&self, var: usize, row: usize, comp: Component) -> f64 {
        if comp == Component::PhaseUnwrapped {
            return self.unwrapped[var][row];
        }
        let base = row * self.n_vars * 2 + var * 2;
        comp.apply(self.data[base], self.data[base + 1])
    }

    pub fn x_span(&self) -> Span {
        if self.n_points == 0 {
            return Span::new(0.0, 0.0);
        }
        Span::new(self.x(0), self.x(self.n_points - 1))
    }

    /// Rows within `span`. Frequency is monotonic, so a binary search works
    /// for both linear and log sweeps.
    pub fn row_range(&self, span: Span) -> (usize, usize) {
        if self.n_points == 0 {
            return (0, 0);
        }
        let mut lo = 0usize;
        let mut hi = self.n_points;
        while lo < hi {
            let m = (lo + hi) / 2;
            if self.x(m) < span.x0 {
                lo = m + 1;
            } else {
                hi = m;
            }
        }
        let r0 = lo.saturating_sub(1);
        let mut lo2 = r0;
        let mut hi2 = self.n_points;
        while lo2 < hi2 {
            let m = (lo2 + hi2) / 2;
            if self.x(m) <= span.x1 {
                lo2 = m + 1;
            } else {
                hi2 = m;
            }
        }
        let r1 = (lo2 + 1).min(self.n_points);
        (r0, r1.max(r0))
    }

    pub fn reduce(&self, var: usize, comp: Component, span: Span, n_buckets: usize) -> Reduced {
        let (r0, r1) = self.row_range(span);
        let rows = r1.saturating_sub(r0);
        if rows == 0 || var >= self.n_vars {
            return Reduced::default();
        }
        let n_out = n_buckets.max(1).min(rows);
        let mut out = Reduced {
            x: Vec::with_capacity(n_out),
            min: Vec::with_capacity(n_out),
            max: Vec::with_capacity(n_out),
            // Always computed from real samples, never from an envelope of an
            // envelope — so this is exact whenever one sample lands per bucket.
            exact: rows <= n_out,
        };
        for i in 0..n_out {
            let s = r0 + rows * i / n_out;
            let mut e = r0 + rows * (i + 1) / n_out;
            if e <= s {
                e = s + 1;
            }
            let mut mn = f32::INFINITY;
            let mut mx = f32::NEG_INFINITY;
            for r in s..e.min(r1) {
                let v = self.value(var, r, comp) as f32;
                if v < mn {
                    mn = v;
                }
                if v > mx {
                    mx = v;
                }
            }
            out.x.push(self.x(s));
            out.min.push(mn);
            out.max.push(mx);
        }
        out
    }

    pub fn value_at(&self, var: usize, comp: Component, x: f64) -> Option<f64> {
        if self.n_points == 0 {
            return None;
        }
        let (r0, r1) = self.row_range(Span::new(x, x));
        let mut best = None;
        let mut best_d = f64::INFINITY;
        for r in r0..r1.max(r0 + 1).min(self.n_points) {
            let d = (self.x(r) - x).abs();
            if d < best_d {
                best_d = d;
                best = Some(self.value(var, r, comp));
            }
        }
        best
    }
}

/// Accumulate phase across ±180° wraps.
fn unwrap_degrees(phases: &[f64]) -> Vec<f64> {
    let mut out = Vec::with_capacity(phases.len());
    let mut offset = 0.0f64;
    let mut prev = 0.0f64;
    for (i, &p) in phases.iter().enumerate() {
        if i > 0 {
            let d = p - prev;
            if d > 180.0 {
                offset -= 360.0;
            } else if d < -180.0 {
                offset += 360.0;
            }
        }
        out.push(p + offset);
        prev = p;
    }
    out
}

/// One projection of one complex variable.
///
/// Five of these can share a single `ComplexStore` with no extra storage —
/// which is the whole point.
pub struct ComplexCurve {
    id: CurveId,
    name: String,
    unit: Unit,
    x_unit: Unit,
    var: usize,
    comp: Component,
    store: Arc<ComplexStore>,
}

impl ComplexCurve {
    pub fn new(
        id: CurveId,
        base_name: &str,
        base_unit: Unit,
        x_unit: Unit,
        var: usize,
        comp: Component,
        store: Arc<ComplexStore>,
    ) -> ComplexCurve {
        ComplexCurve {
            id,
            name: format!("{}({})", comp.prefix(), base_name),
            unit: comp.unit(base_unit),
            x_unit,
            var,
            comp,
            store,
        }
    }

    pub fn component(&self) -> Component {
        self.comp
    }
}

impl Curve for ComplexCurve {
    fn id(&self) -> CurveId {
        self.id
    }
    fn name(&self) -> &str {
        &self.name
    }
    fn unit(&self) -> Unit {
        self.unit
    }
    fn x_unit(&self) -> Unit {
        self.x_unit
    }
    fn kind(&self) -> ValueKind {
        ValueKind::Complex
    }
    fn x_span(&self) -> Span {
        self.store.x_span()
    }
    fn x_scale(&self) -> XScale {
        self.store.x_scale()
    }
    fn reduce(&self, span: Span, n_buckets: usize) -> Poll<Reduced> {
        Poll::Ready(self.store.reduce(self.var, self.comp, span, n_buckets))
    }
    fn value_at(&self, x: f64) -> Poll<Option<f64>> {
        Poll::Ready(self.store.value_at(self.var, self.comp, x))
    }

    fn for_each_sample(&self, span: Span, f: &mut dyn FnMut(f64, f64)) -> Poll<usize> {
        let (r0, r1) = self.store.row_range(span);
        let mut n = 0usize;
        for r in r0..r1.min(self.store.n_points()) {
            let x = self.store.x(r);
            if x < span.x0 || x > span.x1 {
                continue;
            }
            f(x, self.store.value(self.var, r, self.comp));
            n += 1;
        }
        Poll::Ready(n)
    }
}
