use std::collections::{HashMap, HashSet};

use bone_types::{
    EdgeLabel, EdgeRole, FaceLabel, FaceRole, FeatureId, LoopIndex, Parameter, Plane3, Point2,
    PositiveLength, SideKind, SketchEntityId, Tolerance, VertexLabel, VertexRole,
};
use core::f64::consts::TAU;
use truck_modeling::{Edge, EdgeID, FaceID, Solid, Vector3, Vertex, VertexID, Wire, builder};
use uom::si::length::millimeter;

use crate::curve2::{Curve2, Curve2Kind};
use crate::extrude::{ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature, ExtrudeSense};
use crate::intersect::{IntersectionSet, intersect_curves};

use super::build::{SolidLabeling, assemble};
use super::edges::{EdgeCurve3, lift_curve2};
use super::profile::{ExtrudeProfile, ProfileLoop};
use super::{BrepError, BrepSolid, ProfileDefect, TruckGap};

const PROFILE_TOLERANCE_MM: f64 = 1.0e-6;
const PROFILE_TOLERANCE: Tolerance = Tolerance::new(PROFILE_TOLERANCE_MM);
const AREA_TOLERANCE_MM2: f64 = 1.0e-9;
const EXTRUDE_MIN_DEPTH_MM: f64 = 1.0e-6;
const MAX_ARC_SEGMENTS: u32 = 64;
const MAX_SUBARC_RAD: f64 = TAU / 3.0;
const AREA_SAMPLES: u32 = 32;

pub fn evaluate_extrude(
    extrude: FeatureId,
    profile: &ExtrudeProfile,
    feature: &ExtrudeFeature,
) -> Result<BrepSolid, BrepError> {
    let plan = sweep_plan(feature)?;
    let resolved = resolve_profile(profile)?;
    let parent_curves = parent_curves(profile);
    let (solid, labeling) = build_solid(
        profile.plane(),
        &resolved,
        plan,
        extrude,
        closed_curves(profile),
        &parent_curves,
    )?;
    assemble(solid, &labeling)
}

fn parent_curves(profile: &ExtrudeProfile) -> HashMap<SketchEntityId, Curve2Kind> {
    profile
        .loops()
        .iter()
        .flat_map(|profile_loop| match profile_loop {
            ProfileLoop::Closed {
                curve,
                curve_entity,
            } => vec![(*curve_entity, *curve)],
            ProfileLoop::Open(edges) => edges
                .iter()
                .map(|edge| (edge.curve_entity(), edge.curve()))
                .collect(),
        })
        .collect()
}

fn closed_curves(profile: &ExtrudeProfile) -> HashSet<SketchEntityId> {
    profile
        .loops()
        .iter()
        .filter_map(|profile_loop| match profile_loop {
            ProfileLoop::Closed { curve_entity, .. } => Some(*curve_entity),
            ProfileLoop::Open(_) => None,
        })
        .collect()
}

fn invalid(reason: ProfileDefect) -> BrepError {
    BrepError::InvalidProfile { reason }
}

#[derive(Copy, Clone)]
struct SweepPlan {
    depth_mm: f64,
    base_offset_mm: f64,
}

fn sweep_plan(feature: &ExtrudeFeature) -> Result<SweepPlan, BrepError> {
    if feature.draft.is_some() {
        return Err(unsupported(TruckGap::Draft));
    }
    if feature.thin_wall.is_some() {
        return Err(unsupported(TruckGap::ThinWall));
    }
    match feature.direction {
        ExtrudeDirection::Normal {
            sense: ExtrudeSense::Forward,
        } => {}
        ExtrudeDirection::Normal {
            sense: ExtrudeSense::Reverse,
        } => return Err(unsupported(TruckGap::ReverseNormal)),
        ExtrudeDirection::AlongAxis(_) => return Err(unsupported(TruckGap::AxisDirection)),
        ExtrudeDirection::BetweenReferences { .. } => {
            return Err(unsupported(TruckGap::ReferenceDirection));
        }
    }
    match feature.end_condition {
        ExtrudeEndCondition::Blind { depth } => Ok(SweepPlan {
            depth_mm: depth_mm(depth)?,
            base_offset_mm: 0.0,
        }),
        ExtrudeEndCondition::MidPlane { depth } => {
            let total = depth_mm(depth)?;
            Ok(SweepPlan {
                depth_mm: total,
                base_offset_mm: -total / 2.0,
            })
        }
        ExtrudeEndCondition::ThroughAll => Err(unsupported(TruckGap::ThroughAll)),
        ExtrudeEndCondition::UpToNext => Err(unsupported(TruckGap::UpToNext)),
        ExtrudeEndCondition::UpToVertex { .. } => Err(unsupported(TruckGap::UpToVertex)),
        ExtrudeEndCondition::UpToSurface { .. } => Err(unsupported(TruckGap::UpToSurface)),
        ExtrudeEndCondition::OffsetFromSurface { .. } => {
            Err(unsupported(TruckGap::OffsetFromSurface))
        }
        ExtrudeEndCondition::UpToBody { .. } => Err(unsupported(TruckGap::UpToBody)),
    }
}

fn unsupported(detail: TruckGap) -> BrepError {
    BrepError::TruckUnsupported { detail }
}

fn depth_mm(depth: PositiveLength) -> Result<f64, BrepError> {
    let value = depth.get().get::<millimeter>();
    if value <= EXTRUDE_MIN_DEPTH_MM {
        Err(BrepError::EmptyExtrudeDepth)
    } else {
        Ok(value)
    }
}

#[derive(Copy, Clone)]
struct Segment {
    curve: Curve2Kind,
    curve_entity: SketchEntityId,
    corner: Option<SketchEntityId>,
}

struct LoopInfo {
    segments: Vec<Segment>,
    polygon: Vec<(f64, f64)>,
    area: f64,
}

struct ResolvedLoop {
    segments: Vec<Segment>,
    reversed: bool,
    loop_index: LoopIndex,
}

fn resolve_profile(profile: &ExtrudeProfile) -> Result<Vec<ResolvedLoop>, BrepError> {
    let loops = profile.loops();
    if loops.is_empty() {
        return Err(invalid(ProfileDefect::ZeroArea));
    }
    let infos: Vec<LoopInfo> = loops.iter().map(analyze_loop).collect::<Result<_, _>>()?;

    let outer = (0..infos.len())
        .max_by(|&a, &b| {
            infos[a]
                .area
                .abs()
                .total_cmp(&infos[b].area.abs())
                .then_with(|| min_entity(&infos[b].segments).cmp(&min_entity(&infos[a].segments)))
        })
        .unwrap_or(0);

    validate_arrangement(&infos, outer)?;

    let mut holes: Vec<usize> = (0..infos.len()).filter(|&i| i != outer).collect();
    holes.sort_by_key(|&i| min_entity(&infos[i].segments));

    let outer_resolved = ResolvedLoop {
        segments: infos[outer].segments.clone(),
        reversed: infos[outer].area < 0.0,
        loop_index: LoopIndex::OUTER,
    };
    let hole_resolved = holes.iter().enumerate().map(|(rank, &index)| ResolvedLoop {
        segments: infos[index].segments.clone(),
        reversed: infos[index].area > 0.0,
        loop_index: LoopIndex::new(u16::try_from(rank + 1).unwrap_or(u16::MAX)),
    });
    Ok(core::iter::once(outer_resolved)
        .chain(hole_resolved)
        .collect())
}

fn analyze_loop(profile_loop: &ProfileLoop) -> Result<LoopInfo, BrepError> {
    let segments = match profile_loop {
        ProfileLoop::Closed {
            curve,
            curve_entity,
        } => {
            if !curve_closed(*curve) {
                return Err(invalid(ProfileDefect::OpenLoop));
            }
            vec![Segment {
                curve: *curve,
                curve_entity: *curve_entity,
                corner: None,
            }]
        }
        ProfileLoop::Open(edges) => {
            if edges.is_empty() || edges.iter().any(|edge| curve_closed(edge.curve())) {
                return Err(invalid(ProfileDefect::OpenLoop));
            }
            let segments: Vec<Segment> = edges
                .iter()
                .map(|edge| Segment {
                    curve: edge.curve(),
                    curve_entity: edge.curve_entity(),
                    corner: Some(edge.corner()),
                })
                .collect();
            if !loop_closed(&segments) {
                return Err(invalid(ProfileDefect::OpenLoop));
            }
            if self_intersects(&segments) {
                return Err(invalid(ProfileDefect::SelfIntersectingLoop));
            }
            segments
        }
    };
    let polygon = loop_polygon(&segments);
    let area = signed_area(&polygon);
    if area.abs() < AREA_TOLERANCE_MM2 {
        return Err(invalid(ProfileDefect::ZeroArea));
    }
    Ok(LoopInfo {
        segments,
        polygon,
        area,
    })
}

fn validate_arrangement(infos: &[LoopInfo], outer: usize) -> Result<(), BrepError> {
    let outer_info = &infos[outer];
    let holes: Vec<usize> = (0..infos.len()).filter(|&i| i != outer).collect();

    holes.iter().try_for_each(|&i| {
        let contained = !loops_cross(&infos[i].segments, &outer_info.segments)
            && any_point_inside(&infos[i], outer_info);
        contained
            .then_some(())
            .ok_or(invalid(ProfileDefect::UncontainedLoop))
    })?;

    holes.iter().enumerate().try_for_each(|(rank, &a)| {
        holes[rank + 1..].iter().try_for_each(|&b| {
            let overlaps = loops_cross(&infos[a].segments, &infos[b].segments)
                || any_point_inside(&infos[a], &infos[b])
                || any_point_inside(&infos[b], &infos[a]);
            (!overlaps)
                .then_some(())
                .ok_or(invalid(ProfileDefect::OverlappingLoops))
        })
    })
}

fn any_point_inside(inner: &LoopInfo, container: &LoopInfo) -> bool {
    inner
        .polygon
        .iter()
        .any(|&point| point_in_polygon(point, &container.polygon))
}

fn loops_cross(a: &[Segment], b: &[Segment]) -> bool {
    a.iter().any(|sa| {
        b.iter().any(|sb| {
            !matches!(
                intersect_curves(&sa.curve, &sb.curve, PROFILE_TOLERANCE),
                IntersectionSet::Empty
            )
        })
    })
}

fn self_intersects(segments: &[Segment]) -> bool {
    let count = segments.len();
    (0..count).any(|i| {
        let last = if i == 0 { count - 1 } else { count };
        ((i + 2)..last).any(|j| {
            !matches!(
                intersect_curves(&segments[i].curve, &segments[j].curve, PROFILE_TOLERANCE),
                IntersectionSet::Empty
            )
        })
    })
}

fn curve_closed(curve: Curve2Kind) -> bool {
    let start = curve.evaluate(Parameter::new(0.0));
    let end = curve.evaluate(Parameter::new(1.0));
    (end - start).norm_mm() < PROFILE_TOLERANCE_MM
}

fn min_entity(segments: &[Segment]) -> Option<SketchEntityId> {
    segments.iter().map(|seg| seg.curve_entity).min()
}

fn loop_closed(segments: &[Segment]) -> bool {
    let count = segments.len();
    (0..count).all(|index| {
        let end = segments[index].curve.evaluate(Parameter::new(1.0));
        let next = segments[(index + 1) % count]
            .curve
            .evaluate(Parameter::new(0.0));
        (end - next).norm_mm() < PROFILE_TOLERANCE_MM
    })
}

fn loop_polygon(segments: &[Segment]) -> Vec<(f64, f64)> {
    segments
        .iter()
        .flat_map(|seg| {
            let curve = seg.curve;
            let samples = samples_for(curve);
            (0..samples).map(move |i| {
                let t = f64::from(i) / f64::from(samples);
                curve.evaluate(Parameter::new(t)).coords_mm()
            })
        })
        .collect()
}

fn signed_area(points: &[(f64, f64)]) -> f64 {
    let count = points.len();
    points
        .iter()
        .enumerate()
        .map(|(index, &(x0, y0))| {
            let (x1, y1) = points[(index + 1) % count];
            x0 * y1 - x1 * y0
        })
        .sum::<f64>()
        / 2.0
}

fn point_in_polygon(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
    let (px, py) = point;
    let count = polygon.len();
    (0..count).fold(false, |inside, index| {
        let (xi, yi) = polygon[index];
        let (xj, yj) = polygon[(index + count - 1) % count];
        let crosses = (yi > py) != (yj > py) && px < (xj - xi) * (py - yi) / (yj - yi) + xi;
        inside ^ crosses
    })
}

fn samples_for(curve: Curve2Kind) -> u32 {
    match curve {
        Curve2Kind::Line(_) => 1,
        Curve2Kind::Arc(_) | Curve2Kind::Circle(_) => AREA_SAMPLES,
    }
}

struct Step {
    start: Point2,
    transit: Option<Point2>,
    curve_entity: SketchEntityId,
    vertex_entry: (SideKind, SketchEntityId),
}

fn loop_steps(segments: &[Segment], reversed: bool) -> Vec<Step> {
    let count = segments.len();
    let order: Vec<usize> = if reversed {
        (0..count).rev().collect()
    } else {
        (0..count).collect()
    };
    order
        .into_iter()
        .flat_map(|index| {
            let segment = segments[index];
            let first_entry = if reversed {
                junction_entry(segments, (index + 1) % count)
            } else {
                junction_entry(segments, index)
            };
            sub_edges(segment.curve, reversed)
                .into_iter()
                .enumerate()
                .map(move |(position, sub)| Step {
                    start: sub.0,
                    transit: sub.2,
                    curve_entity: segment.curve_entity,
                    vertex_entry: if position == 0 {
                        first_entry
                    } else {
                        (SideKind::Seam, segment.curve_entity)
                    },
                })
        })
        .collect()
}

fn junction_entry(segments: &[Segment], index: usize) -> (SideKind, SketchEntityId) {
    let segment = segments[index];
    segment
        .corner
        .map_or((SideKind::Seam, segment.curve_entity), |corner| {
            (SideKind::Corner, corner)
        })
}

fn sub_edges(curve: Curve2Kind, reversed: bool) -> Vec<(Point2, Point2, Option<Point2>)> {
    match curve {
        Curve2Kind::Line(line) => {
            let (start, end) = if reversed {
                (line.end(), line.start())
            } else {
                (line.start(), line.end())
            };
            vec![(start, end, None)]
        }
        Curve2Kind::Arc(arc) => sub_arcs(curve, arc.sweep_rad().abs(), reversed),
        Curve2Kind::Circle(_) => sub_arcs(curve, TAU, reversed),
    }
}

fn sub_arcs(
    curve: Curve2Kind,
    sweep_abs_rad: f64,
    reversed: bool,
) -> Vec<(Point2, Point2, Option<Point2>)> {
    let segments = arc_segments(sweep_abs_rad);
    let knots: Vec<f64> = (0..=segments)
        .map(|i| {
            let forward = f64::from(i) / f64::from(segments);
            if reversed { 1.0 - forward } else { forward }
        })
        .collect();
    knots
        .windows(2)
        .map(|pair| {
            let (t0, t1) = (pair[0], pair[1]);
            let start = curve.evaluate(Parameter::new(t0));
            let end = curve.evaluate(Parameter::new(t1));
            let transit = curve.evaluate(Parameter::new(f64::midpoint(t0, t1)));
            (start, end, Some(transit))
        })
        .collect()
}

fn arc_segments(sweep_abs_rad: f64) -> u32 {
    (1..=MAX_ARC_SEGMENTS)
        .find(|&segments| sweep_abs_rad / f64::from(segments) <= MAX_SUBARC_RAD)
        .unwrap_or(MAX_ARC_SEGMENTS)
}

#[derive(Default)]
struct Bookkeeping {
    edge_entry: HashMap<EdgeID, (LoopIndex, SketchEntityId)>,
    vertex_entry: HashMap<VertexID, (SideKind, SketchEntityId)>,
}

fn build_solid(
    plane: Plane3,
    resolved: &[ResolvedLoop],
    plan: SweepPlan,
    extrude: FeatureId,
    closed: HashSet<SketchEntityId>,
    parent_curves: &HashMap<SketchEntityId, Curve2Kind>,
) -> Result<(Solid, SolidLabeling), BrepError> {
    let mut book = Bookkeeping::default();
    let wires: Vec<Wire> = resolved
        .iter()
        .map(|resolved_loop| {
            let steps = loop_steps(&resolved_loop.segments, resolved_loop.reversed);
            build_loop(
                plane,
                plan.base_offset_mm,
                &steps,
                resolved_loop.loop_index,
                &mut book,
            )
        })
        .collect();

    let face = builder::try_attach_plane(&wires)
        .map_err(|_| invalid(ProfileDefect::SelfIntersectingLoop))?;
    let (nx, ny, nz) = plane.normal().components();
    let sweep = Vector3::new(nx * plan.depth_mm, ny * plan.depth_mm, nz * plan.depth_mm);
    let solid = builder::tsweep(&face, sweep);
    let labeling = label_solid(&solid, &book, extrude, closed, plane, plan, parent_curves);
    Ok((solid, labeling))
}

fn build_loop(
    plane: Plane3,
    offset_mm: f64,
    steps: &[Step],
    loop_index: LoopIndex,
    book: &mut Bookkeeping,
) -> Wire {
    let count = steps.len();
    let vertices: Vec<Vertex> = steps
        .iter()
        .map(|step| builder::vertex(lift(plane, offset_mm, step.start)))
        .collect();
    vertices.iter().zip(steps).for_each(|(vertex, step)| {
        book.vertex_entry.insert(vertex.id(), step.vertex_entry);
    });
    let edges: Vec<Edge> = (0..count)
        .map(|index| {
            let from = &vertices[index];
            let to = &vertices[(index + 1) % count];
            let step = &steps[index];
            let edge = match step.transit {
                None => builder::line(from, to),
                Some(transit) => builder::circle_arc(from, to, lift(plane, offset_mm, transit)),
            };
            book.edge_entry
                .insert(edge.id(), (loop_index, step.curve_entity));
            edge
        })
        .collect();
    edges.into()
}

fn lift(plane: Plane3, offset_mm: f64, point: Point2) -> truck_modeling::Point3 {
    let (u, v) = point.coords_mm();
    let (wx, wy, wz) = plane.point_at_local(u, v, offset_mm).coords_mm();
    truck_modeling::Point3::new(wx, wy, wz)
}

struct Labels {
    faces: HashMap<FaceID, FaceLabel>,
    edges: HashMap<EdgeID, EdgeLabel>,
    vertices: HashMap<VertexID, VertexLabel>,
    edge_curves: HashMap<EdgeID, EdgeCurve3>,
    feature: FeatureId,
    closed: HashSet<SketchEntityId>,
}

impl Labels {
    fn new(feature: FeatureId, closed: HashSet<SketchEntityId>) -> Self {
        Self {
            faces: HashMap::new(),
            edges: HashMap::new(),
            vertices: HashMap::new(),
            edge_curves: HashMap::new(),
            feature,
            closed,
        }
    }

    fn face(&mut self, id: FaceID, role: FaceRole) {
        self.faces.insert(
            id,
            FaceLabel {
                feature: self.feature,
                role,
            },
        );
    }

    fn edge(&mut self, id: EdgeID, role: EdgeRole) {
        self.edges.insert(
            id,
            EdgeLabel {
                feature: self.feature,
                role,
            },
        );
    }

    fn vertex(&mut self, id: VertexID, role: VertexRole) {
        self.vertices.insert(
            id,
            VertexLabel {
                feature: self.feature,
                role,
            },
        );
    }

    fn edge_curve(&mut self, id: EdgeID, curve: EdgeCurve3) {
        self.edge_curves.insert(id, curve);
    }

    fn into_labeling(self) -> SolidLabeling {
        SolidLabeling {
            faces: self.faces,
            edges: self.edges,
            vertices: self.vertices,
            closed_curves: self.closed,
            edge_curves: self.edge_curves,
        }
    }
}

#[derive(Copy, Clone)]
struct SideFace {
    id: FaceID,
    profile_edge: EdgeID,
    front: VertexID,
    back: VertexID,
}

fn label_solid(
    solid: &Solid,
    book: &Bookkeeping,
    extrude: FeatureId,
    closed: HashSet<SketchEntityId>,
    plane: Plane3,
    plan: SweepPlan,
    parent_curves: &HashMap<SketchEntityId, Curve2Kind>,
) -> SolidLabeling {
    let mut labels = solid
        .boundaries()
        .iter()
        .flat_map(|shell| shell.face_iter())
        .fold(Labels::new(extrude, closed), |mut labels, face| {
            let face_edges: Vec<(EdgeID, VertexID, VertexID)> = face
                .boundaries()
                .iter()
                .flat_map(|wire| {
                    wire.edge_iter()
                        .map(|edge| (edge.id(), edge.front().id(), edge.back().id()))
                })
                .collect();
            let profile: Vec<(EdgeID, VertexID, VertexID)> = face_edges
                .iter()
                .copied()
                .filter(|(edge_id, _, _)| book.edge_entry.contains_key(edge_id))
                .collect();

            match profile.first() {
                None => labels.face(face.id(), FaceRole::EndCap),
                Some(_) if profile.len() == face_edges.len() => {
                    labels.face(face.id(), FaceRole::StartCap);
                }
                Some(&(profile_edge, front, back)) => label_side_face(
                    &mut labels,
                    SideFace {
                        id: face.id(),
                        profile_edge,
                        front,
                        back,
                    },
                    &face_edges,
                    book,
                ),
            }
            labels
        });
    record_cap_curves(&mut labels, plane, plan, parent_curves);
    labels.into_labeling()
}

fn record_cap_curves(
    labels: &mut Labels,
    plane: Plane3,
    plan: SweepPlan,
    parent_curves: &HashMap<SketchEntityId, Curve2Kind>,
) {
    let entries: Vec<(EdgeID, EdgeRole)> = labels
        .edges
        .iter()
        .map(|(id, label)| (*id, label.role))
        .collect();
    entries.into_iter().for_each(|(edge_id, role)| {
        let (offset_mm, curve_entity) = match role {
            EdgeRole::StartCapEdge { from } => (plan.base_offset_mm, from),
            EdgeRole::EndCapEdge { from } => (plan.base_offset_mm + plan.depth_mm, from),
            _ => return,
        };
        let Some(parent) = parent_curves.get(&curve_entity) else {
            return;
        };
        let Some(lifted) = lift_curve2(*parent, plane, offset_mm, PROFILE_TOLERANCE) else {
            return;
        };
        labels.edge_curve(edge_id, lifted);
    });
}

fn label_side_face(
    labels: &mut Labels,
    side: SideFace,
    face_edges: &[(EdgeID, VertexID, VertexID)],
    book: &Bookkeeping,
) {
    let (loop_index, curve_entity) = book.edge_entry[&side.profile_edge];
    let (front_kind, front_entity) = book.vertex_entry[&side.front];
    let (back_kind, back_entity) = book.vertex_entry[&side.back];

    labels.face(
        side.id,
        FaceRole::Side {
            loop_index,
            from: curve_entity,
        },
    );
    labels.edge(
        side.profile_edge,
        EdgeRole::StartCapEdge { from: curve_entity },
    );
    labels.vertex(
        side.front,
        VertexRole::StartCapVertex {
            from: front_entity,
            side: front_kind,
        },
    );
    labels.vertex(
        side.back,
        VertexRole::StartCapVertex {
            from: back_entity,
            side: back_kind,
        },
    );

    face_edges
        .iter()
        .filter(|(edge_id, _, _)| *edge_id != side.profile_edge)
        .for_each(|&(edge_id, edge_front, edge_back)| {
            let touches_front = edge_front == side.front || edge_back == side.front;
            let touches_back = edge_front == side.back || edge_back == side.back;
            match (touches_front, touches_back) {
                (true, false) => {
                    labels.edge(
                        edge_id,
                        EdgeRole::SideEdge {
                            from: front_entity,
                            side: front_kind,
                        },
                    );
                    let top = if edge_front == side.front {
                        edge_back
                    } else {
                        edge_front
                    };
                    labels.vertex(
                        top,
                        VertexRole::EndCapVertex {
                            from: front_entity,
                            side: front_kind,
                        },
                    );
                }
                (false, true) => {
                    labels.edge(
                        edge_id,
                        EdgeRole::SideEdge {
                            from: back_entity,
                            side: back_kind,
                        },
                    );
                    let top = if edge_front == side.back {
                        edge_back
                    } else {
                        edge_front
                    };
                    labels.vertex(
                        top,
                        VertexRole::EndCapVertex {
                            from: back_entity,
                            side: back_kind,
                        },
                    );
                }
                _ => labels.edge(edge_id, EdgeRole::EndCapEdge { from: curve_entity }),
            }
        });
}
