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

//! Core value types — the narrow waist of the viewer.
//!
//! This crate knows nothing about files, threads, rendering, or any GUI
//! toolkit.  Everything above `Curve` consumes curves; everything below
//! produces them.  If a type in here ever grows an `#include <Qt...>`-shaped
//! dependency, the layering has been broken.
//!
//! The one rule that makes GB-scale files work: **no caller may ever ask for
//! a raw sample array.**  `Curve::reduce` is the only data path, and the
//! caller states how many buckets it can actually draw.

/// Stable identity for a curve, unique within a session.
///
/// Deliberately not a name: the same net name occurs in several files when
/// comparing runs or corners, and a curve must stay addressable across a
/// reload.
pub type CurveId = u64;

/// A closed interval on the X axis (time, frequency, sweep parameter, ...).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Span {
    pub x0: f64,
    pub x1: f64,
}

impl Span {
    pub fn new(x0: f64, x1: f64) -> Span {
        Span { x0, x1 }
    }
    pub fn width(&self) -> f64 {
        self.x1 - self.x0
    }
    pub fn contains(&self, x: f64) -> bool {
        x >= self.x0 && x <= self.x1
    }
    pub fn intersect(&self, o: Span) -> Span {
        Span::new(self.x0.max(o.x0), self.x1.min(o.x1))
    }
    pub fn is_empty(&self) -> bool {
        !(self.x1 > self.x0)
    }
}

/// Physical unit of a curve's Y values (or of the X axis).
///
/// Carried in the type system from day one.  Retrofitting units after the
/// fact means touching every measurement, every axis label, and every
/// expression operator.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Unit {
    Second,
    Volt,
    Ampere,
    Hertz,
    Ohm,
    Farad,
    Watt,
    Decibel,
    Degree,
    Ratio,
    Unknown,
}

impl Unit {
    /// Map an ngspice raw-file variable type field.
    pub fn from_raw_type(s: &str) -> Unit {
        match s.trim().to_ascii_lowercase().as_str() {
            "time" => Unit::Second,
            "voltage" => Unit::Volt,
            "current" => Unit::Ampere,
            "frequency" => Unit::Hertz,
            "impedance" => Unit::Ohm,
            "capacitance" => Unit::Farad,
            "power" => Unit::Watt,
            "onoise-spectrum" | "inoise-spectrum" => Unit::Volt,
            _ => Unit::Unknown,
        }
    }

    pub fn symbol(&self) -> &'static str {
        match self {
            Unit::Second => "s",
            Unit::Volt => "V",
            Unit::Ampere => "A",
            Unit::Hertz => "Hz",
            Unit::Ohm => "Ohm",
            Unit::Farad => "F",
            Unit::Watt => "W",
            Unit::Decibel => "dB",
            Unit::Degree => "deg",
            Unit::Ratio => "",
            Unit::Unknown => "?",
        }
    }
}

/// What a stored sample actually is.
///
/// `Complex` exists here on day one even though the first milestone only
/// renders `Real`: an `.ac` raw file is already flagged complex in its
/// header, and widening `f64` to a complex pair later would touch the whole
/// stack.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ValueKind {
    Real,
    Complex,
    Digital,
}

/// How the X axis should be scaled, as declared by the file itself.
///
/// ngspice writes `grid=<GRIDTYPE>` on the X variable; `GRID_XLOG` (3) and
/// `GRID_LOGLOG` (2) mean a log frequency axis.  Guessing this from the data
/// ("the values look logarithmically spaced") is how viewers get `.dc` sweeps
/// wrong, so take what the writer said.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum XScale {
    Linear,
    Log,
}

/// Which projection of a complex sample to display.
///
/// This is a property of the *curve*, not of the stored data.  The same
/// complex variable backs a magnitude curve, a phase curve and a dB curve
/// simultaneously — which is why the reduction must never collapse complex
/// to a scalar at index time.  Doing so discards phase permanently.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Component {
    Magnitude,
    /// 20*log10(|v|)
    Db,
    /// Wrapped phase, degrees, (-180, 180]
    Phase,
    /// Phase accumulated across the sweep.  Cannot be computed from a
    /// windowed view — it depends on every earlier point — which is one more
    /// reason AC data is held exactly rather than reduced.
    PhaseUnwrapped,
    Real,
    Imag,
}

impl Component {
    /// Degrees for phase, matching what a designer expects on a Bode plot.
    pub fn apply(&self, re: f64, im: f64) -> f64 {
        match self {
            Component::Magnitude => re.hypot(im),
            Component::Db => 20.0 * re.hypot(im).log10(),
            Component::Phase | Component::PhaseUnwrapped => im.atan2(re).to_degrees(),
            Component::Real => re,
            Component::Imag => im,
        }
    }

    /// True when `apply` is not monotonic in the sample value, so it may not
    /// be applied to an already-reduced min/max envelope.
    pub fn nonlinear(&self) -> bool {
        !matches!(self, Component::Real | Component::Imag)
    }

    pub fn unit(&self, base: Unit) -> Unit {
        match self {
            Component::Db => Unit::Decibel,
            Component::Phase | Component::PhaseUnwrapped => Unit::Degree,
            _ => base,
        }
    }

    pub fn prefix(&self) -> &'static str {
        match self {
            Component::Magnitude => "mag",
            Component::Db => "db",
            Component::Phase => "ph",
            Component::PhaseUnwrapped => "cph",
            Component::Real => "re",
            Component::Imag => "im",
        }
    }
}

/// The only shape in which curve data is ever handed upward.
///
/// One entry per bucket.  `min`/`max` are `f32` on purpose: they exist to be
/// rasterised, and halving them halves the resident index.  Sample-accurate
/// values come from [`Curve::value_at`], not from here.
#[derive(Clone, Debug, Default)]
pub struct Reduced {
    /// X coordinate at the start of each bucket.
    pub x: Vec<f64>,
    pub min: Vec<f32>,
    pub max: Vec<f32>,
    /// True when each bucket held at most one sample, i.e. `min == max` is
    /// the real sample value rather than an envelope.  Measurements and
    /// cursor readouts must refuse to trust an envelope.
    pub exact: bool,
}

impl Reduced {
    pub fn len(&self) -> usize {
        self.x.len()
    }
    pub fn is_empty(&self) -> bool {
        self.x.is_empty()
    }
    /// Y extent across the whole reduction — what an autoscaling axis wants.
    pub fn y_extent(&self) -> Option<(f32, f32)> {
        if self.is_empty() {
            return None;
        }
        let mut lo = f32::INFINITY;
        let mut hi = f32::NEG_INFINITY;
        for i in 0..self.min.len() {
            if self.min[i] < lo {
                lo = self.min[i];
            }
            if self.max[i] > hi {
                hi = self.max[i];
            }
        }
        if lo.is_finite() && hi.is_finite() {
            Some((lo, hi))
        } else {
            None
        }
    }
}

/// Non-blocking result.
///
/// The UI thread never waits on I/O: it draws what is ready and repaints when
/// the rest arrives.  Making this synchronous now and asynchronous later is a
/// rewrite of every caller, so it is `Poll` from the first line of code.
#[derive(Clone, Debug)]
pub enum Poll<T> {
    Ready(T),
    Pending,
}

impl<T> Poll<T> {
    pub fn ready(self) -> Option<T> {
        match self {
            Poll::Ready(v) => Some(v),
            Poll::Pending => None,
        }
    }
    pub fn is_pending(&self) -> bool {
        matches!(self, Poll::Pending)
    }
}

/// A drawable signal, whatever its provenance.
///
/// File-backed curves, expression results, and digital busses all implement
/// this identically, which is what lets the waveform calculator arrive months
/// after the plotter without touching it.
pub trait Curve: Send + Sync {
    fn id(&self) -> CurveId;
    fn name(&self) -> &str;
    fn unit(&self) -> Unit;
    fn x_unit(&self) -> Unit;
    fn kind(&self) -> ValueKind;

    /// Per-curve X extent.  Explicitly *not* a global time base: overlaid
    /// runs, corners, and different analyses do not share one.
    fn x_span(&self) -> Span;

    /// Declared by the file, not inferred from the samples.
    fn x_scale(&self) -> XScale {
        XScale::Linear
    }

    /// The only bulk data path.  `n_buckets` is a property of the output
    /// device — screen width in pixels, or printer dots — never of the data.
    fn reduce(&self, span: Span, n_buckets: usize) -> Poll<Reduced>;

    /// Sample-accurate value for a cursor readout.  Separate from `reduce`
    /// because a cursor needs the true sample, not a bucket envelope.
    fn value_at(&self, x: f64) -> Poll<Option<f64>>;

    /// Stream every true sample in `span`, in order, to `f`; returns how many
    /// were delivered.
    ///
    /// The third data path, and it exists because measurements need something
    /// neither of the others provides.  `reduce` returns an envelope — a rise
    /// time computed from bucket min/max is not a rise time — and `value_at`
    /// is one point.  A measurement needs every sample and only the samples
    /// in its window.
    ///
    /// Streaming rather than returning a `Vec` on purpose: a full-span
    /// measurement over a 6M-point run would otherwise materialise ~100 MB
    /// that the caller reads once and drops.  Measurements are accumulators;
    /// let them accumulate.
    ///
    /// This is genuinely expensive on a large file — it is the one operation
    /// that must touch real data — so callers should scope it to a cursor
    /// span rather than reaching for it on every repaint.
    fn for_each_sample(&self, span: Span, f: &mut dyn FnMut(f64, f64)) -> Poll<usize>;
}
