//! `When` — the clock-enable driver `f64 × bool -> f64`: forwards `value`
//! verbatim when `gate` is true, and is **quiet** (`eval -> None`, the
//! `Resample` precedent) when it is false.
//!
//! The single clock-enable standard cell of the RTL metaphor (#281): gating
//! belongs in the *stream*, not in per-node gated variants. Because the run
//! loop is freshness-gated, every existing stateful reducer composes gated,
//! unmodified: `SMA(When(x, gate), N)` IS the gated SMA — under
//! `Firing::Any` a consumer holds its last forwarded value across quiet
//! cycles and its state advances per *firing*, not per cycle. A quiet
//! `When` feeding a `Firing::Barrier(N)` group means the group does not
//! fire that cycle — the quiet member is not at the current timestamp,
//! which is the correct reading of the barrier contract (deliberately
//! pinned by an engine test).
//!
//! Two inputs (`value: F64`, `gate: Bool`, each `Firing::Any` — the enable
//! is level-sensitive: the gate *level* is sampled whenever the cell fires,
//! however long ago it was driven). One `f64` output, no params, stateless.
//! Emits `None` while either leg is cold (warm-up), and on every gate-false
//! cycle (the quiet cycle that IS the semantics).

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

/// Clock-enable forwarder: `value` iff `gate`, else quiet (`None`). A false
/// gate is a quiet cycle, not a held or zero output — downstream freshness
/// does not advance.
pub struct When {
    out: [Cell; 1],
}

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

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

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

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

    fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
        let value = ctx.f64_in(0);
        let gate = ctx.bool_in(1);
        if value.is_empty() || gate.is_empty() {
            return None; // cold warm-up (C8) — both legs required
        }
        if !gate[0] {
            return None; // gate low -> QUIET cycle (the semantics, not warm-up)
        }
        self.out[0] = Cell::from_f64(value[0]);
        Some(&self.out)
    }

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

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

    fn gate_inputs() -> Vec<AnyColumn> {
        vec![
            AnyColumn::with_capacity(ScalarKind::F64, 1),
            AnyColumn::with_capacity(ScalarKind::Bool, 1),
        ]
    }

    #[test]
    fn when_forwards_on_true_and_is_quiet_on_false() {
        // PROPERTY: gate true -> the value passes through verbatim (bit-equal);
        // gate false -> a QUIET cycle (None), never a held or zero output.
        let mut node = When::new();
        let mut inputs = gate_inputs();

        inputs[0].push(Scalar::f64(42.5)).unwrap();
        inputs[1].push(Scalar::bool(true)).unwrap();
        assert_eq!(
            node.eval(Ctx::new(&inputs, Timestamp(0))),
            Some([Cell::from_f64(42.5)].as_slice())
        );

        inputs[0].push(Scalar::f64(7.0)).unwrap();
        inputs[1].push(Scalar::bool(false)).unwrap();
        assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None);

        inputs[0].push(Scalar::f64(-3.25)).unwrap();
        inputs[1].push(Scalar::bool(true)).unwrap();
        assert_eq!(
            node.eval(Ctx::new(&inputs, Timestamp(0))),
            Some([Cell::from_f64(-3.25)].as_slice())
        );
    }

    #[test]
    fn when_is_none_while_either_leg_is_cold() {
        // Cold warm-up is distinct from the quiet cycle: before both legs have
        // seen a value the cell is cold (C8), regardless of the gate.
        let mut node = When::new();
        let mut inputs = gate_inputs();
        assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None); // both cold
        inputs[0].push(Scalar::f64(1.0)).unwrap();
        assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None); // gate cold
    }

    #[test]
    fn input_slots_are_named_value_gate() {
        let names: Vec<String> =
            When::builder().schema().inputs.iter().map(|p| p.name.clone()).collect();
        assert_eq!(names, ["value", "gate"]);
    }
}
