use core::time::Duration;

use bone_kernel::{
    BrepSolid, Curve2Kind, ExtrudeFeature, ExtrudeProfile, Line2, ProfileEdge, ProfileLoop,
    evaluate_extrude,
};
use bone_render::{
    CameraTween, EdgeScene, OffscreenContext, SnapshotFrame, SolidRenderer, SolidScene, Style,
    frame_standard_view, single_body_pick,
};
use bone_types::{
    Aabb3, AngleTolerance, Camera3, ChordHeightTolerance, CubicEasing, DisplayMode, FeatureId,
    Length, Point2, Point3, PositiveLength, SketchEntityId, SketchId, StandardView, Tolerance,
    UnitVec3, millimeter,
};
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 CHORD_MM: f64 = 0.05;
const ANGLE_RAD: f64 = 0.2;
const UPDATE_ENV: &str = "BONE_UPDATE_STANDARD_VIEW_GOLDENS";

fn slab() -> BrepSolid {
    let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
    let mut features: SlotMap<FeatureId, ()> = SlotMap::with_key();
    let Ok(plane) = bone_types::Plane3::new(
        Point3::origin(),
        UnitVec3::x_axis(),
        UnitVec3::y_axis(),
        TOLERANCE,
    ) else {
        panic!("x and y axes are orthonormal");
    };
    let corners = [
        Point2::from_mm(0.0, 0.0),
        Point2::from_mm(2.0, 0.0),
        Point2::from_mm(2.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(plane, vec![ProfileLoop::Open(edges)]);
    let Ok(depth) = PositiveLength::new(Length::new::<millimeter>(3.0)) else {
        panic!("3 mm is a positive length");
    };
    let feature = ExtrudeFeature::blind(SketchId::null(), depth);
    let Ok(solid) = evaluate_extrude(features.insert(()), &profile, &feature) else {
        panic!("the rectangle extrudes into a 2x1x3 slab");
    };
    solid
}

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

fn scenes(solid: &BrepSolid) -> (SolidScene, EdgeScene) {
    let Ok(mesh) = solid.tessellate(
        ChordHeightTolerance::from_mm(CHORD_MM),
        AngleTolerance::from_radians(ANGLE_RAD),
    ) else {
        panic!("the solid tessellates");
    };
    let Ok((body, table)) = single_body_pick(solid) else {
        panic!("the solid builds a pick table");
    };
    let Ok(faces) = SolidScene::from_mesh(&mesh, body, &table) else {
        panic!("the mesh packs face pick ids");
    };
    let Ok(edges) = EdgeScene::from_solid(
        solid,
        &mesh,
        ChordHeightTolerance::from_mm(CHORD_MM),
        body,
        &table,
    ) else {
        panic!("the solid packs edge pick ids");
    };
    (faces, edges)
}

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

fn view(solid: &BrepSolid, which: StandardView, size: bone_render::ViewportExtent) -> Camera3 {
    let Ok(camera) = frame_standard_view(aabb_of(solid), size, which, None) else {
        panic!("a fixed standard view frames the slab");
    };
    camera
}

#[test]
fn front_view_matches_golden() {
    let size = extent(256);
    let ctx = make_context(size);
    let solid = slab();
    let (faces, edges) = scenes(&solid);
    let frame = render(
        &ctx,
        &faces,
        &edges,
        view(&solid, StandardView::Front, size),
    );
    check_golden(
        &frame,
        "tests/goldens/standard_view_front_256.png",
        UPDATE_ENV,
    );
}

#[test]
fn top_view_matches_golden() {
    let size = extent(256);
    let ctx = make_context(size);
    let solid = slab();
    let (faces, edges) = scenes(&solid);
    let frame = render(&ctx, &faces, &edges, view(&solid, StandardView::Top, size));
    check_golden(
        &frame,
        "tests/goldens/standard_view_top_256.png",
        UPDATE_ENV,
    );
}

#[test]
fn right_view_matches_golden() {
    let size = extent(256);
    let ctx = make_context(size);
    let solid = slab();
    let (faces, edges) = scenes(&solid);
    let frame = render(
        &ctx,
        &faces,
        &edges,
        view(&solid, StandardView::Right, size),
    );
    check_golden(
        &frame,
        "tests/goldens/standard_view_right_256.png",
        UPDATE_ENV,
    );
}

#[test]
fn front_to_top_tween_midpoint_matches_golden() {
    let size = extent(256);
    let ctx = make_context(size);
    let solid = slab();
    let (faces, edges) = scenes(&solid);
    let tween = CameraTween::eased(
        view(&solid, StandardView::Front, size),
        view(&solid, StandardView::Top, size),
        Duration::from_millis(180),
        CubicEasing::STANDARD,
    );
    let Ok(mid) = tween.sample(Duration::from_millis(90)) else {
        panic!("the tween samples a valid midpoint camera");
    };
    let frame = render(&ctx, &faces, &edges, mid);
    check_golden(
        &frame,
        "tests/goldens/view_tween_front_to_top_mid_256.png",
        UPDATE_ENV,
    );
}
