use bone_document::{Sketch3, Sketch3Entity, Sketch3Relation, evaluate_extrude, evaluate_sketch};
use bone_kernel::ExtrudeFeature;
use bone_render::{
    Camera2, OffscreenContext, SketchRenderer, SketchScene, SolidRenderer, SolidScene, Style,
    single_body_pick,
};
use bone_types::{
    Aabb3, AngleTolerance, BudgetCeiling, Camera3, ChordHeightTolerance, FeatureId, Point2, Point3,
    PositiveLength, Projection, SketchEntityId, SketchId, SketchPlaneBasis, Tolerance, UnitVec3,
};
use slotmap::{Key, SlotMap};
use std::time::{Duration, Instant};
use uom::si::f64::Length as UomLength;
use uom::si::length::millimeter;

const TOLERANCE: Tolerance = Tolerance::new(1.0e-9);
const CHORD: f64 = 0.05;
const ANGLE: f64 = 0.2;
const BASE_DEPTH_MM: f64 = 4.0;
const DEPTH_STEP_MM: f64 = 0.5;

mod common;

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

fn xy_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_rel3(s: &Sketch3, r: Sketch3Relation) -> Sketch3 {
    s.with_relation(r).0
}

fn reference_sketch3() -> (Sketch3, SketchEntityId) {
    let plane = xy_plane();
    let (s, p0) = add_point3(&Sketch3::on_plane(plane), 0.0, 0.0);
    let (s, p1) = add_point3(&s, 10.0, 0.0);
    let (s, p2) = add_point3(&s, 10.0, 5.0);
    let (s, p3) = add_point3(&s, 0.0, 5.0);
    let (s, e_bottom) = add_line3(&s, p0, p1);
    let (s, e_right) = add_line3(&s, p1, p2);
    let (s, e_top) = add_line3(&s, p2, p3);
    let (s, e_left) = add_line3(&s, p3, p0);
    let s = add_rel3(
        &s,
        Sketch3Relation::AlongAxis {
            line: e_bottom,
            axis: plane.x_axis(),
        },
    );
    let s = add_rel3(
        &s,
        Sketch3Relation::AlongAxis {
            line: e_top,
            axis: plane.x_axis(),
        },
    );
    let s = add_rel3(
        &s,
        Sketch3Relation::AlongAxis {
            line: e_right,
            axis: plane.y_axis(),
        },
    );
    let s = add_rel3(
        &s,
        Sketch3Relation::AlongAxis {
            line: e_left,
            axis: plane.y_axis(),
        },
    );
    let s = add_rel3(&s, Sketch3Relation::Fix(p0));
    let s = add_rel3(
        &s,
        Sketch3Relation::Distance {
            a: p0,
            b: p1,
            value: UomLength::new::<millimeter>(10.0),
            reference: false,
        },
    );
    let (s, _circle) = s.with_entity(Sketch3Entity::circle(
        p0,
        UomLength::new::<millimeter>(5.0),
        plane.normal(),
        false,
    ));
    let (s, arc_center) = add_point3(&s, -12.0, 0.0);
    let (s, arc_start) = add_point3(&s, -8.0, 0.0);
    let (s, arc_end) = add_point3(&s, -12.0, 4.0);
    let (s, _arc) = s.with_entity(Sketch3Entity::arc(
        arc_center,
        arc_start,
        arc_end,
        plane.normal(),
        false,
    ));
    (s, p2)
}

#[allow(
    clippy::too_many_arguments,
    reason = "stepper bundles per-frame pipeline state alongside per-step drag inputs"
)]
fn drag_resolve_render(
    current: &Sketch3,
    plane: SketchPlaneBasis,
    dragged: SketchEntityId,
    target: Point2,
    solver_budget: BudgetCeiling,
    renderer: &mut SketchRenderer,
    ctx: &OffscreenContext,
    camera: Camera2,
    style: &Style,
) -> (Sketch3, Duration) {
    let started = Instant::now();
    let (tx, ty) = target.coords_mm();
    let Ok(next) =
        current.solve_with_drag_pins(&[(dragged, Point3::from_mm(tx, ty, 0.0))], solver_budget)
    else {
        panic!("drag must converge inside solver budget");
    };
    let projected = next.project_onto(plane);
    let Ok(scene) = SketchScene::extract(projected.sketch()) else {
        panic!("scene extract must succeed for solved sketch");
    };
    let Ok(_frame) = renderer.render(ctx, &scene, camera, style) else {
        panic!("render must succeed for solved sketch");
    };
    (next, started.elapsed())
}

const DRAG_STEPS: u32 = 16;

#[test]
#[cfg_attr(
    debug_assertions,
    ignore = "frame budget assertions are only meaningful in release builds"
)]
fn drag_resolve_plus_render_fits_frame_budget_on_reference_sketch() {
    let plane = xy_plane();
    let (sketch, dragged) = reference_sketch3();
    let Ok(sketch) = sketch.solve() else {
        panic!("reference sketch must solve at rest");
    };
    let size = extent(256);
    let ctx = make_context(size);
    let mut renderer = SketchRenderer::new(ctx.gpu(), ctx.color_format());
    let camera = Camera2::new(size);
    let style = Style::default();
    let frame_budget = Duration::from_millis(16);
    let worst_ceiling = frame_budget * 2;
    let solver_budget = BudgetCeiling::new(frame_budget / 2_u32);
    let warmup_target = Point2::from_mm(10.0, 4.0);
    let (warmed, _warmup_elapsed) = drag_resolve_render(
        &sketch,
        plane,
        dragged,
        warmup_target,
        solver_budget,
        &mut renderer,
        &ctx,
        camera,
        &style,
    );
    let (_final_sketch, durations) = (0..DRAG_STEPS).fold(
        (warmed, Vec::<Duration>::new()),
        |(current, durations), i| {
            let target = Point2::from_mm(10.0, 5.0 + 0.1 * f64::from(i));
            let (next, elapsed) = drag_resolve_render(
                &current,
                plane,
                dragged,
                target,
                solver_budget,
                &mut renderer,
                &ctx,
                camera,
                &style,
            );
            let next_durations = durations.into_iter().chain([elapsed]).collect();
            (next, next_durations)
        },
    );
    let sorted = {
        let mut v = durations.clone();
        v.sort();
        v
    };
    let median = sorted[sorted.len() / 2];
    let Some(&worst) = sorted.last() else {
        panic!("drag loop produced zero samples");
    };
    assert!(
        median <= frame_budget,
        "median drag+render step {median:?} exceeds {frame_budget:?} frame budget; samples {durations:?}",
    );
    assert!(
        worst <= worst_ceiling,
        "worst drag+render step {worst:?} exceeds {worst_ceiling:?} relaxed ceiling; samples {durations:?}",
    );
    println!("median {median:?}, worst {worst:?}");
}

fn add_point3(s: &Sketch3, x: f64, y: f64) -> (Sketch3, SketchEntityId) {
    s.with_entity(Sketch3Entity::point(Point3::from_mm(x, y, 0.0)))
}

fn add_line3(s: &Sketch3, a: SketchEntityId, b: SketchEntityId) -> (Sketch3, SketchEntityId) {
    s.with_entity(Sketch3Entity::line(a, b, false))
}

fn reference_rectangle() -> Sketch3 {
    let (s, p0) = add_point3(&Sketch3::on_plane(xy_plane()), 0.0, 0.0);
    let (s, p1) = add_point3(&s, 10.0, 0.0);
    let (s, p2) = add_point3(&s, 10.0, 5.0);
    let (s, p3) = add_point3(&s, 0.0, 5.0);
    let (s, _) = add_line3(&s, p0, p1);
    let (s, _) = add_line3(&s, p1, p2);
    let (s, _) = add_line3(&s, p2, p3);
    add_line3(&s, p3, p0).0
}

fn blind(depth_mm: f64) -> ExtrudeFeature {
    let Ok(depth) = PositiveLength::new(UomLength::new::<millimeter>(depth_mm)) else {
        panic!("{depth_mm} mm is a positive length");
    };
    ExtrudeFeature::blind(SketchId::null(), depth)
}

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(UomLength::new::<millimeter>(span * 3.0));
    let Ok(projection) = Projection::orthographic(UomLength::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 frame_slab(sketch: &Sketch3, extrude: FeatureId, depth_mm: f64) -> Camera3 {
    let evaluated = evaluate_sketch(sketch);
    let extruded = evaluate_extrude(extrude, &evaluated, &blind(depth_mm));
    let Some(solid) = extruded.solid() else {
        panic!("the reference rectangle extrudes into a slab");
    };
    let Some(aabb) = solid.bounding_box() else {
        panic!("the slab has a bounding box");
    };
    framed(aabb, direction(1.0, 1.0, 1.0), UnitVec3::z_axis())
}

struct SolidStage<'a> {
    renderer: &'a mut SolidRenderer,
    ctx: &'a OffscreenContext,
    camera: Camera3,
    style: &'a Style,
}

impl SolidStage<'_> {
    fn step(&mut self, sketch: &Sketch3, extrude: FeatureId, depth_mm: f64) -> Duration {
        let started = Instant::now();
        let evaluated = evaluate_sketch(sketch);
        let extruded = evaluate_extrude(extrude, &evaluated, &blind(depth_mm));
        let Some(solid) = extruded.solid() else {
            panic!("the slab evaluates at {depth_mm} mm");
        };
        let Ok(mesh) = solid.tessellate(
            ChordHeightTolerance::from_mm(CHORD),
            AngleTolerance::from_radians(ANGLE),
        ) else {
            panic!("the slab tessellates at {depth_mm} mm");
        };
        let Ok((body, table)) = single_body_pick(solid) else {
            panic!("the slab builds a pick table");
        };
        let Ok(scene) = SolidScene::from_mesh(&mesh, body, &table) else {
            panic!("the slab mesh packs face pick ids");
        };
        let Ok(_frame) = self
            .renderer
            .render(self.ctx, &scene, self.camera, self.style)
        else {
            panic!("solid render must succeed at {depth_mm} mm");
        };
        started.elapsed()
    }
}

#[test]
#[cfg_attr(
    debug_assertions,
    ignore = "frame budget assertions are only meaningful in release builds"
)]
fn drag_resolve_plus_render_fits_frame_budget_on_reference_extrude() {
    let rectangle = reference_rectangle();
    let mut features: SlotMap<FeatureId, ()> = SlotMap::with_key();
    let extrude = features.insert(());
    let size = extent(256);
    let ctx = make_context(size);
    let mut renderer = SolidRenderer::new(ctx.gpu(), ctx.color_format());
    let style = Style::default();
    let max_depth = BASE_DEPTH_MM + DEPTH_STEP_MM * f64::from(DRAG_STEPS - 1);
    let camera = frame_slab(&rectangle, extrude, max_depth);
    let mut stage = SolidStage {
        renderer: &mut renderer,
        ctx: &ctx,
        camera,
        style: &style,
    };
    let _warmup = stage.step(&rectangle, extrude, BASE_DEPTH_MM);
    let durations: Vec<Duration> = (0..DRAG_STEPS)
        .map(|i| {
            let depth_mm = BASE_DEPTH_MM + DEPTH_STEP_MM * f64::from(i);
            stage.step(&rectangle, extrude, depth_mm)
        })
        .collect();
    let sorted = {
        let mut v = durations.clone();
        v.sort();
        v
    };
    let median = sorted[sorted.len() / 2];
    let Some(&worst) = sorted.last() else {
        panic!("drag loop produced zero samples");
    };
    let budget = BudgetCeiling::FRAME_16MS.duration();
    let worst_ceiling = budget * 2;
    if std::env::var("BONE_FRAME_BUDGET_REPORT").is_ok() {
        println!(
            "reference extrude drag: median {median:?}, worst {worst:?}, samples {durations:?}"
        );
    }
    assert!(
        median <= budget,
        "median solve+extrude+tessellate+render step {median:?} exceeds {budget:?} frame budget; samples {durations:?}",
    );
    assert!(
        worst <= worst_ceiling,
        "worst solve+extrude+tessellate+render step {worst:?} exceeds {worst_ceiling:?} relaxed ceiling; samples {durations:?}",
    );
}
