use std::collections::HashSet;
use cord_trig::ir::{NodeId, TrigGraph, TrigOp};

pub fn classify(graph: &TrigGraph) -> ExprInfo {
    let mut info = ExprInfo::default();
    info.node_count = graph.nodes.len();

    for op in &graph.nodes {
        match op {
            TrigOp::InputX => info.uses_x = true,
            TrigOp::InputY => info.uses_y = true,
            TrigOp::InputZ => info.uses_z = true,
            TrigOp::Sin(_) | TrigOp::Cos(_) | TrigOp::Tan(_)
            | TrigOp::Asin(_) | TrigOp::Acos(_) | TrigOp::Atan(_)
            | TrigOp::Sinh(_) | TrigOp::Cosh(_) | TrigOp::Tanh(_)
            | TrigOp::Asinh(_) | TrigOp::Acosh(_) | TrigOp::Atanh(_) => info.has_trig = true,
            TrigOp::Sqrt(_) | TrigOp::Exp(_) | TrigOp::Ln(_) => info.has_multiply = true,
            TrigOp::Hypot(_, _) | TrigOp::Atan2(_, _) => info.has_vectoring = true,
            TrigOp::Mul(_, _) => info.has_multiply = true,
            _ => {}
        }
    }

    info.dimensions = info.uses_x as u8 + info.uses_y as u8 + info.uses_z as u8;

    let cost = graph.cordic_cost();
    info.cordic_rotation = cost.rotation;
    info.cordic_vectoring = cost.vectoring;
    info.cordic_linear = cost.linear;
    info.cordic_binary = cost.binary;

    info
}

#[derive(Debug, Default)]
pub struct ExprInfo {
    pub node_count: usize,
    pub dimensions: u8,
    pub uses_x: bool,
    pub uses_y: bool,
    pub uses_z: bool,
    pub has_trig: bool,
    pub has_vectoring: bool,
    pub has_multiply: bool,
    pub cordic_rotation: u32,
    pub cordic_vectoring: u32,
    pub cordic_linear: u32,
    pub cordic_binary: u32,
}

impl ExprInfo {
    pub fn total_cordic_passes(&self) -> u32 {
        self.cordic_rotation + self.cordic_vectoring + self.cordic_linear
    }

    pub fn dimension_label(&self) -> &'static str {
        match self.dimensions {
            0 => "constant",
            1 => "1D curve",
            2 => "2D surface",
            3 => "3D field",
            _ => "?",
        }
    }
}

pub fn classify_from(graph: &TrigGraph, root: NodeId) -> ExprInfo {
    let mut info = ExprInfo::default();
    let mut visited = HashSet::new();
    let mut stack = vec![root];

    while let Some(id) = stack.pop() {
        if !visited.insert(id) { continue; }
        info.node_count += 1;
        let op = &graph.nodes[id as usize];
        match op {
            TrigOp::InputX => info.uses_x = true,
            TrigOp::InputY => info.uses_y = true,
            TrigOp::InputZ => info.uses_z = true,
            TrigOp::Sin(_) | TrigOp::Cos(_) | TrigOp::Tan(_)
            | TrigOp::Asin(_) | TrigOp::Acos(_) | TrigOp::Atan(_)
            | TrigOp::Sinh(_) | TrigOp::Cosh(_) | TrigOp::Tanh(_)
            | TrigOp::Asinh(_) | TrigOp::Acosh(_) | TrigOp::Atanh(_) => info.has_trig = true,
            TrigOp::Sqrt(_) | TrigOp::Exp(_) | TrigOp::Ln(_) => info.has_multiply = true,
            TrigOp::Hypot(_, _) | TrigOp::Atan2(_, _) => info.has_vectoring = true,
            TrigOp::Mul(_, _) => info.has_multiply = true,
            _ => {}
        }
        match op {
            TrigOp::Add(a, b) | TrigOp::Sub(a, b) | TrigOp::Mul(a, b) | TrigOp::Div(a, b)
            | TrigOp::Hypot(a, b) | TrigOp::Atan2(a, b)
            | TrigOp::Min(a, b) | TrigOp::Max(a, b) => {
                stack.push(*a);
                stack.push(*b);
            }
            TrigOp::Neg(a) | TrigOp::Abs(a)
            | TrigOp::Sin(a) | TrigOp::Cos(a) | TrigOp::Tan(a)
            | TrigOp::Asin(a) | TrigOp::Acos(a) | TrigOp::Atan(a)
            | TrigOp::Sinh(a) | TrigOp::Cosh(a) | TrigOp::Tanh(a)
            | TrigOp::Asinh(a) | TrigOp::Acosh(a) | TrigOp::Atanh(a)
            | TrigOp::Sqrt(a) | TrigOp::Exp(a) | TrigOp::Ln(a) => {
                stack.push(*a);
            }
            TrigOp::Clamp { val, lo, hi } => {
                stack.push(*val);
                stack.push(*lo);
                stack.push(*hi);
            }
            TrigOp::InputX | TrigOp::InputY | TrigOp::InputZ | TrigOp::Const(_) => {}
        }
    }

    info.dimensions = info.uses_x as u8 + info.uses_y as u8 + info.uses_z as u8;
    info
}

pub fn expr_to_sdf(
    graph: &mut TrigGraph,
    expr_root: NodeId,
    dims: u8,
    radius: f64,
    domain: f64,
) -> NodeId {
    let ix = graph.push(TrigOp::InputX);
    let iy = graph.push(TrigOp::InputY);
    let iz = graph.push(TrigOp::InputZ);
    let r = graph.push(TrigOp::Const(radius));
    let dom = graph.push(TrigOp::Const(domain));

    let sdf = match dims {
        0 | 1 => {
            let dy = graph.push(TrigOp::Sub(iy, expr_root));
            let tube = graph.push(TrigOp::Hypot(dy, iz));
            graph.push(TrigOp::Sub(tube, r))
        }
        2 => {
            let dz = graph.push(TrigOp::Sub(iz, expr_root));
            let abs_dz = graph.push(TrigOp::Abs(dz));
            graph.push(TrigOp::Sub(abs_dz, r))
        }
        _ => {
            let abs_f = graph.push(TrigOp::Abs(expr_root));
            graph.push(TrigOp::Sub(abs_f, r))
        }
    };

    let abs_x = graph.push(TrigOp::Abs(ix));
    let abs_y = graph.push(TrigOp::Abs(iy));
    let abs_z = graph.push(TrigOp::Abs(iz));
    let bx = graph.push(TrigOp::Sub(abs_x, dom));
    let by = graph.push(TrigOp::Sub(abs_y, dom));
    let bz = graph.push(TrigOp::Sub(abs_z, dom));
    let bxy = graph.push(TrigOp::Max(bx, by));
    let bbox = graph.push(TrigOp::Max(bxy, bz));

    graph.push(TrigOp::Max(sdf, bbox))
}

#[allow(dead_code)]
pub fn references_ident(source: &str, var: &str) -> bool {
    let bytes = source.as_bytes();
    let var_bytes = var.as_bytes();
    let var_len = var_bytes.len();

    for i in 0..=bytes.len().saturating_sub(var_len) {
        if &bytes[i..i + var_len] == var_bytes {
            let before_ok = i == 0
                || !(bytes[i - 1].is_ascii_alphanumeric() || bytes[i - 1] == b'_');
            let after_ok = i + var_len >= bytes.len()
                || !(bytes[i + var_len].is_ascii_alphanumeric() || bytes[i + var_len] == b'_');
            if before_ok && after_ok {
                return true;
            }
        }
    }
    false
}
