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

//! The calculator seam, proved with one operator and no parser.
//!
//! There is deliberately no expression grammar here yet.  The question this
//! crate answers is narrower and far more urgent: *can a derived curve be
//! indistinguishable from a file-backed one?*  If it can, the real calculator
//! — which is over half the code in a mature waveform viewer — attaches later
//! without touching the plotter.  If it cannot, better to find out in week
//! three than in month six.
//!
//! The subtlety it exposes: a derived curve consumes **reduced** data, not
//! samples, so it must do interval arithmetic.  `a - b` over buckets is
//! `[a.min - b.max, a.max - b.min]`, not `[a.min - b.min, a.max - b.max]`.
//! Envelopes are therefore conservative when zoomed out and tighten to the
//! exact trace as you zoom in — which is correct behaviour, and is why the
//! calculator has to sit on the same reduction API rather than beside it.

use std::sync::Arc;

use wave_core::{Curve, CurveId, Poll, Reduced, Span, Unit, ValueKind};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Op {
    Add,
    Sub,
    Mul,
    Div,
}

impl Op {
    fn symbol(&self) -> &'static str {
        match self {
            Op::Add => "+",
            Op::Sub => "-",
            Op::Mul => "*",
            Op::Div => "/",
        }
    }

    fn apply(&self, a: f64, b: f64) -> f64 {
        match self {
            Op::Add => a + b,
            Op::Sub => a - b,
            Op::Mul => a * b,
            Op::Div => a / b,
        }
    }

    /// Interval arithmetic over two bucket envelopes.
    fn interval(&self, a: [f32; 2], b: [f32; 2]) -> [f32; 2] {
        match self {
            Op::Add => [a[0] + b[0], a[1] + b[1]],
            Op::Sub => [a[0] - b[1], a[1] - b[0]],
            Op::Mul => {
                let c = [a[0] * b[0], a[0] * b[1], a[1] * b[0], a[1] * b[1]];
                [fmin4(&c), fmax4(&c)]
            }
            Op::Div => {
                // A denominator interval spanning zero has no finite bound.
                if b[0] <= 0.0 && b[1] >= 0.0 {
                    [f32::NAN, f32::NAN]
                } else {
                    let c = [a[0] / b[0], a[0] / b[1], a[1] / b[0], a[1] / b[1]];
                    [fmin4(&c), fmax4(&c)]
                }
            }
        }
    }
}

/// Unit algebra.  Cheap here, and it stops `v(a) - i(b)` from silently
/// producing a plausible-looking trace with a meaningless axis label.
fn combine_units(op: Op, a: Unit, b: Unit) -> Unit {
    match op {
        Op::Add | Op::Sub => {
            if a == b {
                a
            } else {
                Unit::Unknown
            }
        }
        Op::Mul => match (a, b) {
            (Unit::Volt, Unit::Ampere) | (Unit::Ampere, Unit::Volt) => Unit::Watt,
            _ => Unit::Unknown,
        },
        Op::Div => match (a, b) {
            (Unit::Volt, Unit::Ampere) => Unit::Ohm,
            (x, y) if x == y => Unit::Ratio,
            _ => Unit::Unknown,
        },
    }
}

pub struct BinOpCurve {
    id: CurveId,
    name: String,
    unit: Unit,
    op: Op,
    a: Arc<dyn Curve>,
    b: Arc<dyn Curve>,
}

impl BinOpCurve {
    pub fn new(op: Op, a: Arc<dyn Curve>, b: Arc<dyn Curve>) -> BinOpCurve {
        let name = format!("{} {} {}", a.name(), op.symbol(), b.name());
        let unit = combine_units(op, a.unit(), b.unit());
        // Derived from operand identities, so it survives a reload.
        let id = a
            .id()
            .rotate_left(17)
            ^ b.id().wrapping_mul(0x9E3779B97F4A7C15)
            ^ (op as u64).wrapping_add(0xD1B54A32D192ED03);
        BinOpCurve {
            id,
            name,
            unit,
            op,
            a,
            b,
        }
    }
}

impl Curve for BinOpCurve {
    fn id(&self) -> CurveId {
        self.id
    }
    fn name(&self) -> &str {
        &self.name
    }
    fn unit(&self) -> Unit {
        self.unit
    }
    fn x_unit(&self) -> Unit {
        self.a.x_unit()
    }
    fn kind(&self) -> ValueKind {
        ValueKind::Real
    }
    fn x_span(&self) -> Span {
        self.a.x_span().intersect(self.b.x_span())
    }

    fn reduce(&self, span: Span, n_buckets: usize) -> Poll<Reduced> {
        let ra = match self.a.reduce(span, n_buckets) {
            Poll::Ready(r) => r,
            Poll::Pending => return Poll::Pending,
        };
        let rb = match self.b.reduce(span, n_buckets) {
            Poll::Ready(r) => r,
            Poll::Pending => return Poll::Pending,
        };
        // Operands sharing a file share bucket boundaries.  Across files they
        // do not, and resampling one onto the other's X grid is the next
        // piece of work — flagged rather than faked.
        let n = ra.len().min(rb.len());
        let mut out = Reduced {
            x: ra.x[..n].to_vec(),
            min: Vec::with_capacity(n),
            max: Vec::with_capacity(n),
            exact: ra.exact && rb.exact,
        };
        for i in 0..n {
            let iv = self
                .op
                .interval([ra.min[i], ra.max[i]], [rb.min[i], rb.max[i]]);
            out.min.push(iv[0]);
            out.max.push(iv[1]);
        }
        Poll::Ready(out)
    }

    fn value_at(&self, x: f64) -> Poll<Option<f64>> {
        // Sample-accurate on both sides, so no interval widening here.
        let a = match self.a.value_at(x) {
            Poll::Ready(v) => v,
            Poll::Pending => return Poll::Pending,
        };
        let b = match self.b.value_at(x) {
            Poll::Ready(v) => v,
            Poll::Pending => return Poll::Pending,
        };
        Poll::Ready(match (a, b) {
            (Some(a), Some(b)) => Some(self.op.apply(a, b)),
            _ => None,
        })
    }

    /// Exact samples of a derived curve.
    ///
    /// Buffers only the right-hand operand and walks the left one against it,
    /// interpolating where the two do not share an X grid — which is the
    /// normal case across two different runs.  Interval arithmetic is not
    /// involved here: these are real samples on both sides, so the result is
    /// the true difference, not an envelope.
    fn for_each_sample(&self, span: Span, f: &mut dyn FnMut(f64, f64)) -> Poll<usize> {
        let mut rhs: Vec<(f64, f64)> = Vec::new();
        if self.b.for_each_sample(span, &mut |x, v| rhs.push((x, v))).is_pending() {
            return Poll::Pending;
        }
        if rhs.is_empty() {
            return Poll::Ready(0);
        }
        let mut j = 0usize;
        let mut n = 0usize;
        let op = self.op;
        let res = self.a.for_each_sample(span, &mut |x, va| {
            while j + 1 < rhs.len() && rhs[j + 1].0 <= x {
                j += 1;
            }
            let vb = if rhs[j].0 == x || j + 1 >= rhs.len() {
                rhs[j].1
            } else {
                let (x0, y0) = rhs[j];
                let (x1, y1) = rhs[j + 1];
                if x1 > x0 {
                    y0 + (y1 - y0) * (x - x0) / (x1 - x0)
                } else {
                    y0
                }
            };
            f(x, op.apply(va, vb));
            n += 1;
        });
        if res.is_pending() {
            return Poll::Pending;
        }
        Poll::Ready(n)
    }
}

fn fmin4(v: &[f32; 4]) -> f32 {
    v.iter().fold(f32::INFINITY, |m, x| if *x < m { *x } else { m })
}
fn fmax4(v: &[f32; 4]) -> f32 {
    v.iter()
        .fold(f32::NEG_INFINITY, |m, x| if *x > m { *x } else { m })
}
