use bone_document::{
    DimensionKind, EditOutcome, Sketch, SketchDimension, SketchEdit, SketchEntity, SketchRelation,
    evaluate_extrude, evaluate_sketch,
};
use bone_kernel::{
    ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature, ExtrudeSense, MergeResult,
};
use bone_render::{
    Camera2, OffscreenContext, SketchRenderer, SketchScene, SolidRenderer, SolidScene, Style,
};
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_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:?}");
}

fn reference_rectangle() -> Sketch {
    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));
    add_linear_dim(s, p0, p1, 10.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 {
        sketch: SketchId::null(),
        direction: ExtrudeDirection::Normal {
            sense: ExtrudeSense::Forward,
        },
        end_condition: ExtrudeEndCondition::Blind { depth },
        draft: None,
        thin_wall: None,
        merge_result: MergeResult::Merge,
    }
}

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: &Sketch, 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: &Sketch, 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(scene) = SolidScene::from_mesh(&mesh) 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:?}",
    );
}
