use crate::SdfNode;
use std::fmt::Write;

/// Convert an SdfNode tree to valid OpenSCAD source.
pub fn sdf_to_scad(node: &SdfNode) -> String {
    let mut out = String::new();
    emit_scad(node, 0, &mut out);
    out
}

fn indent(depth: usize, out: &mut String) {
    for _ in 0..depth {
        out.push_str("  ");
    }
}

fn fmt(v: f64) -> String {
    if v == v.round() && v.abs() < 1e9 {
        format!("{}", v as i64)
    } else {
        let s = format!("{:.6}", v);
        let s = s.trim_end_matches('0');
        let s = s.trim_end_matches('.');
        s.to_string()
    }
}

fn emit_scad(node: &SdfNode, depth: usize, out: &mut String) {
    match node {
        SdfNode::Sphere { radius } => {
            indent(depth, out);
            let _ = writeln!(out, "sphere(r={});", fmt(*radius));
        }

        SdfNode::Box { half_extents: h } => {
            indent(depth, out);
            let _ = writeln!(
                out, "cube([{}, {}, {}], center=true);",
                fmt(h[0] * 2.0), fmt(h[1] * 2.0), fmt(h[2] * 2.0)
            );
        }

        SdfNode::Cylinder { radius, height } => {
            indent(depth, out);
            let _ = writeln!(
                out, "cylinder(h={}, r={}, center=true);",
                fmt(*height), fmt(*radius)
            );
        }

        SdfNode::Translate { offset, child } => {
            indent(depth, out);
            let _ = writeln!(
                out, "translate([{}, {}, {}])",
                fmt(offset[0]), fmt(offset[1]), fmt(offset[2])
            );
            emit_scad(child, depth + 1, out);
        }

        SdfNode::Rotate { axis, angle_deg, child } => {
            let rot = if axis[0].abs() > 0.9 {
                format!("[{}, 0, 0]", fmt(*angle_deg))
            } else if axis[1].abs() > 0.9 {
                format!("[0, {}, 0]", fmt(*angle_deg))
            } else {
                format!("[0, 0, {}]", fmt(*angle_deg))
            };
            indent(depth, out);
            let _ = writeln!(out, "rotate({rot})");
            emit_scad(child, depth + 1, out);
        }

        SdfNode::Scale { factor, child } => {
            indent(depth, out);
            if (factor[0] - factor[1]).abs() < 1e-10
                && (factor[1] - factor[2]).abs() < 1e-10
            {
                let _ = writeln!(out, "scale({})", fmt(factor[0]));
            } else {
                let _ = writeln!(
                    out, "scale([{}, {}, {}])",
                    fmt(factor[0]), fmt(factor[1]), fmt(factor[2])
                );
            }
            emit_scad(child, depth + 1, out);
        }

        SdfNode::Union(children) => {
            if children.len() == 1 {
                emit_scad(&children[0], depth, out);
                return;
            }
            indent(depth, out);
            let _ = writeln!(out, "union() {{");
            for c in children {
                emit_scad(c, depth + 1, out);
            }
            indent(depth, out);
            let _ = writeln!(out, "}}");
        }

        SdfNode::Intersection(children) => {
            if children.len() == 1 {
                emit_scad(&children[0], depth, out);
                return;
            }
            indent(depth, out);
            let _ = writeln!(out, "intersection() {{");
            for c in children {
                emit_scad(c, depth + 1, out);
            }
            indent(depth, out);
            let _ = writeln!(out, "}}");
        }

        SdfNode::Difference { base, subtract } => {
            indent(depth, out);
            let _ = writeln!(out, "difference() {{");
            emit_scad(base, depth + 1, out);
            for s in subtract {
                emit_scad(s, depth + 1, out);
            }
            indent(depth, out);
            let _ = writeln!(out, "}}");
        }

        SdfNode::SmoothUnion { children, k } => {
            indent(depth, out);
            let _ = writeln!(out, "smooth_union(k={}) {{", fmt(*k));
            for c in children {
                emit_scad(c, depth + 1, out);
            }
            indent(depth, out);
            let _ = writeln!(out, "}}");
        }
    }
}

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

    #[test]
    fn sphere_roundtrip() {
        let node = SdfNode::Sphere { radius: 5.0 };
        let scad = sdf_to_scad(&node);
        assert_eq!(scad.trim(), "sphere(r=5);");
        let reparsed = crate::lower::lower_program(
            &cord_parse::parse(&scad).unwrap()
        ).unwrap();
        assert!(matches!(reparsed, SdfNode::Sphere { radius } if (radius - 5.0).abs() < 1e-10));
    }

    #[test]
    fn box_roundtrip() {
        let node = SdfNode::Box { half_extents: [1.0, 2.0, 3.0] };
        let scad = sdf_to_scad(&node);
        assert!(scad.contains("cube([2, 4, 6], center=true);"));
        let reparsed = crate::lower::lower_program(
            &cord_parse::parse(&scad).unwrap()
        ).unwrap();
        if let SdfNode::Box { half_extents: h } = reparsed {
            assert!((h[0] - 1.0).abs() < 1e-10);
            assert!((h[1] - 2.0).abs() < 1e-10);
            assert!((h[2] - 3.0).abs() < 1e-10);
        } else {
            panic!("expected Box, got {:?}", reparsed);
        }
    }

    #[test]
    fn cylinder_roundtrip() {
        let node = SdfNode::Cylinder { radius: 2.0, height: 6.0 };
        let scad = sdf_to_scad(&node);
        assert!(scad.contains("cylinder(h=6, r=2, center=true);"));
        let reparsed = crate::lower::lower_program(
            &cord_parse::parse(&scad).unwrap()
        ).unwrap();
        if let SdfNode::Cylinder { radius, height } = reparsed {
            assert!((radius - 2.0).abs() < 1e-10);
            assert!((height - 6.0).abs() < 1e-10);
        } else {
            panic!("expected Cylinder, got {:?}", reparsed);
        }
    }

    #[test]
    fn translate_sphere_roundtrip() {
        let node = SdfNode::Translate {
            offset: [1.0, 2.0, 3.0],
            child: Box::new(SdfNode::Sphere { radius: 1.0 }),
        };
        let scad = sdf_to_scad(&node);
        assert!(scad.contains("translate([1, 2, 3])"));
        assert!(scad.contains("sphere(r=1);"));
        let reparsed = crate::lower::lower_program(
            &cord_parse::parse(&scad).unwrap()
        ).unwrap();
        if let SdfNode::Translate { offset, child } = reparsed {
            assert!((offset[0] - 1.0).abs() < 1e-10);
            assert!((offset[1] - 2.0).abs() < 1e-10);
            assert!((offset[2] - 3.0).abs() < 1e-10);
            assert!(matches!(*child, SdfNode::Sphere { .. }));
        } else {
            panic!("expected Translate, got {:?}", reparsed);
        }
    }

    #[test]
    fn difference_roundtrip() {
        let node = SdfNode::Difference {
            base: Box::new(SdfNode::Box { half_extents: [5.0, 5.0, 5.0] }),
            subtract: vec![SdfNode::Sphere { radius: 4.0 }],
        };
        let scad = sdf_to_scad(&node);
        assert!(scad.contains("difference()"));
        let reparsed = crate::lower::lower_program(
            &cord_parse::parse(&scad).unwrap()
        ).unwrap();
        assert!(matches!(reparsed, SdfNode::Difference { .. }));
    }

    #[test]
    fn union_roundtrip() {
        let node = SdfNode::Union(vec![
            SdfNode::Sphere { radius: 1.0 },
            SdfNode::Sphere { radius: 2.0 },
        ]);
        let scad = sdf_to_scad(&node);
        assert!(scad.contains("union()"));
        let reparsed = crate::lower::lower_program(
            &cord_parse::parse(&scad).unwrap()
        ).unwrap();
        assert!(matches!(reparsed, SdfNode::Union(_)));
    }

    #[test]
    fn rotate_z_roundtrip() {
        let node = SdfNode::Rotate {
            axis: [0.0, 0.0, 1.0],
            angle_deg: 45.0,
            child: Box::new(SdfNode::Box { half_extents: [1.0, 2.0, 1.0] }),
        };
        let scad = sdf_to_scad(&node);
        assert!(scad.contains("rotate([0, 0, 45])"));
        let reparsed = crate::lower::lower_program(
            &cord_parse::parse(&scad).unwrap()
        ).unwrap();
        if let SdfNode::Rotate { angle_deg, axis, .. } = reparsed {
            assert!((angle_deg - 45.0).abs() < 1e-10);
            assert!(axis[2].abs() > 0.9);
        } else {
            panic!("expected Rotate, got {:?}", reparsed);
        }
    }

    #[test]
    fn scad_file_roundtrip_eval() {
        let scad = "difference() {\n  cube([10, 10, 10], center=true);\n  sphere(r=4);\n}\n";
        let program = cord_parse::parse(scad).unwrap();
        let sdf = crate::lower::lower_program(&program).unwrap();
        let emitted = sdf_to_scad(&sdf);
        let program2 = cord_parse::parse(&emitted).unwrap();
        let sdf2 = crate::lower::lower_program(&program2).unwrap();

        let g1 = crate::sdf_to_trig(&sdf);
        let g2 = crate::sdf_to_trig(&sdf2);

        let points = [
            (0.0, 0.0, 0.0),
            (3.0, 0.0, 0.0),
            (5.0, 0.0, 0.0),
            (0.0, 5.0, 0.0),
            (0.0, 0.0, 5.0),
            (6.0, 0.0, 0.0),
        ];
        for (x, y, z) in points {
            let v1 = cord_trig::eval::evaluate(&g1, x, y, z);
            let v2 = cord_trig::eval::evaluate(&g2, x, y, z);
            assert!(
                (v1 - v2).abs() < 1e-6,
                "divergence at ({x},{y},{z}): {v1} vs {v2}"
            );
        }
    }
}
