//! `Delay` — the C5 register: outputs the value its single input had `lag` fired
//! cycles ago (`prevHigh15` in the session-breakout strategy is `Delay[1]` on
//! `high15`).
//!
//! The first **state** node. The lag history lives in a node-owned ring of length
//! `lag` (a `Box<[f64]>` plus a `pos` write cursor and a `count` warm-up counter —
//! the same shape and roles as `Sma`'s `ring`/`pos`/`count`), so `lookbacks()` is
//! `1`: the input column is depth-1 and the past is held in node state, NOT read
//! back out of the input column (that would be the input-column-lookback design,
//! fork B; this is the node-owned-ring design, fork A).
//!
//! Why node-owned: the output at cycle T is then a pure function of state captured
//! BEFORE T — the slot the next push will overwrite holds the lag-ago value, read
//! out before the new sample lands. That precondition is what lets a future engine
//! exclude a delay's in-edge from the topo sort and close a feedback loop (the
//! RTL register C5 names): a delay's output never depends on the current cycle's
//! upstream, only on history.
//!
//! Warm-up is **skip-emit**: `None` until `lag` samples have passed, because the
//! lag-ago value does not exist yet — a `Delay[1]` has no previous bar on its
//! first cycle. It NEVER fabricates a sentinel past value; a downstream comparator
//! must not be handed a bar that never streamed.

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

/// The C5 lag-N register: emits the value its `series` input carried `lag` fired
/// cycles ago, held in a node-owned ring. `None` during warm-up (the first `lag`
/// cycles, when the lag-ago value does not yet exist).
pub struct Delay {
    lag: usize,
    // The lag history, node-owned (so the input column is depth-1): `ring[pos]` is
    // the value `lag` cycles ago — the one this cycle emits and the next push
    // evicts. Sized once at construction (C7).
    ring: Box<[f64]>,
    pos: usize,
    // Samples seen so far — the warm-up gate (silent until `lag`, like `Sma`).
    count: usize,
    out: [Cell; 1],
}

impl Delay {
    /// Build a lag-`lag` register (must be >= 1: a zero-lag register is the
    /// identity and is forbidden — mirror `Sma::new`'s `length >= 1`).
    pub fn new(lag: usize) -> Self {
        assert!(lag >= 1, "Delay lag must be >= 1");
        Self {
            lag,
            ring: vec![0.0; lag].into_boxed_slice(),
            pos: 0,
            count: 0,
            out: [Cell::from_f64(0.0)],
        }
    }

    /// The param-generic recipe for a blueprint primitive: declares `lag` and
    /// builds through `Delay::new` (the slice is kind-checked before `build` runs,
    /// so the typed read is total).
    pub fn builder() -> PrimitiveBuilder {
        PrimitiveBuilder::new(
            "Delay",
            NodeSchema {
                inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "series".into() }],
                output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
                params: vec![ParamSpec { name: "lag".into(), kind: ScalarKind::I64 }],
            },
            |p| Box::new(Delay::new(p[0].i64() as usize)),
        )
    }
}

impl Node for Delay {
    // The lag history lives in node state, so only the newest sample is read each
    // cycle — `lag` sizes the ring, not the input column (like `Sma`).
    fn lookbacks(&self) -> Vec<usize> {
        vec![1]
    }

    fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
        let w = ctx.f64_in(0);
        if w.is_empty() {
            return None; // empty-input cycle: no sample to register
        }
        let x = w[0]; // index 0 = newest (financial indexing)

        // `ring[pos]` is the value `lag` cycles ago — read it OUT before the new
        // sample overwrites it (so the output is a pure function of pre-cycle state,
        // never the cycle's own upstream — the C5 feedback-loop precondition).
        let lagged = self.ring[self.pos];
        self.ring[self.pos] = x;
        self.pos = (self.pos + 1) % self.lag;
        if self.count < self.lag {
            self.count += 1;
            return None; // warm-up: the lag-ago value does not exist yet (skip-emit)
        }

        self.out[0] = Cell::from_f64(lagged);
        Some(&self.out)
    }

    fn label(&self) -> String {
        format!("Delay({})", self.lag)
    }
}

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

    #[test]
    fn delay_one_emits_the_immediately_prior_value() {
        // The headline property (this is `prevHigh15` = Delay[1] on high15): each
        // fire emits the value the input carried the cycle before, and the first
        // cycle is None — there is no previous bar yet (warm-up skip-emit, never a
        // fabricated sentinel).
        let node_for_depth = Delay::new(1);

        // size the input column from the node's lookback, as bootstrap will at wiring
        let mut inputs =
            vec![AnyColumn::with_capacity(ScalarKind::F64, node_for_depth.lookbacks()[0])];
        let mut node = node_for_depth;

        let feed = [10.0_f64, 20.0, 30.0];
        let expect = [None, Some(10.0), Some(20.0)];

        for (v, want) in feed.iter().zip(expect) {
            inputs[0].push(Scalar::f64(*v)).unwrap();
            let got = node.eval(Ctx::new(&inputs, Timestamp(0)));
            match want {
                None => assert_eq!(got, None),
                Some(p) => assert_eq!(got, Some([Cell::from_f64(p)].as_slice())),
            }
        }
    }

    #[test]
    fn delay_two_emits_the_value_two_cycles_ago() {
        // lag-N generalisation: Delay[2] holds two cycles of history, so it is None
        // for the first two cycles, then emits the value from two cycles back.
        let mut node = Delay::new(2);
        let mut inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)];

        let feed = [10.0_f64, 20.0, 30.0, 40.0];
        let expect = [None, None, Some(10.0), Some(20.0)];

        for (v, want) in feed.iter().zip(expect) {
            inputs[0].push(Scalar::f64(*v)).unwrap();
            let got = node.eval(Ctx::new(&inputs, Timestamp(0)));
            match want {
                None => assert_eq!(got, None),
                Some(p) => assert_eq!(got, Some([Cell::from_f64(p)].as_slice())),
            }
        }
    }

    #[test]
    fn delay_is_none_on_an_empty_input_cycle() {
        // a degenerate empty-input cycle filters (the universal warm-up/empty guard)
        let mut node = Delay::new(1);
        let inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)];
        assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None);
    }

    #[test]
    fn lag_must_be_at_least_one() {
        // a zero-lag register is the identity and is forbidden (mirror Sma::new)
        let r = std::panic::catch_unwind(|| Delay::new(0));
        assert!(r.is_err(), "Delay::new(0) must panic — a zero-lag register is the identity");
    }
}
