//! CPU-side hit-testing for wire geometry.
//!
//! All tests are performed in **screen space** — wire vertices are projected
//! to 2-D pixel coordinates, then compared against the cursor or selection box.
//! This matches the visual result the user sees.

use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};

use acadrust::Handle;
use glam::Mat4;
use iced::{Point, Rectangle};

use crate::scene::model::hatch_model::HatchModel;
use crate::scene::model::mesh_model::MeshModel;
use crate::scene::model::wire_model::WireModel;
use crate::scene::pick::interaction_index::WireSource;

/// Pixel radius used for single-click wire detection.
pub const CLICK_THRESHOLD_PX: f32 = 8.0;

/// Pick radius for one wire, in screen pixels.
///
/// A wire renders as a band `line_weight_px` wide, so testing every wire at the
/// bare [`CLICK_THRESHOLD_PX`] would leave the outer part of a heavy line
/// unselectable — the cursor would sit on solid ink and miss. Widening to the
/// rendered half-width keeps "looks like I'm on it" and "picks it" the same
/// thing at any zoom: both quantities are screen-space, so the relation holds
/// however far in the view is.
///
/// `lw_display` mirrors the wire shader's `select(0.5, half_width, ...)`
/// (`wire.wgsl`) — with lineweight display off the line collapses to 1 px, so
/// the pick band must collapse with it rather than stay secretly fat.
///
/// The standard weights all land under the threshold today (the widest, 2.11 mm,
/// renders 7.97 px half-width), so this only bites for out-of-range weights —
/// and it keeps the two sides from silently drifting apart if the display boost
/// in `view::render::lineweight_to_px` ever changes.
pub fn pick_tolerance_px(wire: &WireModel, lw_display: bool) -> f32 {
    let half_width = if lw_display {
        wire.line_weight_px * 0.5
    } else {
        0.5
    };
    CLICK_THRESHOLD_PX.max(half_width)
}

/// Is `aabb` — a wire's world-space XY box — further than `tol` pixels from
/// `cursor` once projected, so the wire can be skipped without touching its
/// geometry?
///
/// Only sound in a flat (untilted) view, where a point's screen x/y depends on
/// its world x/y alone and the box therefore projects exactly. Callers must
/// check that themselves, and must skip the unbounded sentinel.
fn aabb_rejects(
    aabb: [f32; 4],
    cursor: Point,
    tol: f32,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
) -> bool {
    let [minx, miny, maxx, maxy] = aabb;
    // Project all four corners — a plan view can be rotated about Z, so the
    // screen footprint isn't axis-aligned and the two diagonal corners alone
    // wouldn't bound it.
    let (mut sx0, mut sy0, mut sx1, mut sy1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
    for (cx, cy) in [(minx, miny), (maxx, miny), (maxx, maxy), (minx, maxy)] {
        let s = world_to_screen(
            glam::DVec3::new(cx as f64, cy as f64, 0.0),
            view_rot,
            eye,
            bounds,
        );
        sx0 = sx0.min(s.x);
        sx1 = sx1.max(s.x);
        sy0 = sy0.min(s.y);
        sy1 = sy1.max(s.y);
    }
    cursor.x < sx0 - tol || cursor.x > sx1 + tol || cursor.y < sy0 - tol || cursor.y > sy1 + tol
}

/// Depth of the first triangle in `tris` whose screen projection contains
/// `cursor`, as the mean NDC z of its corners; `None` when none do.
///
/// `tris` is a flat vertex list, 3 per triangle, and `tris_low` its
/// double-single residual — empty meaning an all-zero low half, per the
/// [`WireModel`] contract.
fn tris_hit_depth(
    cursor: Point,
    tris: &[[f32; 3]],
    tris_low: &[[f32; 3]],
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
) -> Option<f32> {
    let mut t = 0;
    while t + 2 < tris.len() {
        let mut sp = [Point::ORIGIN; 3];
        let mut depth = 0.0f32;
        for j in 0..3 {
            let k = t + j;
            let hi = tris[k];
            let lo = tris_low.get(k).copied().unwrap_or([0.0; 3]);
            let world = glam::DVec3::new(
                hi[0] as f64 + lo[0] as f64,
                hi[1] as f64 + lo[1] as f64,
                hi[2] as f64 + lo[2] as f64,
            );
            let ndc = view_rot.project_point3((world - eye).as_vec3());
            sp[j] = Point::new(
                (ndc.x + 1.0) * 0.5 * bounds.width,
                (1.0 - ndc.y) * 0.5 * bounds.height,
            );
            depth += ndc.z;
        }
        t += 3;
        if point_in_polygon(cursor, &sp) {
            return Some(depth / 3.0);
        }
    }
    None
}

fn triangle_ref_hit_depth(
    cursor: Point,
    wire: &WireModel,
    start: usize,
    pick_only: bool,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
) -> Option<f32> {
    let (points, low) = if pick_only {
        (&wire.pick_tris, &wire.pick_tris_low)
    } else {
        (&wire.fill_tris, &wire.fill_tris_low)
    };
    let points = points.get(start..start + 3)?;
    let mut tri = [[0.0; 3]; 3];
    let mut tri_low = [[0.0; 3]; 3];
    tri.copy_from_slice(points);
    for (dst, index) in tri_low.iter_mut().zip(start..start + 3) {
        *dst = low.get(index).copied().unwrap_or([0.0; 3]);
    }
    tris_hit_depth(cursor, &tri, &tri_low, view_rot, eye, bounds)
}

/// Screen-space area of the smallest SDF glyph quad containing `cursor`.
///
/// Block expansion batches same-style text runs into one [`WireModel`], so the
/// wire AABB is the union of every run and may span large empty gaps (notably
/// for XREFs). Glyph vertices retain the exact six-vertex quad boundaries after
/// batching, letting picking stay tight without splitting the GPU batch.
fn text_quad_hit_area(
    cursor: Point,
    verts: &[crate::scene::pipeline::text_gpu::TextVertex],
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
) -> Option<f32> {
    let mut best = f32::MAX;
    for quad in verts.chunks_exact(6) {
        // push_glyph_vertices emits BL, BR, TR, BL, TR, TL.
        let mut screen = [Point::ORIGIN; 4];
        for (dst, src) in screen.iter_mut().zip([0usize, 1, 2, 5]) {
            let v = quad[src];
            let world = glam::DVec3::new(
                v.pos[0] as f64 + v.pos_low[0] as f64,
                v.pos[1] as f64 + v.pos_low[1] as f64,
                v.pos[2] as f64 + v.pos_low[2] as f64,
            );
            *dst = world_to_screen(world, view_rot, eye, bounds);
        }
        if point_in_polygon(cursor, &screen) {
            let (mut min_x, mut min_y, mut max_x, mut max_y) =
                (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
            for p in screen {
                min_x = min_x.min(p.x);
                min_y = min_y.min(p.y);
                max_x = max_x.max(p.x);
                max_y = max_y.max(p.y);
            }
            best = best.min((max_x - min_x) * (max_y - min_y));
        }
    }
    (best < f32::MAX).then_some(best)
}

// ── Single-click hit test ─────────────────────────────────────────────────

/// Return the `name` of the closest wire whose screen-space segments pass
/// within that wire's [`pick_tolerance_px`] of `cursor`.
///
/// Returns `None` when no wire is close enough.
pub fn click_hit<'a, W: WireSource + ?Sized>(
    cursor: Point,
    wires: &'a W,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
    lw_display: bool,
) -> Option<&'a str> {
    // A click outside the pane rectangle (e.g. on the paper around a floating
    // viewport) must not reach geometry scissored out of the viewport.
    if cursor.x < 0.0 || cursor.x > bounds.width || cursor.y < 0.0 || cursor.y > bounds.height {
        return None;
    }
    // Each wire brings its own threshold now (a heavy line catches over its full
    // rendered width), so the running best can't double as the cut-off.
    let mut best_dist = f32::MAX;
    let mut best: Option<&str> = None;

    // World z only shifts the *screen* x/y when the view is tilted (orbit /
    // perspective). In the flat top-down ortho view — the case where hover lag
    // on large drawings actually bites — a wire's screen position depends only
    // on its world x/y, so its world-space AABB projects exactly and we can
    // reject wires nowhere near the cursor without projecting any of their
    // points (the dominant per-move cost on 100 k-wire drawings).
    let z_flat = view_rot.z_axis.x.abs() < 1e-9 && view_rot.z_axis.y.abs() < 1e-9;

    if let Some(segments) = wires.segments() {
        // The interaction index already narrowed long/batched wires to segments
        // touching the cursor aperture. Hover and click therefore project only
        // those local edges, not every point of every candidate wire.
        for segment in segments {
            let Some(wire) = wires.source_wire(segment.wire) else {
                continue;
            };
            let start = segment.start as usize;
            if start + 1 >= wire.points.len() {
                continue;
            }
            let p0 = world_to_screen(
                wp64(wire.points[start], &wire.points_low, start),
                view_rot,
                eye,
                bounds,
            );
            let p1 = world_to_screen(
                wp64(wire.points[start + 1], &wire.points_low, start + 1),
                view_rot,
                eye,
                bounds,
            );
            let d = dist_point_to_segment(cursor, p0, p1);
            if d < pick_tolerance_px(wire, lw_display) && d < best_dist {
                best_dist = d;
                best = Some(&wire.name);
            }
        }
    } else {
        // Q: lazy projection — no Vec allocation per wire; NaN resets the segment chain.
        for wire in wires.iter() {
            let tol = pick_tolerance_px(wire, lw_display);
            // Cheap AABB pre-reject (flat view only; never for the unbounded
            // sentinel used by previews / greeked text).
            if z_flat
                && wire.aabb != WireModel::UNBOUNDED_AABB
                && aabb_rejects(wire.aabb, cursor, tol, view_rot, eye, bounds)
            {
                continue;
            }
            let mut prev: Option<Point> = None;
            for (i, &[px, py, pz]) in wire.points.iter().enumerate() {
                if px.is_nan() {
                    prev = None;
                    continue;
                }
                let cur = world_to_screen(
                    wp64([px, py, pz], &wire.points_low, i),
                    view_rot,
                    eye,
                    bounds,
                );
                if let Some(p0) = prev {
                    let d = dist_point_to_segment(cursor, p0, cur);
                    if d < tol && d < best_dist {
                        best_dist = d;
                        best = Some(&wire.name);
                    }
                }
                prev = Some(cur);
            }
        }
    }

    if best.is_some() {
        return best;
    }

    // No edge close enough. Mesh entities (PolyfaceMesh / PolygonMesh / SubD
    // Mesh) carry their shaded faces as `fill_tris`; test those so a mesh is
    // selectable by clicking its surface — not only its thin edges — the way a
    // 3D solid is. Same projected-triangle containment as `mesh_click_hit`,
    // front-most wins.
    let mut best_fill: Option<(f32, &str)> = None;
    if let Some(triangles) = wires.fill_triangles() {
        for triangle in triangles {
            let Some(wire) = wires.source_wire(triangle.wire) else {
                continue;
            };
            if let Some(depth) = triangle_ref_hit_depth(
                cursor,
                wire,
                triangle.start as usize,
                false,
                view_rot,
                eye,
                bounds,
            ) {
                if best_fill.is_none_or(|(best, _)| depth < best) {
                    best_fill = Some((depth, wire.name.as_str()));
                }
            }
        }
    } else {
        for wire in wires.iter() {
            if wire.fill_tris.is_empty() {
                continue;
            }
            if let Some(depth) = tris_hit_depth(
                cursor,
                &wire.fill_tris,
                &wire.fill_tris_low,
                view_rot,
                eye,
                bounds,
            ) {
                if best_fill.is_none_or(|(best, _)| depth < best) {
                    best_fill = Some((depth, wire.name.as_str()));
                }
            }
        }
    }
    if let Some((_, n)) = best_fill {
        return Some(n);
    }

    // No fill of this wire's own either. `pick_tris` closes the surfaces that
    // `points` only bounds: a thickness wall (drawn as four edges with nothing
    // between them) and a wide polyline's band (drawn, but by the hatch
    // pipeline, so no fill hangs off this wire). Without them the cursor falls
    // through what plainly reads as solid. Front-most wins.
    //
    // Ranked below `fill_tris` because that geometry is this wire's own drawn
    // surface — where the two overlap, the nearer thing to the eye is decided
    // by depth, but a wire that has a real fill should win on it first.
    let mut best_wall: Option<(f32, &str)> = None;
    if let Some(triangles) = wires.pick_triangles() {
        for triangle in triangles {
            let Some(wire) = wires.source_wire(triangle.wire) else {
                continue;
            };
            if let Some(depth) = triangle_ref_hit_depth(
                cursor,
                wire,
                triangle.start as usize,
                true,
                view_rot,
                eye,
                bounds,
            ) {
                if best_wall.is_none_or(|(best, _)| depth < best) {
                    best_wall = Some((depth, wire.name.as_str()));
                }
            }
        }
    } else {
        for wire in wires.iter() {
            if wire.pick_tris.is_empty() {
                continue;
            }
            // This runs on every hover that misses everything else — the common case
            // over empty space — and a wall is two triangles per base segment, so an
            // extruded circle alone is ~128 of them. Reject on the box first.
            if z_flat
                && wire.aabb != WireModel::UNBOUNDED_AABB
                && aabb_rejects(wire.aabb, cursor, 0.0, view_rot, eye, bounds)
            {
                continue;
            }
            if let Some(depth) = tris_hit_depth(
                cursor,
                &wire.pick_tris,
                &wire.pick_tris_low,
                view_rot,
                eye,
                bounds,
            ) {
                if best_wall.is_none_or(|(best, _)| depth < best) {
                    best_wall = Some((depth, wire.name.as_str()));
                }
            }
        }
    }
    if let Some((_, n)) = best_wall {
        return Some(n);
    }

    // SDF text renders as glyph quads, not strokes. Test those exact quads:
    // block expansion may batch distant same-style text runs into one wire,
    // whose union AABB includes empty space between them (#438). Lowest
    // priority — real edges and fills above always win.
    let mut best_area = f32::MAX;
    let mut best_box: Option<&str> = None;
    if let Some(glyphs) = wires.glyphs() {
        for glyph in glyphs {
            let Some(wire) = wires.source_wire(glyph.wire) else {
                continue;
            };
            let start = glyph.start as usize;
            let Some(vertices) = wire.text_verts.get(start..start + 6) else {
                continue;
            };
            if let Some(area) = text_quad_hit_area(cursor, vertices, view_rot, eye, bounds) {
                if area < best_area {
                    best_area = area;
                    best_box = Some(wire.name.as_str());
                }
            }
        }
    } else {
        for wire in wires.iter() {
            if wire.text_verts.is_empty() {
                continue;
            }
            if let Some(area) = text_quad_hit_area(cursor, &wire.text_verts, view_rot, eye, bounds)
            {
                if area < best_area {
                    best_area = area;
                    best_box = Some(wire.name.as_str());
                }
            }
        }
    }
    best_box
}

/// Like `click_hit` but returns every wire within the click threshold,
/// nearest first. Used by selection cycling to step through overlapping
/// objects under the cursor.
pub fn click_hits_all<'a, W: WireSource + ?Sized>(
    cursor: Point,
    wires: &'a W,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
    lw_display: bool,
) -> Vec<&'a str> {
    if cursor.x < 0.0 || cursor.x > bounds.width || cursor.y < 0.0 || cursor.y > bounds.height {
        return Vec::new();
    }
    let mut hits: Vec<(f32, &str)> = Vec::new();
    if let Some(segments) = wires.segments() {
        let mut best_by_wire: HashMap<u32, f32> = HashMap::default();
        for segment in segments {
            let Some(wire) = wires.source_wire(segment.wire) else {
                continue;
            };
            let start = segment.start as usize;
            if start + 1 >= wire.points.len() {
                continue;
            }
            let p0 = world_to_screen(
                wp64(wire.points[start], &wire.points_low, start),
                view_rot,
                eye,
                bounds,
            );
            let p1 = world_to_screen(
                wp64(wire.points[start + 1], &wire.points_low, start + 1),
                view_rot,
                eye,
                bounds,
            );
            let d = dist_point_to_segment(cursor, p0, p1);
            if d < pick_tolerance_px(wire, lw_display) {
                best_by_wire
                    .entry(segment.wire)
                    .and_modify(|best| *best = best.min(d))
                    .or_insert(d);
            }
        }
        hits.extend(best_by_wire.into_iter().filter_map(|(wire_idx, distance)| {
            wires
                .source_wire(wire_idx)
                .map(|wire| (distance, wire.name.as_str()))
        }));
    } else {
        for wire in wires.iter() {
            let tol = pick_tolerance_px(wire, lw_display);
            let mut prev: Option<Point> = None;
            let mut best_for_wire = tol;
            let mut hit = false;
            for (i, &[px, py, pz]) in wire.points.iter().enumerate() {
                if px.is_nan() {
                    prev = None;
                    continue;
                }
                let cur = world_to_screen(
                    wp64([px, py, pz], &wire.points_low, i),
                    view_rot,
                    eye,
                    bounds,
                );
                if let Some(p0) = prev {
                    let d = dist_point_to_segment(cursor, p0, cur);
                    if d < best_for_wire {
                        best_for_wire = d;
                        hit = true;
                    }
                }
                prev = Some(cur);
            }
            if hit {
                hits.push((best_for_wire, &wire.name));
            }
        }
    }
    // Filled mesh faces join the cycle too, matching `click_hit`. Ranked at
    // threshold distance because face depth is not comparable to edge distance.
    if let Some(triangles) = wires.fill_triangles() {
        let mut matched: HashSet<u32> = HashSet::default();
        for triangle in triangles {
            if matched.contains(&triangle.wire) {
                continue;
            }
            let Some(wire) = wires.source_wire(triangle.wire) else {
                continue;
            };
            if hits.iter().any(|&(_, name)| name == wire.name) {
                matched.insert(triangle.wire);
                continue;
            }
            if triangle_ref_hit_depth(
                cursor,
                wire,
                triangle.start as usize,
                false,
                view_rot,
                eye,
                bounds,
            )
            .is_some()
            {
                hits.push((CLICK_THRESHOLD_PX, wire.name.as_str()));
                matched.insert(triangle.wire);
            }
        }
    } else {
        for wire in wires.iter() {
            if wire.fill_tris.is_empty() || hits.iter().any(|&(_, name)| name == wire.name) {
                continue;
            }
            if tris_hit_depth(
                cursor,
                &wire.fill_tris,
                &wire.fill_tris_low,
                view_rot,
                eye,
                bounds,
            )
            .is_some()
            {
                hits.push((CLICK_THRESHOLD_PX, wire.name.as_str()));
            }
        }
    }

    // Thickness walls join the cycle so an extruded entity picked on its wall
    // can be stepped past to whatever sits behind it. Ranked at the threshold,
    // below every proximity hit — same convention the text boxes below use.
    //
    // A wall's own edges live on the same wire, so skip any wire the loop above
    // already caught: cycling must not offer one entity twice.
    if let Some(triangles) = wires.pick_triangles() {
        let mut matched: HashSet<u32> = HashSet::default();
        for triangle in triangles {
            if matched.contains(&triangle.wire) {
                continue;
            }
            let Some(wire) = wires.source_wire(triangle.wire) else {
                continue;
            };
            if hits.iter().any(|&(_, name)| name == wire.name) {
                matched.insert(triangle.wire);
                continue;
            }
            if triangle_ref_hit_depth(
                cursor,
                wire,
                triangle.start as usize,
                true,
                view_rot,
                eye,
                bounds,
            )
            .is_some()
            {
                hits.push((CLICK_THRESHOLD_PX, wire.name.as_str()));
                matched.insert(triangle.wire);
            }
        }
    } else {
        for wire in wires.iter() {
            if wire.pick_tris.is_empty() || hits.iter().any(|&(_, name)| name == wire.name) {
                continue;
            }
            if tris_hit_depth(
                cursor,
                &wire.pick_tris,
                &wire.pick_tris_low,
                view_rot,
                eye,
                bounds,
            )
            .is_some()
            {
                hits.push((CLICK_THRESHOLD_PX, wire.name.as_str()));
            }
        }
    }
    // SDF text: use the same exact glyph-quad test as `click_hit`; a batched
    // text wire's union AABB may cover empty space between distant runs (#438).
    // Ranked after real geometry (distance = the click threshold).
    if let Some(glyphs) = wires.glyphs() {
        let mut matched: HashSet<u32> = HashSet::default();
        for glyph in glyphs {
            if matched.contains(&glyph.wire) {
                continue;
            }
            let Some(wire) = wires.source_wire(glyph.wire) else {
                continue;
            };
            if hits.iter().any(|&(_, name)| name == wire.name) {
                matched.insert(glyph.wire);
                continue;
            }
            let start = glyph.start as usize;
            let Some(vertices) = wire.text_verts.get(start..start + 6) else {
                continue;
            };
            if text_quad_hit_area(cursor, vertices, view_rot, eye, bounds).is_some() {
                hits.push((CLICK_THRESHOLD_PX, wire.name.as_str()));
                matched.insert(glyph.wire);
            }
        }
    } else {
        for wire in wires.iter() {
            if wire.text_verts.is_empty() || hits.iter().any(|&(_, name)| name == wire.name) {
                continue;
            }
            if text_quad_hit_area(cursor, &wire.text_verts, view_rot, eye, bounds).is_some() {
                hits.push((CLICK_THRESHOLD_PX, wire.name.as_str()));
            }
        }
    }
    hits.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
    hits.into_iter().map(|(_, name)| name).collect()
}

pub fn mesh_click_hit<'a>(
    cursor: Point,
    meshes: impl Iterator<Item = (Handle, &'a MeshModel)>,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
) -> Option<Handle> {
    let mut best: Option<(f32, Handle)> = None;
    for (handle, mesh) in meshes {
        let v = &mesh.verts;
        let idx = &mesh.indices;
        let lo = &mesh.verts_low;
        // Indexed meshes reuse most vertices across several triangles.
        // Project once per vertex instead of three matrix transforms per
        // triangle; dense solid misses are otherwise the worst-case hover.
        let projected: Vec<(Point, f32)> = v
            .iter()
            .enumerate()
            .map(|(i, &vertex)| {
                let ndc =
                    view_rot.project_point3((mesh_vert(vertex, lo, i) - eye).as_vec3());
                (
                    Point::new(
                        (ndc.x + 1.0) * 0.5 * bounds.width,
                        (1.0 - ndc.y) * 0.5 * bounds.height,
                    ),
                    ndc.z,
                )
            })
            .collect();
        let mut t = 0;
        while t + 2 < idx.len() {
            let tri = [idx[t] as usize, idx[t + 1] as usize, idx[t + 2] as usize];
            t += 3;
            let mut sp = [Point::ORIGIN; 3];
            let mut depth = 0.0f32;
            for (j, &k) in tri.iter().enumerate() {
                let (point, z) = projected[k];
                sp[j] = point;
                depth += z;
            }
            if point_in_polygon(cursor, &sp) {
                let d = depth / 3.0;
                if best.map_or(true, |(bd, _)| d < bd) {
                    best = Some((d, handle));
                }
                break; // one hit per mesh is enough
            }
        }
    }
    best.map(|(_, h)| h)
}

/// Reconstruct a mesh vertex's absolute f64 position from its high/low pair —
/// without the low residual the f32 high alone is ~0.5 m off at UTM scale and
/// box / lasso / face selection lands on the wrong place.
#[inline]
fn mesh_vert(hi: [f32; 3], low: &[[f32; 3]], i: usize) -> glam::DVec3 {
    let l = low.get(i).copied().unwrap_or([0.0; 3]);
    glam::DVec3::new(
        hi[0] as f64 + l[0] as f64,
        hi[1] as f64 + l[1] as f64,
        hi[2] as f64 + l[2] as f64,
    )
}

/// Project a mesh's vertices to screen space.
fn project_mesh_verts(
    mesh: &MeshModel,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
) -> Vec<Point> {
    mesh.verts
        .iter()
        .enumerate()
        .map(|(i, &w)| {
            let ndc = view_rot.project_point3((mesh_vert(w, &mesh.verts_low, i) - eye).as_vec3());
            Point::new(
                (ndc.x + 1.0) * 0.5 * bounds.width,
                (1.0 - ndc.y) * 0.5 * bounds.height,
            )
        })
        .collect()
}

/// True when any of `mesh`'s projected triangles contains one of `pts`
/// (used so a crossing box / lasso entirely inside a solid still selects it).
fn mesh_covers_any(proj: &[Point], indices: &[u32], pts: &[Point]) -> bool {
    let mut t = 0;
    while t + 2 < indices.len() {
        let tri = [
            proj[indices[t] as usize],
            proj[indices[t + 1] as usize],
            proj[indices[t + 2] as usize],
        ];
        t += 3;
        if pts.iter().any(|p| point_in_polygon(*p, &tri)) {
            return true;
        }
    }
    false
}

/// Solid (mesh) handles caught by a rectangular selection box. Window mode
/// (`crossing == false`) needs every projected vertex inside the box;
/// crossing mode needs any vertex inside, or the box to sit inside the solid.
pub fn mesh_box_hit<'a>(
    a: Point,
    b: Point,
    crossing: bool,
    meshes: impl Iterator<Item = (Handle, &'a MeshModel)>,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
) -> Vec<Handle> {
    let (min_x, max_x) = (a.x.min(b.x), a.x.max(b.x));
    let (min_y, max_y) = (a.y.min(b.y), a.y.max(b.y));
    let in_box = |p: &Point| p.x >= min_x && p.x <= max_x && p.y >= min_y && p.y <= max_y;
    let corners = [
        Point::new(min_x, min_y),
        Point::new(max_x, min_y),
        Point::new(max_x, max_y),
        Point::new(min_x, max_y),
    ];
    let mut out = Vec::new();
    for (h, mesh) in meshes {
        let proj = project_mesh_verts(mesh, view_rot, eye, bounds);
        if proj.is_empty() {
            continue;
        }
        let hit = if crossing {
            proj.iter().any(in_box) || mesh_covers_any(&proj, &mesh.indices, &corners)
        } else {
            proj.iter().all(in_box)
        };
        if hit {
            out.push(h);
        }
    }
    out
}

/// Solid (mesh) handles caught by a lasso polygon. Window mode needs every
/// projected vertex inside the lasso; crossing mode needs any vertex inside,
/// or the lasso to sit inside the solid.
pub fn mesh_poly_hit<'a>(
    poly: &[Point],
    crossing: bool,
    meshes: impl Iterator<Item = (Handle, &'a MeshModel)>,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
) -> Vec<Handle> {
    if poly.len() < 3 {
        return Vec::new();
    }
    let mut out = Vec::new();
    for (h, mesh) in meshes {
        let proj = project_mesh_verts(mesh, view_rot, eye, bounds);
        if proj.is_empty() {
            continue;
        }
        let hit = if crossing {
            proj.iter().any(|p| point_in_polygon(*p, poly))
                || mesh_covers_any(&proj, &mesh.indices, poly)
        } else {
            proj.iter().all(|p| point_in_polygon(*p, poly))
        };
        if hit {
            out.push(h);
        }
    }
    out
}

// ── Box / window selection ────────────────────────────────────────────────

fn projected_wire_triangle(
    wire: &WireModel,
    start: usize,
    pick_only: bool,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
) -> Option<[Point; 3]> {
    let (points, low) = if pick_only {
        (&wire.pick_tris, &wire.pick_tris_low)
    } else {
        (&wire.fill_tris, &wire.fill_tris_low)
    };
    let points = points.get(start..start + 3)?;
    Some(std::array::from_fn(|offset| {
        world_to_screen(
            wp64(points[offset], low, start + offset),
            view_rot,
            eye,
            bounds,
        )
    }))
}

fn projected_text_quad(
    wire: &WireModel,
    start: usize,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
) -> Option<[Point; 6]> {
    let vertices = wire.text_verts.get(start..start + 6)?;
    Some(std::array::from_fn(|offset| {
        let vertex = vertices[offset];
        world_to_screen(
            glam::DVec3::new(
                vertex.pos[0] as f64 + vertex.pos_low[0] as f64,
                vertex.pos[1] as f64 + vertex.pos_low[1] as f64,
                vertex.pos[2] as f64 + vertex.pos_low[2] as f64,
            ),
            view_rot,
            eye,
            bounds,
        )
    }))
}

fn triangle_crosses_box(triangle: [Point; 3], corners: [Point; 4]) -> bool {
    let min_x = corners[0].x;
    let min_y = corners[0].y;
    let max_x = corners[2].x;
    let max_y = corners[2].y;
    let inside_box =
        |point: Point| point.x >= min_x && point.x <= max_x && point.y >= min_y && point.y <= max_y;
    triangle.iter().copied().any(inside_box)
        || corners
            .iter()
            .copied()
            .any(|corner| point_in_polygon(corner, &triangle))
        || (0..3).any(|tri_edge| {
            let a = triangle[tri_edge];
            let b = triangle[(tri_edge + 1) % 3];
            (0..4).any(|box_edge| {
                segments_intersect(a, b, corners[box_edge], corners[(box_edge + 1) % 4])
            })
        })
}

fn triangle_crosses_polygon(triangle: [Point; 3], poly: &[Point]) -> bool {
    triangle
        .iter()
        .copied()
        .any(|point| point_in_polygon(point, poly))
        || poly
            .iter()
            .copied()
            .any(|point| point_in_polygon(point, &triangle))
        || (0..3)
            .any(|edge| segment_crosses_polygon(triangle[edge], triangle[(edge + 1) % 3], poly))
}

fn indexed_box_crossing_hits<'a, W: WireSource + ?Sized>(
    wires: &'a W,
    corners: [Point; 4],
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
) -> Vec<&'a str> {
    let mut out = Vec::new();
    let mut seen: HashSet<&str> = HashSet::default();
    let inside = |point: Point| {
        point.x >= corners[0].x
            && point.x <= corners[2].x
            && point.y >= corners[0].y
            && point.y <= corners[2].y
    };
    let segment_hits = |a: Point, b: Point| {
        inside(a)
            || inside(b)
            || (0..4).any(|edge| segments_intersect(a, b, corners[edge], corners[(edge + 1) % 4]))
    };

    for segment in wires.segments().unwrap_or_default() {
        let Some(wire) = wires.source_wire(segment.wire) else {
            continue;
        };
        let start = segment.start as usize;
        if start + 1 >= wire.points.len() {
            continue;
        }
        let a = world_to_screen(
            wp64(wire.points[start], &wire.points_low, start),
            view_rot,
            eye,
            bounds,
        );
        let b = world_to_screen(
            wp64(wire.points[start + 1], &wire.points_low, start + 1),
            view_rot,
            eye,
            bounds,
        );
        if segment_hits(a, b) && seen.insert(wire.name.as_str()) {
            out.push(wire.name.as_str());
        }
    }
    for (triangles, pick_only) in [
        (wires.fill_triangles().unwrap_or_default(), false),
        (wires.pick_triangles().unwrap_or_default(), true),
    ] {
        for triangle in triangles {
            let Some(wire) = wires.source_wire(triangle.wire) else {
                continue;
            };
            if seen.contains(wire.name.as_str()) {
                continue;
            }
            if projected_wire_triangle(
                wire,
                triangle.start as usize,
                pick_only,
                view_rot,
                eye,
                bounds,
            )
            .is_some_and(|triangle| triangle_crosses_box(triangle, corners))
                && seen.insert(wire.name.as_str())
            {
                out.push(wire.name.as_str());
            }
        }
    }
    for glyph in wires.glyphs().unwrap_or_default() {
        let Some(wire) = wires.source_wire(glyph.wire) else {
            continue;
        };
        if seen.contains(wire.name.as_str()) {
            continue;
        }
        let Some(screen) =
            projected_text_quad(wire, glyph.start as usize, view_rot, eye, bounds)
        else {
            continue;
        };
        if [0usize, 3].into_iter().any(|offset| {
            triangle_crosses_box(
                [screen[offset], screen[offset + 1], screen[offset + 2]],
                corners,
            )
        }) && seen.insert(wire.name.as_str())
        {
            out.push(wire.name.as_str());
        }
    }

    // Degenerate point-only wires have no indexed segment or surface primitive.
    for wire in wires.iter() {
        if seen.contains(wire.name.as_str())
            || wire.points.len() >= 2
            || !wire.fill_tris.is_empty()
            || !wire.pick_tris.is_empty()
            || !wire.text_verts.is_empty()
        {
            continue;
        }
        if wire.points.iter().enumerate().any(|(index, &point)| {
            point[0].is_finite()
                && inside(world_to_screen(
                    wp64(point, &wire.points_low, index),
                    view_rot,
                    eye,
                    bounds,
                ))
        }) && seen.insert(wire.name.as_str())
        {
            out.push(wire.name.as_str());
        }
    }
    out
}

fn indexed_polygon_crossing_hits<'a, W: WireSource + ?Sized>(
    wires: &'a W,
    poly: &[Point],
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
) -> Vec<&'a str> {
    let mut out = Vec::new();
    let mut seen: HashSet<&str> = HashSet::default();
    for segment in wires.segments().unwrap_or_default() {
        let Some(wire) = wires.source_wire(segment.wire) else {
            continue;
        };
        let start = segment.start as usize;
        if start + 1 >= wire.points.len() {
            continue;
        }
        let a = world_to_screen(
            wp64(wire.points[start], &wire.points_low, start),
            view_rot,
            eye,
            bounds,
        );
        let b = world_to_screen(
            wp64(wire.points[start + 1], &wire.points_low, start + 1),
            view_rot,
            eye,
            bounds,
        );
        if (point_in_polygon(a, poly)
            || point_in_polygon(b, poly)
            || segment_crosses_polygon(a, b, poly))
            && seen.insert(wire.name.as_str())
        {
            out.push(wire.name.as_str());
        }
    }
    for (triangles, pick_only) in [
        (wires.fill_triangles().unwrap_or_default(), false),
        (wires.pick_triangles().unwrap_or_default(), true),
    ] {
        for triangle in triangles {
            let Some(wire) = wires.source_wire(triangle.wire) else {
                continue;
            };
            if seen.contains(wire.name.as_str()) {
                continue;
            }
            if projected_wire_triangle(
                wire,
                triangle.start as usize,
                pick_only,
                view_rot,
                eye,
                bounds,
            )
            .is_some_and(|triangle| triangle_crosses_polygon(triangle, poly))
                && seen.insert(wire.name.as_str())
            {
                out.push(wire.name.as_str());
            }
        }
    }
    for glyph in wires.glyphs().unwrap_or_default() {
        let Some(wire) = wires.source_wire(glyph.wire) else {
            continue;
        };
        if seen.contains(wire.name.as_str()) {
            continue;
        }
        let Some(screen) =
            projected_text_quad(wire, glyph.start as usize, view_rot, eye, bounds)
        else {
            continue;
        };
        if [0usize, 3].into_iter().any(|offset| {
            triangle_crosses_polygon(
                [screen[offset], screen[offset + 1], screen[offset + 2]],
                poly,
            )
        }) && seen.insert(wire.name.as_str())
        {
            out.push(wire.name.as_str());
        }
    }
    for wire in wires.iter() {
        if seen.contains(wire.name.as_str())
            || wire.points.len() >= 2
            || !wire.fill_tris.is_empty()
            || !wire.pick_tris.is_empty()
            || !wire.text_verts.is_empty()
        {
            continue;
        }
        if wire.points.iter().enumerate().any(|(index, &point)| {
            point[0].is_finite()
                && point_in_polygon(
                    world_to_screen(wp64(point, &wire.points_low, index), view_rot, eye, bounds),
                    poly,
                )
        }) && seen.insert(wire.name.as_str())
        {
            out.push(wire.name.as_str());
        }
    }
    out
}

/// Return the names of wires selected by a completed rectangular selection box.
///
/// - **Window mode** (`crossing = false`, left→right drag):
///   ALL projected points must lie inside the box.
/// - **Crossing mode** (`crossing = true`, right→left drag):
///   ANY projected point inside the box, OR any wire segment crosses the box
///   boundary (so large entities like viewport frames are caught even when
///   no corner falls inside the selection rectangle).
pub fn box_hit<'a, W: WireSource + ?Sized>(
    corner_a: Point,
    corner_b: Point,
    crossing: bool,
    wires: &'a W,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
) -> Vec<&'a str> {
    // Clamp the selection box to the pane rectangle so it can't reach geometry
    // the GPU scissored out of a floating viewport (the hit-test wire set runs
    // past the visible rect). No-op in model space, where bounds is the canvas.
    let min_x = corner_a.x.min(corner_b.x).max(0.0);
    let max_x = corner_a.x.max(corner_b.x).min(bounds.width);
    let min_y = corner_a.y.min(corner_b.y).max(0.0);
    let max_y = corner_a.y.max(corner_b.y).min(bounds.height);

    // Ignore zero-area boxes (including a box clamped entirely off-pane).
    if (max_x - min_x) < 1.0 || (max_y - min_y) < 1.0 {
        return vec![];
    }

    let inside = |sp: Point| sp.x >= min_x && sp.x <= max_x && sp.y >= min_y && sp.y <= max_y;

    // Box corners for segment-intersection tests (crossing mode only).
    let box_tl = Point { x: min_x, y: min_y };
    let box_tr = Point { x: max_x, y: min_y };
    let box_bl = Point { x: min_x, y: max_y };
    let box_br = Point { x: max_x, y: max_y };
    let box_corners = [box_tl, box_tr, box_br, box_bl];
    if crossing && wires.segments().is_some() {
        return indexed_box_crossing_hits(
            wires,
            box_corners,
            view_rot,
            eye,
            bounds,
        );
    }

    // Q: lazy projection — accumulate screen points without allocating per-wire Vec.
    wires
        .iter()
        .filter_map(|wire| {
            // Fallback: when wire has no line geometry (e.g. greek text emits
            // only fill_tris) treat the AABB rectangle as the hit-test shape
            // so low-LOD text stays selectable. See #19.
            let aabb_pts: Vec<[f32; 3]>;
            let empty_pts: [[f32; 3]; 0] = [];
            let pts: &[[f32; 3]] = if !wire.points.is_empty() {
                &wire.points
            } else if !wire.text_verts.is_empty() {
                &empty_pts
            } else if wire.aabb != WireModel::UNBOUNDED_AABB {
                let [ax, ay, bx, by] = wire.aabb;
                aabb_pts = vec![
                    [ax, ay, 0.0],
                    [bx, ay, 0.0],
                    [bx, by, 0.0],
                    [ax, by, 0.0],
                    [ax, ay, 0.0],
                ];
                &aabb_pts
            } else {
                return None;
            };

            // Low residual parallel to `pts` (empty for the AABB fallback,
            // whose coarse f32 box doesn't carry one).
            let low: &[[f32; 3]] = if !wire.points.is_empty() {
                &wire.points_low
            } else {
                &[]
            };
            let mut hit = false;
            let mut all_inside = true;
            let mut prev: Option<Point> = None;

            for (i, &[px, py, pz]) in pts.iter().enumerate() {
                if px.is_nan() {
                    prev = None;
                    continue;
                }
                let sp = world_to_screen(wp64([px, py, pz], low, i), view_rot, eye, bounds);
                if crossing {
                    if inside(sp) {
                        hit = true;
                    }
                    if let Some(p0) = prev {
                        if !hit {
                            hit = segments_intersect(p0, sp, box_tl, box_tr)
                                || segments_intersect(p0, sp, box_tr, box_br)
                                || segments_intersect(p0, sp, box_br, box_bl)
                                || segments_intersect(p0, sp, box_bl, box_tl);
                        }
                    }
                } else {
                    if !inside(sp) {
                        all_inside = false;
                    }
                }
                prev = Some(sp);
            }

            let glyphs_present = !wire.text_verts.is_empty();
            let mut glyph_crosses = false;
            let mut glyphs_inside = true;
            for start in (0..wire.text_verts.len()).step_by(6) {
                let Some(screen) = projected_text_quad(wire, start, view_rot, eye, bounds) else {
                    continue;
                };
                if crossing {
                    if [0usize, 3].into_iter().any(|offset| {
                        triangle_crosses_box(
                            [screen[offset], screen[offset + 1], screen[offset + 2]],
                            box_corners,
                        )
                    }) {
                        glyph_crosses = true;
                        break;
                    }
                } else if !screen.iter().copied().all(inside) {
                    glyphs_inside = false;
                    break;
                }
            }

            let result = if crossing {
                hit || glyph_crosses
            } else {
                all_inside && glyphs_inside && (prev.is_some() || glyphs_present)
            };
            if result {
                Some(wire.name.as_str())
            } else {
                None
            }
        })
        .collect()
}

// ── Polygon / lasso selection ─────────────────────────────────────────────

/// Return the names of wires selected by a freehand polygon lasso.
///
/// - **Window mode** (`crossing = false`): ALL projected points inside polygon.
/// - **Crossing mode** (`crossing = true`): ANY point inside OR any wire
///   segment crosses a polygon edge.
pub fn poly_hit<'a, W: WireSource + ?Sized>(
    poly: &[Point],
    crossing: bool,
    wires: &'a W,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
) -> Vec<&'a str> {
    if poly.len() < 3 {
        return vec![];
    }
    if crossing && wires.segments().is_some() {
        return indexed_polygon_crossing_hits(wires, poly, view_rot, eye, bounds);
    }

    // Q: lazy projection — no Vec allocation per wire.
    wires
        .iter()
        .filter_map(|wire| {
            // Same AABB fallback as `box_hit`: when a wire has no line
            // geometry (e.g. greek-LOD text emits only fill_tris) treat the
            // AABB rectangle as the hit-test shape so low-LOD text stays
            // selectable. See #19.
            let aabb_pts: Vec<[f32; 3]>;
            let empty_pts: [[f32; 3]; 0] = [];
            let pts: &[[f32; 3]] = if !wire.points.is_empty() {
                &wire.points
            } else if !wire.text_verts.is_empty() {
                &empty_pts
            } else if wire.aabb != WireModel::UNBOUNDED_AABB {
                let [ax, ay, bx, by] = wire.aabb;
                aabb_pts = vec![
                    [ax, ay, 0.0],
                    [bx, ay, 0.0],
                    [bx, by, 0.0],
                    [ax, by, 0.0],
                    [ax, ay, 0.0],
                ];
                &aabb_pts
            } else {
                return None;
            };

            let low: &[[f32; 3]] = if !wire.points.is_empty() {
                &wire.points_low
            } else {
                &[]
            };
            let mut hit = false;
            let mut all_inside = true;
            let mut prev: Option<Point> = None;

            for (i, &[px, py, pz]) in pts.iter().enumerate() {
                if px.is_nan() {
                    prev = None;
                    continue;
                }
                let sp = world_to_screen(wp64([px, py, pz], low, i), view_rot, eye, bounds);
                // Reject points the GPU scissored out of a floating viewport so
                // the lasso can't reach clipped geometry. No-op in model space.
                if sp.x < 0.0 || sp.x > bounds.width || sp.y < 0.0 || sp.y > bounds.height {
                    all_inside = false;
                    prev = None;
                    continue;
                }
                if crossing {
                    if point_in_polygon(sp, poly) {
                        hit = true;
                    }
                    if !hit {
                        if let Some(p0) = prev {
                            if segment_crosses_polygon(p0, sp, poly) {
                                hit = true;
                            }
                        }
                    }
                } else {
                    if !point_in_polygon(sp, poly) {
                        all_inside = false;
                    }
                }
                prev = Some(sp);
            }

            let glyphs_present = !wire.text_verts.is_empty();
            let mut glyph_crosses = false;
            let mut glyphs_inside = true;
            for start in (0..wire.text_verts.len()).step_by(6) {
                let Some(screen) = projected_text_quad(wire, start, view_rot, eye, bounds) else {
                    continue;
                };
                if crossing {
                    if [0usize, 3].into_iter().any(|offset| {
                        triangle_crosses_polygon(
                            [screen[offset], screen[offset + 1], screen[offset + 2]],
                            poly,
                        )
                    }) {
                        glyph_crosses = true;
                        break;
                    }
                } else if !screen.iter().copied().all(|point| {
                    point.x >= 0.0
                        && point.x <= bounds.width
                        && point.y >= 0.0
                        && point.y <= bounds.height
                        && point_in_polygon(point, poly)
                }) {
                    glyphs_inside = false;
                    break;
                }
            }

            let result = if crossing {
                hit || glyph_crosses
            } else {
                all_inside && glyphs_inside && (prev.is_some() || glyphs_present)
            };
            if result {
                Some(wire.name.as_str())
            } else {
                None
            }
        })
        .collect()
}

// ── Helpers ───────────────────────────────────────────────────────────────

fn world_to_screen(
    world: glam::DVec3,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
) -> Point {
    let ndc = view_rot.project_point3((world - eye).as_vec3());
    Point::new(
        (ndc.x + 1.0) * 0.5 * bounds.width,
        (1.0 - ndc.y) * 0.5 * bounds.height,
    )
}

/// Reconstruct the absolute-f64 world position of wire vertex `i` from its
/// double-single high (`points`) + low (`points_low`) pair. At UTM scale the
/// high f32 alone is ~0.5 m off, which throws box / lasso / click selection
/// edges off by metres; adding the low residual restores f64 precision.
#[inline]
fn wp64(hi: [f32; 3], low: &[[f32; 3]], i: usize) -> glam::DVec3 {
    let l = low.get(i).copied().unwrap_or([0.0; 3]);
    glam::DVec3::new(
        hi[0] as f64 + l[0] as f64,
        hi[1] as f64 + l[1] as f64,
        hi[2] as f64 + l[2] as f64,
    )
}

/// Even-odd ray-casting test: is `p` inside the polygon?
///
/// Handles multi-path boundaries: NaN points (used as path separators by
/// hatches with islands / holes) reset the previous-vertex tracking so
/// that the ray-cast doesn't draw a spurious closing edge between the
/// end of one sub-path and the start of the next. Each sub-path with at
/// least 2 finite vertices contributes its segments to the parity flip.
fn point_in_polygon(p: Point, poly: &[Point]) -> bool {
    // Ray-cast crossing test for a single edge a→b.
    fn cross(p: Point, a: Point, b: Point, inside: &mut bool) {
        if (a.y > p.y) != (b.y > p.y) && p.x < (b.x - a.x) * (p.y - a.y) / (b.y - a.y) + a.x {
            *inside = !*inside;
        }
    }

    let mut inside = false;
    let mut prev: Option<Point> = None;
    let mut path_start: Option<Point> = None;
    // Vertices in the current sub-path. A boundary can be encoded either as a
    // ring (`[v0,v1,v2,v3]`, needs an implicit closing edge) or as an explicit
    // edge list (`[v0,v1, NaN, v1,v2, NaN, …]`, already closed). Only close a
    // sub-path that is a real ring (≥3 verts); closing a 2-point explicit edge
    // would add a degenerate back-edge that cancels its own crossing.
    let mut count = 0usize;
    let close =
        |prev: Option<Point>, path_start: Option<Point>, count: usize, inside: &mut bool| {
            if count >= 3 {
                if let (Some(pv), Some(sv)) = (prev, path_start) {
                    cross(p, pv, sv, inside);
                }
            }
        };
    for &pt in poly {
        if !pt.x.is_finite() || !pt.y.is_finite() {
            close(prev, path_start, count, &mut inside);
            prev = None;
            path_start = None;
            count = 0;
            continue;
        }
        if let Some(prev_v) = prev {
            cross(p, prev_v, pt, &mut inside);
        } else {
            path_start = Some(pt);
        }
        prev = Some(pt);
        count += 1;
    }
    close(prev, path_start, count, &mut inside);
    inside
}

/// Does segment `[a, b]` cross any edge of the polygon?
fn segment_crosses_polygon(a: Point, b: Point, poly: &[Point]) -> bool {
    let n = poly.len();
    for i in 0..n {
        let c = poly[i];
        let d = poly[(i + 1) % n];
        if segments_intersect(a, b, c, d) {
            return true;
        }
    }
    false
}

/// Do segments `[a,b]` and `[c,d]` intersect?
fn segments_intersect(a: Point, b: Point, c: Point, d: Point) -> bool {
    let cross = |o: Point, p: Point, q: Point| -> f32 {
        (p.x - o.x) * (q.y - o.y) - (p.y - o.y) * (q.x - o.x)
    };
    let d1 = cross(c, d, a);
    let d2 = cross(c, d, b);
    let d3 = cross(a, b, c);
    let d4 = cross(a, b, d);
    if ((d1 > 0.0 && d2 < 0.0) || (d1 < 0.0 && d2 > 0.0))
        && ((d3 > 0.0 && d4 < 0.0) || (d3 < 0.0 && d4 > 0.0))
    {
        return true;
    }
    false
}

// ── Hatch hit-testing ─────────────────────────────────────────────────────

/// Return the Handle of the first hatch whose screen-space boundary polygon
/// contains `cursor`.
pub fn click_hit_hatch(
    cursor: Point,
    hatches: &HashMap<Handle, HatchModel>,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
    candidate_handles: Option<&HashSet<Handle>>,
) -> Option<Handle> {
    for (&handle, hatch) in hatches {
        if candidate_handles.is_some_and(|handles| !handles.contains(&handle)) {
            continue;
        }
        if hatch_contains_screen_point(hatch, cursor, view_rot, eye, bounds) {
            return Some(handle);
        }
    }
    None
}

/// Same as `click_hit_hatch` but tests block-internal hatches grouped by
/// their parent Insert handle. The first matching model returns its Insert so
/// clicking a sub-hatch of a block selects the Insert, matching
/// AutoCAD's behaviour for block sub-entities.
pub fn click_hit_insert_hatch(
    cursor: Point,
    insert_hatches: &HashMap<Handle, Vec<HatchModel>>,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
    candidate_handles: Option<&HashSet<Handle>>,
) -> Option<Handle> {
    if let Some(handles) = candidate_handles {
        for handle in handles {
            let Some(hatches) = insert_hatches.get(handle) else {
                continue;
            };
            if hatches
                .iter()
                .any(|hatch| hatch_contains_screen_point(hatch, cursor, view_rot, eye, bounds))
            {
                return Some(*handle);
            }
        }
        return None;
    }
    for (handle, hatches) in insert_hatches {
        if hatches
            .iter()
            .any(|hatch| hatch_contains_screen_point(hatch, cursor, view_rot, eye, bounds))
        {
            return Some(*handle);
        }
    }
    None
}

fn hatch_contains_screen_point(
    hatch: &HatchModel,
    cursor: Point,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
) -> bool {
    // A cursor outside the pane rectangle can't pick a hatch scissored out of a
    // floating viewport. No-op in model space (bounds is the canvas).
    if cursor.x < 0.0 || cursor.x > bounds.width || cursor.y < 0.0 || cursor.y > bounds.height {
        return false;
    }
    // boundary verts are stored as small f32 offsets from
    // `world_origin` (f64). Reconstruct offset-rel WCS before
    // projecting to screen.
    let (ox, oy) = (hatch.world_origin[0], hatch.world_origin[1]);
    let screen: Vec<Point> = hatch
        .boundary
        .iter()
        .map(|&[x, y]| {
            if x.is_finite() && y.is_finite() {
                world_to_screen(
                    glam::DVec3::new(x as f64 + ox, y as f64 + oy, 0.0),
                    view_rot,
                    eye,
                    bounds,
                )
            } else {
                // Preserve path separators for the NaN-aware
                // point_in_polygon ray-cast.
                Point::new(f32::NAN, f32::NAN)
            }
        })
        .collect();
    screen.len() >= 3 && point_in_polygon(cursor, &screen)
}

/// Return Handles of hatches selected by a completed rectangular selection box.
fn hatch_box_hit(
    corner_a: Point,
    corner_b: Point,
    crossing: bool,
    hatch: &HatchModel,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
) -> bool {
    let min_x = corner_a.x.min(corner_b.x);
    let max_x = corner_a.x.max(corner_b.x);
    let min_y = corner_a.y.min(corner_b.y);
    let max_y = corner_a.y.max(corner_b.y);
    if (max_x - min_x) < 1.0 || (max_y - min_y) < 1.0 || hatch.boundary.is_empty() {
        return false;
    }
    let inside =
        |point: Point| point.x >= min_x && point.x <= max_x && point.y >= min_y && point.y <= max_y;
    let (ox, oy) = (hatch.world_origin[0], hatch.world_origin[1]);
    let screen: Vec<Point> = hatch
        .boundary
        .iter()
        .map(|&[x, y]| {
            if x.is_finite() && y.is_finite() {
                world_to_screen(
                    glam::DVec3::new(x as f64 + ox, y as f64 + oy, 0.0),
                    view_rot,
                    eye,
                    bounds,
                )
            } else {
                Point::new(f32::NAN, f32::NAN)
            }
        })
        .collect();
    if !screen
        .iter()
        .any(|point| point.x.is_finite() && point.y.is_finite())
    {
        return false;
    }
    if !crossing {
        return screen
            .iter()
            .filter(|point| point.x.is_finite() && point.y.is_finite())
            .copied()
            .all(inside);
    }
    let corners = [
        Point::new(min_x, min_y),
        Point::new(max_x, min_y),
        Point::new(max_x, max_y),
        Point::new(min_x, max_y),
    ];
    screen.iter().copied().any(inside)
        || screen
            .windows(2)
            .filter(|edge| {
                edge[0].x.is_finite()
                    && edge[0].y.is_finite()
                    && edge[1].x.is_finite()
                    && edge[1].y.is_finite()
            })
            .any(|edge| {
                (0..4).any(|side| {
                    segments_intersect(edge[0], edge[1], corners[side], corners[(side + 1) % 4])
                })
            })
        || corners
            .iter()
            .copied()
            .any(|corner| point_in_polygon(corner, &screen))
}

pub fn box_hit_hatch(
    corner_a: Point,
    corner_b: Point,
    crossing: bool,
    hatches: &HashMap<Handle, HatchModel>,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
    candidate_handles: Option<&HashSet<Handle>>,
) -> Vec<Handle> {
    hatches
        .iter()
        .filter_map(|(&handle, hatch)| {
            if candidate_handles.is_some_and(|handles| !handles.contains(&handle)) {
                return None;
            }
            hatch_box_hit(corner_a, corner_b, crossing, hatch, view_rot, eye, bounds)
                .then_some(handle)
        })
        .collect()
}

pub fn box_hit_insert_hatch(
    corner_a: Point,
    corner_b: Point,
    crossing: bool,
    insert_hatches: &HashMap<Handle, Vec<HatchModel>>,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
    candidate_handles: Option<&HashSet<Handle>>,
) -> Vec<Handle> {
    let mut out = Vec::new();
    if let Some(handles) = candidate_handles {
        for handle in handles {
            if insert_hatches.get(handle).is_some_and(|hatches| {
                let test = |hatch: &HatchModel| {
                    hatch_box_hit(corner_a, corner_b, crossing, hatch, view_rot, eye, bounds)
                };
                if crossing {
                    hatches.iter().any(test)
                } else {
                    !hatches.is_empty() && hatches.iter().all(test)
                }
            }) {
                out.push(*handle);
            }
        }
    } else {
        for (handle, hatches) in insert_hatches {
            let test = |hatch: &HatchModel| {
                hatch_box_hit(corner_a, corner_b, crossing, hatch, view_rot, eye, bounds)
            };
            let hit = if crossing {
                hatches.iter().any(test)
            } else {
                !hatches.is_empty() && hatches.iter().all(test)
            };
            if hit {
                out.push(*handle);
            }
        }
    }
    out
}

/// Return Handles of hatches selected by a freehand polygon lasso.
fn hatch_polygon_hit(
    poly: &[Point],
    crossing: bool,
    hatch: &HatchModel,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
) -> bool {
    if poly.len() < 3 || hatch.boundary.is_empty() {
        return false;
    }
    let (ox, oy) = (hatch.world_origin[0], hatch.world_origin[1]);
    let screen: Vec<Point> = hatch
        .boundary
        .iter()
        .map(|&[x, y]| {
            if x.is_finite() && y.is_finite() {
                world_to_screen(
                    glam::DVec3::new(x as f64 + ox, y as f64 + oy, 0.0),
                    view_rot,
                    eye,
                    bounds,
                )
            } else {
                Point::new(f32::NAN, f32::NAN)
            }
        })
        .collect();
    if !screen
        .iter()
        .any(|point| point.x.is_finite() && point.y.is_finite())
    {
        return false;
    }
    if crossing {
        screen
            .iter()
            .copied()
            .any(|point| point_in_polygon(point, poly))
            || screen
                .windows(2)
                .filter(|edge| {
                    edge[0].x.is_finite()
                        && edge[0].y.is_finite()
                        && edge[1].x.is_finite()
                        && edge[1].y.is_finite()
                })
                .any(|edge| segment_crosses_polygon(edge[0], edge[1], poly))
            || poly
                .iter()
                .copied()
                .any(|point| point_in_polygon(point, &screen))
    } else {
        screen
            .iter()
            .filter(|point| point.x.is_finite() && point.y.is_finite())
            .all(|point| point_in_polygon(*point, poly))
    }
}

pub fn poly_hit_hatch(
    poly: &[Point],
    crossing: bool,
    hatches: &HashMap<Handle, HatchModel>,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
    candidate_handles: Option<&HashSet<Handle>>,
) -> Vec<Handle> {
    hatches
        .iter()
        .filter_map(|(&handle, hatch)| {
            if candidate_handles.is_some_and(|handles| !handles.contains(&handle)) {
                return None;
            }
            hatch_polygon_hit(poly, crossing, hatch, view_rot, eye, bounds).then_some(handle)
        })
        .collect()
}

pub fn poly_hit_insert_hatch(
    poly: &[Point],
    crossing: bool,
    insert_hatches: &HashMap<Handle, Vec<HatchModel>>,
    view_rot: Mat4,
    eye: glam::DVec3,
    bounds: Rectangle,
    candidate_handles: Option<&HashSet<Handle>>,
) -> Vec<Handle> {
    let mut out = Vec::new();
    if let Some(handles) = candidate_handles {
        for handle in handles {
            if insert_hatches.get(handle).is_some_and(|hatches| {
                let test = |hatch: &HatchModel| {
                    hatch_polygon_hit(poly, crossing, hatch, view_rot, eye, bounds)
                };
                if crossing {
                    hatches.iter().any(test)
                } else {
                    !hatches.is_empty() && hatches.iter().all(test)
                }
            }) {
                out.push(*handle);
            }
        }
    } else {
        for (handle, hatches) in insert_hatches {
            let test = |hatch: &HatchModel| {
                hatch_polygon_hit(poly, crossing, hatch, view_rot, eye, bounds)
            };
            let hit = if crossing {
                hatches.iter().any(test)
            } else {
                !hatches.is_empty() && hatches.iter().all(test)
            };
            if hit {
                out.push(*handle);
            }
        }
    }
    out
}

/// Minimum distance from point `p` to line segment `[a, b]` in 2-D.
fn dist_point_to_segment(p: Point, a: Point, b: Point) -> f32 {
    let abx = b.x - a.x;
    let aby = b.y - a.y;
    let len2 = abx * abx + aby * aby;
    let t = if len2 < 1e-6 {
        0.0
    } else {
        let apx = p.x - a.x;
        let apy = p.y - a.y;
        ((apx * abx + apy * aby) / len2).clamp(0.0, 1.0)
    };
    let cx = a.x + t * abx;
    let cy = a.y + t * aby;
    let dx = p.x - cx;
    let dy = p.y - cy;
    (dx * dx + dy * dy).sqrt()
}

#[cfg(test)]
mod aabb_reject_tests {
    use super::*;

    fn wire(name: &str, pts: Vec<[f32; 3]>, aabb: [f32; 4]) -> WireModel {
        let mut w = WireModel::solid(name.to_string(), pts, [1.0; 4], false);
        w.aabb = aabb;
        w
    }

    // Identity ortho view: world (x,y) → screen ((x+1)*100, (1-y)*100) for a
    // 200×200 viewport. The view is flat (z_axis.xy == 0) so the AABB pre-reject
    // is active — these tests guard it against false negatives.
    #[test]
    fn aabb_reject_keeps_near_wire_drops_far() {
        let vp = Mat4::IDENTITY;
        let bounds = Rectangle {
            x: 0.0,
            y: 0.0,
            width: 200.0,
            height: 200.0,
        };
        let cursor = Point::new(100.0, 100.0); // world origin

        let near = wire(
            "5",
            vec![[-0.02, 0.0, 0.0], [0.02, 0.0, 0.0]],
            [-0.02, 0.0, 0.02, 0.0],
        );
        let far = wire(
            "9",
            vec![[0.9, 0.9, 0.0], [0.95, 0.9, 0.0]],
            [0.9, 0.9, 0.95, 0.9],
        );

        let eye = glam::DVec3::ZERO;
        assert_eq!(
            click_hit(cursor, std::slice::from_ref(&near), vp, eye, bounds, true),
            Some("5")
        );
        assert_eq!(
            click_hit(cursor, std::slice::from_ref(&far), vp, eye, bounds, true),
            None
        );
        // The far wire must be rejected without hiding the near one.
        assert_eq!(
            click_hit(cursor, &[far, near], vp, eye, bounds, true),
            Some("5")
        );
    }

    #[test]
    fn crossing_hits_glyphs_batched_with_distant_block_geometry() {
        use crate::scene::pipeline::text_gpu::TextVertex;

        let vertex = |x, y| TextVertex {
            pos: [x, y, 0.0],
            pos_low: [0.0; 3],
            uv: [0.0; 2],
            color: [1.0; 4],
            draw_depth: 0.0,
        };
        let mut block = wire(
            "479",
            vec![[0.75, 0.75, 0.0], [0.9, 0.75, 0.0]],
            [-0.1, -0.1, 0.9, 0.75],
        );
        block.text_verts = vec![
            vertex(-0.1, -0.1),
            vertex(0.1, -0.1),
            vertex(0.1, 0.1),
            vertex(-0.1, -0.1),
            vertex(0.1, 0.1),
            vertex(-0.1, 0.1),
        ];

        let bounds = Rectangle {
            x: 0.0,
            y: 0.0,
            width: 200.0,
            height: 200.0,
        };
        let wires = std::slice::from_ref(&block);
        assert_eq!(
            box_hit(
                Point::new(85.0, 85.0),
                Point::new(115.0, 115.0),
                true,
                wires,
                Mat4::IDENTITY,
                glam::DVec3::ZERO,
                bounds,
            ),
            vec!["479"],
        );
        assert_eq!(
            poly_hit(
                &[
                    Point::new(85.0, 85.0),
                    Point::new(115.0, 85.0),
                    Point::new(115.0, 115.0),
                    Point::new(85.0, 115.0),
                ],
                true,
                wires,
                Mat4::IDENTITY,
                glam::DVec3::ZERO,
                bounds,
            ),
            vec!["479"],
        );
    }
}
