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

use bone_types::{
    BrepEdgeId, BrepFaceId, BrepLoopId, BrepShellId, BrepVertexId, CreaseAngle, EdgeLabel,
    EdgeRole, FaceLabel, Parameter, Point3, SideKind, SketchEntityId, Tolerance, UnitVec3,
    VertexLabel, VertexRole,
};
use slotmap::{Key, SlotMap};
use truck_modeling::{
    BoundedCurve, Edge, EdgeID, FaceID, ParameterDivision1D, ParametricCurve, ParametricSurface3D,
    SPHint2D, SearchNearestParameter, Shell, Solid, VertexID,
};

use super::convert::{point_from_truck, try_unit_from_truck};
use super::edges::{EdgeCurve3, crease_from_normals, line_between};
use super::{BrepEdge, BrepError, BrepFace, BrepLoop, BrepShell, BrepSolid, BrepVertex, LabelKind};
use crate::curve3::Curve3;

#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct BoundaryIndex(usize);

#[derive(Clone, Copy, Debug)]
pub(crate) struct EdgeArenaHandle(usize);

#[derive(Clone)]
pub(crate) struct Arena {
    solid: Solid,
    edges: Vec<Edge>,
    face_index: HashMap<FaceID, BrepFaceId>,
}

impl Arena {
    pub(crate) fn boundary(&self, index: BoundaryIndex) -> &Shell {
        &self.solid.boundaries()[index.0]
    }

    pub(crate) fn edge(&self, handle: EdgeArenaHandle) -> &Edge {
        &self.edges[handle.0]
    }

    pub(crate) fn solid(&self) -> &Solid {
        &self.solid
    }

    pub(crate) fn face_index(&self) -> &HashMap<FaceID, BrepFaceId> {
        &self.face_index
    }
}

const LENGTH_DIVISION_TOLERANCE: Tolerance = Tolerance::new(1.0e-4);

pub(crate) fn edge_points(edge: &Edge) -> Vec<truck_modeling::Point3> {
    let curve = edge.curve();
    let (_, points) =
        curve.parameter_division(curve.range_tuple(), LENGTH_DIVISION_TOLERANCE.value());
    points
}

pub(crate) fn edge_length(edge: &Edge) -> f64 {
    edge_points(edge)
        .windows(2)
        .map(|pair| {
            let span = pair[1] - pair[0];
            span.x.hypot(span.y).hypot(span.z)
        })
        .sum()
}

pub(crate) struct SolidLabeling {
    pub(crate) faces: HashMap<FaceID, FaceLabel>,
    pub(crate) edges: HashMap<EdgeID, EdgeLabel>,
    pub(crate) vertices: HashMap<VertexID, VertexLabel>,
    pub(crate) closed_curves: HashSet<SketchEntityId>,
    pub(crate) edge_curves: HashMap<EdgeID, EdgeCurve3>,
}

fn suppressed_edge(label: EdgeLabel, closed: &HashSet<SketchEntityId>) -> bool {
    matches!(label.role, EdgeRole::SideEdge { from, side: SideKind::Seam } if !closed.contains(&from))
}

fn suppressed_vertex(label: VertexLabel, closed: &HashSet<SketchEntityId>) -> bool {
    matches!(
        label.role,
        VertexRole::StartCapVertex { from, side: SideKind::Seam }
            | VertexRole::EndCapVertex { from, side: SideKind::Seam }
        if !closed.contains(&from)
    )
}

pub(crate) fn assemble(solid: Solid, labeling: &SolidLabeling) -> Result<BrepSolid, BrepError> {
    let vertices = build_vertices(&solid, labeling)?;
    let truck_faces: HashMap<FaceID, truck_modeling::Face> = solid
        .boundaries()
        .iter()
        .flat_map(Shell::face_iter)
        .map(|face| (face.id(), face.clone()))
        .collect();
    let truck_face_adj = gather_truck_face_adjacency(&solid);
    let edges = build_edges(
        &solid,
        labeling,
        &vertices.by_truck,
        &vertices.positions,
        &truck_face_adj,
        &truck_faces,
    )?;
    let faces = build_faces(&solid, labeling, &edges)?;

    let shell_order = ordered_by(&faces.shells, |shell| shell.boundary_index);
    let face_order = ordered_by(&faces.faces, BrepFace::label);
    let edge_order = ordered_by(&edges.map, BrepEdge::label);
    let vertex_order = ordered_by(&vertices.map, BrepVertex::label);
    let loop_order: Vec<BrepLoopId> = face_order
        .iter()
        .flat_map(|face_id| faces.faces[*face_id].loops().iter().copied())
        .collect();

    let reattach = super::persist::capture(&solid, labeling)?;

    Ok(BrepSolid {
        arena: Arena {
            solid,
            edges: edges.arena,
            face_index: faces.face_index,
        },
        shells: faces.shells,
        faces: faces.faces,
        loops: faces.loops,
        edges: edges.map,
        vertices: vertices.map,
        shell_order,
        face_order,
        loop_order,
        edge_order,
        vertex_order,
        reattach,
    })
}

fn gather_truck_face_adjacency(solid: &Solid) -> HashMap<EdgeID, Vec<FaceID>> {
    solid.boundaries().iter().flat_map(Shell::face_iter).fold(
        HashMap::<EdgeID, Vec<FaceID>>::new(),
        |mut adj, face| {
            face.boundaries().iter().for_each(|wire| {
                wire.edge_iter().for_each(|edge| {
                    adj.entry(edge.id()).or_default().push(face.id());
                });
            });
            adj
        },
    )
}

fn ordered_by<K: Key, V, T: Ord>(map: &SlotMap<K, V>, key: impl Fn(&V) -> T) -> Vec<K> {
    let mut keys: Vec<K> = map.keys().collect();
    keys.sort_by_cached_key(|k| key(&map[*k]));
    keys
}

struct VertexTable {
    map: SlotMap<BrepVertexId, BrepVertex>,
    by_truck: HashMap<VertexID, BrepVertexId>,
    positions: HashMap<BrepVertexId, Point3>,
}

fn build_vertices(solid: &Solid, labeling: &SolidLabeling) -> Result<VertexTable, BrepError> {
    let (map, _by_label, by_truck, positions) = solid.vertex_iter().try_fold(
        (
            SlotMap::with_key(),
            HashMap::<VertexLabel, BrepVertexId>::new(),
            HashMap::<VertexID, BrepVertexId>::new(),
            HashMap::<BrepVertexId, Point3>::new(),
        ),
        |(mut map, mut by_label, mut by_truck, mut positions), vertex| {
            if by_truck.contains_key(&vertex.id()) {
                return Ok::<_, BrepError>((map, by_label, by_truck, positions));
            }
            let label = *labeling
                .vertices
                .get(&vertex.id())
                .ok_or(BrepError::MissingLabel {
                    kind: LabelKind::Vertex,
                })?;
            if suppressed_vertex(label, &labeling.closed_curves) {
                return Ok((map, by_label, by_truck, positions));
            }
            let position = point_from_truck(vertex.point());
            let brep_id = by_label.get(&label).copied().unwrap_or_else(|| {
                let id = map.insert_with_key(|id| BrepVertex {
                    id,
                    label,
                    position,
                });
                by_label.insert(label, id);
                id
            });
            by_truck.insert(vertex.id(), brep_id);
            positions.entry(brep_id).or_insert(position);
            Ok((map, by_label, by_truck, positions))
        },
    )?;
    Ok(VertexTable {
        map,
        by_truck,
        positions,
    })
}

struct EdgeTable {
    map: SlotMap<BrepEdgeId, BrepEdge>,
    by_truck: HashMap<EdgeID, BrepEdgeId>,
    arena: Vec<Edge>,
}

struct EdgeAccum {
    handles: Vec<EdgeArenaHandle>,
    incident: Vec<BrepVertexId>,
    analytic: Option<EdgeCurve3>,
}

fn build_edges(
    solid: &Solid,
    labeling: &SolidLabeling,
    vertex_dedup: &HashMap<VertexID, BrepVertexId>,
    vertex_positions: &HashMap<BrepVertexId, Point3>,
    truck_face_adj: &HashMap<EdgeID, Vec<FaceID>>,
    truck_faces: &HashMap<FaceID, truck_modeling::Face>,
) -> Result<EdgeTable, BrepError> {
    let (accum, arena, truck_label) = solid.edge_iter().try_fold(
        (
            HashMap::<EdgeLabel, EdgeAccum>::new(),
            Vec::<Edge>::new(),
            HashMap::<EdgeID, EdgeLabel>::new(),
        ),
        |(mut accum, mut arena, mut truck_label), edge| {
            if truck_label.contains_key(&edge.id()) {
                return Ok::<_, BrepError>((accum, arena, truck_label));
            }
            let label = *labeling
                .edges
                .get(&edge.id())
                .ok_or(BrepError::MissingLabel {
                    kind: LabelKind::Edge,
                })?;
            if suppressed_edge(label, &labeling.closed_curves) {
                return Ok((accum, arena, truck_label));
            }
            let handle = EdgeArenaHandle(arena.len());
            arena.push(edge.clone());
            let accumulator = accum.entry(label).or_insert_with(|| EdgeAccum {
                handles: Vec::new(),
                incident: Vec::new(),
                analytic: None,
            });
            accumulator.handles.push(handle);
            if accumulator.analytic.is_none() {
                accumulator.analytic = labeling.edge_curves.get(&edge.id()).cloned();
            }
            [edge.front().id(), edge.back().id()]
                .into_iter()
                .filter_map(|truck_vertex| vertex_dedup.get(&truck_vertex).copied())
                .for_each(|brep_vertex| accumulator.incident.push(brep_vertex));
            truck_label.insert(edge.id(), label);
            Ok((accum, arena, truck_label))
        },
    )?;

    let mut ordered_edges: Vec<(EdgeLabel, EdgeAccum)> = accum.into_iter().collect();
    ordered_edges.sort_by_key(|(label, _)| *label);
    let (map, label_to_id) = ordered_edges.into_iter().fold(
        (SlotMap::with_key(), HashMap::<EdgeLabel, BrepEdgeId>::new()),
        |(mut map, mut label_to_id), (label, slot)| {
            let primary = &arena[slot.handles[0].0];
            let curve = resolve_curve(slot.analytic, primary);
            let vertices = ordered_endpoints(&slot.incident, &curve, vertex_positions);
            let crease = compute_crease(truck_face_adj.get(&primary.id()), truck_faces, primary);
            let id = map.insert_with_key(|id| BrepEdge {
                id,
                label,
                handles: slot.handles,
                vertices,
                curve,
                crease,
            });
            label_to_id.insert(label, id);
            (map, label_to_id)
        },
    );

    let by_truck = truck_label
        .into_iter()
        .map(|(truck_id, label)| (truck_id, label_to_id[&label]))
        .collect();

    Ok(EdgeTable {
        map,
        by_truck,
        arena,
    })
}

fn resolve_curve(analytic: Option<EdgeCurve3>, primary: &Edge) -> EdgeCurve3 {
    analytic.unwrap_or_else(|| {
        let front = point_from_truck(primary.absolute_front().point());
        let back = point_from_truck(primary.absolute_back().point());
        line_between(front, back)
    })
}

fn compute_crease(
    adjacent: Option<&Vec<FaceID>>,
    truck_faces: &HashMap<FaceID, truck_modeling::Face>,
    edge: &Edge,
) -> CreaseAngle {
    let Some(face_ids) = adjacent else {
        return CreaseAngle::FLAT;
    };
    let midpoint = edge_midpoint(edge);
    let normals: Vec<UnitVec3> = face_ids
        .iter()
        .filter_map(|face_id| truck_faces.get(face_id))
        .filter_map(|face| face_normal_at(face, midpoint))
        .collect();
    match normals.as_slice() {
        [n1, n2] => crease_from_normals(*n1, *n2),
        _ => CreaseAngle::FLAT,
    }
}

fn edge_midpoint(edge: &Edge) -> truck_modeling::Point3 {
    let curve = edge.curve();
    let (t0, t1) = curve.range_tuple();
    curve.subs(f64::midpoint(t0, t1))
}

fn face_normal_at(face: &truck_modeling::Face, point: truck_modeling::Point3) -> Option<UnitVec3> {
    let surface = face.surface();
    let (u, v) = surface.search_nearest_parameter(point, SPHint2D::None, 20)?;
    let mut normal = surface.normal(u, v);
    if !face.orientation() {
        normal = -normal;
    }
    try_unit_from_truck(normal, Tolerance::new(1.0e-9))
}

fn ordered_endpoints(
    incident: &[BrepVertexId],
    curve: &EdgeCurve3,
    vertex_positions: &HashMap<BrepVertexId, Point3>,
) -> [BrepVertexId; 2] {
    let counts = incident.iter().fold(
        HashMap::<BrepVertexId, usize>::new(),
        |mut counts, vertex| {
            *counts.entry(*vertex).or_insert(0) += 1;
            counts
        },
    );
    let mut odd: Vec<BrepVertexId> = counts
        .iter()
        .filter_map(|(vertex, count)| (*count % 2 == 1).then_some(*vertex))
        .collect();
    odd.sort_unstable();
    if let [a, b] = odd.as_slice() {
        let start = curve.evaluate(Parameter::new(0.0));
        let a_dist = vertex_positions
            .get(a)
            .map_or(f64::INFINITY, |pos| squared_distance(*pos, start));
        let b_dist = vertex_positions
            .get(b)
            .map_or(f64::INFINITY, |pos| squared_distance(*pos, start));
        if a_dist <= b_dist { [*a, *b] } else { [*b, *a] }
    } else {
        let representative = counts
            .keys()
            .copied()
            .min()
            .or_else(|| vertex_positions.keys().copied().min())
            .unwrap_or_default();
        [representative, representative]
    }
}

fn squared_distance(a: Point3, b: Point3) -> f64 {
    let (ax, ay, az) = a.coords_mm();
    let (bx, by, bz) = b.coords_mm();
    let dx = ax - bx;
    let dy = ay - by;
    let dz = az - bz;
    dx * dx + dy * dy + dz * dz
}

struct FaceTable {
    faces: SlotMap<BrepFaceId, BrepFace>,
    loops: SlotMap<BrepLoopId, BrepLoop>,
    shells: SlotMap<BrepShellId, BrepShell>,
    face_index: HashMap<FaceID, BrepFaceId>,
}

#[derive(Default)]
struct Gathered {
    adjacency: HashMap<EdgeID, Vec<FaceLabel>>,
    patches: HashMap<FaceLabel, Vec<Vec<Vec<EdgeID>>>>,
    shell_members: Vec<Vec<FaceLabel>>,
    face_to_label: Vec<(FaceID, FaceLabel)>,
}

fn build_faces(
    solid: &Solid,
    labeling: &SolidLabeling,
    edges: &EdgeTable,
) -> Result<FaceTable, BrepError> {
    let Gathered {
        adjacency,
        patches,
        shell_members,
        face_to_label,
    } = gather_faces(solid, labeling)?;

    let mut ordered_patches: Vec<(FaceLabel, Vec<Vec<Vec<EdgeID>>>)> =
        patches.into_iter().collect();
    ordered_patches.sort_by_key(|(label, _)| *label);
    let (faces, loops, label_to_face) = ordered_patches.into_iter().fold(
        (
            SlotMap::with_key(),
            SlotMap::with_key(),
            HashMap::<FaceLabel, BrepFaceId>::new(),
        ),
        |(mut faces, mut loops, mut label_to_face), (label, patch_list)| {
            let loop_ids = face_loops(&patch_list, label, &adjacency, edges, &mut loops);
            let id = faces.insert_with_key(|id| BrepFace {
                id,
                label,
                loops: loop_ids,
            });
            label_to_face.insert(label, id);
            (faces, loops, label_to_face)
        },
    );

    let shells = shell_members.into_iter().enumerate().fold(
        SlotMap::with_key(),
        |mut shells, (boundary, members)| {
            let face_ids = dedup_preserving(members)
                .into_iter()
                .map(|label| label_to_face[&label])
                .collect();
            shells.insert_with_key(|id| BrepShell {
                id,
                boundary_index: BoundaryIndex(boundary),
                faces: face_ids,
            });
            shells
        },
    );

    let face_index = face_to_label
        .into_iter()
        .map(|(face_id, label)| (face_id, label_to_face[&label]))
        .collect();

    Ok(FaceTable {
        faces,
        loops,
        shells,
        face_index,
    })
}

fn gather_faces(solid: &Solid, labeling: &SolidLabeling) -> Result<Gathered, BrepError> {
    solid
        .boundaries()
        .iter()
        .try_fold(Gathered::default(), |mut gathered, shell| {
            let members = shell
                .face_iter()
                .map(|face| {
                    let label = *labeling
                        .faces
                        .get(&face.id())
                        .ok_or(BrepError::MissingLabel {
                            kind: LabelKind::Face,
                        })?;
                    let wires: Vec<Vec<EdgeID>> = face
                        .boundaries()
                        .iter()
                        .map(|wire| wire.edge_iter().map(Edge::id).collect())
                        .collect();
                    wires.iter().flatten().for_each(|edge_id| {
                        gathered.adjacency.entry(*edge_id).or_default().push(label);
                    });
                    gathered.patches.entry(label).or_default().push(wires);
                    gathered.face_to_label.push((face.id(), label));
                    Ok::<FaceLabel, BrepError>(label)
                })
                .collect::<Result<Vec<_>, _>>()?;
            gathered.shell_members.push(members);
            Ok(gathered)
        })
}

fn face_loops(
    patches: &[Vec<Vec<EdgeID>>],
    label: FaceLabel,
    adjacency: &HashMap<EdgeID, Vec<FaceLabel>>,
    edges: &EdgeTable,
    loops: &mut SlotMap<BrepLoopId, BrepLoop>,
) -> Vec<BrepLoopId> {
    if let [single] = patches {
        single
            .iter()
            .filter_map(|wire| {
                let loop_edges =
                    dedup_cycle(wire.iter().map(|edge_id| edges.by_truck[edge_id]).collect());
                (!loop_edges.is_empty()).then(|| {
                    loops.insert_with_key(|id| BrepLoop {
                        id,
                        edges: loop_edges,
                    })
                })
            })
            .collect()
    } else {
        let mut seen = HashSet::new();
        let boundary: Vec<BrepEdgeId> = patches
            .iter()
            .flatten()
            .flatten()
            .filter(|edge_id| !internal_to_group(adjacency, **edge_id, label))
            .filter_map(|edge_id| {
                let brep_id = edges.by_truck[edge_id];
                seen.insert(brep_id).then_some(brep_id)
            })
            .collect();
        chain_loops(boundary, &edges.map)
            .into_iter()
            .map(|loop_edges| {
                loops.insert_with_key(|id| BrepLoop {
                    id,
                    edges: loop_edges,
                })
            })
            .collect()
    }
}

fn internal_to_group(
    adjacency: &HashMap<EdgeID, Vec<FaceLabel>>,
    edge_id: EdgeID,
    label: FaceLabel,
) -> bool {
    adjacency
        .get(&edge_id)
        .is_some_and(|adjacent| adjacent.len() == 2 && adjacent.iter().all(|other| *other == label))
}

fn chain_loops(
    boundary: Vec<BrepEdgeId>,
    edges: &SlotMap<BrepEdgeId, BrepEdge>,
) -> Vec<Vec<BrepEdgeId>> {
    let mut sorted = boundary;
    sorted.sort_by_key(|edge| edges[*edge].label());
    let (closed, open): (Vec<BrepEdgeId>, Vec<BrepEdgeId>) = sorted
        .into_iter()
        .partition(|edge| edges[*edge].vertices()[0] == edges[*edge].vertices()[1]);
    let closed_loops = closed.into_iter().map(|edge| vec![edge]);
    closed_loops.chain(walk_open(open, edges)).collect()
}

fn walk_open(open: Vec<BrepEdgeId>, edges: &SlotMap<BrepEdgeId, BrepEdge>) -> Vec<Vec<BrepEdgeId>> {
    let mut remaining = open;
    std::iter::from_fn(move || {
        let first = (!remaining.is_empty()).then(|| remaining.remove(0))?;
        let start = edges[first].vertices()[0];
        let next = edges[first].vertices()[1];
        Some(extend_loop(next, start, &mut remaining, edges, vec![first]))
    })
    .collect()
}

fn extend_loop(
    current: BrepVertexId,
    start: BrepVertexId,
    remaining: &mut Vec<BrepEdgeId>,
    edges: &SlotMap<BrepEdgeId, BrepEdge>,
    acc: Vec<BrepEdgeId>,
) -> Vec<BrepEdgeId> {
    if current == start {
        return acc;
    }
    match remaining
        .iter()
        .position(|edge| edges[*edge].vertices().contains(&current))
    {
        None => acc,
        Some(position) => {
            let edge = remaining.remove(position);
            let ends = edges[edge].vertices();
            let next = if ends[0] == current { ends[1] } else { ends[0] };
            extend_loop(
                next,
                start,
                remaining,
                edges,
                acc.into_iter().chain(std::iter::once(edge)).collect(),
            )
        }
    }
}

fn dedup_cycle(ids: Vec<BrepEdgeId>) -> Vec<BrepEdgeId> {
    let mut out = ids.into_iter().fold(Vec::new(), |mut acc, id| {
        if acc.last() != Some(&id) {
            acc.push(id);
        }
        acc
    });
    if out.len() > 1 && out.first() == out.last() {
        out.pop();
    }
    out
}

fn dedup_preserving(labels: Vec<FaceLabel>) -> Vec<FaceLabel> {
    let mut seen = HashSet::new();
    labels
        .into_iter()
        .filter(|label| seen.insert(*label))
        .collect()
}
