use core::f64::consts::TAU;
use std::collections::{HashMap, HashSet};

use bone_types::{
    Angle, EdgeLabel, EdgeRole, FaceLabel, FaceRole, Length, Plane3, Point3, PositiveLength,
    Tolerance, UnitVec3, VertexLabel, VertexRole,
};
use truck_modeling::{
    Curve, Edge, EdgeID, Face, Line, Plane as TPlane, Processor, RevolutedCurve, Shell, Solid,
    Surface, Vector3, Wire, builder,
};
use uom::si::angle::radian;
use uom::si::length::millimeter;

use super::super::edges::EdgeCurve3;
use super::super::{BrepError, BrepSolid, build};
use super::stitch::{VertexRec, intern_vertex, label_solid, truck_point};
use super::{PlanarBody, PlanarFace, as_vec, basis, extract_planar_body, unit};
use crate::Arc3;
use crate::cylinder_surface::CylinderSurface;

pub(crate) struct ToolCylinder {
    pub surface: CylinderSurface,
    pub wall: FaceLabel,
    pub bottom_cap: FaceLabel,
    pub top_cap: FaceLabel,
}

#[derive(Copy, Clone)]
struct Ring {
    center: Point3,
    normal: bone_types::UnitVec3,
    radius_mm: f64,
}

fn ring_of(curve: &EdgeCurve3) -> Option<Ring> {
    match curve {
        EdgeCurve3::Circle(c) => Some(Ring {
            center: c.center(),
            normal: c.normal(),
            radius_mm: c.radius_mm(),
        }),
        EdgeCurve3::Arc(_) | EdgeCurve3::Line(_) => None,
    }
}

fn distance(a: Point3, b: Point3) -> f64 {
    let (ax, ay, az) = a.coords_mm();
    let (bx, by, bz) = b.coords_mm();
    ((ax - bx).powi(2) + (ay - by).powi(2) + (az - bz).powi(2)).sqrt()
}

fn same_ring(a: Ring, b: Ring, tolerance: Tolerance) -> bool {
    let eps = tolerance.value();
    (a.radius_mm - b.radius_mm).abs() <= eps
        && a.normal.dot(b.normal).abs() >= 1.0 - eps
        && distance(a.center, b.center) <= eps
}

fn merge_rings(edges: &[Ring], tolerance: Tolerance) -> Vec<Ring> {
    edges.iter().fold(Vec::new(), |mut kept, &ring| {
        if !kept.iter().any(|seen| same_ring(*seen, ring, tolerance)) {
            kept.push(ring);
        }
        kept
    })
}

fn cap_label(
    solid: &BrepSolid,
    center: Point3,
    axis: bone_types::UnitVec3,
    tolerance: Tolerance,
) -> Option<FaceLabel> {
    solid.iter_faces().find_map(|face| {
        let label = face.label();
        let is_cap = matches!(label.role, FaceRole::StartCap | FaceRole::EndCap);
        let along = solid.face_plane_basis(face.id()).map(|basis| {
            (Plane3::from(basis).origin() - center)
                .dot_mm2(super::as_vec(axis))
                .abs()
        });
        (is_cap && along.is_some_and(|d| d <= tolerance.value())).then_some(label)
    })
}

pub(crate) fn recognize_cylinder(solid: &BrepSolid, tolerance: Tolerance) -> Option<ToolCylinder> {
    let rings: Vec<Ring> = solid
        .iter_edges()
        .filter_map(|edge| ring_of(edge.curve()))
        .collect();
    let merged = merge_rings(&rings, tolerance);
    let [a, b] = merged.as_slice() else {
        return None;
    };
    let eps = tolerance.value();
    if (a.radius_mm - b.radius_mm).abs() > eps || a.normal.dot(b.normal).abs() < 1.0 - eps {
        return None;
    }
    let span = b.center - a.center;
    let axis = super::unit(span, tolerance)?;
    if axis.dot(a.normal).abs() < 1.0 - eps {
        return None;
    }
    let height = distance(a.center, b.center);
    if height <= eps {
        return None;
    }
    let radius = PositiveLength::new(Length::new::<millimeter>(a.radius_mm)).ok()?;
    let (x_axis, y_axis) = super::basis(axis, tolerance)?;
    let base = Plane3::new_unchecked(a.center, x_axis, y_axis);
    let surface = CylinderSurface::new(
        base,
        radius.get(),
        Angle::new::<radian>(0.0),
        Angle::new::<radian>(TAU),
        Length::new::<millimeter>(height),
        tolerance,
    )
    .ok()?;
    let wall = solid
        .iter_faces()
        .find(|face| matches!(face.label().role, FaceRole::Side { .. }))?
        .label();
    let bottom_cap = cap_label(solid, a.center, axis, tolerance)?;
    let top_cap = cap_label(solid, b.center, axis, tolerance)?;
    Some(ToolCylinder {
        surface,
        wall,
        bottom_cap,
        top_cap,
    })
}

enum OtherEnd {
    Floor { center: Point3, label: FaceLabel },
    Exit { face: usize, center: Point3 },
}

struct CutPlan {
    entry: usize,
    entry_center: Point3,
    other: OtherEnd,
    axis: UnitVec3,
    rx: UnitVec3,
    ry: UnitVec3,
    radius: Length,
}

pub(crate) fn cylinder_cut(
    block: &BrepSolid,
    tool: &ToolCylinder,
    tolerance: Tolerance,
) -> Result<BrepSolid, BrepError> {
    let body = super::imprint::conform(&extract_planar_body(block, tolerance)?, tolerance);
    let plan = analyze(&body, tool, tolerance)?;
    assemble_cut(&body, tool, &plan, tolerance)
}

fn gap(detail: super::super::BooleanGap) -> BrepError {
    BrepError::BooleanUnsupported { detail }
}

fn axis_plane_hit(
    plane: &Plane3,
    cap: Point3,
    axis: UnitVec3,
    tolerance: Tolerance,
) -> Option<Point3> {
    let normal = plane.normal();
    let denom = axis.dot(normal);
    if denom.abs() <= 1.0 - tolerance.value() {
        return None;
    }
    let t = (plane.origin() - cap).dot_mm2(as_vec(normal)) / denom;
    Some(cap + as_vec(axis) * t)
}

fn circle_inside(face: &PlanarFace, center: Point3, radius_mm: f64, tolerance: Tolerance) -> bool {
    let c = super::classify::local(face, center);
    let locals: Vec<(f64, f64)> = face
        .polygon
        .iter()
        .map(|v| super::classify::local(face, v.position))
        .collect();
    let count = locals.len();
    let area: f64 = (0..count)
        .map(|i| {
            let (x0, y0) = locals[i];
            let (x1, y1) = locals[(i + 1) % count];
            x0 * y1 - x1 * y0
        })
        .sum();
    let orient = area.signum();
    (0..count).all(|i| {
        let (ax, ay) = locals[i];
        let (bx, by) = locals[(i + 1) % count];
        let length = (bx - ax).hypot(by - ay);
        if length <= tolerance.value() {
            return false;
        }
        let cross = (bx - ax) * (c.1 - ay) - (by - ay) * (c.0 - ax);
        (cross / length) * orient >= radius_mm - tolerance.value()
    })
}

fn analyze(
    body: &PlanarBody,
    tool: &ToolCylinder,
    tolerance: Tolerance,
) -> Result<CutPlan, BrepError> {
    let axis = tool.surface.axis();
    let radius = tool.surface.radius();
    let radius_mm = tool.surface.radius_mm();
    let cap0 = tool.surface.center();
    let cap1 = cap0 + as_vec(axis) * tool.surface.height_mm();

    let perpendicular: Vec<usize> = body
        .iter()
        .enumerate()
        .filter(|(_, face)| face.plane.normal().dot(axis).abs() > 1.0 - tolerance.value())
        .map(|(index, _)| index)
        .collect();
    if perpendicular.is_empty() {
        return Err(gap(super::super::BooleanGap::ObliqueCylinder));
    }

    let height = tool.surface.height_mm();
    let openings: Vec<(usize, Point3)> = perpendicular
        .iter()
        .filter_map(|&index| {
            let face = &body[index];
            let center = axis_plane_hit(&face.plane, cap0, axis, tolerance)?;
            let along = (face.plane.origin() - cap0).dot_mm2(as_vec(axis));
            let reaches = along > -tolerance.value() && along < height + tolerance.value();
            (reaches && circle_inside(face, center, radius_mm, tolerance))
                .then_some((index, center))
        })
        .collect();

    let (rx, ry) = basis(axis, tolerance).ok_or(BrepError::BooleanIndeterminate)?;

    match openings.as_slice() {
        [(entry, entry_center)] => {
            let extent = axial_extent(body, cap0, axis);
            let inside = [(cap0, tool.bottom_cap), (cap1, tool.top_cap)]
                .into_iter()
                .find(|(cap, _)| {
                    let ax = (*cap - cap0).dot_mm2(as_vec(axis));
                    ax > extent.0 + tolerance.value() && ax < extent.1 - tolerance.value()
                });
            let (floor, label) =
                inside.ok_or(gap(super::super::BooleanGap::PartialCircleImprint))?;
            Ok(CutPlan {
                entry: *entry,
                entry_center: *entry_center,
                other: OtherEnd::Floor {
                    center: floor,
                    label,
                },
                axis,
                rx,
                ry,
                radius,
            })
        }
        [(a, a_center), (b, b_center)] => Ok(CutPlan {
            entry: *a,
            entry_center: *a_center,
            other: OtherEnd::Exit {
                face: *b,
                center: *b_center,
            },
            axis,
            rx,
            ry,
            radius,
        }),
        _ => Err(gap(super::super::BooleanGap::PartialCircleImprint)),
    }
}

fn axial_extent(body: &PlanarBody, cap0: Point3, axis: UnitVec3) -> (f64, f64) {
    body.iter()
        .flat_map(|face| face.polygon.iter())
        .map(|vertex| (vertex.position - cap0).dot_mm2(as_vec(axis)))
        .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), value| {
            (lo.min(value), hi.max(value))
        })
}

struct RingTopo {
    wire: Wire,
    pos: Edge,
    neg: Edge,
    v0: usize,
    v1: usize,
}

struct Build {
    verts: Vec<VertexRec>,
    inherited: HashMap<EdgeID, EdgeLabel>,
    edge_curves: HashMap<EdgeID, EdgeCurve3>,
}

fn make_ring(
    build: &mut Build,
    center: Point3,
    plan: &CutPlan,
    faces: (FaceLabel, FaceLabel),
    tolerance: Tolerance,
) -> Result<RingTopo, BrepError> {
    let rxv = as_vec(plan.rx);
    let ryv = as_vec(plan.ry);
    let r = plan.radius.get::<millimeter>();
    let p0 = center + rxv * r;
    let p1 = center - rxv * r;
    let transit_pos = center + ryv * r;
    let transit_neg = center - ryv * r;

    let (lo, hi) = if faces.0 <= faces.1 {
        (faces.0, faces.1)
    } else {
        (faces.1, faces.0)
    };
    let rim_vertex = |point: Point3| VertexLabel {
        feature: lo.feature,
        role: VertexRole::Cut {
            key: super::stitch::cut_key(point),
        },
    };
    let i0 = intern_vertex(&mut build.verts, p0, rim_vertex(p0), lo, tolerance);
    let i1 = intern_vertex(&mut build.verts, p1, rim_vertex(p1), lo, tolerance);

    let pos = builder::circle_arc(
        &build.verts[i0].vertex,
        &build.verts[i1].vertex,
        truck_point(transit_pos),
    );
    let neg = builder::circle_arc(
        &build.verts[i1].vertex,
        &build.verts[i0].vertex,
        truck_point(transit_neg),
    );

    let plane = Plane3::new_unchecked(center, plan.rx, plan.ry);
    let half = Angle::new::<radian>(TAU / 2.0);
    let arc_pos = Arc3::new(
        plane,
        plan.radius,
        Angle::new::<radian>(0.0),
        half,
        tolerance,
    )
    .map_err(|_| BrepError::BooleanIndeterminate)?;
    let arc_neg = Arc3::new(plane, plan.radius, half, half, tolerance)
        .map_err(|_| BrepError::BooleanIndeterminate)?;
    build.edge_curves.insert(pos.id(), EdgeCurve3::Arc(arc_pos));
    build.edge_curves.insert(neg.id(), EdgeCurve3::Arc(arc_neg));

    let elabel = EdgeLabel {
        feature: lo.feature.min(hi.feature),
        role: EdgeRole::intersection(lo, hi),
    };
    build.inherited.insert(pos.id(), elabel);
    build.inherited.insert(neg.id(), elabel);

    let wire: Wire = vec![pos.clone(), neg.clone()].into_iter().collect();
    Ok(RingTopo {
        wire,
        pos,
        neg,
        v0: i0,
        v1: i1,
    })
}

fn plane_surface(plane: &Plane3) -> Surface {
    let origin = plane.origin();
    let one = origin + plane.x_axis().into_vec(Length::new::<millimeter>(1.0));
    let two = origin + plane.y_axis().into_vec(Length::new::<millimeter>(1.0));
    Surface::Plane(TPlane::new(
        truck_point(origin),
        truck_point(one),
        truck_point(two),
    ))
}

fn outer_wire(ring: &[usize], line_edges: &HashMap<(usize, usize), Edge>) -> Wire {
    let count = ring.len();
    (0..count)
        .map(|k| {
            let a = ring[k];
            let b = ring[(k + 1) % count];
            let key = if a <= b { (a, b) } else { (b, a) };
            let edge = &line_edges[&key];
            if a <= b { edge.clone() } else { edge.inverse() }
        })
        .collect()
}

fn wall_surface(lo: Point3, hi: Point3, plan: &CutPlan) -> Surface {
    let rxv = as_vec(plan.rx);
    let r = plan.radius.get::<millimeter>();
    let generator = Curve::Line(Line(truck_point(lo + rxv * r), truck_point(hi + rxv * r)));
    let (ax, ay, az) = plan.axis.components();
    let revolved =
        RevolutedCurve::by_revolution(generator, truck_point(lo), Vector3::new(ax, ay, az));
    Surface::RevolutedCurve(Processor::new(revolved))
}

struct WallHalves {
    pos: Face,
    neg: Face,
    entry_inner: Wire,
    other_inner: Wire,
}

struct BlockTopo {
    build: Build,
    rings: Vec<Vec<usize>>,
    line_edges: HashMap<(usize, usize), Edge>,
}

fn intern_block(body: &PlanarBody, tolerance: Tolerance) -> BlockTopo {
    let mut build = Build {
        verts: Vec::new(),
        inherited: HashMap::new(),
        edge_curves: HashMap::new(),
    };
    let rings: Vec<Vec<usize>> = body
        .iter()
        .map(|face| {
            face.polygon
                .iter()
                .map(|pv| {
                    intern_vertex(
                        &mut build.verts,
                        pv.position,
                        pv.label,
                        face.label,
                        tolerance,
                    )
                })
                .collect()
        })
        .collect();
    let line_edges: HashMap<(usize, usize), Edge> =
        body.iter()
            .zip(&rings)
            .fold(HashMap::new(), |mut acc, (face, ring)| {
                let count = ring.len();
                (0..count).for_each(|k| {
                    let a = ring[k];
                    let b = ring[(k + 1) % count];
                    let key = if a <= b { (a, b) } else { (b, a) };
                    let edge = acc.entry(key).or_insert_with(|| {
                        builder::line(&build.verts[key.0].vertex, &build.verts[key.1].vertex)
                    });
                    build.inherited.insert(edge.id(), face.edges[k]);
                });
                acc
            });
    BlockTopo {
        build,
        rings,
        line_edges,
    }
}

fn build_walls(
    build: &Build,
    plan: &CutPlan,
    entry_ring: &RingTopo,
    other_ring: &RingTopo,
    other_center: Point3,
) -> Result<WallHalves, BrepError> {
    let entry_first = axial(plan.entry_center, plan) <= axial(other_center, plan);
    let (lo_center, hi_center, lo_ring, hi_ring) = if entry_first {
        (plan.entry_center, other_center, entry_ring, other_ring)
    } else {
        (other_center, plan.entry_center, other_ring, entry_ring)
    };
    let seam0 = builder::line(
        &build.verts[lo_ring.v0].vertex,
        &build.verts[hi_ring.v0].vertex,
    );
    let seam1 = builder::line(
        &build.verts[lo_ring.v1].vertex,
        &build.verts[hi_ring.v1].vertex,
    );
    let surface = truck_modeling::Invertible::inverse(&wall_surface(lo_center, hi_center, plan));
    let pos_wire: Wire = vec![
        lo_ring.pos.clone(),
        seam1.clone(),
        hi_ring.pos.inverse(),
        seam0.inverse(),
    ]
    .into_iter()
    .collect();
    let neg_wire: Wire = vec![
        lo_ring.neg.clone(),
        seam0.clone(),
        hi_ring.neg.inverse(),
        seam1.inverse(),
    ]
    .into_iter()
    .collect();
    let pos = Face::try_new(vec![pos_wire], surface.clone())
        .map_err(|_| BrepError::BooleanIndeterminate)?
        .inverse();
    let neg = Face::try_new(vec![neg_wire], surface)
        .map_err(|_| BrepError::BooleanIndeterminate)?
        .inverse();
    let entry_inner = if entry_first {
        entry_ring.wire.clone()
    } else {
        entry_ring.wire.inverse()
    };
    let other_inner = if entry_first {
        other_ring.wire.inverse()
    } else {
        other_ring.wire.clone()
    };
    Ok(WallHalves {
        pos,
        neg,
        entry_inner,
        other_inner,
    })
}

fn assemble_cut(
    body: &PlanarBody,
    tool: &ToolCylinder,
    plan: &CutPlan,
    tolerance: Tolerance,
) -> Result<BrepSolid, BrepError> {
    let BlockTopo {
        mut build,
        rings,
        line_edges,
    } = intern_block(body, tolerance);

    let entry_face = body[plan.entry].label;
    let entry_ring = make_ring(
        &mut build,
        plan.entry_center,
        plan,
        (entry_face, tool.wall),
        tolerance,
    )?;

    let other_center = match plan.other {
        OtherEnd::Floor { center, .. } | OtherEnd::Exit { center, .. } => center,
    };
    let other_label = match plan.other {
        OtherEnd::Floor { label, .. } => label,
        OtherEnd::Exit { face, .. } => body[face].label,
    };
    let other_ring = make_ring(
        &mut build,
        other_center,
        plan,
        (other_label, tool.wall),
        tolerance,
    )?;

    let walls = build_walls(&build, plan, &entry_ring, &other_ring, other_center)?;

    let exit_index = match plan.other {
        OtherEnd::Exit { face, .. } => Some(face),
        OtherEnd::Floor { .. } => None,
    };

    let plain_faces: Vec<(Face, FaceLabel)> = body
        .iter()
        .enumerate()
        .filter(|(index, _)| *index != plan.entry && Some(*index) != exit_index)
        .map(|(index, face)| {
            let wire = outer_wire(&rings[index], &line_edges);
            Face::try_new(vec![wire], plane_surface(&face.plane))
                .map(|f| (f, face.label))
                .map_err(|_| BrepError::BooleanIndeterminate)
        })
        .collect::<Result<_, _>>()?;

    let entry_outer = outer_wire(&rings[plan.entry], &line_edges);
    let entry_face_built = Face::try_new(
        vec![entry_outer, walls.entry_inner],
        plane_surface(&body[plan.entry].plane),
    )
    .map_err(|_| BrepError::BooleanIndeterminate)?;

    let end_faces: Vec<(Face, FaceLabel)> = match plan.other {
        OtherEnd::Exit { face, .. } => {
            let outer = outer_wire(&rings[face], &line_edges);
            let exit_face = Face::try_new(
                vec![outer, walls.other_inner],
                plane_surface(&body[face].plane),
            )
            .map_err(|_| BrepError::BooleanIndeterminate)?;
            vec![(exit_face, body[face].label)]
        }
        OtherEnd::Floor { center, .. } => {
            let normal = unit(plan.entry_center - center, tolerance)
                .ok_or(BrepError::BooleanIndeterminate)?;
            let (fx, fy) = basis(normal, tolerance).ok_or(BrepError::BooleanIndeterminate)?;
            let floor_plane = Plane3::new_unchecked(center, fx, fy);
            let floor = Face::try_new(vec![walls.other_inner], plane_surface(&floor_plane))
                .map_err(|_| BrepError::BooleanIndeterminate)?;
            vec![(floor, other_label)]
        }
    };

    let faces: Vec<(Face, FaceLabel)> = plain_faces
        .into_iter()
        .chain([
            (entry_face_built, entry_face),
            (walls.pos, tool.wall),
            (walls.neg, tool.wall),
        ])
        .chain(end_faces)
        .collect();

    let shell: Shell = faces.iter().map(|(face, _)| face.clone()).collect();
    let solid = Solid::try_new(vec![shell]).map_err(|_| BrepError::BooleanIndeterminate)?;
    let face_labels = 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(),
    );
    build::assemble(solid, &labeling)
}

fn axial(point: Point3, plan: &CutPlan) -> f64 {
    let (ax, ay, az) = plan.axis.components();
    let (px, py, pz) = point.coords_mm();
    ax * px + ay * py + az * pz
}
