use std::collections::BTreeSet;
use std::collections::HashMap;
use std::collections::HashSet;

use bone_types::{
    EdgeLabel, EdgeRole, FaceLabel, Length, Point3, Tolerance, VertexLabel, VertexRole,
};
use truck_modeling::{
    Edge, EdgeID, Face, FaceID, Plane, Point3 as TPoint, Shell, Solid, Surface, Vertex, VertexID,
    builder,
};
use uom::si::length::millimeter;

use super::super::build::{FragmentId, SolidLabeling, assemble};
use super::super::{BooleanGap, BrepError, BrepSolid};
use super::{Face as BFace, PlanarFace};

pub(super) struct VertexRec {
    pub(super) position: Point3,
    pub(super) vertex: Vertex,
    pub(super) label: VertexLabel,
    pub(super) owner: FaceLabel,
}

pub(super) fn truck_point(p: Point3) -> TPoint {
    let (x, y, z) = p.coords_mm();
    TPoint::new(x, y, z)
}

fn surface_of(face: &PlanarFace) -> Surface {
    let origin = face.plane.origin();
    let one = origin + face.plane.x_axis().into_vec(Length::new::<millimeter>(1.0));
    let another = origin + face.plane.y_axis().into_vec(Length::new::<millimeter>(1.0));
    Surface::Plane(Plane::new(
        truck_point(origin),
        truck_point(one),
        truck_point(another),
    ))
}

pub(super) fn intern_vertex(
    verts: &mut Vec<VertexRec>,
    position: Point3,
    label: VertexLabel,
    owner: FaceLabel,
    tolerance: Tolerance,
) -> usize {
    let found = verts
        .iter()
        .position(|rec| (rec.position - position).norm_mm() <= tolerance.value());
    if let Some(index) = found {
        if owner < verts[index].owner {
            verts[index].owner = owner;
            verts[index].label = label;
        }
        index
    } else {
        verts.push(VertexRec {
            position,
            vertex: builder::vertex(truck_point(position)),
            label,
            owner,
        });
        verts.len() - 1
    }
}

fn edge_key(a: usize, b: usize) -> (usize, usize) {
    if a <= b { (a, b) } else { (b, a) }
}

fn resolve_intersection_vertices(
    verts: &mut [VertexRec],
    kept: &[PlanarFace],
    face_rings: &[Vec<usize>],
) {
    let incidence = face_rings.iter().zip(kept).fold(
        HashMap::<usize, BTreeSet<FaceLabel>>::new(),
        |mut acc, (ring, face)| {
            ring.iter().for_each(|index| {
                acc.entry(*index).or_default().insert(face.label);
            });
            acc
        },
    );
    verts.iter_mut().enumerate().for_each(|(index, rec)| {
        if matches!(rec.label.role, VertexRole::Intersection { .. }) {
            let faces = three_smallest(incidence.get(&index));
            rec.label = VertexLabel {
                feature: faces[0].feature,
                role: VertexRole::Intersection { faces },
            };
        }
    });
}

fn three_smallest(incident: Option<&BTreeSet<FaceLabel>>) -> [FaceLabel; 3] {
    let ordered: Vec<FaceLabel> = incident.into_iter().flatten().copied().collect();
    let pick = |index: usize| ordered[index.min(ordered.len().saturating_sub(1))];
    [pick(0), pick(1), pick(2)]
}

pub(crate) fn assemble_result(
    kept: &[BFace],
    tolerance: Tolerance,
) -> Result<BrepSolid, BrepError> {
    let curved = kept.iter().any(|face| match face {
        BFace::Tube(_) => true,
        BFace::Planar(planar) => {
            !planar.holes.is_empty()
                || planar.polygon.len() < 3
                || planar
                    .curves
                    .iter()
                    .any(|curve| !matches!(curve, EdgeCurve3::Line(_)))
        }
    });
    if curved {
        return assemble_curved(kept, tolerance);
    }
    let planar: Vec<PlanarFace> = kept
        .iter()
        .map(|face| match face {
            BFace::Planar(planar) => Ok((**planar).clone()),
            BFace::Tube(_) => Err(BrepError::BooleanUnsupported {
                detail: BooleanGap::CurvedFace,
            }),
        })
        .collect::<Result<_, _>>()?;
    assemble_planar(&planar, tolerance)
}

fn assemble_planar(kept: &[PlanarFace], tolerance: Tolerance) -> Result<BrepSolid, BrepError> {
    if kept.is_empty() {
        return Err(BrepError::BooleanEmpty);
    }
    let mut verts: Vec<VertexRec> = Vec::new();
    let face_rings: Vec<Vec<usize>> = kept
        .iter()
        .map(|face| {
            face.polygon
                .iter()
                .map(|pv| intern_vertex(&mut verts, pv.position, pv.label, face.label, tolerance))
                .collect()
        })
        .collect();

    if face_rings.iter().any(|ring| ring_is_degenerate(ring)) {
        return Err(BrepError::BooleanIndeterminate);
    }

    resolve_intersection_vertices(&mut verts, kept, &face_rings);

    let edges = build_edges(&verts, &face_rings);
    let faces = build_faces(kept, &face_rings, &edges)?;

    let face_labels: HashMap<FaceID, FaceLabel> = faces
        .iter()
        .zip(kept)
        .map(|(face, source)| (face.id(), source.label))
        .collect();

    let inherited = inherited_edge_labels(kept, &face_rings, &edges);
    let shells = into_shells(&faces, &face_rings, &verts);
    let solid = Solid::try_new(shells).map_err(|_| BrepError::BooleanIndeterminate)?;
    let labeling = label_solid(
        &solid,
        &verts,
        &face_labels,
        &inherited,
        HashMap::new(),
        HashSet::new(),
    );
    assemble(solid, &labeling)
}

fn inherited_edge_labels(
    kept: &[PlanarFace],
    face_rings: &[Vec<usize>],
    edges: &HashMap<(usize, usize), Edge>,
) -> HashMap<EdgeID, EdgeLabel> {
    let agreed = kept.iter().zip(face_rings).fold(
        HashMap::<(usize, usize), Option<EdgeLabel>>::new(),
        |mut acc, (face, ring)| {
            let count = ring.len();
            (0..count).for_each(|index| {
                let key = edge_key(ring[index], ring[(index + 1) % count]);
                let label = face.edges[index];
                acc.entry(key)
                    .and_modify(|current| {
                        if *current != Some(label) {
                            *current = None;
                        }
                    })
                    .or_insert(Some(label));
            });
            acc
        },
    );
    edges
        .iter()
        .filter_map(|(key, edge)| {
            agreed
                .get(key)
                .copied()
                .flatten()
                .map(|label| (edge.id(), label))
        })
        .collect()
}

fn ring_is_degenerate(ring: &[usize]) -> bool {
    let count = ring.len();
    count < 3 || (0..count).any(|i| ring[i] == ring[(i + 1) % count])
}

fn build_edges(verts: &[VertexRec], face_rings: &[Vec<usize>]) -> HashMap<(usize, usize), Edge> {
    face_rings
        .iter()
        .flat_map(|ring| ring_pairs(ring))
        .fold(HashMap::new(), |mut edges, (a, b)| {
            let key = edge_key(a, b);
            edges
                .entry(key)
                .or_insert_with(|| builder::line(&verts[key.0].vertex, &verts[key.1].vertex));
            edges
        })
}

fn ring_pairs(ring: &[usize]) -> Vec<(usize, usize)> {
    let count = ring.len();
    (0..count)
        .map(|i| (ring[i], ring[(i + 1) % count]))
        .collect()
}

fn build_faces(
    kept: &[PlanarFace],
    face_rings: &[Vec<usize>],
    edges: &HashMap<(usize, usize), Edge>,
) -> Result<Vec<Face>, BrepError> {
    kept.iter()
        .zip(face_rings)
        .map(|(source, ring)| {
            let wire: truck_modeling::Wire = ring_pairs(ring)
                .into_iter()
                .map(|(a, b)| {
                    let canonical = &edges[&edge_key(a, b)];
                    if a <= b {
                        canonical.clone()
                    } else {
                        canonical.inverse()
                    }
                })
                .collect();
            Face::try_new(vec![wire], surface_of(source))
                .map_err(|_| BrepError::BooleanIndeterminate)
        })
        .collect()
}

fn into_shells(faces: &[Face], face_rings: &[Vec<usize>], verts: &[VertexRec]) -> Vec<Shell> {
    let mut parent: Vec<usize> = (0..faces.len()).collect();
    let edge_owners = face_rings.iter().enumerate().fold(
        HashMap::<(usize, usize), Vec<usize>>::new(),
        |mut owners, (face_index, ring)| {
            ring_pairs(ring).into_iter().for_each(|(a, b)| {
                owners.entry(edge_key(a, b)).or_default().push(face_index);
            });
            owners
        },
    );
    edge_owners.values().for_each(|members| {
        members.windows(2).for_each(|pair| {
            let ra = find(&mut parent, pair[0]);
            let rb = find(&mut parent, pair[1]);
            if ra != rb {
                parent[ra] = rb;
            }
        });
    });
    let mut groups: HashMap<usize, Vec<usize>> = HashMap::new();
    (0..faces.len()).for_each(|index| {
        let root = find(&mut parent, index);
        groups.entry(root).or_default().push(index);
    });
    let mut ordered: Vec<(usize, Vec<usize>)> = groups.into_iter().collect();
    ordered.sort_by(|(root_a, members_a), (root_b, members_b)| {
        shell_extent(members_b, face_rings, verts)
            .total_cmp(&shell_extent(members_a, face_rings, verts))
            .then(root_a.cmp(root_b))
    });
    ordered
        .into_iter()
        .map(|(_, members)| {
            members
                .iter()
                .map(|index| faces[*index].clone())
                .collect::<Shell>()
        })
        .collect()
}

fn shell_extent(members: &[usize], face_rings: &[Vec<usize>], verts: &[VertexRec]) -> f64 {
    let bounds = members
        .iter()
        .flat_map(|index| face_rings[*index].iter())
        .map(|vertex| verts[*vertex].position.coords_mm())
        .fold(None, |acc: Option<([f64; 3], [f64; 3])>, (x, y, z)| {
            Some(acc.map_or(([x, y, z], [x, y, z]), |(lo, hi)| {
                (
                    [lo[0].min(x), lo[1].min(y), lo[2].min(z)],
                    [hi[0].max(x), hi[1].max(y), hi[2].max(z)],
                )
            }))
        });
    bounds.map_or(0.0, |(lo, hi)| {
        (hi[0] - lo[0]) * (hi[1] - lo[1]) * (hi[2] - lo[2])
    })
}

fn find(parent: &mut Vec<usize>, x: usize) -> usize {
    if parent[x] == x {
        x
    } else {
        let root = find(parent, parent[x]);
        parent[x] = root;
        root
    }
}

pub(super) fn label_solid(
    solid: &Solid,
    verts: &[VertexRec],
    face_labels: &HashMap<FaceID, FaceLabel>,
    inherited: &HashMap<EdgeID, EdgeLabel>,
    edge_curves: HashMap<EdgeID, super::super::edges::EdgeCurve3>,
    closed_curves: HashSet<bone_types::SketchEntityId>,
) -> SolidLabeling {
    let vertices: HashMap<VertexID, VertexLabel> = verts
        .iter()
        .map(|rec| (rec.vertex.id(), rec.label))
        .collect();

    let faces: HashMap<FaceID, FaceLabel> = solid
        .face_iter()
        .filter_map(|face| face_labels.get(&face.id()).map(|label| (face.id(), *label)))
        .collect();

    let adjacency = solid.face_iter().fold(
        HashMap::<truck_modeling::EdgeID, Vec<FaceLabel>>::new(),
        |mut adjacency, face| {
            let label = face_labels.get(&face.id()).copied();
            face.boundaries().iter().for_each(|wire| {
                wire.edge_iter().for_each(|edge| {
                    if let Some(label) = label {
                        adjacency.entry(edge.id()).or_default().push(label);
                    }
                });
            });
            adjacency
        },
    );

    let edges: HashMap<truck_modeling::EdgeID, EdgeLabel> = adjacency
        .into_iter()
        .filter_map(|(edge_id, labels)| {
            inherited
                .get(&edge_id)
                .copied()
                .or_else(|| edge_label(&labels))
                .map(|label| (edge_id, label))
        })
        .collect();

    let face_prints: HashMap<FaceID, Vec<[f64; 3]>> = solid
        .face_iter()
        .map(|face| (face.id(), face_fingerprint(face)))
        .collect();
    let edge_prints: HashMap<EdgeID, Vec<[f64; 3]>> = solid
        .edge_iter()
        .map(|edge| (edge.id(), edge_fingerprint(&edge)))
        .collect();

    let face_fragments = assign_fragments(
        faces
            .iter()
            .map(|(id, label)| (*id, *label, lookup(&face_prints, id))),
    );
    let edge_fragments = assign_fragments(
        edges
            .iter()
            .map(|(id, label)| (*id, *label, lookup(&edge_prints, id))),
    );

    SolidLabeling {
        faces,
        edges,
        vertices,
        closed_curves,
        edge_curves,
        face_fragments,
        edge_fragments,
        vertex_fragments: HashMap::new(),
    }
}

fn lookup<Id: std::hash::Hash + Eq>(map: &HashMap<Id, Vec<[f64; 3]>>, id: &Id) -> Vec<[f64; 3]> {
    map.get(id).cloned().unwrap_or_default()
}

fn to_array(point: TPoint) -> [f64; 3] {
    [point.x, point.y, point.z]
}

fn cmp_point(a: &[f64; 3], b: &[f64; 3]) -> std::cmp::Ordering {
    a[0].total_cmp(&b[0])
        .then(a[1].total_cmp(&b[1]))
        .then(a[2].total_cmp(&b[2]))
}

fn cmp_print(a: &[[f64; 3]], b: &[[f64; 3]]) -> std::cmp::Ordering {
    a.iter()
        .zip(b)
        .map(|(p, q)| cmp_point(p, q))
        .find(|ordering| *ordering != std::cmp::Ordering::Equal)
        .unwrap_or_else(|| a.len().cmp(&b.len()))
}

fn sorted_points(points: impl Iterator<Item = TPoint>) -> Vec<[f64; 3]> {
    let mut out: Vec<[f64; 3]> = points.map(to_array).collect();
    out.sort_by(cmp_point);
    out
}

fn face_fingerprint(face: &Face) -> Vec<[f64; 3]> {
    sorted_points(
        face.boundaries()
            .iter()
            .flat_map(|wire| wire.edge_iter().map(|edge| edge.front().point())),
    )
}

fn edge_fingerprint(edge: &Edge) -> Vec<[f64; 3]> {
    sorted_points([edge.front().point(), edge.back().point()].into_iter())
}

fn assign_fragments<Id, L>(
    entries: impl Iterator<Item = (Id, L, Vec<[f64; 3]>)>,
) -> HashMap<Id, FragmentId>
where
    Id: std::hash::Hash + Eq + Copy,
    L: std::hash::Hash + Eq,
{
    let groups = entries.fold(
        HashMap::<L, Vec<(Id, Vec<[f64; 3]>)>>::new(),
        |mut acc, (id, label, key)| {
            acc.entry(label).or_default().push((id, key));
            acc
        },
    );
    groups
        .into_values()
        .flat_map(|mut members| {
            members.sort_by(|(_, a), (_, b)| cmp_print(a, b));
            members
                .into_iter()
                .enumerate()
                .map(|(index, (id, _))| {
                    (
                        id,
                        FragmentId::new(u16::try_from(index).unwrap_or(u16::MAX)),
                    )
                })
                .collect::<Vec<_>>()
        })
        .collect()
}

fn edge_label(face_labels: &[FaceLabel]) -> Option<EdgeLabel> {
    let mut sorted = face_labels.to_vec();
    sorted.sort_unstable();
    match (sorted.first().copied(), sorted.last().copied()) {
        (Some(a), Some(b)) => Some(EdgeLabel {
            feature: a.feature.min(b.feature),
            role: EdgeRole::intersection(a, b),
        }),
        _ => None,
    }
}

use bone_types::{Angle, UnitVec3};
use truck_modeling::{Curve, Line, Processor, RevolutedCurve, Vector3};
use uom::si::angle::radian;

use super::super::edges::EdgeCurve3;
use super::as_vec;
use super::model::Tube;
use crate::arc3::Arc3;
use crate::circle3::Circle3;
use crate::curve3::Curve3;
use crate::line3::Line3;

const ARC_HALF: f64 = core::f64::consts::PI;

use super::model::{canonical_axis, canonical_basis};

fn loop_is_circular(curves: &[EdgeCurve3]) -> bool {
    curves
        .iter()
        .any(|curve| matches!(curve, EdgeCurve3::Circle(_) | EdgeCurve3::Arc(_)))
}

fn circular_normal(curves: &[EdgeCurve3], fallback: UnitVec3) -> UnitVec3 {
    curves
        .iter()
        .find_map(|curve| match curve {
            EdgeCurve3::Circle(circle) => Some(circle.normal()),
            EdgeCurve3::Arc(arc) => Some(arc.normal()),
            EdgeCurve3::Line(_) => None,
        })
        .unwrap_or(fallback)
}

fn synth_vertex(owner: FaceLabel, position: Point3) -> VertexLabel {
    VertexLabel {
        feature: owner.feature,
        role: VertexRole::Cut {
            key: cut_key(position),
        },
    }
}

struct CurvedBuild {
    verts: Vec<VertexRec>,
    edges: HashMap<(usize, usize, [u64; 3]), Edge>,
    inherited: HashMap<EdgeID, EdgeLabel>,
    edge_curves: HashMap<EdgeID, EdgeCurve3>,
}

impl CurvedBuild {
    fn new() -> Self {
        Self {
            verts: Vec::new(),
            edges: HashMap::new(),
            inherited: HashMap::new(),
            edge_curves: HashMap::new(),
        }
    }
}

fn quantize(point: Point3) -> [u64; 3] {
    let (x, y, z) = point.coords_mm();
    let scale = 1.0e6;
    let grid = |value: f64| {
        let snapped = (value * scale).round();
        let normalized = if snapped == 0.0 { 0.0 } else { snapped };
        normalized.to_bits()
    };
    [grid(x), grid(y), grid(z)]
}

pub(super) fn cut_key(point: Point3) -> bone_types::CutKey {
    bone_types::CutKey::new(quantize(point))
}

struct HalfEdge {
    start: Point3,
    end: Point3,
    start_label: VertexLabel,
    end_label: VertexLabel,
    curve: EdgeCurve3,
    label: EdgeLabel,
}

fn intern_edge(
    build: &mut CurvedBuild,
    half: &HalfEdge,
    owner: FaceLabel,
    tolerance: Tolerance,
) -> Edge {
    let ia = intern_vertex(
        &mut build.verts,
        half.start,
        half.start_label,
        owner,
        tolerance,
    );
    let ib = intern_vertex(&mut build.verts, half.end, half.end_label, owner, tolerance);
    let mid = half.curve.evaluate(bone_types::Parameter::new(0.5));
    let key = (ia.min(ib), ia.max(ib), quantize(mid));
    if let Some(edge) = build.edges.get(&key) {
        return if ia <= ib {
            edge.clone()
        } else {
            edge.inverse()
        };
    }
    let (lo, hi, lo_curve) = if ia <= ib {
        (ia, ib, half.curve.clone())
    } else {
        (ib, ia, super::model::reverse_curve(&half.curve))
    };
    let edge = match &lo_curve {
        EdgeCurve3::Arc(arc) => builder::circle_arc(
            &build.verts[lo].vertex,
            &build.verts[hi].vertex,
            truck_point(arc.evaluate(bone_types::Parameter::new(0.5))),
        ),
        EdgeCurve3::Line(_) | EdgeCurve3::Circle(_) => {
            builder::line(&build.verts[lo].vertex, &build.verts[hi].vertex)
        }
    };
    build.inherited.insert(edge.id(), half.label);
    build.edge_curves.insert(edge.id(), lo_curve);
    build.edges.insert(key, edge.clone());
    if ia <= ib { edge } else { edge.inverse() }
}

fn arc_curve(
    center: Point3,
    rx: UnitVec3,
    ry: UnitVec3,
    radius: Length,
    start: f64,
    sweep: f64,
) -> EdgeCurve3 {
    let plane = bone_types::Plane3::new_unchecked(center, rx, ry);
    Arc3::new(
        plane,
        radius,
        Angle::new::<radian>(start),
        Angle::new::<radian>(sweep),
        Tolerance::new(1.0e-9),
    )
    .map_or_else(
        |_| EdgeCurve3::Line(Line3::new_unchecked(center, center)),
        EdgeCurve3::Arc,
    )
}

fn loop_half_edges(
    verts: &[super::model::PlanarVertex],
    edges: &[EdgeLabel],
    curves: &[EdgeCurve3],
    owner: FaceLabel,
    tolerance: Tolerance,
) -> Vec<HalfEdge> {
    let count = verts.len();
    (0..count)
        .flat_map(|i| {
            let a = verts[i];
            let b = verts[(i + 1) % count];
            let curve = &curves[i];
            let label = edges[i];
            match curve {
                EdgeCurve3::Circle(circle) => circle_half_edges(circle, label, owner, tolerance),
                _ => vec![HalfEdge {
                    start: a.position,
                    end: b.position,
                    start_label: a.label,
                    end_label: b.label,
                    curve: curve.clone(),
                    label,
                }],
            }
        })
        .collect()
}

fn circle_half_edges(
    circle: &Circle3,
    label: EdgeLabel,
    owner: FaceLabel,
    tolerance: Tolerance,
) -> Vec<HalfEdge> {
    let Some((rx, ry)) = canonical_basis(circle.normal(), tolerance) else {
        return Vec::new();
    };
    let center = circle.center();
    let r = circle.radius_mm();
    let p0 = center + as_vec(rx) * r;
    let p1 = center - as_vec(rx) * r;
    let label_p0 = synth_vertex(owner, p0);
    let label_p1 = synth_vertex(owner, p1);
    vec![
        HalfEdge {
            start: p0,
            end: p1,
            start_label: label_p0,
            end_label: label_p1,
            curve: arc_curve(center, rx, ry, circle.radius(), 0.0, ARC_HALF),
            label,
        },
        HalfEdge {
            start: p1,
            end: p0,
            start_label: label_p1,
            end_label: label_p0,
            curve: arc_curve(center, rx, ry, circle.radius(), ARC_HALF, ARC_HALF),
            label,
        },
    ]
}

fn reverse_half_edges(chain: Vec<HalfEdge>) -> Vec<HalfEdge> {
    chain
        .into_iter()
        .rev()
        .map(|half| HalfEdge {
            start: half.end,
            end: half.start,
            start_label: half.end_label,
            end_label: half.start_label,
            curve: super::model::reverse_curve(&half.curve),
            label: half.label,
        })
        .collect()
}

fn plane_surface(plane: &Plane) -> Surface {
    Surface::Plane(*plane)
}

fn truck_plane(plane: &bone_types::Plane3) -> Plane {
    Plane::origin_axes(plane.origin(), plane.x_axis(), plane.y_axis())
}

trait PlaneCtor {
    fn origin_axes(origin: Point3, x: UnitVec3, y: UnitVec3) -> Self;
}

impl PlaneCtor for Plane {
    fn origin_axes(origin: Point3, x: UnitVec3, y: UnitVec3) -> Self {
        let o = truck_point(origin);
        let px = truck_point(origin + x.into_vec(Length::new::<millimeter>(1.0)));
        let py = truck_point(origin + y.into_vec(Length::new::<millimeter>(1.0)));
        Plane::new(o, px, py)
    }
}

fn wire_from(
    build: &mut CurvedBuild,
    chain: &[HalfEdge],
    owner: FaceLabel,
    tolerance: Tolerance,
) -> truck_modeling::Wire {
    chain
        .iter()
        .map(|half| intern_edge(build, half, owner, tolerance))
        .collect()
}

fn build_planar(
    build: &mut CurvedBuild,
    face: &PlanarFace,
    tolerance: Tolerance,
) -> Result<(Face, FaceLabel), BrepError> {
    let outward = face.plane.normal();
    let mut outer = loop_half_edges(
        &face.polygon,
        &face.edges,
        &face.curves,
        face.label,
        tolerance,
    );
    if loop_is_circular(&face.curves)
        && super::model::dot(
            canonical_axis(circular_normal(&face.curves, outward)),
            outward,
        ) < 0.0
    {
        outer = reverse_half_edges(outer);
    }
    let outer_wire = wire_from(build, &outer, face.label, tolerance);
    let holes = face
        .holes
        .iter()
        .map(|hole| {
            let chain = loop_half_edges(
                &hole.verts,
                &hole.edges,
                &hole.curves,
                face.label,
                tolerance,
            );
            let oriented = if super::model::dot(
                canonical_axis(circular_normal(&hole.curves, outward)),
                outward,
            ) > 0.0
            {
                reverse_half_edges(chain)
            } else {
                chain
            };
            wire_from(build, &oriented, face.label, tolerance)
        })
        .collect::<Vec<_>>();
    let mut wires = vec![outer_wire];
    wires.extend(holes);
    let surface = plane_surface(&truck_plane(&face.plane));
    let truck_face = Face::try_new(wires, surface).map_err(|_| BrepError::BooleanIndeterminate)?;
    Ok((truck_face, face.label))
}

fn wall_surface(lo: Point3, hi: Point3, axis: UnitVec3, rx: UnitVec3, radius: f64) -> Surface {
    let generator = Curve::Line(Line(
        truck_point(lo + as_vec(rx) * radius),
        truck_point(hi + as_vec(rx) * radius),
    ));
    let (ax, ay, az) = axis.components();
    let revolved =
        RevolutedCurve::by_revolution(generator, truck_point(lo), Vector3::new(ax, ay, az));
    Surface::RevolutedCurve(Processor::new(revolved))
}

struct Patch {
    a: f64,
    b: f64,
}

fn split_span(theta0: f64, theta1: f64) -> Vec<Patch> {
    let total = theta1 - theta0;
    if total >= core::f64::consts::TAU - 1.0e-6 {
        let mid = theta0 + total / 2.0;
        return vec![Patch { a: theta0, b: mid }, Patch { a: mid, b: theta1 }];
    }
    vec![Patch {
        a: theta0,
        b: theta1,
    }]
}

fn build_tube(
    build: &mut CurvedBuild,
    tube: &Tube,
    tolerance: Tolerance,
) -> Result<Vec<(Face, FaceLabel)>, BrepError> {
    let cyl = tube.cyl;
    let Some((rx, ry)) = canonical_basis(cyl.axis, tolerance) else {
        return Err(BrepError::BooleanIndeterminate);
    };
    let canon = canonical_axis(cyl.axis);
    let radius = cyl.radius;
    let r = cyl.radius_mm();
    let lo_center = cyl.axis_origin + as_vec(cyl.axis) * tube.z0;
    let hi_center = cyl.axis_origin + as_vec(cyl.axis) * tube.z1;
    let along = super::model::dot(cyl.axis, canon);
    let point_at = |center: Point3, theta: f64| -> Point3 {
        center + as_vec(rx) * (r * theta.cos()) + as_vec(ry) * (r * theta.sin())
    };
    let patches = split_span(tube.theta0, tube.theta1);
    patches
        .iter()
        .map(|patch| {
            let (a, b) = (patch.a, patch.b);
            let start_bottom = point_at(lo_center, a);
            let end_bottom = point_at(lo_center, b);
            let start_top = point_at(hi_center, a);
            let end_top = point_at(hi_center, b);
            let label_start_bottom = synth_vertex(tube.label, start_bottom);
            let label_end_bottom = synth_vertex(tube.label, end_bottom);
            let label_start_top = synth_vertex(tube.label, start_top);
            let label_end_top = synth_vertex(tube.label, end_top);
            let bottom = HalfEdge {
                start: start_bottom,
                end: end_bottom,
                start_label: label_start_bottom,
                end_label: label_end_bottom,
                curve: arc_curve(lo_center, rx, ry, radius, a, b - a),
                label: tube.rim_bottom,
            };
            let top = HalfEdge {
                start: start_top,
                end: end_top,
                start_label: label_start_top,
                end_label: label_end_top,
                curve: arc_curve(hi_center, rx, ry, radius, a, b - a),
                label: tube.rim_top,
            };
            let seam_a = HalfEdge {
                start: start_bottom,
                end: start_top,
                start_label: label_start_bottom,
                end_label: label_start_top,
                curve: EdgeCurve3::Line(Line3::new_unchecked(start_bottom, start_top)),
                label: tube.seam,
            };
            let seam_b = HalfEdge {
                start: end_bottom,
                end: end_top,
                start_label: label_end_bottom,
                end_label: label_end_top,
                curve: EdgeCurve3::Line(Line3::new_unchecked(end_bottom, end_top)),
                label: tube.seam,
            };
            let chain = vec![bottom, seam_b, flip_half(&top), flip_half(&seam_a)];
            let wire = wire_from(build, &chain, tube.label, tolerance);
            let base = wall_surface(lo_center, hi_center, cyl.axis, rx, r);
            let surface = truck_modeling::Invertible::inverse(&base);
            let outward_face = tube.outward == (along > 0.0);
            let wall =
                Face::try_new(vec![wire], surface).map_err(|_| BrepError::BooleanIndeterminate)?;
            let face = if outward_face { wall } else { wall.inverse() };
            Ok((face, tube.label))
        })
        .collect()
}

fn flip_half(half: &HalfEdge) -> HalfEdge {
    HalfEdge {
        start: half.end,
        end: half.start,
        start_label: half.end_label,
        end_label: half.start_label,
        curve: super::model::reverse_curve(&half.curve),
        label: half.label,
    }
}

fn build_curved_faces(
    build: &mut CurvedBuild,
    bface: &BFace,
    tolerance: Tolerance,
) -> Result<Vec<(Face, FaceLabel)>, BrepError> {
    match bface {
        BFace::Planar(planar) => build_planar(build, planar, tolerance).map(|face| vec![face]),
        BFace::Tube(tube) => build_tube(build, tube, tolerance),
    }
}

fn group_shells(faces: &[(Face, FaceLabel)]) -> Vec<Shell> {
    let mut parent: Vec<usize> = (0..faces.len()).collect();
    let edge_owners = faces.iter().enumerate().fold(
        HashMap::<EdgeID, Vec<usize>>::new(),
        |mut owners, (index, (face, _))| {
            face.boundaries().iter().for_each(|wire| {
                wire.edge_iter().for_each(|edge| {
                    owners.entry(edge.id()).or_default().push(index);
                });
            });
            owners
        },
    );
    edge_owners.values().for_each(|members| {
        members.windows(2).for_each(|pair| {
            let ra = find(&mut parent, pair[0]);
            let rb = find(&mut parent, pair[1]);
            if ra != rb {
                parent[ra] = rb;
            }
        });
    });
    let mut groups: HashMap<usize, Vec<usize>> = HashMap::new();
    (0..faces.len()).for_each(|index| {
        let root = find(&mut parent, index);
        groups.entry(root).or_default().push(index);
    });
    let mut ordered: Vec<(usize, Vec<usize>)> = groups.into_iter().collect();
    ordered.sort_by(|(root_a, members_a), (root_b, members_b)| {
        face_group_extent(members_b, faces)
            .total_cmp(&face_group_extent(members_a, faces))
            .then(root_a.cmp(root_b))
    });
    ordered
        .into_iter()
        .map(|(_, members)| {
            members
                .iter()
                .map(|index| faces[*index].0.clone())
                .collect()
        })
        .collect()
}

fn face_group_extent(members: &[usize], faces: &[(Face, FaceLabel)]) -> f64 {
    let points: Vec<[f64; 3]> = members
        .iter()
        .flat_map(|&index| {
            faces[index]
                .0
                .boundaries()
                .iter()
                .flat_map(|wire| {
                    wire.edge_iter()
                        .map(|edge| {
                            let p = edge.front().point();
                            [p.x, p.y, p.z]
                        })
                        .collect::<Vec<_>>()
                })
                .collect::<Vec<_>>()
        })
        .collect();
    let bounds = points
        .into_iter()
        .fold(None, |acc: Option<([f64; 3], [f64; 3])>, p| {
            Some(acc.map_or((p, p), |(lo, hi)| {
                (
                    [lo[0].min(p[0]), lo[1].min(p[1]), lo[2].min(p[2])],
                    [hi[0].max(p[0]), hi[1].max(p[1]), hi[2].max(p[2])],
                )
            }))
        });
    bounds.map_or(0.0, |(lo, hi)| {
        (hi[0] - lo[0]) * (hi[1] - lo[1]) * (hi[2] - lo[2])
    })
}

pub(crate) fn assemble_curved(
    kept: &[BFace],
    tolerance: Tolerance,
) -> Result<BrepSolid, BrepError> {
    if kept.is_empty() {
        return Err(BrepError::BooleanEmpty);
    }
    let mut build = CurvedBuild::new();
    let faces: Vec<(Face, FaceLabel)> = kept
        .iter()
        .map(|bface| build_curved_faces(&mut build, bface, tolerance))
        .collect::<Result<Vec<_>, _>>()?
        .into_iter()
        .flatten()
        .collect();
    if faces.is_empty() {
        return Err(BrepError::BooleanEmpty);
    }
    build.verts.iter_mut().for_each(|rec| {
        if matches!(rec.label.role, VertexRole::Intersection { .. }) {
            rec.label = VertexLabel {
                feature: rec.label.feature,
                role: VertexRole::Cut {
                    key: cut_key(rec.position),
                },
            };
        }
    });
    let shells = group_shells(&faces);
    let solid = Solid::try_new(shells).map_err(|_| BrepError::BooleanIndeterminate)?;
    let face_labels: HashMap<FaceID, FaceLabel> = faces
        .iter()
        .map(|(face, label)| (face.id(), *label))
        .collect();
    let labeling = label_solid(
        &solid,
        &build.verts,
        &face_labels,
        &build.inherited,
        build.edge_curves,
        HashSet::new(),
    );
    assemble(solid, &labeling)
}
