//! ger40-lab — an aura research project: node logic + the exported vocabulary.
//!
//! Every node type this crate provides is registered in `vocabulary()` /
//! `type_ids()` under the `ger40_lab::` namespace prefix; the `aura` host loads
//! this cdylib per invocation and merges it with the std vocabulary.

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

/// One-input scalar gain: emits `input * factor`. Emits `None` until its
/// input has a value (warm-up filter, C8).
pub struct Scale {
    factor: f64,
    out: [Cell; 1],
}

impl Scale {
    pub fn new(factor: f64) -> Self {
        Self { factor, out: [Cell::from_f64(0.0)] }
    }
    pub fn builder() -> PrimitiveBuilder {
        PrimitiveBuilder::new(
            "ger40_lab::Scale",
            NodeSchema {
                inputs: vec![PortSpec {
                    kind: ScalarKind::F64,
                    firing: Firing::Any,
                    name: "value".into(),
                }],
                output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
                params: vec![ParamSpec { name: "factor".into(), kind: ScalarKind::F64 }],
            },
            |p| Box::new(Scale::new(p[0].f64())),
        )
    }
}

impl Node for Scale {
    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;
        }
        self.out[0] = Cell::from_f64(w[0] * self.factor);
        Some(&self.out)
    }
    fn label(&self) -> String {
        format!("ger40_lab::Scale({})", self.factor)
    }
}

/// One-input momentum: emits `price_now - price_{length}_bars_ago`, i.e. the
/// raw change over `length` cycles. Positive => up-move, negative => down-move.
/// Reads a window of `length + 1` cycles (financial-style indexing, index 0 =
/// newest; design ledger C4/RingBuffer). Emits `None` until that much history
/// has accumulated (warm-up filter, C8).
pub struct Momentum {
    length: usize,
    out: [Cell; 1],
}

impl Momentum {
    pub fn new(length: i64) -> Self {
        Self { length: length.max(1) as usize, out: [Cell::from_f64(0.0)] }
    }

    pub fn builder() -> PrimitiveBuilder {
        PrimitiveBuilder::new(
            "ger40_lab::Momentum",
            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: "length".into(), kind: ScalarKind::I64 }],
            },
            |p| Box::new(Momentum::new(p[0].i64())),
        )
    }
}

impl Node for Momentum {
    fn lookbacks(&self) -> Vec<usize> {
        vec![self.length + 1]
    }
    fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
        let w = ctx.f64_in(0);
        if w.len() <= self.length {
            return None;
        }
        self.out[0] = Cell::from_f64(w[0] - w[self.length]);
        Some(&self.out)
    }
    fn label(&self) -> String {
        format!("ger40_lab::Momentum({})", self.length)
    }
}

fn vocabulary(type_id: &str) -> Option<PrimitiveBuilder> {
    match type_id {
        "ger40_lab::Scale" => Some(Scale::builder()),
        "ger40_lab::Momentum" => Some(Momentum::builder()),
        _ => None,
    }
}

fn type_ids() -> &'static [&'static str] {
    &["ger40_lab::Scale", "ger40_lab::Momentum"]
}

aura_core::aura_project! {
    namespace: "ger40_lab",
    vocabulary: vocabulary,
    type_ids: type_ids,
}
