use bone_document::{
    DimensionKind, EditOutcome, Sketch, SketchDimension, SketchEdit, SketchEntity, SketchRelation,
};
use bone_render::{Camera2, OffscreenContext, SketchRenderer, SketchScene, Style};
use bone_types::{
    BudgetCeiling, Point2, Point3, SketchEntityId, SketchPlaneBasis, Tolerance, UnitVec3,
};
use std::time::{Duration, Instant};
use uom::si::f64::Length as UomLength;
use uom::si::length::millimeter;

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_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 {
    let Ok((next, _)) = s.apply(SketchEdit::AddEntity(SketchEntity::circle(
        center,
        UomLength::new::<millimeter>(radius_mm),
        false,
    ))) else {
        panic!("add circle");
    };
    next
}

fn add_arc(
    s: Sketch,
    center: SketchEntityId,
    start: SketchEntityId,
    end: SketchEntityId,
) -> Sketch {
    let Ok((next, _)) = s.apply(SketchEdit::AddEntity(SketchEntity::arc(
        center, start, end, false,
    ))) else {
        panic!("add arc");
    };
    next
}

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

fn add_linear_dim(s: Sketch, a: SketchEntityId, b: SketchEntityId, value_mm: f64) -> Sketch {
    let Ok((next, _)) = s.apply(SketchEdit::AddDimension(SketchDimension::Linear {
        a,
        b,
        value: UomLength::new::<millimeter>(value_mm),
        kind: DimensionKind::Driving,
    })) else {
        panic!("add linear dimension");
    };
    next
}

fn reference_sketch() -> (Sketch, SketchEntityId) {
    let s = Sketch::new(xy_plane());
    let (s, p0) = add_point(s, 0.0, 0.0);
    let (s, p1) = add_point(s, 10.0, 0.0);
    let (s, p2) = add_point(s, 10.0, 5.0);
    let (s, p3) = add_point(s, 0.0, 5.0);
    let (s, e_bottom) = add_line(s, p0, p1);
    let (s, e_right) = add_line(s, p1, p2);
    let (s, e_top) = add_line(s, p2, p3);
    let (s, e_left) = add_line(s, p3, p0);
    let s = add_relation(s, SketchRelation::Horizontal(e_bottom));
    let s = add_relation(s, SketchRelation::Horizontal(e_top));
    let s = add_relation(s, SketchRelation::Vertical(e_right));
    let s = add_relation(s, SketchRelation::Vertical(e_left));
    let s = add_relation(s, SketchRelation::Fix(p0));
    let s = add_linear_dim(s, p0, p1, 10.0);
    let s = add_circle(s, p0, 5.0);
    let (s, arc_center) = add_point(s, -12.0, 0.0);
    let (s, arc_start) = add_point(s, -8.0, 0.0);
    let (s, arc_end) = add_point(s, -12.0, 4.0);
    let s = add_arc(s, arc_center, arc_start, arc_end);
    (s, p2)
}

#[allow(
    clippy::too_many_arguments,
    reason = "stepper bundles per-frame pipeline state alongside per-step drag inputs"
)]
fn drag_resolve_render(
    current: &Sketch,
    dragged: SketchEntityId,
    target: Point2,
    solver_budget: BudgetCeiling,
    renderer: &mut SketchRenderer,
    ctx: &OffscreenContext,
    camera: Camera2,
    style: &Style,
) -> (Sketch, Duration) {
    let started = Instant::now();
    let Ok(next) = current.solve_with_drag(dragged, target, solver_budget) else {
        panic!("drag must converge inside solver budget");
    };
    let Ok(scene) = SketchScene::extract(&next) 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 (sketch, dragged) = reference_sketch();
    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,
        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,
                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:?}");
}
