/// Index into the graph's node array.
pub type NodeId = u32;

/// A directed acyclic graph of trig operations.
///
/// Nodes are stored in topological order: a node can only reference
/// earlier nodes (lower indices). Evaluation walks forward through
/// the array, so every dependency is resolved before it's needed.
///
/// This is the universal intermediate representation. Every SDF
/// primitive, every transform, every boolean operation decomposes
/// into this. Every node maps to exactly one CORDIC mode or a
/// direct binary operation.
#[derive(Clone)]
pub struct TrigGraph {
    pub nodes: Vec<TrigOp>,
    pub output: NodeId,
}

/// A single operation in the trig IR.
///
/// CORDIC mapping:
///   Sin, Cos        → rotation mode (angle → sin/cos)
///   Hypot, Atan2    → vectoring mode (x,y → magnitude/angle)
///   Mul             → linear mode (multiply-accumulate)
///   Add, Sub, Neg, Abs, Min, Max → direct binary (no CORDIC)
#[derive(Debug, Clone)]
pub enum TrigOp {
    // Evaluation point inputs
    InputX,
    InputY,
    InputZ,

    // Constant
    Const(f64),

    // Arithmetic — direct binary ops
    Add(NodeId, NodeId),
    Sub(NodeId, NodeId),
    Mul(NodeId, NodeId),
    Div(NodeId, NodeId),
    Neg(NodeId),
    Abs(NodeId),

    // Trig — CORDIC rotation mode
    Sin(NodeId),
    Cos(NodeId),
    Tan(NodeId),

    // Inverse trig
    Asin(NodeId),
    Acos(NodeId),
    Atan(NodeId),

    // Hyperbolic
    Sinh(NodeId),
    Cosh(NodeId),
    Tanh(NodeId),

    // Inverse hyperbolic
    Asinh(NodeId),
    Acosh(NodeId),
    Atanh(NodeId),

    // Transcendental
    Sqrt(NodeId),
    Exp(NodeId),
    Ln(NodeId),

    // Magnitude — CORDIC vectoring mode
    // sqrt(a² + b²) in a single pass
    Hypot(NodeId, NodeId),

    // Angle — CORDIC vectoring mode
    // atan2(y, x) in a single pass
    Atan2(NodeId, NodeId),

    // Comparison — direct binary
    Min(NodeId, NodeId),
    Max(NodeId, NodeId),

    // Clamp(value, lo, hi) = max(lo, min(value, hi))
    Clamp { val: NodeId, lo: NodeId, hi: NodeId },
}

impl TrigGraph {
    pub fn new() -> Self {
        Self {
            nodes: Vec::new(),
            output: 0,
        }
    }

    pub fn push(&mut self, op: TrigOp) -> NodeId {
        let id = self.nodes.len() as NodeId;
        self.nodes.push(op);
        id
    }

    pub fn set_output(&mut self, id: NodeId) {
        self.output = id;
    }

    pub fn node_count(&self) -> usize {
        self.nodes.len()
    }

    /// Count how many of each CORDIC mode this graph requires.
    pub fn cordic_cost(&self) -> CORDICCost {
        let mut cost = CORDICCost::default();
        for op in &self.nodes {
            match op {
                TrigOp::Sin(_) | TrigOp::Cos(_) | TrigOp::Tan(_) => cost.rotation += 1,
                TrigOp::Asin(_) | TrigOp::Acos(_) | TrigOp::Atan(_) => cost.rotation += 1,
                TrigOp::Sinh(_) | TrigOp::Cosh(_) | TrigOp::Tanh(_) => cost.rotation += 1,
                TrigOp::Asinh(_) | TrigOp::Acosh(_) | TrigOp::Atanh(_) => cost.rotation += 1,
                TrigOp::Hypot(_, _) | TrigOp::Atan2(_, _) => cost.vectoring += 1,
                TrigOp::Mul(_, _) | TrigOp::Div(_, _) => cost.linear += 1,
                TrigOp::Sqrt(_) | TrigOp::Exp(_) | TrigOp::Ln(_) => cost.linear += 1,
                TrigOp::Add(_, _) | TrigOp::Sub(_, _) | TrigOp::Neg(_)
                | TrigOp::Abs(_) | TrigOp::Min(_, _) | TrigOp::Max(_, _)
                | TrigOp::Clamp { .. } => cost.binary += 1,
                TrigOp::InputX | TrigOp::InputY | TrigOp::InputZ
                | TrigOp::Const(_) => {}
            }
        }
        cost
    }
}

#[derive(Debug, Default)]
pub struct CORDICCost {
    pub rotation: u32,
    pub vectoring: u32,
    pub linear: u32,
    pub binary: u32,
}

impl CORDICCost {
    pub fn total_cordic_passes(&self) -> u32 {
        self.rotation + self.vectoring + self.linear
    }
}

// === TrigGraph binary serialization ===
// Format: b"TRIG" | u32 node_count | u32 output | [node...]
// Each node: u8 opcode | operands (NodeId = u32, Const = f64)

const TRIG_MAGIC: &[u8; 4] = b"TRIG";

const OP_INPUT_X: u8 = 0;
const OP_INPUT_Y: u8 = 1;
const OP_INPUT_Z: u8 = 2;
const OP_CONST: u8 = 3;
const OP_ADD: u8 = 4;
const OP_SUB: u8 = 5;
const OP_MUL: u8 = 6;
const OP_DIV: u8 = 16;
const OP_NEG: u8 = 7;
const OP_ABS: u8 = 8;
const OP_SIN: u8 = 9;
const OP_COS: u8 = 10;
const OP_HYPOT: u8 = 11;
const OP_ATAN2: u8 = 12;
const OP_MIN: u8 = 13;
const OP_MAX: u8 = 14;
const OP_CLAMP: u8 = 15;
const OP_TAN: u8 = 17;
const OP_ASIN: u8 = 18;
const OP_ACOS: u8 = 19;
const OP_ATAN: u8 = 20;
const OP_SINH: u8 = 21;
const OP_COSH: u8 = 22;
const OP_TANH: u8 = 23;
const OP_ASINH: u8 = 24;
const OP_ACOSH: u8 = 25;
const OP_ATANH: u8 = 26;
const OP_SQRT: u8 = 27;
const OP_EXP: u8 = 28;
const OP_LN: u8 = 29;

impl TrigGraph {
    pub fn to_bytes(&self) -> Vec<u8> {
        let mut buf = Vec::new();
        buf.extend_from_slice(TRIG_MAGIC);
        buf.extend_from_slice(&(self.nodes.len() as u32).to_le_bytes());
        buf.extend_from_slice(&self.output.to_le_bytes());

        for op in &self.nodes {
            match op {
                TrigOp::InputX => buf.push(OP_INPUT_X),
                TrigOp::InputY => buf.push(OP_INPUT_Y),
                TrigOp::InputZ => buf.push(OP_INPUT_Z),
                TrigOp::Const(v) => {
                    buf.push(OP_CONST);
                    buf.extend_from_slice(&v.to_le_bytes());
                }
                TrigOp::Add(a, b) => { buf.push(OP_ADD); push_pair(&mut buf, *a, *b); }
                TrigOp::Sub(a, b) => { buf.push(OP_SUB); push_pair(&mut buf, *a, *b); }
                TrigOp::Mul(a, b) => { buf.push(OP_MUL); push_pair(&mut buf, *a, *b); }
                TrigOp::Div(a, b) => { buf.push(OP_DIV); push_pair(&mut buf, *a, *b); }
                TrigOp::Neg(a) => { buf.push(OP_NEG); buf.extend_from_slice(&a.to_le_bytes()); }
                TrigOp::Abs(a) => { buf.push(OP_ABS); buf.extend_from_slice(&a.to_le_bytes()); }
                TrigOp::Sin(a) => { buf.push(OP_SIN); buf.extend_from_slice(&a.to_le_bytes()); }
                TrigOp::Cos(a) => { buf.push(OP_COS); buf.extend_from_slice(&a.to_le_bytes()); }
                TrigOp::Tan(a) => { buf.push(OP_TAN); buf.extend_from_slice(&a.to_le_bytes()); }
                TrigOp::Asin(a) => { buf.push(OP_ASIN); buf.extend_from_slice(&a.to_le_bytes()); }
                TrigOp::Acos(a) => { buf.push(OP_ACOS); buf.extend_from_slice(&a.to_le_bytes()); }
                TrigOp::Atan(a) => { buf.push(OP_ATAN); buf.extend_from_slice(&a.to_le_bytes()); }
                TrigOp::Sinh(a) => { buf.push(OP_SINH); buf.extend_from_slice(&a.to_le_bytes()); }
                TrigOp::Cosh(a) => { buf.push(OP_COSH); buf.extend_from_slice(&a.to_le_bytes()); }
                TrigOp::Tanh(a) => { buf.push(OP_TANH); buf.extend_from_slice(&a.to_le_bytes()); }
                TrigOp::Asinh(a) => { buf.push(OP_ASINH); buf.extend_from_slice(&a.to_le_bytes()); }
                TrigOp::Acosh(a) => { buf.push(OP_ACOSH); buf.extend_from_slice(&a.to_le_bytes()); }
                TrigOp::Atanh(a) => { buf.push(OP_ATANH); buf.extend_from_slice(&a.to_le_bytes()); }
                TrigOp::Sqrt(a) => { buf.push(OP_SQRT); buf.extend_from_slice(&a.to_le_bytes()); }
                TrigOp::Exp(a) => { buf.push(OP_EXP); buf.extend_from_slice(&a.to_le_bytes()); }
                TrigOp::Ln(a) => { buf.push(OP_LN); buf.extend_from_slice(&a.to_le_bytes()); }
                TrigOp::Hypot(a, b) => { buf.push(OP_HYPOT); push_pair(&mut buf, *a, *b); }
                TrigOp::Atan2(a, b) => { buf.push(OP_ATAN2); push_pair(&mut buf, *a, *b); }
                TrigOp::Min(a, b) => { buf.push(OP_MIN); push_pair(&mut buf, *a, *b); }
                TrigOp::Max(a, b) => { buf.push(OP_MAX); push_pair(&mut buf, *a, *b); }
                TrigOp::Clamp { val, lo, hi } => {
                    buf.push(OP_CLAMP);
                    buf.extend_from_slice(&val.to_le_bytes());
                    buf.extend_from_slice(&lo.to_le_bytes());
                    buf.extend_from_slice(&hi.to_le_bytes());
                }
            }
        }
        buf
    }

    pub fn from_bytes(data: &[u8]) -> Option<Self> {
        if data.len() < 12 || &data[0..4] != TRIG_MAGIC {
            return None;
        }
        let node_count = u32::from_le_bytes(data[4..8].try_into().ok()?) as usize;
        let output = u32::from_le_bytes(data[8..12].try_into().ok()?);
        let mut pos = 12;
        let mut nodes = Vec::with_capacity(node_count);

        for _ in 0..node_count {
            if pos >= data.len() { return None; }
            let opcode = data[pos];
            pos += 1;
            let op = match opcode {
                OP_INPUT_X => TrigOp::InputX,
                OP_INPUT_Y => TrigOp::InputY,
                OP_INPUT_Z => TrigOp::InputZ,
                OP_CONST => {
                    let v = f64::from_le_bytes(data[pos..pos+8].try_into().ok()?);
                    pos += 8;
                    TrigOp::Const(v)
                }
                OP_ADD => { let (a, b) = read_pair(data, &mut pos)?; TrigOp::Add(a, b) }
                OP_SUB => { let (a, b) = read_pair(data, &mut pos)?; TrigOp::Sub(a, b) }
                OP_MUL => { let (a, b) = read_pair(data, &mut pos)?; TrigOp::Mul(a, b) }
                OP_DIV => { let (a, b) = read_pair(data, &mut pos)?; TrigOp::Div(a, b) }
                OP_NEG => { let a = read_u32(data, &mut pos)?; TrigOp::Neg(a) }
                OP_ABS => { let a = read_u32(data, &mut pos)?; TrigOp::Abs(a) }
                OP_SIN => { let a = read_u32(data, &mut pos)?; TrigOp::Sin(a) }
                OP_COS => { let a = read_u32(data, &mut pos)?; TrigOp::Cos(a) }
                OP_TAN => { let a = read_u32(data, &mut pos)?; TrigOp::Tan(a) }
                OP_ASIN => { let a = read_u32(data, &mut pos)?; TrigOp::Asin(a) }
                OP_ACOS => { let a = read_u32(data, &mut pos)?; TrigOp::Acos(a) }
                OP_ATAN => { let a = read_u32(data, &mut pos)?; TrigOp::Atan(a) }
                OP_SINH => { let a = read_u32(data, &mut pos)?; TrigOp::Sinh(a) }
                OP_COSH => { let a = read_u32(data, &mut pos)?; TrigOp::Cosh(a) }
                OP_TANH => { let a = read_u32(data, &mut pos)?; TrigOp::Tanh(a) }
                OP_ASINH => { let a = read_u32(data, &mut pos)?; TrigOp::Asinh(a) }
                OP_ACOSH => { let a = read_u32(data, &mut pos)?; TrigOp::Acosh(a) }
                OP_ATANH => { let a = read_u32(data, &mut pos)?; TrigOp::Atanh(a) }
                OP_SQRT => { let a = read_u32(data, &mut pos)?; TrigOp::Sqrt(a) }
                OP_EXP => { let a = read_u32(data, &mut pos)?; TrigOp::Exp(a) }
                OP_LN => { let a = read_u32(data, &mut pos)?; TrigOp::Ln(a) }
                OP_HYPOT => { let (a, b) = read_pair(data, &mut pos)?; TrigOp::Hypot(a, b) }
                OP_ATAN2 => { let (a, b) = read_pair(data, &mut pos)?; TrigOp::Atan2(a, b) }
                OP_MIN => { let (a, b) = read_pair(data, &mut pos)?; TrigOp::Min(a, b) }
                OP_MAX => { let (a, b) = read_pair(data, &mut pos)?; TrigOp::Max(a, b) }
                OP_CLAMP => {
                    let val = read_u32(data, &mut pos)?;
                    let lo = read_u32(data, &mut pos)?;
                    let hi = read_u32(data, &mut pos)?;
                    TrigOp::Clamp { val, lo, hi }
                }
                _ => return None,
            };
            nodes.push(op);
        }

        Some(TrigGraph { nodes, output })
    }
}

fn push_pair(buf: &mut Vec<u8>, a: NodeId, b: NodeId) {
    buf.extend_from_slice(&a.to_le_bytes());
    buf.extend_from_slice(&b.to_le_bytes());
}

fn read_u32(data: &[u8], pos: &mut usize) -> Option<u32> {
    let v = u32::from_le_bytes(data[*pos..*pos+4].try_into().ok()?);
    *pos += 4;
    Some(v)
}

fn read_pair(data: &[u8], pos: &mut usize) -> Option<(NodeId, NodeId)> {
    let a = read_u32(data, pos)?;
    let b = read_u32(data, pos)?;
    Some((a, b))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::eval::evaluate;

    #[test]
    fn trig_roundtrip() {
        let mut g = TrigGraph::new();
        let x = g.push(TrigOp::InputX);
        let y = g.push(TrigOp::InputY);
        let z = g.push(TrigOp::InputZ);
        let r = g.push(TrigOp::Const(3.0));
        let xy = g.push(TrigOp::Hypot(x, y));
        let mag = g.push(TrigOp::Hypot(xy, z));
        let out = g.push(TrigOp::Sub(mag, r));
        g.set_output(out);

        let bytes = g.to_bytes();
        let g2 = TrigGraph::from_bytes(&bytes).unwrap();

        assert_eq!(g.nodes.len(), g2.nodes.len());
        assert_eq!(g.output, g2.output);
        let v1 = evaluate(&g, 1.0, 2.0, 2.0);
        let v2 = evaluate(&g2, 1.0, 2.0, 2.0);
        assert!((v1 - v2).abs() < 1e-15);
    }
}
