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

use bone_types::{
    EdgeLabel, EdgeRole, FaceLabel, FaceRole, FeatureId, ImportOrdinal, SolidKey, StepEntityKind,
    VertexLabel, VertexRole,
};
use truck_modeling::{
    BSplineCurve, Curve, Cut, Matrix4, ParametricCurve, Point3, RevolutedCurve, Shell, Solid,
    Surface,
};
use truck_stepio::r#in::Table;
use truck_stepio::r#in::alias as step_in;
use truck_stepio::r#in::ruststep::ast::{DataSection, EntityInstance, Name, Parameter};
use truck_stepio::r#in::ruststep::parser::exchange::data_section;
use truck_stepio::out::StepModels;
use truck_topology::compress::{
    CompressedEdge, CompressedEdgeIndex, CompressedFace, CompressedShell,
};

use super::build::{SolidLabeling, assemble};
use super::persist::{
    BrepReattach, content_key, ordered_edges, ordered_faces, ordered_vertices, to_labeling,
};
use super::{BrepError, BrepSolid};

impl BrepSolid {
    pub fn to_step_body(&self) -> Result<String, BrepError> {
        let compressed = self.arena.solid().compress();
        Ok(StepModels::from_iter([&compressed]).to_string())
    }

    pub fn from_step(
        text: &str,
        feature: FeatureId,
        expected: Option<(SolidKey, &BrepReattach)>,
    ) -> Result<BrepSolid, BrepError> {
        let solid = parse_step_solid(text)?;
        let key = content_key(&solid);
        let labeling = match expected {
            Some((expected_key, reattach)) if expected_key == key => {
                to_labeling(&solid, reattach).unwrap_or_else(|_| imported_labeling(&solid, feature))
            }
            _ => imported_labeling(&solid, feature),
        };
        assemble(solid, &labeling)
    }
}

const CLOSED_SHELL: &str = "CLOSED_SHELL";
const SOLID_ROOT_KEYWORDS: &[&str] = &["MANIFOLD_SOLID_BREP", "BREP_WITH_VOIDS"];

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

struct StepTopology {
    closed_shells: BTreeSet<StepEntityId>,
    solid_roots: BTreeSet<StepEntityId>,
    references: HashMap<StepEntityId, BTreeSet<StepEntityId>>,
}

fn parse_step_solid(text: &str) -> Result<Solid, BrepError> {
    let data = first_data_section(text)?;
    let topology = classify_topology(&data);
    if topology.solid_roots.len() >= 2 {
        return Err(BrepError::StepMultipleSolids {
            count: topology.solid_roots.len(),
        });
    }
    let table = Table::from_data_section(&data);
    let shells = solid_shell_keys(&table, &topology)?
        .iter()
        .map(|key| {
            let holder = &table.shell[&key.0];
            let compressed = table
                .to_compressed_shell(holder)
                .map_err(|_| BrepError::StepShellMalformed)?;
            let bridged = bridge_shell(compressed)?;
            let normalized = split_closed_edges(bridged);
            Shell::extract(normalized).map_err(|_| BrepError::StepShellMalformed)
        })
        .collect::<Result<Vec<Shell>, BrepError>>()?;
    match shells.len() {
        0 => Err(BrepError::StepEmpty),
        _ => Ok(Solid::new_unchecked(shells)),
    }
}

fn solid_shell_keys(
    table: &Table,
    topology: &StepTopology,
) -> Result<Vec<StepEntityId>, BrepError> {
    let chosen: BTreeSet<StepEntityId> = if topology.solid_roots.is_empty() {
        topology.closed_shells.clone()
    } else {
        shells_reachable(
            &topology.references,
            &topology.closed_shells,
            &topology.solid_roots,
        )
    };
    let mut keys: Vec<StepEntityId> = chosen
        .into_iter()
        .filter(|key| table.shell.contains_key(&key.0))
        .collect();
    if topology.solid_roots.is_empty() && keys.len() >= 2 {
        return Err(BrepError::StepMultipleSolids { count: keys.len() });
    }
    keys.sort_unstable();
    Ok(keys)
}

fn classify_topology(data: &DataSection) -> StepTopology {
    data.entities.iter().fold(
        StepTopology {
            closed_shells: BTreeSet::new(),
            solid_roots: BTreeSet::new(),
            references: HashMap::new(),
        },
        |mut topology, instance| {
            let (id, keywords, parameters) = entity_record(instance);
            if keywords
                .iter()
                .any(|name| name.eq_ignore_ascii_case(CLOSED_SHELL))
            {
                topology.closed_shells.insert(id);
            }
            if keywords.iter().any(|name| {
                SOLID_ROOT_KEYWORDS
                    .iter()
                    .any(|root| name.eq_ignore_ascii_case(root))
            }) {
                topology.solid_roots.insert(id);
            }
            let references: BTreeSet<StepEntityId> =
                parameters.iter().flat_map(|&p| entity_refs(p)).collect();
            if !references.is_empty() {
                topology.references.insert(id, references);
            }
            topology
        },
    )
}

fn entity_record(instance: &EntityInstance) -> (StepEntityId, Vec<&str>, Vec<&Parameter>) {
    match instance {
        EntityInstance::Simple { id, record } => (
            StepEntityId(*id),
            vec![record.name.as_str()],
            vec![&record.parameter],
        ),
        EntityInstance::Complex { id, subsuper } => (
            StepEntityId(*id),
            subsuper
                .0
                .iter()
                .map(|record| record.name.as_str())
                .collect(),
            subsuper.0.iter().map(|record| &record.parameter).collect(),
        ),
    }
}

fn entity_refs(parameter: &Parameter) -> BTreeSet<StepEntityId> {
    match parameter {
        Parameter::Ref(Name::Entity(id)) => BTreeSet::from([StepEntityId(*id)]),
        Parameter::List(items) => items.iter().flat_map(entity_refs).collect(),
        Parameter::Typed { parameter, .. } => entity_refs(parameter),
        _ => BTreeSet::new(),
    }
}

fn shells_reachable(
    references: &HashMap<StepEntityId, BTreeSet<StepEntityId>>,
    closed_shells: &BTreeSet<StepEntityId>,
    seeds: &BTreeSet<StepEntityId>,
) -> BTreeSet<StepEntityId> {
    let expanded: BTreeSet<StepEntityId> = seeds
        .iter()
        .filter(|id| !closed_shells.contains(id))
        .flat_map(|id| references.get(id).into_iter().flatten().copied())
        .chain(seeds.iter().copied())
        .collect();
    if expanded.len() == seeds.len() {
        expanded.intersection(closed_shells).copied().collect()
    } else {
        shells_reachable(references, closed_shells, &expanded)
    }
}

fn first_data_section(text: &str) -> Result<DataSection, BrepError> {
    text.match_indices("DATA")
        .find_map(|(at, _)| data_section(&text[at..]).map(|(_, section)| section).ok())
        .ok_or_else(|| {
            if text.contains("DATA") {
                BrepError::StepSyntax
            } else {
                BrepError::StepNoData
            }
        })
}

type StepShell = CompressedShell<truck_modeling::Point3, step_in::Curve3D, step_in::Surface>;
type ModelShell = CompressedShell<truck_modeling::Point3, Curve, Surface>;

fn bridge_shell(shell: StepShell) -> Result<ModelShell, BrepError> {
    let edges = shell
        .edges
        .into_iter()
        .map(|edge| {
            Ok(CompressedEdge {
                vertices: edge.vertices,
                curve: bridge_curve(edge.curve)?,
            })
        })
        .collect::<Result<Vec<_>, BrepError>>()?;
    let faces = shell
        .faces
        .into_iter()
        .map(|face| {
            Ok(CompressedFace {
                boundaries: face.boundaries,
                orientation: face.orientation,
                surface: bridge_surface(face.surface)?,
            })
        })
        .collect::<Result<Vec<_>, BrepError>>()?;
    Ok(CompressedShell {
        vertices: shell.vertices,
        edges,
        faces,
    })
}

const CIRCLE_POSITION_TOL: f64 = 1.0e-4;
const CIRCLE_DERIVATIVE_TOL: f64 = 1.0e-3;
const CIRCLE_TRIALS: usize = 24;

fn split_closed_edges(shell: ModelShell) -> ModelShell {
    let CompressedShell {
        vertices,
        edges,
        faces,
    } = shell;
    let (edges, vertices, remap) = edges.into_iter().fold(
        (Vec::new(), vertices, Vec::<Vec<usize>>::new()),
        |(mut edges, mut vertices, mut remap), edge| {
            if let Some((midpoint, head, tail)) = split_closed_edge(&edge, vertices.len()) {
                vertices.push(midpoint);
                remap.push(vec![edges.len(), edges.len() + 1]);
                edges.push(head);
                edges.push(tail);
            } else {
                remap.push(vec![edges.len()]);
                edges.push(edge);
            }
            (edges, vertices, remap)
        },
    );
    let faces = faces
        .into_iter()
        .map(|face| CompressedFace {
            boundaries: face
                .boundaries
                .into_iter()
                .map(|wire| {
                    wire.into_iter()
                        .flat_map(|step| expand_edge(step, &remap))
                        .collect()
                })
                .collect(),
            orientation: face.orientation,
            surface: face.surface,
        })
        .collect();
    CompressedShell {
        vertices,
        edges,
        faces,
    }
}

fn split_closed_edge(
    edge: &CompressedEdge<Curve>,
    midpoint_index: usize,
) -> Option<(Point3, CompressedEdge<Curve>, CompressedEdge<Curve>)> {
    if edge.vertices.0 != edge.vertices.1 {
        return None;
    }
    let (start, end) = edge.curve.try_range_tuple()?;
    let middle = f64::midpoint(start, end);
    let midpoint = edge.curve.subs(middle);
    let mut head = edge.curve.clone();
    let tail = head.cut(middle);
    Some((
        midpoint,
        CompressedEdge {
            vertices: (edge.vertices.0, midpoint_index),
            curve: head,
        },
        CompressedEdge {
            vertices: (midpoint_index, edge.vertices.1),
            curve: tail,
        },
    ))
}

fn expand_edge(step: CompressedEdgeIndex, remap: &[Vec<usize>]) -> Vec<CompressedEdgeIndex> {
    let pieces = &remap[step.index];
    if step.orientation {
        pieces
            .iter()
            .map(|&index| CompressedEdgeIndex {
                index,
                orientation: true,
            })
            .collect()
    } else {
        pieces
            .iter()
            .rev()
            .map(|&index| CompressedEdgeIndex {
                index,
                orientation: false,
            })
            .collect()
    }
}

fn bridge_curve(curve: step_in::Curve3D) -> Result<Curve, BrepError> {
    match curve {
        step_in::Curve3D::Line(line) => Ok(Curve::Line(line)),
        step_in::Curve3D::BSplineCurve(spline) => Ok(Curve::BSplineCurve(spline)),
        step_in::Curve3D::NurbsCurve(nurbs) => Ok(Curve::NurbsCurve(nurbs)),
        step_in::Curve3D::Conic(step_in::Conic3D::Ellipse(circle)) => bridge_circle(&circle),
        step_in::Curve3D::Conic(_) => Err(unsupported(StepEntityKind::ConicCurve)),
        step_in::Curve3D::Polyline(_) => Err(unsupported(StepEntityKind::PolylineCurve)),
        step_in::Curve3D::PCurve(_) => Err(unsupported(StepEntityKind::ParametricCurve)),
    }
}

fn bridge_circle(circle: &step_in::Ellipse<Point3, Matrix4>) -> Result<Curve, BrepError> {
    let range = circle
        .try_range_tuple()
        .ok_or(unsupported(StepEntityKind::ConicCurve))?;
    BSplineCurve::cubic_approximation(
        circle,
        range,
        CIRCLE_POSITION_TOL,
        CIRCLE_DERIVATIVE_TOL,
        CIRCLE_TRIALS,
    )
    .map(Curve::BSplineCurve)
    .ok_or(unsupported(StepEntityKind::ConicCurve))
}

fn bridge_surface(surface: step_in::Surface) -> Result<Surface, BrepError> {
    match surface {
        step_in::Surface::ElementarySurface(elementary) => match *elementary {
            step_in::ElementarySurface::Plane(plane) => Ok(Surface::Plane(plane)),
            step_in::ElementarySurface::CylindricalSurface(cylinder) => Ok(revolution(cylinder)),
            step_in::ElementarySurface::ConicalSurface(cone) => Ok(revolution(cone)),
            step_in::ElementarySurface::Sphere(_) => {
                Err(unsupported(StepEntityKind::SphericalSurface))
            }
            step_in::ElementarySurface::ToroidalSurface(_) => {
                Err(unsupported(StepEntityKind::ToroidalSurface))
            }
        },
        step_in::Surface::BSplineSurface(spline) => Ok(Surface::BSplineSurface(*spline)),
        step_in::Surface::NurbsSurface(nurbs) => Ok(Surface::NurbsSurface(*nurbs)),
        step_in::Surface::SweptCurve(_) => Err(unsupported(StepEntityKind::SweptSurface)),
    }
}

fn revolution(surface: step_in::CylindricalSurface) -> Surface {
    Surface::RevolutedCurve(surface.map_ref(|revolved| {
        RevolutedCurve::by_revolution(
            Curve::Line(*revolved.entity_curve()),
            revolved.origin(),
            revolved.axis(),
        )
    }))
}

fn unsupported(kind: StepEntityKind) -> BrepError {
    BrepError::StepUnsupported { kind }
}

fn imported_labeling(solid: &Solid, feature: FeatureId) -> SolidLabeling {
    let faces = ordered_faces(solid)
        .into_iter()
        .enumerate()
        .map(|(index, id)| {
            (
                id,
                FaceLabel {
                    feature,
                    role: FaceRole::Imported {
                        ordinal: ordinal(index),
                    },
                },
            )
        })
        .collect();
    let edges = ordered_edges(solid)
        .into_iter()
        .enumerate()
        .map(|(index, id)| {
            (
                id,
                EdgeLabel {
                    feature,
                    role: EdgeRole::Imported {
                        ordinal: ordinal(index),
                    },
                },
            )
        })
        .collect();
    let vertices = ordered_vertices(solid)
        .into_iter()
        .enumerate()
        .map(|(index, id)| {
            (
                id,
                VertexLabel {
                    feature,
                    role: VertexRole::Imported {
                        ordinal: ordinal(index),
                    },
                },
            )
        })
        .collect();
    SolidLabeling {
        faces,
        edges,
        vertices,
        closed_curves: HashSet::new(),
        edge_curves: HashMap::new(),
    }
}

fn ordinal(index: usize) -> ImportOrdinal {
    ImportOrdinal::new(u32::try_from(index).unwrap_or(u32::MAX))
}

#[cfg(test)]
mod tests {
    use super::{
        CompressedEdge, CompressedEdgeIndex, bridge_curve, bridge_surface, expand_edge,
        split_closed_edge,
    };
    use core::str::FromStr;
    use truck_modeling::{Curve, Matrix4, ParametricCurve, ParametricSurface, Point3, Surface};
    use truck_stepio::r#in::ruststep::ast::DataSection;
    use truck_stepio::r#in::ruststep::tables::{EntityTable, Holder};
    use truck_stepio::r#in::{CircleHolder, ElementarySurfaceAnyHolder, Table, alias};

    const AXES: &str = "#2 = AXIS2_PLACEMENT_3D('', #3, #4, #5);\n#3 = CARTESIAN_POINT('', (0.0, 0.0, 0.0));\n#4 = DIRECTION('', (0.0, 0.0, 1.0));\n#5 = DIRECTION('', (1.0, 0.0, 0.0));\n";

    fn first<H>(step: &str) -> H::Owned
    where
        H: Holder<Table = Table>,
        Table: EntityTable<H>,
    {
        let Ok(section) = DataSection::from_str(step) else {
            panic!("data section parses");
        };
        let table = Table::from_data_section(&section);
        let Ok(owned) = EntityTable::<H>::get_owned(&table, 1) else {
            panic!("entity #1 resolves");
        };
        owned
    }

    fn distance(a: Point3, b: Point3) -> f64 {
        let (dx, dy, dz) = (a.x - b.x, a.y - b.y, a.z - b.z);
        f64::sqrt(dx * dx + dy * dy + dz * dz)
    }

    #[test]
    fn cylindrical_surface_bridges_to_an_exact_revolution() {
        let step = format!("DATA;\n#1 = CYLINDRICAL_SURFACE('', #2, 5.0);\n{AXES}ENDSEC;");
        let surface: alias::ElementarySurface =
            (&first::<ElementarySurfaceAnyHolder>(&step)).into();
        let Ok(bridged) = bridge_surface(alias::Surface::ElementarySurface(Box::new(surface)))
        else {
            panic!("a cylindrical surface bridges into the kernel");
        };
        assert!(matches!(bridged, Surface::RevolutedCurve(_)));
        let base = bridged.subs(0.0, 3.0);
        assert!(
            distance(base, Point3::new(5.0, 0.0, 3.0)) < 1.0e-9,
            "the seam sits at radius 5 on the x axis: {base:?}"
        );
        let quarter = bridged.subs(core::f64::consts::FRAC_PI_2, 1.0);
        assert!(
            distance(quarter, Point3::new(0.0, 5.0, 1.0)) < 1.0e-9,
            "a quarter turn lands on the y axis: {quarter:?}"
        );
    }

    #[test]
    fn conical_surface_bridges_to_an_exact_revolution() {
        let step = format!("DATA;\n#1 = CONICAL_SURFACE('', #2, 5.0, 0.5);\n{AXES}ENDSEC;");
        let surface: alias::ElementarySurface =
            (&first::<ElementarySurfaceAnyHolder>(&step)).into();
        let Ok(bridged) = bridge_surface(alias::Surface::ElementarySurface(Box::new(surface)))
        else {
            panic!("a conical surface bridges into the kernel");
        };
        assert!(matches!(bridged, Surface::RevolutedCurve(_)));
        let radius = |p: Point3| (p.x * p.x + p.y * p.y).sqrt();
        let seam = bridged.subs(0.0, 0.3);
        let spun = bridged.subs(core::f64::consts::FRAC_PI_2, 0.3);
        assert!(
            (radius(seam) - radius(spun)).abs() < 1.0e-9,
            "a revolution holds one radius around the axis: {seam:?} vs {spun:?}"
        );
        let low = radius(bridged.subs(0.0, 0.0));
        let high = radius(bridged.subs(0.0, 1.0));
        assert!(
            (low - high).abs() > 1.0e-3,
            "a cone changes radius along its axis: {low} vs {high}"
        );
    }

    #[test]
    fn circle_bridges_within_tolerance() {
        let step = format!("DATA;\n#1 = CIRCLE('', #2, 5.0);\n{AXES}ENDSEC;");
        let Ok(circle): Result<alias::Ellipse<Point3, Matrix4>, _> =
            (&first::<CircleHolder>(&step)).try_into()
        else {
            panic!("a circle converts to truck's analytic ellipse");
        };
        let Some((lo, hi)) = circle.try_range_tuple() else {
            panic!("a parsed circle carries a bounded parameter range");
        };
        let analytic = circle;
        let Ok(bridged) = bridge_curve(alias::Curve3D::Conic(alias::Conic3D::Ellipse(circle)))
        else {
            panic!("a circle bridges into the kernel");
        };
        assert!(matches!(bridged, Curve::BSplineCurve(_)));
        (0..=12).for_each(|i| {
            let t = lo + (hi - lo) * (f64::from(i) / 12.0);
            let drift = distance(bridged.subs(t), analytic.subs(t));
            assert!(
                drift < 1.0e-2,
                "circle approximation drifts {drift} at t={t}"
            );
        });
    }

    #[test]
    fn spherical_surface_stays_unsupported() {
        let step = format!("DATA;\n#1 = SPHERICAL_SURFACE('', #2, 5.0);\n{AXES}ENDSEC;");
        let surface: alias::ElementarySurface =
            (&first::<ElementarySurfaceAnyHolder>(&step)).into();
        assert!(bridge_surface(alias::Surface::ElementarySurface(Box::new(surface))).is_err());
    }

    fn circle_curve() -> Curve {
        let step = format!("DATA;\n#1 = CIRCLE('', #2, 5.0);\n{AXES}ENDSEC;");
        let Ok(circle): Result<alias::Ellipse<Point3, Matrix4>, _> =
            (&first::<CircleHolder>(&step)).try_into()
        else {
            panic!("a circle converts to truck's analytic ellipse");
        };
        let Ok(bridged) = bridge_curve(alias::Curve3D::Conic(alias::Conic3D::Ellipse(circle)))
        else {
            panic!("a circle bridges into the kernel");
        };
        bridged
    }

    #[test]
    fn closed_edge_splits_into_two_open_halves() {
        let edge = CompressedEdge {
            vertices: (0, 0),
            curve: circle_curve(),
        };
        let Some((midpoint, head, tail)) = split_closed_edge(&edge, 1) else {
            panic!("a closed circular edge splits at its parameter midpoint");
        };
        assert_eq!(
            head.vertices,
            (0, 1),
            "the head runs from the original endpoint to the fresh midpoint vertex"
        );
        assert_eq!(
            tail.vertices,
            (1, 0),
            "the tail runs from the midpoint vertex back to the original endpoint"
        );
        let Some((_, head_end)) = head.curve.try_range_tuple() else {
            panic!("the head half carries a bounded range");
        };
        let Some((tail_start, _)) = tail.curve.try_range_tuple() else {
            panic!("the tail half carries a bounded range");
        };
        assert!(
            distance(head.curve.subs(head_end), midpoint) < 1.0e-6,
            "the head half ends at the midpoint"
        );
        assert!(
            distance(tail.curve.subs(tail_start), midpoint) < 1.0e-6,
            "the tail half starts at the midpoint"
        );
    }

    #[test]
    fn open_edge_is_left_intact() {
        let edge = CompressedEdge {
            vertices: (0, 1),
            curve: circle_curve(),
        };
        assert!(
            split_closed_edge(&edge, 2).is_none(),
            "an edge with distinct endpoints is never split"
        );
    }

    #[test]
    fn expand_edge_rewrites_boundaries_through_the_remap() {
        let remap = vec![vec![5usize], vec![7usize, 8usize]];
        let as_pairs = |steps: Vec<CompressedEdgeIndex>| {
            steps
                .iter()
                .map(|step| (step.index, step.orientation))
                .collect::<Vec<_>>()
        };
        assert_eq!(
            as_pairs(expand_edge(
                CompressedEdgeIndex {
                    index: 1,
                    orientation: true,
                },
                &remap,
            )),
            vec![(7, true), (8, true)],
            "a forward split edge expands to head then tail, both forward"
        );
        assert_eq!(
            as_pairs(expand_edge(
                CompressedEdgeIndex {
                    index: 1,
                    orientation: false,
                },
                &remap,
            )),
            vec![(8, false), (7, false)],
            "a reversed split edge expands to tail then head, both reversed"
        );
        assert_eq!(
            as_pairs(expand_edge(
                CompressedEdgeIndex {
                    index: 0,
                    orientation: true,
                },
                &remap,
            )),
            vec![(5, true)],
            "an unsplit edge passes through unchanged"
        );
    }
}
