use bone_kernel::{
    BrepSolid, Circle2, Curve2Kind, ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature,
    ExtrudeProfile, ExtrudeSense, Line2, MergeResult, ProfileEdge, ProfileLoop, SolidMesh,
    evaluate_extrude,
};
use bone_render::{
    EdgeScene, OffscreenContext, SnapshotFrame, SolidRenderer, SolidScene, Style, frame_isometric,
};
use bone_types::{
    Aabb3, Angle, AngleTolerance, Camera3, ChordHeightTolerance, DisplayMode, FeatureId, Length,
    Plane3, Point2, Point3, PositiveLength, Projection, SketchEntityId, SketchId, Tolerance,
    UnitVec3, millimeter, radian,
};
use slotmap::{Key, SlotMap};

mod common;

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

const TOLERANCE: Tolerance = Tolerance::new(1.0e-9);
const UPDATE_ENV: &str = "BONE_UPDATE_EDGES_GOLDENS";
const DISPLAY_ENV: &str = "BONE_UPDATE_DISPLAY_GOLDENS";
const CHORD_MM: f64 = 0.05;
const ANGLE_RAD: f64 = 0.2;

fn xy_plane() -> Plane3 {
    let Ok(plane) = Plane3::new(
        Point3::origin(),
        UnitVec3::x_axis(),
        UnitVec3::y_axis(),
        TOLERANCE,
    ) else {
        panic!("x and y axes are orthonormal");
    };
    plane
}

fn blind(depth_mm: f64) -> ExtrudeFeature {
    let Ok(depth) = PositiveLength::new(Length::new::<millimeter>(depth_mm)) else {
        panic!("{depth_mm} mm is a positive length");
    };
    ExtrudeFeature {
        sketch: SketchId::null(),
        direction: ExtrudeDirection::Normal {
            sense: ExtrudeSense::Forward,
        },
        end_condition: ExtrudeEndCondition::Blind { depth },
        draft: None,
        thin_wall: None,
        merge_result: MergeResult::Merge,
    }
}

fn unit_cube() -> BrepSolid {
    let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
    let mut features: SlotMap<FeatureId, ()> = SlotMap::with_key();
    let corners = [
        Point2::from_mm(0.0, 0.0),
        Point2::from_mm(1.0, 0.0),
        Point2::from_mm(1.0, 1.0),
        Point2::from_mm(0.0, 1.0),
    ];
    let edges = (0..4)
        .map(|index| {
            let start = corners[index];
            let end = corners[(index + 1) % 4];
            let Ok(segment) = Line2::new(start, end, TOLERANCE) else {
                panic!("rectangle endpoints are distinct");
            };
            ProfileEdge::new(
                Curve2Kind::Line(segment),
                entities.insert(()),
                entities.insert(()),
            )
        })
        .collect();
    let profile = ExtrudeProfile::new(xy_plane(), vec![ProfileLoop::Open(edges)]);
    let Ok(solid) = evaluate_extrude(features.insert(()), &profile, &blind(1.0)) else {
        panic!("the unit square extrudes into a cube");
    };
    solid
}

fn cylinder() -> BrepSolid {
    let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
    let mut features: SlotMap<FeatureId, ()> = SlotMap::with_key();
    let Ok(disk) = Circle2::new(
        Point2::from_mm(0.0, 0.0),
        Length::new::<millimeter>(5.0),
        TOLERANCE,
    ) else {
        panic!("circle radius is positive");
    };
    let profile = ExtrudeProfile::new(
        xy_plane(),
        vec![ProfileLoop::Closed {
            curve: Curve2Kind::Circle(disk),
            curve_entity: entities.insert(()),
        }],
    );
    let Ok(solid) = evaluate_extrude(features.insert(()), &profile, &blind(10.0)) else {
        panic!("the disk extrudes into a cylinder");
    };
    solid
}

fn mesh_of(solid: &BrepSolid) -> SolidMesh {
    let Ok(mesh) = solid.tessellate(
        ChordHeightTolerance::from_mm(CHORD_MM),
        AngleTolerance::from_radians(ANGLE_RAD),
    ) else {
        panic!("the solid tessellates");
    };
    mesh
}

fn scenes(solid: &BrepSolid) -> (SolidScene, EdgeScene) {
    let mesh = mesh_of(solid);
    let Ok(faces) = SolidScene::from_mesh(&mesh) else {
        panic!("the mesh packs face pick ids");
    };
    let Ok(edges) = EdgeScene::from_solid(solid, &mesh, ChordHeightTolerance::from_mm(CHORD_MM))
    else {
        panic!("the solid packs edge pick ids");
    };
    (faces, edges)
}

fn aabb_of(solid: &BrepSolid) -> Aabb3 {
    let Some(aabb) = solid.bounding_box() else {
        panic!("the solid has a bounding box");
    };
    aabb
}

fn direction(x: f64, y: f64, z: f64) -> UnitVec3 {
    let Ok(unit) = UnitVec3::try_from_components(x, y, z, TOLERANCE) else {
        panic!("({x}, {y}, {z}) is a nonzero direction");
    };
    unit
}

fn framed(aabb: Aabb3, from: UnitVec3, up: UnitVec3) -> Camera3 {
    let center = aabb.center();
    let span = 0.5 * aabb.extent().norm_mm();
    let eye = center + from.into_vec(Length::new::<millimeter>(span * 3.0));
    let Ok(projection) = Projection::orthographic(Length::new::<millimeter>(span * 1.2)) else {
        panic!("half height is positive");
    };
    let Ok(camera) = Camera3::new(eye, center, up, projection) else {
        panic!("camera is non-degenerate");
    };
    camera
}

fn cube_camera(solid: &BrepSolid, size: bone_render::ViewportExtent) -> Camera3 {
    let Ok(camera) = frame_isometric(aabb_of(solid), size) else {
        panic!("the cube frames isometrically");
    };
    camera
}

fn framed_perspective(aabb: Aabb3, from: UnitVec3, up: UnitVec3) -> Camera3 {
    let center = aabb.center();
    let span = 0.5 * aabb.extent().norm_mm();
    let eye = center + from.into_vec(Length::new::<millimeter>(span * 4.0));
    let Ok(projection) = Projection::perspective(
        Angle::new::<radian>(core::f64::consts::FRAC_PI_4),
        Length::new::<millimeter>(span),
        Length::new::<millimeter>(span * 12.0),
    ) else {
        panic!("the field of view is within (0, pi)");
    };
    let Ok(camera) = Camera3::new(eye, center, up, projection) else {
        panic!("camera is non-degenerate");
    };
    camera
}

fn render(
    ctx: &OffscreenContext,
    faces: &SolidScene,
    edges: &EdgeScene,
    camera: Camera3,
    mode: DisplayMode,
) -> SnapshotFrame {
    let mut renderer = SolidRenderer::new(ctx.gpu(), ctx.color_format());
    let Ok(frame) = renderer.render_display(ctx, faces, edges, camera, &Style::default(), mode)
    else {
        panic!("SolidRenderer::render_display failed");
    };
    frame
}

fn cube_frame(mode: DisplayMode) -> SnapshotFrame {
    let size = extent(256);
    let ctx = make_context(size);
    let solid = unit_cube();
    let (faces, edges) = scenes(&solid);
    render(&ctx, &faces, &edges, cube_camera(&solid, size), mode)
}

fn cylinder_frame(mode: DisplayMode) -> SnapshotFrame {
    let size = extent(256);
    let ctx = make_context(size);
    let solid = cylinder();
    let (faces, edges) = scenes(&solid);
    let camera = framed(
        aabb_of(&solid),
        direction(1.0, 1.0, 0.6),
        UnitVec3::z_axis(),
    );
    render(&ctx, &faces, &edges, camera, mode)
}

#[test]
fn cube_shaded_with_edges_matches_golden() {
    check_golden(
        &cube_frame(DisplayMode::ShadedWithEdges),
        "tests/goldens/shaded_with_edges_256.png",
        UPDATE_ENV,
    );
}

#[test]
fn cube_hidden_line_removed_matches_golden() {
    check_golden(
        &cube_frame(DisplayMode::HiddenLineRemoved),
        "tests/goldens/hidden_line_removed_256.png",
        UPDATE_ENV,
    );
}

#[test]
fn cube_perspective_with_edges_matches_golden() {
    let size = extent(256);
    let ctx = make_context(size);
    let solid = unit_cube();
    let (faces, edges) = scenes(&solid);
    let camera = framed_perspective(
        aabb_of(&solid),
        direction(1.0, 1.0, 0.6),
        UnitVec3::z_axis(),
    );
    let frame = render(&ctx, &faces, &edges, camera, DisplayMode::ShadedWithEdges);
    check_golden(
        &frame,
        "tests/goldens/perspective_with_edges_256.png",
        UPDATE_ENV,
    );
}

#[test]
fn cylinder_shaded_with_edges_matches_golden() {
    check_golden(
        &cylinder_frame(DisplayMode::ShadedWithEdges),
        "tests/goldens/shaded_with_edges_cylinder_256.png",
        UPDATE_ENV,
    );
}

#[test]
fn cylinder_hidden_line_removed_matches_golden() {
    check_golden(
        &cylinder_frame(DisplayMode::HiddenLineRemoved),
        "tests/goldens/hidden_line_removed_cylinder_256.png",
        UPDATE_ENV,
    );
}

#[test]
fn cube_wireframe_matches_golden() {
    check_golden(
        &cube_frame(DisplayMode::Wireframe),
        "tests/goldens/wireframe_256.png",
        DISPLAY_ENV,
    );
}

#[test]
fn cylinder_wireframe_matches_golden() {
    check_golden(
        &cylinder_frame(DisplayMode::Wireframe),
        "tests/goldens/wireframe_cylinder_256.png",
        DISPLAY_ENV,
    );
}

#[test]
fn cube_hidden_line_gray_matches_golden() {
    check_golden(
        &cube_frame(DisplayMode::HiddenLineGray),
        "tests/goldens/hidden_line_gray_256.png",
        DISPLAY_ENV,
    );
}

#[test]
fn cylinder_hidden_line_gray_matches_golden() {
    check_golden(
        &cylinder_frame(DisplayMode::HiddenLineGray),
        "tests/goldens/hidden_line_gray_cylinder_256.png",
        DISPLAY_ENV,
    );
}

#[test]
fn cube_shaded_no_edges_matches_golden() {
    check_golden(
        &cube_frame(DisplayMode::ShadedNoEdges),
        "tests/goldens/shaded_no_edges_256.png",
        DISPLAY_ENV,
    );
}

#[test]
fn cylinder_shaded_no_edges_matches_golden() {
    check_golden(
        &cylinder_frame(DisplayMode::ShadedNoEdges),
        "tests/goldens/shaded_no_edges_cylinder_256.png",
        DISPLAY_ENV,
    );
}

#[test]
fn cube_genuine_edges_classify_as_creases() {
    let solid = unit_cube();
    let mesh = mesh_of(&solid);
    let Ok(edges) = EdgeScene::from_solid(&solid, &mesh, ChordHeightTolerance::from_mm(CHORD_MM))
    else {
        panic!("the cube packs edge pick ids");
    };
    assert!(!edges.genuine().is_empty(), "the cube emits genuine edges");
    let right_angle = core::f64::consts::FRAC_PI_2;
    assert!(
        edges
            .genuine()
            .iter()
            .all(|edge| (edge.crease().radians() - right_angle).abs() < 1.0e-6),
        "every cube edge is a 90-degree crease",
    );
}

#[test]
fn cube_flat_faces_emit_no_silhouette_candidates() {
    let solid = unit_cube();
    let mesh = mesh_of(&solid);
    let Ok(edges) = EdgeScene::from_solid(&solid, &mesh, ChordHeightTolerance::from_mm(CHORD_MM))
    else {
        panic!("the cube packs edge pick ids");
    };
    assert!(
        edges.silhouettes().is_empty(),
        "coplanar interior edges never straddle, so a flat-faced cube yields no silhouette candidates",
    );
}

#[test]
fn cylinder_side_yields_silhouette_candidates() {
    let solid = cylinder();
    let mesh = mesh_of(&solid);
    let Ok(edges) = EdgeScene::from_solid(&solid, &mesh, ChordHeightTolerance::from_mm(CHORD_MM))
    else {
        panic!("the cylinder packs edge pick ids");
    };
    assert!(
        !edges.silhouettes().is_empty(),
        "the tessellated cylinder side yields silhouette candidates",
    );
    assert!(
        edges
            .silhouettes()
            .iter()
            .any(|candidate| candidate.normal_a() != candidate.normal_b()),
        "neighboring side facets disagree in orientation so a silhouette can form",
    );
}
