use bone_document::{
    ArcData, CircleData, DimensionValue, LineData, Sketch, SketchDimension, SketchEntity,
    SketchRelation,
};
use bone_kernel::{Aabb2, Arc2, BrepSolid, FaceMesh, SolidMesh, arc_bounding_box};
use bone_types::{
    Angle, ChordHeightTolerance, CreaseAngle, Length, LinearRgba, Point2, Point3,
    SketchDimensionId, SketchEntityId, SketchRelationId, Tolerance, UnitVec3, Vec2,
};
use core::f64::consts::FRAC_1_SQRT_2;
use std::collections::BTreeMap;
use uom::si::angle::degree;
use uom::si::length::millimeter;

use crate::pick::{EntityKindTag, PickId, PickIdError, PickIndex};

const ARC_TOLERANCE: Tolerance = Tolerance::new(1.0e-9);

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ScenePoint {
    at: Point2,
    entity: SketchEntityId,
    pick: PickId,
}

impl ScenePoint {
    #[must_use]
    pub const fn at(self) -> Point2 {
        self.at
    }

    #[must_use]
    pub const fn pick(self) -> PickId {
        self.pick
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct SceneLine {
    a: Point2,
    b: Point2,
    entity: SketchEntityId,
    pick: PickId,
    for_construction: bool,
}

impl SceneLine {
    #[must_use]
    pub const fn a(self) -> Point2 {
        self.a
    }

    #[must_use]
    pub const fn b(self) -> Point2 {
        self.b
    }

    #[must_use]
    pub const fn pick(self) -> PickId {
        self.pick
    }

    #[must_use]
    pub const fn for_construction(self) -> bool {
        self.for_construction
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct SceneArc {
    center: Point2,
    radius: Length,
    start_angle: Angle,
    sweep_angle: Angle,
    entity: SketchEntityId,
    pick: PickId,
    for_construction: bool,
}

impl SceneArc {
    #[must_use]
    pub const fn center(self) -> Point2 {
        self.center
    }

    #[must_use]
    pub const fn radius(self) -> Length {
        self.radius
    }

    #[must_use]
    pub const fn start_angle(self) -> Angle {
        self.start_angle
    }

    #[must_use]
    pub const fn sweep_angle(self) -> Angle {
        self.sweep_angle
    }

    #[must_use]
    pub const fn pick(self) -> PickId {
        self.pick
    }

    #[must_use]
    pub const fn for_construction(self) -> bool {
        self.for_construction
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct SceneCircle {
    center: Point2,
    radius: Length,
    entity: SketchEntityId,
    pick: PickId,
    for_construction: bool,
}

impl SceneCircle {
    #[must_use]
    pub const fn center(self) -> Point2 {
        self.center
    }

    #[must_use]
    pub const fn radius(self) -> Length {
        self.radius
    }

    #[must_use]
    pub const fn pick(self) -> PickId {
        self.pick
    }

    #[must_use]
    pub const fn for_construction(self) -> bool {
        self.for_construction
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[repr(u32)]
pub enum RelationGlyphKind {
    Coincident = 0,
    Horizontal = 1,
    Vertical = 2,
    Parallel = 3,
    Perpendicular = 4,
    Tangent = 5,
    Equal = 6,
    Concentric = 7,
    Fix = 8,
    Midpoint = 9,
    Symmetric = 10,
}

impl RelationGlyphKind {
    #[must_use]
    pub const fn tile_index(self) -> u32 {
        self as u32
    }

    #[must_use]
    pub const fn from_index(idx: u32) -> Option<Self> {
        match idx {
            0 => Some(Self::Coincident),
            1 => Some(Self::Horizontal),
            2 => Some(Self::Vertical),
            3 => Some(Self::Parallel),
            4 => Some(Self::Perpendicular),
            5 => Some(Self::Tangent),
            6 => Some(Self::Equal),
            7 => Some(Self::Concentric),
            8 => Some(Self::Fix),
            9 => Some(Self::Midpoint),
            10 => Some(Self::Symmetric),
            _ => None,
        }
    }

    #[must_use]
    pub const fn from_relation(rel: SketchRelation) -> Self {
        match rel {
            SketchRelation::Coincident(_, _) => Self::Coincident,
            SketchRelation::Horizontal(_) => Self::Horizontal,
            SketchRelation::Vertical(_) => Self::Vertical,
            SketchRelation::Parallel(_, _) => Self::Parallel,
            SketchRelation::Perpendicular(_, _) => Self::Perpendicular,
            SketchRelation::Tangent(_, _) => Self::Tangent,
            SketchRelation::Equal(_, _) => Self::Equal,
            SketchRelation::Concentric(_, _) => Self::Concentric,
            SketchRelation::Midpoint { .. } => Self::Midpoint,
            SketchRelation::Symmetric { .. } => Self::Symmetric,
            SketchRelation::Fix(_) => Self::Fix,
        }
    }

    #[must_use]
    pub const fn all() -> [Self; 11] {
        [
            Self::Coincident,
            Self::Horizontal,
            Self::Vertical,
            Self::Parallel,
            Self::Perpendicular,
            Self::Tangent,
            Self::Equal,
            Self::Concentric,
            Self::Fix,
            Self::Midpoint,
            Self::Symmetric,
        ]
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct SceneRelationGlyph {
    anchor_mm: Point2,
    offset_dir: Vec2,
    kind: RelationGlyphKind,
    relation: SketchRelationId,
    pick: PickId,
}

impl SceneRelationGlyph {
    #[must_use]
    pub const fn anchor_mm(self) -> Point2 {
        self.anchor_mm
    }

    #[must_use]
    pub const fn offset_dir(self) -> Vec2 {
        self.offset_dir
    }

    #[must_use]
    pub const fn kind(self) -> RelationGlyphKind {
        self.kind
    }

    #[must_use]
    pub const fn relation(self) -> SketchRelationId {
        self.relation
    }

    #[must_use]
    pub const fn pick(self) -> PickId {
        self.pick
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct SceneDimension {
    anchor_mm: Point2,
    text: String,
    dimension: SketchDimensionId,
    pick: PickId,
}

impl SceneDimension {
    #[must_use]
    pub const fn anchor_mm(&self) -> Point2 {
        self.anchor_mm
    }

    #[must_use]
    pub fn text(&self) -> &str {
        &self.text
    }

    #[must_use]
    pub const fn dimension(&self) -> SketchDimensionId {
        self.dimension
    }

    #[must_use]
    pub const fn pick(&self) -> PickId {
        self.pick
    }
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct SketchScene {
    points: Vec<ScenePoint>,
    lines: Vec<SceneLine>,
    arcs: Vec<SceneArc>,
    circles: Vec<SceneCircle>,
    relations: Vec<SceneRelationGlyph>,
    dimensions: Vec<SceneDimension>,
}

impl SketchScene {
    #[must_use]
    pub const fn empty() -> Self {
        Self {
            points: Vec::new(),
            lines: Vec::new(),
            arcs: Vec::new(),
            circles: Vec::new(),
            relations: Vec::new(),
            dimensions: Vec::new(),
        }
    }

    pub fn extract(sketch: &Sketch) -> Result<Self, PickIdError> {
        let with_entities = sketch
            .entity_order()
            .iter()
            .copied()
            .try_fold(Self::empty(), |acc, id| acc.push_from_sketch(sketch, id))?;
        let with_relations = sketch
            .relation_order()
            .iter()
            .copied()
            .try_fold(with_entities, |acc, id| acc.push_relation(sketch, id))?;
        sketch
            .dimension_order()
            .iter()
            .copied()
            .try_fold(with_relations, |acc, id| acc.push_dimension(sketch, id))
    }

    #[must_use]
    pub fn points(&self) -> &[ScenePoint] {
        &self.points
    }

    #[must_use]
    pub fn lines(&self) -> &[SceneLine] {
        &self.lines
    }

    #[must_use]
    pub fn arcs(&self) -> &[SceneArc] {
        &self.arcs
    }

    #[must_use]
    pub fn circles(&self) -> &[SceneCircle] {
        &self.circles
    }

    #[must_use]
    pub fn relations(&self) -> &[SceneRelationGlyph] {
        &self.relations
    }

    #[must_use]
    pub fn dimensions(&self) -> &[SceneDimension] {
        &self.dimensions
    }

    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.points.is_empty()
            && self.lines.is_empty()
            && self.arcs.is_empty()
            && self.circles.is_empty()
            && self.relations.is_empty()
            && self.dimensions.is_empty()
    }

    #[must_use]
    pub fn aabb(&self) -> Option<Aabb2> {
        let points = self
            .points
            .iter()
            .map(|p| Aabb2::from_corners(p.at(), p.at()));
        let lines = self.lines.iter().map(|l| Aabb2::from_corners(l.a(), l.b()));
        let circles = self.circles.iter().map(|c| {
            let (cx, cy) = c.center().coords_mm();
            let r = c.radius().get::<millimeter>();
            Aabb2::from_corners(
                Point2::from_mm(cx - r, cy - r),
                Point2::from_mm(cx + r, cy + r),
            )
        });
        let arcs = self
            .arcs
            .iter()
            .map(|a| arc_bounding_box(a.center(), a.radius(), a.start_angle(), a.sweep_angle()));
        points
            .chain(lines)
            .chain(circles)
            .chain(arcs)
            .reduce(|a, b| a.extend_point(b.min()).extend_point(b.max()))
    }

    pub fn pick_index(&self) -> Result<PickIndex, PickIdError> {
        let entities = self
            .points
            .iter()
            .map(|p| (p.entity, EntityKindTag::Point))
            .chain(self.lines.iter().map(|l| (l.entity, EntityKindTag::Line)))
            .chain(self.arcs.iter().map(|a| (a.entity, EntityKindTag::Arc)))
            .chain(
                self.circles
                    .iter()
                    .map(|c| (c.entity, EntityKindTag::Circle)),
            );
        PickIndex::build(
            entities,
            self.relations.iter().map(|g| g.relation),
            self.dimensions.iter().map(|d| d.dimension),
        )
    }

    fn push_relation(mut self, sketch: &Sketch, id: SketchRelationId) -> Result<Self, PickIdError> {
        let Some(rel) = sketch.relations().get(id).copied() else {
            return Ok(self);
        };
        let Some((anchor, offset_dir)) = relation_anchor(sketch, rel) else {
            return Ok(self);
        };
        self.relations.push(SceneRelationGlyph {
            anchor_mm: anchor,
            offset_dir,
            kind: RelationGlyphKind::from_relation(rel),
            relation: id,
            pick: PickId::relation(id)?,
        });
        Ok(self)
    }

    fn push_dimension(
        mut self,
        sketch: &Sketch,
        id: SketchDimensionId,
    ) -> Result<Self, PickIdError> {
        let Some(dim) = sketch.dimensions().get(id).copied() else {
            return Ok(self);
        };
        let Some(anchor) = dimension_anchor(sketch, dim) else {
            return Ok(self);
        };
        self.dimensions.push(SceneDimension {
            anchor_mm: anchor,
            text: format_dimension(dim),
            dimension: id,
            pick: PickId::dimension(id)?,
        });
        Ok(self)
    }

    fn push_from_sketch(
        mut self,
        sketch: &Sketch,
        id: SketchEntityId,
    ) -> Result<Self, PickIdError> {
        let Some(entity) = sketch.entities().get(id).copied() else {
            unreachable!(
                "entity_order references id {id:?} missing from entities — Sketch invariants broken"
            );
        };
        match entity {
            SketchEntity::Point(p) => {
                self.points.push(ScenePoint {
                    at: p.at(),
                    entity: id,
                    pick: PickId::point(id)?,
                });
            }
            SketchEntity::Line(l) => {
                self.lines.push(SceneLine {
                    a: point_position(sketch, l.a()),
                    b: point_position(sketch, l.b()),
                    entity: id,
                    pick: PickId::line(id)?,
                    for_construction: l.for_construction(),
                });
            }
            SketchEntity::Arc(a) => {
                if let Some(arc) = derive_arc(sketch, id, a, PickId::arc(id)?) {
                    self.arcs.push(arc);
                }
            }
            SketchEntity::Circle(c) => {
                self.circles.push(SceneCircle {
                    center: point_position(sketch, c.center()),
                    radius: c.radius(),
                    entity: id,
                    pick: PickId::circle(id)?,
                    for_construction: c.for_construction(),
                });
            }
        }
        Ok(self)
    }
}

fn point_position(sketch: &Sketch, id: SketchEntityId) -> Point2 {
    let Some(entity) = sketch.entities().get(id) else {
        unreachable!(
            "SketchEntityId {id:?} missing from sketch entities — Sketch invariants broken"
        );
    };
    let SketchEntity::Point(p) = entity else {
        unreachable!(
            "expected Point at {id:?}, got {:?} — Sketch::apply guarantees references resolve to Points",
            entity.kind()
        );
    };
    p.at()
}

fn relation_anchor(sketch: &Sketch, rel: SketchRelation) -> Option<(Point2, Vec2)> {
    rel.references()
        .into_iter()
        .next()
        .and_then(|id| entity_anchor(sketch, id))
}

fn entity_anchor(sketch: &Sketch, id: SketchEntityId) -> Option<(Point2, Vec2)> {
    let e = sketch.entities().get(id)?;
    Some(match *e {
        SketchEntity::Point(p) => (p.at(), Vec2::from_mm(FRAC_1_SQRT_2, FRAC_1_SQRT_2)),
        SketchEntity::Line(l) => line_anchor(sketch, l),
        SketchEntity::Arc(a) => arc_anchor(sketch, a),
        SketchEntity::Circle(c) => circle_anchor(sketch, c),
    })
}

fn line_anchor(sketch: &Sketch, line: LineData) -> (Point2, Vec2) {
    let (ax, ay) = point_position(sketch, line.a()).coords_mm();
    let (bx, by) = point_position(sketch, line.b()).coords_mm();
    let mid = Point2::from_mm(0.5 * (ax + bx), 0.5 * (ay + by));
    let tangent = Vec2::from_mm(bx - ax, by - ay);
    (mid, unit_or(tangent.perp_ccw(), Vec2::from_mm(0.0, 1.0)))
}

fn arc_anchor(sketch: &Sketch, arc: ArcData) -> (Point2, Vec2) {
    let center = point_position(sketch, arc.center());
    let start = point_position(sketch, arc.start());
    let (cx, cy) = center.coords_mm();
    let (sx, sy) = start.coords_mm();
    let radial = Vec2::from_mm(sx - cx, sy - cy);
    (start, unit_or(radial, Vec2::from_mm(1.0, 0.0)))
}

fn circle_anchor(sketch: &Sketch, circle: CircleData) -> (Point2, Vec2) {
    let (cx, cy) = point_position(sketch, circle.center()).coords_mm();
    let r_mm = circle.radius().get::<millimeter>();
    (Point2::from_mm(cx + r_mm, cy), Vec2::from_mm(1.0, 0.0))
}

fn dimension_anchor(sketch: &Sketch, dim: SketchDimension) -> Option<Point2> {
    match dim {
        SketchDimension::Linear { a, b, .. } | SketchDimension::Angular { a, b, .. } => {
            let (ax, ay) = entity_anchor(sketch, a)?.0.coords_mm();
            let (bx, by) = entity_anchor(sketch, b)?.0.coords_mm();
            Some(Point2::from_mm(0.5 * (ax + bx), 0.5 * (ay + by)))
        }
        SketchDimension::Radius { target, .. } | SketchDimension::Diameter { target, .. } => {
            target_center(sketch, target)
        }
    }
}

fn target_center(sketch: &Sketch, id: SketchEntityId) -> Option<Point2> {
    match sketch.entities().get(id)? {
        SketchEntity::Arc(a) => Some(point_position(sketch, a.center())),
        SketchEntity::Circle(c) => Some(point_position(sketch, c.center())),
        _ => None,
    }
}

fn format_dimension(dim: SketchDimension) -> String {
    let body = match (dim, dim.value()) {
        (SketchDimension::Linear { .. }, DimensionValue::Length(len)) => {
            format!("{:.2} mm", len.get::<millimeter>())
        }
        (SketchDimension::Radius { .. }, DimensionValue::Length(len)) => {
            format!("R {:.2}", len.get::<millimeter>())
        }
        (SketchDimension::Diameter { .. }, DimensionValue::Length(len)) => {
            format!("D {:.2}", len.get::<millimeter>())
        }
        (SketchDimension::Angular { .. }, DimensionValue::Angle(a)) => {
            format!("{:.1}°", a.get::<degree>())
        }
        _ => unreachable!(
            "SketchDimension::value pins Length for Linear/Radius/Diameter and Angle for Angular"
        ),
    };
    match dim.kind() {
        bone_document::DimensionKind::Driving => body,
        bone_document::DimensionKind::Driven => format!("({body})"),
    }
}

fn unit_or(v: Vec2, fallback: Vec2) -> Vec2 {
    let len = v.norm_mm();
    if len > 1e-9 {
        let (x, y) = v.coords_mm();
        Vec2::from_mm(x / len, y / len)
    } else {
        fallback
    }
}

fn derive_arc(
    sketch: &Sketch,
    id: SketchEntityId,
    data: bone_document::ArcData,
    pick: PickId,
) -> Option<SceneArc> {
    let center = point_position(sketch, data.center());
    let start = point_position(sketch, data.start());
    let end = point_position(sketch, data.end());
    let arc = Arc2::from_center_start_end(center, start, end, ARC_TOLERANCE).ok()?;
    Some(SceneArc {
        center: arc.center(),
        radius: arc.radius(),
        start_angle: arc.start_angle(),
        sweep_angle: arc.sweep_angle(),
        entity: id,
        pick,
        for_construction: data.for_construction(),
    })
}

const WHITE: LinearRgba = LinearRgba::new(1.0, 1.0, 1.0, 1.0);

#[derive(Clone, Debug, PartialEq)]
pub struct SolidScene {
    positions: Vec<Point3>,
    normals: Vec<UnitVec3>,
    colors: Vec<LinearRgba>,
    pick_ids: Vec<PickId>,
    triangles: Vec<[u32; 3]>,
}

impl SolidScene {
    #[must_use]
    pub fn empty() -> Self {
        Self {
            positions: Vec::new(),
            normals: Vec::new(),
            colors: Vec::new(),
            pick_ids: Vec::new(),
            triangles: Vec::new(),
        }
    }

    pub fn from_mesh(mesh: &SolidMesh) -> Result<Self, PickIdError> {
        mesh.faces()
            .iter()
            .try_fold(Self::empty(), |mut scene, face| {
                let Ok(base) = u32::try_from(scene.positions.len()) else {
                    panic!("solid mesh vertex count fits a u32 index");
                };
                let pick = PickId::brep_face(face.face())?;
                scene.positions.extend_from_slice(face.positions());
                scene.normals.extend_from_slice(face.normals());
                scene.colors.extend(face.positions().iter().map(|_| WHITE));
                scene.pick_ids.extend(face.positions().iter().map(|_| pick));
                scene
                    .triangles
                    .extend(face.triangles().iter().map(|tri| tri.map(|i| i + base)));
                Ok(scene)
            })
    }

    #[must_use]
    pub fn from_parts(
        positions: Vec<Point3>,
        normals: Vec<UnitVec3>,
        colors: Vec<LinearRgba>,
        triangles: Vec<[u32; 3]>,
    ) -> Self {
        assert_eq!(
            positions.len(),
            normals.len(),
            "solid scene needs one normal per position",
        );
        assert_eq!(
            positions.len(),
            colors.len(),
            "solid scene needs one color per position",
        );
        let Ok(vertex_count) = u32::try_from(positions.len()) else {
            panic!("solid scene vertex count fits a u32 index");
        };
        assert!(
            triangles
                .iter()
                .flatten()
                .all(|&index| index < vertex_count),
            "solid scene triangle index out of range",
        );
        let pick_ids = vec![PickId::NONE; positions.len()];
        Self {
            positions,
            normals,
            colors,
            pick_ids,
            triangles,
        }
    }

    #[must_use]
    pub fn merge(mut self, other: Self) -> Self {
        let Ok(base) = u32::try_from(self.positions.len()) else {
            panic!("merged solid scene vertex count fits a u32 index");
        };
        self.positions.extend(other.positions);
        self.normals.extend(other.normals);
        self.colors.extend(other.colors);
        self.pick_ids.extend(other.pick_ids);
        self.triangles
            .extend(other.triangles.into_iter().map(|tri| tri.map(|i| i + base)));
        self
    }

    #[must_use]
    pub fn positions(&self) -> &[Point3] {
        &self.positions
    }

    #[must_use]
    pub fn normals(&self) -> &[UnitVec3] {
        &self.normals
    }

    #[must_use]
    pub fn colors(&self) -> &[LinearRgba] {
        &self.colors
    }

    #[must_use]
    pub fn pick_ids(&self) -> &[PickId] {
        &self.pick_ids
    }

    #[must_use]
    pub fn triangles(&self) -> &[[u32; 3]] {
        &self.triangles
    }
}

const NORMAL_TOLERANCE: Tolerance = Tolerance::new(1.0e-12);
const COPLANAR_DOT: f64 = 1.0 - 1.0e-9;

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct GenuineEdge {
    a: Point3,
    b: Point3,
    pick: PickId,
    crease: CreaseAngle,
}

impl GenuineEdge {
    #[must_use]
    pub const fn new(a: Point3, b: Point3, pick: PickId, crease: CreaseAngle) -> Self {
        Self { a, b, pick, crease }
    }

    #[must_use]
    pub const fn a(self) -> Point3 {
        self.a
    }

    #[must_use]
    pub const fn b(self) -> Point3 {
        self.b
    }

    #[must_use]
    pub const fn pick(self) -> PickId {
        self.pick
    }

    #[must_use]
    pub const fn crease(self) -> CreaseAngle {
        self.crease
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct SilhouetteCandidate {
    a: Point3,
    b: Point3,
    normal_a: UnitVec3,
    normal_b: UnitVec3,
    pick: PickId,
}

impl SilhouetteCandidate {
    #[must_use]
    pub const fn a(self) -> Point3 {
        self.a
    }

    #[must_use]
    pub const fn pick(self) -> PickId {
        self.pick
    }

    #[must_use]
    pub const fn b(self) -> Point3 {
        self.b
    }

    #[must_use]
    pub const fn normal_a(self) -> UnitVec3 {
        self.normal_a
    }

    #[must_use]
    pub const fn normal_b(self) -> UnitVec3 {
        self.normal_b
    }
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct EdgeScene {
    genuine: Vec<GenuineEdge>,
    silhouettes: Vec<SilhouetteCandidate>,
}

impl EdgeScene {
    #[must_use]
    pub const fn empty() -> Self {
        Self {
            genuine: Vec::new(),
            silhouettes: Vec::new(),
        }
    }

    #[must_use]
    pub fn from_genuine(genuine: Vec<GenuineEdge>) -> Self {
        Self {
            genuine,
            silhouettes: Vec::new(),
        }
    }

    pub fn from_solid(
        solid: &BrepSolid,
        mesh: &SolidMesh,
        chord: ChordHeightTolerance,
    ) -> Result<Self, PickIdError> {
        let genuine =
            solid
                .edges_for_render(chord)
                .iter()
                .try_fold(Vec::new(), |mut acc, polyline| {
                    let pick = PickId::brep_edge(polyline.edge())?;
                    let crease = polyline.crease();
                    acc.extend(polyline.points().windows(2).map(|pair| GenuineEdge {
                        a: pair[0],
                        b: pair[1],
                        pick,
                        crease,
                    }));
                    Ok::<_, PickIdError>(acc)
                })?;
        let silhouettes = mesh.faces().iter().try_fold(Vec::new(), |mut acc, slab| {
            let pick = PickId::brep_face(slab.face())?;
            acc.extend(slab_silhouettes(slab, pick));
            Ok::<_, PickIdError>(acc)
        })?;
        Ok(Self {
            genuine,
            silhouettes,
        })
    }

    #[must_use]
    pub fn merge(mut self, other: Self) -> Self {
        self.genuine.extend(other.genuine);
        self.silhouettes.extend(other.silhouettes);
        self
    }

    #[must_use]
    pub fn genuine(&self) -> &[GenuineEdge] {
        &self.genuine
    }

    #[must_use]
    pub fn silhouettes(&self) -> &[SilhouetteCandidate] {
        &self.silhouettes
    }

    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.genuine.is_empty() && self.silhouettes.is_empty()
    }
}

struct EdgeAdjacency {
    a: Point3,
    b: Point3,
    normals: [Option<UnitVec3>; 2],
    incidence: usize,
}

impl EdgeAdjacency {
    const fn new(a: Point3, b: Point3) -> Self {
        Self {
            a,
            b,
            normals: [None, None],
            incidence: 0,
        }
    }

    fn record(mut self, normal: UnitVec3) -> Self {
        if self.incidence < 2 {
            self.normals[self.incidence] = Some(normal);
        }
        self.incidence += 1;
        self
    }

    fn into_candidate(self, pick: PickId) -> Option<SilhouetteCandidate> {
        match (self.incidence, self.normals) {
            (2, [Some(normal_a), Some(normal_b)]) if normal_a.dot(normal_b) < COPLANAR_DOT => {
                Some(SilhouetteCandidate {
                    a: self.a,
                    b: self.b,
                    normal_a,
                    normal_b,
                    pick,
                })
            }
            _ => None,
        }
    }
}

fn slab_silhouettes(slab: &FaceMesh, pick: PickId) -> Vec<SilhouetteCandidate> {
    let positions = slab.positions();
    slab.triangles()
        .iter()
        .fold(
            BTreeMap::<EdgeKey, EdgeAdjacency>::new(),
            |adjacency, tri| match triangle_normal(positions, *tri) {
                Some(normal) => {
                    triangle_edges(*tri)
                        .into_iter()
                        .fold(adjacency, |mut adjacency, (i, j)| {
                            let (a, b) = (positions[i as usize], positions[j as usize]);
                            let key = EdgeKey::new(a, b);
                            let entry = adjacency
                                .remove(&key)
                                .unwrap_or_else(|| EdgeAdjacency::new(a, b));
                            adjacency.insert(key, entry.record(normal));
                            adjacency
                        })
                }
                None => adjacency,
            },
        )
        .into_values()
        .filter_map(|entry| entry.into_candidate(pick))
        .collect()
}

fn triangle_edges(tri: [u32; 3]) -> [(u32, u32); 3] {
    [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])]
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct VertexKey([u64; 3]);

impl VertexKey {
    fn new(p: Point3) -> Self {
        let (x, y, z) = p.coords_mm();
        Self([x.to_bits(), y.to_bits(), z.to_bits()])
    }
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct EdgeKey(VertexKey, VertexKey);

impl EdgeKey {
    fn new(a: Point3, b: Point3) -> Self {
        let (ka, kb) = (VertexKey::new(a), VertexKey::new(b));
        if ka <= kb { Self(ka, kb) } else { Self(kb, ka) }
    }
}

fn triangle_normal(positions: &[Point3], tri: [u32; 3]) -> Option<UnitVec3> {
    let a = positions[tri[0] as usize];
    let b = positions[tri[1] as usize];
    let c = positions[tri[2] as usize];
    (b - a).cross(c - a).try_normalize(NORMAL_TOLERANCE).ok()
}

#[cfg(test)]
mod tests {
    use super::*;
    use bone_document::{EditOutcome, SketchEdit};
    use bone_types::{Point3, SketchPlaneBasis, Tolerance, UnitVec3};
    use uom::si::angle::radian;
    use uom::si::length::millimeter;

    fn 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 len_mm(v: f64) -> Length {
        Length::new::<millimeter>(v)
    }

    fn add_point(sketch: Sketch, x: f64, y: f64) -> (Sketch, SketchEntityId) {
        let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity(
            SketchEntity::point(Point2::from_mm(x, y)),
        )) else {
            panic!("add point");
        };
        (next, id)
    }

    fn add_line(
        sketch: Sketch,
        a: SketchEntityId,
        b: SketchEntityId,
        construction: bool,
    ) -> (Sketch, SketchEntityId) {
        let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity(
            SketchEntity::line(a, b, construction),
        )) else {
            panic!("add line");
        };
        (next, id)
    }

    fn add_circle(
        sketch: Sketch,
        center: SketchEntityId,
        radius_mm: f64,
    ) -> (Sketch, SketchEntityId) {
        let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity(
            SketchEntity::circle(center, len_mm(radius_mm), false),
        )) else {
            panic!("add circle");
        };
        (next, id)
    }

    fn add_arc_ids(
        sketch: Sketch,
        center: SketchEntityId,
        start: SketchEntityId,
        end: SketchEntityId,
    ) -> (Sketch, SketchEntityId) {
        let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity(
            SketchEntity::arc(center, start, end, false),
        )) else {
            panic!("add arc");
        };
        (next, id)
    }

    #[test]
    fn empty_scene_has_no_items() {
        let s = Sketch::new(plane());
        let Ok(scene) = SketchScene::extract(&s) else {
            panic!("extract empty");
        };
        assert!(scene.is_empty());
    }

    #[test]
    fn rectangle_extracts_four_points_and_four_lines() {
        let s = Sketch::new(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, _) = add_line(s, p0, p1, false);
        let (s, _) = add_line(s, p1, p2, false);
        let (s, _) = add_line(s, p2, p3, false);
        let (s, _) = add_line(s, p3, p0, false);
        let Ok(scene) = SketchScene::extract(&s) else {
            panic!("extract");
        };
        assert_eq!(scene.points().len(), 4);
        assert_eq!(scene.lines().len(), 4);
        assert!(scene.arcs().is_empty());
        assert!(scene.circles().is_empty());
        assert_eq!(scene.lines()[0].a(), Point2::from_mm(0.0, 0.0));
        assert_eq!(scene.lines()[0].b(), Point2::from_mm(10.0, 0.0));
    }

    #[test]
    fn construction_flag_propagates_to_line() {
        let s = Sketch::new(plane());
        let (s, a) = add_point(s, 0.0, 0.0);
        let (s, b) = add_point(s, 3.0, 4.0);
        let (s, _) = add_line(s, a, b, true);
        let Ok(scene) = SketchScene::extract(&s) else {
            panic!("extract");
        };
        assert!(scene.lines()[0].for_construction());
    }

    #[test]
    fn circle_extracts_with_center_and_radius() {
        let s = Sketch::new(plane());
        let (s, c) = add_point(s, 1.0, 2.0);
        let (s, _) = add_circle(s, c, 4.0);
        let Ok(scene) = SketchScene::extract(&s) else {
            panic!("extract");
        };
        assert_eq!(scene.circles().len(), 1);
        assert_eq!(scene.circles()[0].center(), Point2::from_mm(1.0, 2.0));
        assert!((scene.circles()[0].radius().get::<millimeter>() - 4.0).abs() < 1e-12);
    }

    #[test]
    fn arc_derives_radius_and_ccw_sweep() {
        let s = Sketch::new(plane());
        let (s, c) = add_point(s, 0.0, 0.0);
        let (s, start) = add_point(s, 5.0, 0.0);
        let (s, end) = add_point(s, 0.0, 5.0);
        let (s, _) = add_arc_ids(s, c, start, end);
        let Ok(scene) = SketchScene::extract(&s) else {
            panic!("extract");
        };
        let arc = scene.arcs()[0];
        assert!((arc.radius().get::<millimeter>() - 5.0).abs() < 1e-12);
        let start_rad = arc.start_angle().get::<radian>();
        let sweep_rad = arc.sweep_angle().get::<radian>();
        assert!(start_rad.abs() < 1e-12);
        assert!((sweep_rad - core::f64::consts::FRAC_PI_2).abs() < 1e-12);
    }

    #[test]
    fn pick_index_round_trips_each_scene_item() {
        use crate::pick::PickedItem;
        let s = Sketch::new(plane());
        let (s, p0) = add_point(s, 0.0, 0.0);
        let (s, p1) = add_point(s, 1.0, 0.0);
        let (s, line) = add_line(s, p0, p1, false);
        let (s, cc) = add_point(s, 5.0, 0.0);
        let (s, circle) = add_circle(s, cc, 2.0);
        let (s, ac) = add_point(s, 10.0, 0.0);
        let (s, a_start) = add_point(s, 12.0, 0.0);
        let (s, a_end) = add_point(s, 10.0, 2.0);
        let (s, arc) = add_arc_ids(s, ac, a_start, a_end);
        let Ok(scene) = SketchScene::extract(&s) else {
            panic!("extract");
        };
        let Ok(index) = scene.pick_index() else {
            panic!("pick index");
        };
        assert_eq!(
            scene.points()[0].pick().unpack(&index),
            Some(PickedItem::Point(p0))
        );
        assert_eq!(
            scene.lines()[0].pick().unpack(&index),
            Some(PickedItem::Line(line))
        );
        assert_eq!(
            scene.arcs()[0].pick().unpack(&index),
            Some(PickedItem::Arc(arc))
        );
        assert_eq!(
            scene.circles()[0].pick().unpack(&index),
            Some(PickedItem::Circle(circle))
        );
    }

    #[test]
    fn arc_coincident_start_and_end_is_dropped() {
        let s = Sketch::new(plane());
        let (s, c) = add_point(s, 0.0, 0.0);
        let (s, start) = add_point(s, 5.0, 0.0);
        let (s, end) = add_point(s, 5.0, 0.0);
        let (s, _) = add_arc_ids(s, c, start, end);
        let Ok(scene) = SketchScene::extract(&s) else {
            panic!("extract");
        };
        assert!(scene.arcs().is_empty());
    }

    #[test]
    fn arc_with_zero_radius_is_dropped() {
        let s = Sketch::new(plane());
        let (s, c) = add_point(s, 0.0, 0.0);
        let (s, start) = add_point(s, 0.0, 0.0);
        let (s, end) = add_point(s, 5.0, 0.0);
        let (s, _) = add_arc_ids(s, c, start, end);
        let Ok(scene) = SketchScene::extract(&s) else {
            panic!("extract");
        };
        assert!(scene.arcs().is_empty());
    }

    #[test]
    fn arc_sweep_follows_ccw_convention() {
        let s = Sketch::new(plane());
        let (s, c) = add_point(s, 0.0, 0.0);
        let (s, start) = add_point(s, 0.0, 5.0);
        let (s, end) = add_point(s, 5.0, 0.0);
        let (s, _) = add_arc_ids(s, c, start, end);
        let Ok(scene) = SketchScene::extract(&s) else {
            panic!("extract");
        };
        let arc = scene.arcs()[0];
        let start_rad = arc.start_angle().get::<radian>();
        let sweep_rad = arc.sweep_angle().get::<radian>();
        assert!((start_rad - core::f64::consts::FRAC_PI_2).abs() < 1e-12);
        assert!((sweep_rad - 3.0 * core::f64::consts::FRAC_PI_2).abs() < 1e-12);
    }
}
