use bone_kernel::{
    BrepEdge, BrepFace, BrepSolid, BrepVertex, Circle2, Curve2Kind, ExtrudeDirection,
    ExtrudeEndCondition, ExtrudeFeature, ExtrudeProfile, ExtrudeSense, MergeResult, ProfileLoop,
    evaluate_extrude,
};
use bone_render::{
    OffscreenContext, PickId, PickIndex, PickQuery, PickedItem, SnapshotFrame, SolidRenderer,
    SolidScene, Style, ViewportPx,
};
use bone_types::{
    Aabb3, AngleTolerance, Camera3, ChordHeightTolerance, FeatureId, Length, Plane3, Point2,
    Point3, PositiveLength, Projection, ShadingModel, SketchEntityId, SketchId, 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 UPDATE_ENV: &str = "BONE_UPDATE_SOLID_GOLDENS";
const CHORD: f64 = 0.05;
const ANGLE: 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 circle_loop(entities: &mut SlotMap<SketchEntityId, ()>, radius_mm: f64) -> ProfileLoop {
    let Ok(disk) = Circle2::new(
        Point2::from_mm(0.0, 0.0),
        Length::new::<millimeter>(radius_mm),
        TOLERANCE,
    ) else {
        panic!("circle radius is positive");
    };
    ProfileLoop::Closed {
        curve: Curve2Kind::Circle(disk),
        curve_entity: entities.insert(()),
    }
}

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 solid_from(loops: Vec<ProfileLoop>, depth_mm: f64) -> BrepSolid {
    let mut features: SlotMap<FeatureId, ()> = SlotMap::with_key();
    let profile = ExtrudeProfile::new(xy_plane(), loops);
    let Ok(solid) = evaluate_extrude(features.insert(()), &profile, &blind(depth_mm)) else {
        panic!("the profile and feature describe a buildable extrude");
    };
    solid
}

fn cylinder() -> BrepSolid {
    let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
    solid_from(vec![circle_loop(&mut entities, 5.0)], 10.0)
}

fn donut() -> BrepSolid {
    let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
    let outer = circle_loop(&mut entities, 10.0);
    let inner = circle_loop(&mut entities, 4.0);
    solid_from(vec![outer, inner], 6.0)
}

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 mesh_scene(solid: &BrepSolid) -> SolidScene {
    let Ok(mesh) = solid.tessellate(
        ChordHeightTolerance::from_mm(CHORD),
        AngleTolerance::from_radians(ANGLE),
    ) else {
        panic!("the solid tessellates");
    };
    let Ok(scene) = SolidScene::from_mesh(&mesh) else {
        panic!("the mesh packs face pick ids");
    };
    scene
}

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

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

#[test]
fn cylinder_front_matches_golden() {
    let ctx = make_context(extent(256));
    let solid = cylinder();
    let camera = framed(
        aabb_of(&solid),
        direction(1.0, 1.0, 0.0),
        UnitVec3::z_axis(),
    );
    let frame = render(&ctx, &mesh_scene(&solid), camera);
    check_golden(
        &frame,
        "tests/goldens/solid_cylinder_front_256.png",
        UPDATE_ENV,
    );
}

#[test]
fn donut_top_matches_golden() {
    let ctx = make_context(extent(256));
    let solid = donut();
    let camera = framed(aabb_of(&solid), UnitVec3::z_axis(), UnitVec3::y_axis());
    let frame = render(&ctx, &mesh_scene(&solid), camera);
    check_golden(&frame, "tests/goldens/solid_donut_top_256.png", UPDATE_ENV);
}

#[test]
fn solid_pass_writes_unpackable_face_pick_ids() {
    let ctx = make_context(extent(256));
    let solid = cylinder();
    let scene = mesh_scene(&solid);
    let camera = framed(
        aabb_of(&solid),
        direction(1.0, 1.0, 0.0),
        UnitVec3::z_axis(),
    );
    let _frame = render(&ctx, &scene, camera);

    let Ok(index) = PickIndex::build_solid(
        solid.iter_faces().map(BrepFace::id),
        solid.iter_edges().map(BrepEdge::id),
        solid.iter_vertices().map(BrepVertex::id),
    ) else {
        panic!("the solid's faces, edges, and vertices build a pick index");
    };
    let picker = ctx.picker(index.clone());

    let Ok(corner) = picker.raw_at(PickQuery::new(ViewportPx::new(3), ViewportPx::new(3))) else {
        panic!("the corner pixel reads back");
    };
    assert_eq!(
        corner,
        PickId::NONE,
        "background must clear to PickId::NONE, got {corner}",
    );

    let Ok(center) = picker.raw_at(PickQuery::new(ViewportPx::new(128), ViewportPx::new(128)))
    else {
        panic!("the center pixel reads back");
    };
    let Some(item) = center.unpack(&index) else {
        panic!("center pick {center} did not resolve against the solid");
    };
    assert!(
        matches!(item, PickedItem::BrepFace(_)),
        "the center of the cylinder must pick a B-rep face, got {item:?}",
    );
}

#[test]
fn phong_variant_adds_specular_highlight() {
    let ctx = make_context(extent(128));
    let solid = cylinder();
    let scene = mesh_scene(&solid);
    let camera = framed(
        aabb_of(&solid),
        direction(-0.302, -0.503, 0.809),
        UnitVec3::z_axis(),
    );
    let style = Style::default();
    let mut lit = SolidRenderer::new(ctx.gpu(), ctx.color_format());
    let mut phong =
        SolidRenderer::new(ctx.gpu(), ctx.color_format()).with_shading_model(ShadingModel::Phong);
    let Ok(default_frame) = lit.render(&ctx, &scene, camera, &style) else {
        panic!("default render failed");
    };
    let Ok(phong_frame) = phong.render(&ctx, &scene, camera, &style) else {
        panic!("phong render failed");
    };
    assert_ne!(
        default_frame.rgba(),
        phong_frame.rgba(),
        "the Phong specular highlight must change the shaded output",
    );
}
