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

use bone_types::{
    EdgeLabel, EdgeRole, FaceLabel, FaceRole, FeatureId, ImportOrdinal, SolidKey, StepEntityKind,
    VertexLabel, VertexRole,
};
use truck_modeling::{Curve, 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;
use truck_stepio::r#in::ruststep::parser::exchange::data_section;
use truck_stepio::out::StepModels;
use truck_topology::compress::{CompressedEdge, 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)
    }
}

fn parse_step_solid(text: &str) -> Result<Solid, BrepError> {
    let data = first_data_section(text)?;
    let table = Table::from_data_section(&data);
    let mut shell_keys: Vec<u64> = table.shell.keys().copied().collect();
    shell_keys.sort_unstable();
    let shells = shell_keys
        .iter()
        .map(|key| {
            let holder = &table.shell[key];
            let compressed = table
                .to_compressed_shell(holder)
                .map_err(|_| BrepError::StepShellMalformed)?;
            let bridged = bridge_shell(compressed)?;
            Shell::extract(bridged).map_err(|_| BrepError::StepShellMalformed)
        })
        .collect::<Result<Vec<Shell>, BrepError>>()?;
    match shells.len() {
        0 => Err(BrepError::StepEmpty),
        1 => Ok(Solid::new_unchecked(shells)),
        count => Err(BrepError::StepMultipleShells { count }),
    }
}

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,
    })
}

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(_) => Err(unsupported(StepEntityKind::ConicCurve)),
        step_in::Curve3D::Polyline(_) => Err(unsupported(StepEntityKind::PolylineCurve)),
        step_in::Curve3D::PCurve(_) => Err(unsupported(StepEntityKind::ParametricCurve)),
    }
}

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::Sphere(_) => {
                Err(unsupported(StepEntityKind::SphericalSurface))
            }
            step_in::ElementarySurface::CylindricalSurface(_) => {
                Err(unsupported(StepEntityKind::CylindricalSurface))
            }
            step_in::ElementarySurface::ToroidalSurface(_) => {
                Err(unsupported(StepEntityKind::ToroidalSurface))
            }
            step_in::ElementarySurface::ConicalSurface(_) => {
                Err(unsupported(StepEntityKind::ConicalSurface))
            }
        },
        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 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))
}
