use std::path::PathBuf;

use bone_document::{EditOutcome, Sketch, SketchEdit, SketchEntity, SketchRelation};
use bone_render::{
    Camera2, OffscreenContext, PixelDiff, PixelDiffThreshold, PixelsPerMm, RelationGlyphKind,
    SketchRenderer, SketchScene, Style, decode_png, encode_png,
};
use bone_types::{Length, Point2, Point3, SketchEntityId, SketchPlaneBasis, Tolerance, UnitVec3};
use uom::si::length::millimeter;

mod common;

use common::{extent_square as extent, make_context};

const GOLDEN: &str = "tests/goldens/relations_256.png";
const UPDATE_ENV: &str = "BONE_UPDATE_RELATIONS_GOLDEN";
const DIFF_TOLERANCE: f64 = 16.0 / 255.0;

fn plane() -> SketchPlaneBasis {
    let Ok(basis) = SketchPlaneBasis::new(
        Point3::origin(),
        UnitVec3::x_axis(),
        UnitVec3::y_axis(),
        Tolerance::new(1e-9),
    ) else {
        panic!("xy plane basis is orthogonal");
    };
    basis
}

fn add_point(s: Sketch, x: f64, y: f64) -> (Sketch, SketchEntityId) {
    let Ok((next, EditOutcome::Entity(id))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
        Point2::from_mm(x, y),
    ))) else {
        panic!("add point");
    };
    (next, id)
}

fn add_line(s: Sketch, a: SketchEntityId, b: SketchEntityId) -> (Sketch, SketchEntityId) {
    let Ok((next, EditOutcome::Entity(id))) =
        s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
    else {
        panic!("add line");
    };
    (next, id)
}

fn add_circle(s: Sketch, center: SketchEntityId, radius_mm: f64) -> (Sketch, SketchEntityId) {
    let Ok((next, EditOutcome::Entity(id))) = s.apply(SketchEdit::AddEntity(SketchEntity::circle(
        center,
        Length::new::<millimeter>(radius_mm),
        false,
    ))) else {
        panic!("add circle");
    };
    (next, id)
}

fn add_relation(s: Sketch, rel: SketchRelation) -> Sketch {
    let Ok((next, _)) = s.apply(SketchEdit::AddRelation(rel)) else {
        panic!("add relation {rel:?}");
    };
    next
}

fn one_per_kind_scene() -> SketchScene {
    let s = Sketch::new(plane());

    let (s, coin_a) = add_point(s, -4.0, 4.0);
    let (s, coin_b) = add_point(s, -4.0, 4.0);
    let s = add_relation(s, SketchRelation::Coincident(coin_a, coin_b));

    let (s, ha) = add_point(s, -1.5, 4.2);
    let (s, hb) = add_point(s, 1.5, 4.2);
    let (s, hl) = add_line(s, ha, hb);
    let s = add_relation(s, SketchRelation::Horizontal(hl));

    let (s, va) = add_point(s, 4.0, 3.0);
    let (s, vb) = add_point(s, 4.0, 5.2);
    let (s, vl) = add_line(s, va, vb);
    let s = add_relation(s, SketchRelation::Vertical(vl));

    let (s, pa0) = add_point(s, -5.0, 1.0);
    let (s, pa1) = add_point(s, -2.5, 1.9);
    let (s, pla) = add_line(s, pa0, pa1);
    let (s, pb0) = add_point(s, -5.0, -0.5);
    let (s, pb1) = add_point(s, -2.5, 0.4);
    let (s, plb) = add_line(s, pb0, pb1);
    let s = add_relation(s, SketchRelation::Parallel(pla, plb));

    let (s, xa0) = add_point(s, -0.5, 1.4);
    let (s, xa1) = add_point(s, 1.3, 1.4);
    let (s, xla) = add_line(s, xa0, xa1);
    let (s, xb0) = add_point(s, 0.5, 0.3);
    let (s, xb1) = add_point(s, 0.5, 1.9);
    let (s, xlb) = add_line(s, xb0, xb1);
    let s = add_relation(s, SketchRelation::Perpendicular(xla, xlb));

    let (s, tc) = add_point(s, 4.5, 1.2);
    let (s, tcirc) = add_circle(s, tc, 0.7);
    let (s, tla) = add_point(s, 3.0, 1.9);
    let (s, tlb) = add_point(s, 6.0, 1.9);
    let (s, tline) = add_line(s, tla, tlb);
    let s = add_relation(s, SketchRelation::Tangent(tcirc, tline));

    let (s, ea0) = add_point(s, -4.8, -2.5);
    let (s, ea1) = add_point(s, -3.0, -2.5);
    let (s, ela) = add_line(s, ea0, ea1);
    let (s, eb0) = add_point(s, -4.8, -3.8);
    let (s, eb1) = add_point(s, -3.0, -3.8);
    let (s, elb) = add_line(s, eb0, eb1);
    let s = add_relation(s, SketchRelation::Equal(ela, elb));

    let (s, ccc) = add_point(s, -0.2, -2.8);
    let (s, cca) = add_circle(s, ccc, 0.8);
    let (s, ccb) = add_circle(s, ccc, 1.6);
    let s = add_relation(s, SketchRelation::Concentric(cca, ccb));

    let (s, fp) = add_point(s, 4.5, -3.0);
    let s = add_relation(s, SketchRelation::Fix(fp));

    let (s, mp_a) = add_point(s, 2.0, -3.0);
    let (s, mp_b) = add_point(s, 5.0, -3.0);
    let (s, mp_line) = add_line(s, mp_a, mp_b);
    let (s, mp_point) = add_point(s, 3.5, -3.0);
    let s = add_relation(
        s,
        SketchRelation::Midpoint {
            point: mp_point,
            line: mp_line,
        },
    );

    let (s, sym_a0) = add_point(s, 6.5, -2.5);
    let (s, sym_a1) = add_point(s, 9.0, -2.5);
    let (s, sym_axis) = add_line(s, sym_a0, sym_a1);
    let (s, sym_p) = add_point(s, 7.5, -1.5);
    let (s, sym_q) = add_point(s, 7.5, -3.5);
    let s = add_relation(
        s,
        SketchRelation::Symmetric {
            a: sym_p,
            b: sym_q,
            axis: sym_axis,
        },
    );

    let Ok(scene) = SketchScene::extract(&s) else {
        panic!("scene extract");
    };
    assert_eq!(
        scene.relations().len(),
        RelationGlyphKind::all().len(),
        "expected one scene glyph per relation kind",
    );
    scene
}

fn render_scene(ctx: &OffscreenContext, scene: &SketchScene) -> bone_render::SnapshotFrame {
    let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
    let camera = Camera2::new(ctx.extent()).with_zoom(PixelsPerMm::new(20.0));
    let style = Style::default();
    let Ok(frame) = renderer.render(ctx, scene, camera, &style) else {
        panic!("SketchRenderer::render failed");
    };
    frame
}

fn golden_path() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(GOLDEN)
}

#[test]
fn relation_kinds_match_golden() {
    let size = extent(256);
    let ctx = make_context(size);
    let scene = one_per_kind_scene();
    let frame = render_scene(&ctx, &scene);
    let path = golden_path();

    if std::env::var(UPDATE_ENV).is_ok() {
        let Ok(bytes) = encode_png(&frame) else {
            panic!("encode_png failed");
        };
        if let Some(parent) = path.parent()
            && let Err(e) = std::fs::create_dir_all(parent)
        {
            panic!("create goldens dir {}: {e}", parent.display());
        }
        if let Err(e) = std::fs::write(&path, &bytes) {
            panic!("write golden {}: {e}", path.display());
        }
        return;
    }

    let Ok(bytes) = std::fs::read(&path) else {
        panic!(
            "golden missing at {}: rerun with {UPDATE_ENV}=1 to generate",
            path.display()
        );
    };
    let Ok((golden_extent, golden_rgba)) = decode_png(&bytes) else {
        panic!("failed to decode golden PNG");
    };
    assert_eq!(golden_extent, size, "golden extent drift");
    let threshold = PixelDiffThreshold::new(DIFF_TOLERANCE);
    let Ok(report) = PixelDiff::compare(&frame, &golden_rgba, threshold) else {
        panic!("PixelDiff rejected inputs");
    };
    assert!(
        report.is_clean(),
        "relation render drifted from golden: {} mismatches, worst {:?}, backend {}",
        report.over_threshold(),
        report.worst(),
        frame.backend(),
    );
}

#[test]
fn relation_glyph_pick_ids_survive_render() {
    let size = extent(256);
    let ctx = make_context(size);
    let scene = one_per_kind_scene();
    let frame = render_scene(&ctx, &scene);
    let Ok(index) = scene.pick_index() else {
        panic!("pick index build");
    };

    let unresolved: Vec<RelationGlyphKind> = scene
        .relations()
        .iter()
        .filter(|g| g.pick().unpack(&index).is_none())
        .map(|g| g.kind())
        .collect();
    assert!(
        unresolved.is_empty(),
        "relation pick ids did not decode: {unresolved:?}",
    );

    assert_eq!(frame.extent(), size);
}
