//! `Add` — two-input f64 sum (input 0 plus input 1), the companion to `Sub`.
//! Combines two signal streams into one — the most basic combinator for the
//! north-star "combine one signal with another" research move (C10).

use aura_core::{Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, ScalarKind};

/// Two-input f64 sum: input 0 plus input 1. Emits `None` until both inputs
/// have a value.
///
/// # Firing and warm-up
///
/// Both inputs are [`Firing::Any`](aura_core::Firing::Any) — a *mode-A as-of
/// join*: the node fires on every cycle in which either leg is fresh (once both
/// have produced a value), pairing the fresh leg with the held value of the
/// other. Until both legs have a value it emits `None` (no cold-leg-as-`0.0`).
/// With heterogeneous sources sharing a timestamp (same `ts` from two sources =
/// two distinct cycles, C4), a fired node emits one row per *cycle*, so a
/// recorded combined stream may carry more than one row per timestamp.
pub struct Add {
    out: [Cell; 1],
}

impl Add {
    /// Build an `Add` node.
    pub fn new() -> Self {
        Self { out: [Cell::from_f64(0.0)] }
    }

    /// The param-generic recipe for a blueprint primitive: paramless, builds through
    /// `Add::new`.
    pub fn builder() -> PrimitiveBuilder {
        PrimitiveBuilder::new(
            "Add",
            NodeSchema {
                inputs: vec![
                    PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "lhs".into() },
                    PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "rhs".into() },
                ],
                output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
                params: vec![],
            },
            |_| Box::new(Add::new()),
        )
    }
}

impl Default for Add {
    fn default() -> Self {
        Self::new()
    }
}

impl Node for Add {
    fn lookbacks(&self) -> Vec<usize> {
        vec![1, 1]
    }

    fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
        let a = ctx.f64_in(0);
        let b = ctx.f64_in(1);
        if a.is_empty() || b.is_empty() {
            return None;
        }
        self.out[0] = Cell::from_f64(a[0] + b[0]);
        Some(&self.out)
    }

    fn label(&self) -> String {
        "Add".to_string()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use aura_core::{AnyColumn, Scalar, Timestamp};

    #[test]
    fn add_is_sum_once_both_inputs_present() {
        let mut add = Add::new();
        let mut inputs = vec![
            AnyColumn::with_capacity(ScalarKind::F64, 1),
            AnyColumn::with_capacity(ScalarKind::F64, 1),
        ];

        // only input 0 present -> None
        inputs[0].push(Scalar::f64(10.0)).unwrap();
        assert_eq!(add.eval(Ctx::new(&inputs, Timestamp(0))), None);

        // both present -> a + b
        inputs[1].push(Scalar::f64(4.0)).unwrap();
        assert_eq!(add.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(14.0)].as_slice()));
    }

    #[test]
    fn input_slots_are_named_lhs_rhs() {
        let a = Add::builder();
        let names: Vec<&str> = a.schema().inputs.iter().map(|p| p.name.as_str()).collect();
        assert_eq!(names, ["lhs", "rhs"]);
    }
}
