use std::collections::BTreeMap;

use bone_kernel::{BrepEdge, BrepFace, BrepSolid, BrepVertex, ExtrudeFeature};
use bone_types::{BodyId, DocumentId, ExtrudeId, FeatureId, SchemaHeader, SketchId};
use serde::{Deserialize, Serialize};
use slotmap::KeyData;

use crate::Sketch;

pub mod feature_tree;

pub use feature_tree::{FeatureEdge, FeatureNode, FeatureTree, PrincipalPlane};

#[derive(Clone)]
pub struct ImportedSolid {
    solid: BrepSolid,
}

impl ImportedSolid {
    #[must_use]
    pub fn new(solid: BrepSolid) -> Self {
        Self { solid }
    }

    #[must_use]
    pub fn solid(&self) -> &BrepSolid {
        &self.solid
    }
}

impl core::fmt::Debug for ImportedSolid {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("ImportedSolid")
            .field("content_key", &self.solid.content_key())
            .finish()
    }
}

impl PartialEq for ImportedSolid {
    fn eq(&self, other: &Self) -> bool {
        self.solid.content_key() == other.solid.content_key()
            && self
                .solid
                .iter_faces()
                .map(BrepFace::label)
                .eq(other.solid.iter_faces().map(BrepFace::label))
            && self
                .solid
                .iter_edges()
                .map(BrepEdge::label)
                .eq(other.solid.iter_edges().map(BrepEdge::label))
            && self
                .solid
                .iter_vertices()
                .map(BrepVertex::label)
                .eq(other.solid.iter_vertices().map(BrepVertex::label))
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum UnitsPreference {
    Millimetre,
    Centimetre,
    Metre,
    Inch,
    Foot,
}

impl UnitsPreference {
    pub const DEFAULT: Self = Self::Millimetre;
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SketchRegistryEntry {
    pub label: String,
    pub filename: String,
}

#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SketchRegistry {
    order: Vec<SketchId>,
    entries: BTreeMap<SketchId, SketchRegistryEntry>,
}

impl SketchRegistry {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    #[must_use]
    pub fn order(&self) -> &[SketchId] {
        &self.order
    }

    #[must_use]
    pub fn entry(&self, id: SketchId) -> Option<&SketchRegistryEntry> {
        self.entries.get(&id)
    }

    pub fn iter(&self) -> impl Iterator<Item = (SketchId, &SketchRegistryEntry)> + '_ {
        self.order
            .iter()
            .filter_map(|id| self.entries.get(id).map(|e| (*id, e)))
    }

    pub(crate) fn insert(&mut self, id: SketchId, entry: SketchRegistryEntry) {
        if !self.entries.contains_key(&id) {
            self.order.push(id);
        }
        self.entries.insert(id, entry);
    }

    pub(crate) fn remove(&mut self, id: SketchId) -> Option<SketchRegistryEntry> {
        self.order.retain(|other| *other != id);
        self.entries.remove(&id)
    }

    pub(crate) fn label_mut(&mut self, id: SketchId) -> Option<&mut String> {
        self.entries.get_mut(&id).map(|entry| &mut entry.label)
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum RenameSketchError {
    #[error("sketch {0:?} not found")]
    UnknownSketch(SketchId),
    #[error("sketch label must contain non-whitespace characters")]
    EmptyLabel,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum RenameExtrudeError {
    #[error("extrude {0:?} not found")]
    UnknownExtrude(ExtrudeId),
    #[error("extrude label must contain non-whitespace characters")]
    EmptyLabel,
}

#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DocumentParameters {
    order: Vec<String>,
    entries: BTreeMap<String, f64>,
}

impl DocumentParameters {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    #[must_use]
    pub fn order(&self) -> &[String] {
        &self.order
    }

    #[must_use]
    pub fn get(&self, name: &str) -> Option<f64> {
        self.entries.get(name).copied()
    }

    pub fn insert(&mut self, name: String, value: f64) {
        if !self.entries.contains_key(&name) {
            self.order.push(name.clone());
        }
        self.entries.insert(name, value);
    }

    pub fn remove(&mut self, name: &str) -> Option<f64> {
        self.order.retain(|n| n != name);
        self.entries.remove(name)
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DocumentHeader {
    pub schema: SchemaHeader,
    pub id: DocumentId,
    pub name: String,
    pub units: UnitsPreference,
    pub parameters: DocumentParameters,
    pub feature_tree: FeatureTree,
    pub sketches: SketchRegistry,
    #[serde(skip)]
    pub extrudes: BTreeMap<ExtrudeId, ExtrudeFeature>,
    #[serde(skip)]
    pub extrude_labels: BTreeMap<ExtrudeId, String>,
}

impl DocumentHeader {
    #[must_use]
    pub fn new(id: DocumentId, name: String) -> Self {
        Self {
            schema: SchemaHeader::bone_document(),
            id,
            name,
            units: UnitsPreference::DEFAULT,
            parameters: DocumentParameters::new(),
            feature_tree: FeatureTree::seeded(),
            sketches: SketchRegistry::new(),
            extrudes: BTreeMap::new(),
            extrude_labels: BTreeMap::new(),
        }
    }

    pub(crate) fn rebuild_edges(&mut self) {
        self.feature_tree
            .rebuild_edges(self.extrudes.iter().map(|(id, feature)| (*id, feature)));
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SketchFile {
    pub schema: SchemaHeader,
    pub sketch: Sketch,
}

impl SketchFile {
    #[must_use]
    pub fn new(sketch: Sketch) -> Self {
        Self {
            schema: SchemaHeader::bone_document(),
            sketch,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExtrudeFile {
    pub schema: SchemaHeader,
    pub feature: ExtrudeFeature,
    pub label: String,
}

impl ExtrudeFile {
    #[must_use]
    pub fn new(feature: ExtrudeFeature, label: String) -> Self {
        Self {
            schema: SchemaHeader::bone_document(),
            feature,
            label,
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct Document {
    header: DocumentHeader,
    sketches: BTreeMap<SketchId, Sketch>,
    bodies: BTreeMap<BodyId, ImportedSolid>,
}

impl Document {
    #[must_use]
    pub fn new(id: DocumentId, name: String) -> Self {
        Self {
            header: DocumentHeader::new(id, name),
            sketches: BTreeMap::new(),
            bodies: BTreeMap::new(),
        }
    }

    pub(crate) fn from_parts(
        mut header: DocumentHeader,
        sketches: BTreeMap<SketchId, Sketch>,
        bodies: BTreeMap<BodyId, ImportedSolid>,
    ) -> Self {
        header.rebuild_edges();
        Self {
            header,
            sketches,
            bodies,
        }
    }

    #[must_use]
    pub fn header(&self) -> &DocumentHeader {
        &self.header
    }

    #[must_use]
    pub fn id(&self) -> DocumentId {
        self.header.id
    }

    #[must_use]
    pub fn name(&self) -> &str {
        &self.header.name
    }

    #[must_use]
    pub fn units(&self) -> UnitsPreference {
        self.header.units
    }

    #[must_use]
    pub fn feature_tree(&self) -> &FeatureTree {
        &self.header.feature_tree
    }

    #[must_use]
    pub fn parameters(&self) -> &DocumentParameters {
        &self.header.parameters
    }

    #[must_use]
    pub fn registry(&self) -> &SketchRegistry {
        &self.header.sketches
    }

    #[must_use]
    pub fn sketch(&self, id: SketchId) -> Option<&Sketch> {
        self.sketches.get(&id)
    }

    #[must_use]
    pub fn sketch_label(&self, id: SketchId) -> Option<&str> {
        self.header.sketches.entry(id).map(|e| e.label.as_str())
    }

    pub fn rename_sketch(&mut self, id: SketchId, label: &str) -> Result<(), RenameSketchError> {
        let trimmed = label.trim();
        if trimmed.is_empty() {
            return Err(RenameSketchError::EmptyLabel);
        }
        let slot = self
            .header
            .sketches
            .label_mut(id)
            .ok_or(RenameSketchError::UnknownSketch(id))?;
        trimmed.clone_into(slot);
        Ok(())
    }

    pub fn sketches(&self) -> impl Iterator<Item = (SketchId, &Sketch)> + '_ {
        self.header
            .sketches
            .order()
            .iter()
            .filter_map(|id| self.sketches.get(id).map(|s| (*id, s)))
    }

    #[must_use]
    pub fn sketches_map(&self) -> &BTreeMap<SketchId, Sketch> {
        &self.sketches
    }

    pub fn set_name(&mut self, name: String) {
        self.header.name = name;
    }

    pub fn set_units(&mut self, units: UnitsPreference) {
        self.header.units = units;
    }

    pub fn insert_sketch(&mut self, id: SketchId, label: String, sketch: Sketch) {
        let filename = sketch_filename(id);
        self.header
            .sketches
            .insert(id, SketchRegistryEntry { label, filename });
        self.header.feature_tree.push_sketch(id);
        self.sketches.insert(id, sketch);
        let dependents: Vec<ExtrudeId> = self.extrudes_of_sketch(id).collect();
        dependents.iter().for_each(|extrude| {
            if let Some(feature) = self.header.extrudes.get(extrude).copied() {
                self.header.feature_tree.push_extrude(*extrude, &feature);
            }
        });
    }

    pub fn replace_sketch(&mut self, id: SketchId, sketch: Sketch) -> Option<Sketch> {
        self.sketches.insert(id, sketch)
    }

    pub fn insert_extrude(&mut self, id: ExtrudeId, feature: ExtrudeFeature) {
        self.header.feature_tree.push_extrude(id, &feature);
        self.header.extrudes.insert(id, feature);
        self.header
            .extrude_labels
            .entry(id)
            .or_insert_with(|| format!("Extrude{}", key_index(id)));
    }

    #[must_use]
    pub fn commit_extrude(&mut self, feature: ExtrudeFeature) -> ExtrudeId {
        let id = next_key(self.header.extrudes.keys().copied());
        self.insert_extrude(id, feature);
        id
    }

    pub fn remove_extrude(&mut self, id: ExtrudeId) -> Option<ExtrudeFeature> {
        self.header.feature_tree.remove_extrude(id);
        self.header.extrude_labels.remove(&id);
        self.header.extrudes.remove(&id)
    }

    #[must_use]
    pub fn extrude(&self, id: ExtrudeId) -> Option<&ExtrudeFeature> {
        self.header.extrudes.get(&id)
    }

    #[must_use]
    pub fn extrude_label(&self, id: ExtrudeId) -> Option<&str> {
        self.header.extrude_labels.get(&id).map(String::as_str)
    }

    pub fn rename_extrude(&mut self, id: ExtrudeId, label: &str) -> Result<(), RenameExtrudeError> {
        let trimmed = label.trim();
        if trimmed.is_empty() {
            return Err(RenameExtrudeError::EmptyLabel);
        }
        let slot = self
            .header
            .extrude_labels
            .get_mut(&id)
            .ok_or(RenameExtrudeError::UnknownExtrude(id))?;
        trimmed.clone_into(slot);
        Ok(())
    }

    fn extrudes_of_sketch(&self, sketch: SketchId) -> impl Iterator<Item = ExtrudeId> + '_ {
        self.header
            .extrudes
            .iter()
            .filter(move |(_, feature)| feature.sketch == sketch)
            .map(|(extrude, _)| *extrude)
    }

    pub fn remove_sketch(&mut self, id: SketchId) -> Option<Sketch> {
        let dependents: Vec<ExtrudeId> = self.extrudes_of_sketch(id).collect();
        dependents.iter().for_each(|extrude| {
            self.remove_extrude(*extrude);
        });
        self.header.sketches.remove(id);
        self.header.feature_tree.remove_sketch(id);
        self.sketches.remove(&id)
    }

    pub fn set_parameter(&mut self, name: String, value: f64) {
        self.header.parameters.insert(name, value);
    }

    #[must_use]
    pub fn sketch_of_feature(&self, feature: FeatureId) -> Option<&Sketch> {
        match self.header.feature_tree.node(feature)? {
            FeatureNode::Sketch(id) => self.sketches.get(&id),
            FeatureNode::Origin
            | FeatureNode::PrincipalPlane(_)
            | FeatureNode::Extrude(_)
            | FeatureNode::ImportedBody(_) => None,
        }
    }

    #[must_use]
    pub fn extrude_of_feature(&self, feature: FeatureId) -> Option<&ExtrudeFeature> {
        match self.header.feature_tree.node(feature)? {
            FeatureNode::Extrude(id) => self.header.extrudes.get(&id),
            FeatureNode::Origin
            | FeatureNode::PrincipalPlane(_)
            | FeatureNode::Sketch(_)
            | FeatureNode::ImportedBody(_) => None,
        }
    }

    pub fn import_body<E>(
        &mut self,
        build: impl FnOnce(FeatureId) -> Result<BrepSolid, E>,
    ) -> Result<(FeatureId, BodyId), E> {
        let feature = self.header.feature_tree.allocate();
        let body = next_key(self.bodies.keys().copied());
        let solid = build(feature)?;
        self.header.feature_tree.push_imported_body(feature, body);
        self.bodies.insert(body, ImportedSolid::new(solid));
        Ok((feature, body))
    }

    #[must_use]
    pub fn imported_body(&self, body: BodyId) -> Option<&BrepSolid> {
        self.bodies.get(&body).map(ImportedSolid::solid)
    }

    #[must_use]
    pub fn imported_body_of_feature(&self, feature: FeatureId) -> Option<&BrepSolid> {
        match self.header.feature_tree.node(feature)? {
            FeatureNode::ImportedBody(body) => self.imported_body(body),
            FeatureNode::Origin
            | FeatureNode::PrincipalPlane(_)
            | FeatureNode::Sketch(_)
            | FeatureNode::Extrude(_) => None,
        }
    }

    pub fn imported_bodies(&self) -> impl Iterator<Item = (BodyId, &BrepSolid)> + '_ {
        self.bodies.iter().map(|(id, body)| (*id, body.solid()))
    }
}

pub(crate) fn key_index<K: slotmap::Key>(id: K) -> u32 {
    let Ok(idx) = u32::try_from(id.data().as_ffi() & 0xFFFF_FFFF) else {
        panic!("lower 32 bits of an ffi key fit in u32");
    };
    idx
}

pub(crate) fn key_from_index<K: slotmap::Key>(idx: u32) -> K {
    K::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx)))
}

pub(crate) fn next_key<K: slotmap::Key>(existing: impl Iterator<Item = K>) -> K {
    let highest = existing.map(key_index).max().unwrap_or(0);
    let Some(next) = highest.checked_add(1) else {
        panic!(
            "exhausted 32-bit key space for {}",
            core::any::type_name::<K>()
        );
    };
    key_from_index(next)
}

fn id_stem<K: slotmap::Key>(id: K) -> String {
    format!("{:016x}", id.data().as_ffi())
}

#[must_use]
pub fn sketch_filename(id: SketchId) -> String {
    format!("{}.ron", id_stem(id))
}

#[must_use]
pub fn extrude_filename(id: ExtrudeId) -> String {
    format!("{}.ron", id_stem(id))
}

#[must_use]
pub fn body_brep_filename(id: BodyId) -> String {
    format!("{}.brep", id_stem(id))
}

#[must_use]
pub fn body_labels_filename(id: BodyId) -> String {
    format!("{}.labels", id_stem(id))
}

#[cfg(test)]
mod tests {
    use super::feature_tree::sample_blind_extrude;
    use super::{Document, RenameExtrudeError, RenameSketchError, Sketch, SketchId};
    use bone_types::{DocumentId, ExtrudeId, Point3, SketchPlaneBasis, Tolerance, UnitVec3};

    fn xy_basis() -> SketchPlaneBasis {
        let Ok(basis) = SketchPlaneBasis::new(
            Point3::origin(),
            UnitVec3::x_axis(),
            UnitVec3::y_axis(),
            Tolerance::new(1e-9),
        ) else {
            panic!("xy plane basis is orthogonal");
        };
        basis
    }

    fn doc_with_sketch() -> (Document, SketchId) {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let id = SketchId::default();
        document.insert_sketch(id, "Sketch1".to_owned(), Sketch::new(xy_basis()));
        (document, id)
    }

    #[test]
    fn rename_sketch_updates_label() {
        let (mut document, id) = doc_with_sketch();
        let Ok(()) = document.rename_sketch(id, "Profile") else {
            panic!("rename should accept non-empty label");
        };
        assert_eq!(document.sketch_label(id), Some("Profile"));
    }

    #[test]
    fn rename_sketch_trims_whitespace() {
        let (mut document, id) = doc_with_sketch();
        let Ok(()) = document.rename_sketch(id, "  Base \n") else {
            panic!("rename should trim and accept");
        };
        assert_eq!(document.sketch_label(id), Some("Base"));
    }

    #[test]
    fn rename_sketch_rejects_empty_label() {
        let (mut document, id) = doc_with_sketch();
        let Err(err) = document.rename_sketch(id, "   ") else {
            panic!("whitespace-only label must be rejected");
        };
        assert_eq!(err, RenameSketchError::EmptyLabel);
        assert_eq!(document.sketch_label(id), Some("Sketch1"));
    }

    #[test]
    fn insert_extrude_resolves_through_feature_node() {
        let (mut document, sketch) = doc_with_sketch();
        let extrude = ExtrudeId::default();
        let feature = sample_blind_extrude(sketch);
        document.insert_extrude(extrude, feature);

        let Some(extrude_feature) = document.feature_tree().feature_of_extrude(extrude) else {
            panic!("extrude node present after insert");
        };
        assert_eq!(document.extrude_of_feature(extrude_feature), Some(&feature));
        assert_eq!(document.sketch_of_feature(extrude_feature), None);
    }

    #[test]
    fn rename_sketch_rejects_unknown_id() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let stranger = SketchId::default();
        let Err(err) = document.rename_sketch(stranger, "Profile") else {
            panic!("unknown sketch must be rejected");
        };
        assert_eq!(err, RenameSketchError::UnknownSketch(stranger));
    }

    #[test]
    fn remove_extrude_resolves_to_none_afterward() {
        let (mut document, sketch) = doc_with_sketch();
        let extrude = ExtrudeId::default();
        document.insert_extrude(extrude, sample_blind_extrude(sketch));
        let Some(feature) = document.feature_tree().feature_of_extrude(extrude) else {
            panic!("extrude node present after insert");
        };
        assert!(document.extrude_of_feature(feature).is_some());

        let removed = document.remove_extrude(extrude);

        assert_eq!(removed, Some(sample_blind_extrude(sketch)));
        assert_eq!(document.feature_tree().feature_of_extrude(extrude), None);
    }

    #[test]
    fn remove_sketch_cascades_dependent_extrude() {
        let (mut document, sketch) = doc_with_sketch();
        let extrude = ExtrudeId::default();
        document.insert_extrude(extrude, sample_blind_extrude(sketch));
        assert_eq!(document.feature_tree().edges().len(), 1);

        document.remove_sketch(sketch);

        assert_eq!(document.feature_tree().feature_of_extrude(extrude), None);
        assert!(document.feature_tree().edges().is_empty());
    }

    #[test]
    fn insert_sketch_backlinks_pending_extrude() {
        let mut document = Document::new(DocumentId::default(), "doc".to_owned());
        let sketch = SketchId::default();
        let extrude = ExtrudeId::default();
        document.insert_extrude(extrude, sample_blind_extrude(sketch));
        assert!(document.feature_tree().edges().is_empty());

        document.insert_sketch(sketch, "Sketch1".to_owned(), Sketch::new(xy_basis()));

        let Some(sketch_feature) = document.feature_tree().feature_of_sketch(sketch) else {
            panic!("sketch node present after insert");
        };
        let Some(extrude_feature) = document.feature_tree().feature_of_extrude(extrude) else {
            panic!("extrude node present");
        };
        assert_eq!(
            document.feature_tree().edges(),
            &[super::FeatureEdge::SketchToExtrude {
                sketch: sketch_feature,
                extrude: extrude_feature,
            }]
        );
    }

    #[test]
    fn commit_extrude_assigns_incrementing_default_labels() {
        let (mut document, sketch) = doc_with_sketch();
        let first = document.commit_extrude(sample_blind_extrude(sketch));
        let second = document.commit_extrude(sample_blind_extrude(sketch));
        assert_ne!(first, second);
        assert_eq!(document.extrude_label(first), Some("Extrude1"));
        assert_eq!(document.extrude_label(second), Some("Extrude2"));
    }

    #[test]
    fn rename_extrude_updates_and_trims_label() {
        let (mut document, sketch) = doc_with_sketch();
        let id = document.commit_extrude(sample_blind_extrude(sketch));
        let Ok(()) = document.rename_extrude(id, "  Boss \n") else {
            panic!("rename should trim and accept");
        };
        assert_eq!(document.extrude_label(id), Some("Boss"));
    }

    #[test]
    fn rename_extrude_rejects_empty_and_unknown() {
        let (mut document, sketch) = doc_with_sketch();
        let id = document.commit_extrude(sample_blind_extrude(sketch));
        assert_eq!(
            document.rename_extrude(id, "   "),
            Err(RenameExtrudeError::EmptyLabel)
        );
        assert_eq!(document.extrude_label(id), Some("Extrude1"));
        let stranger = ExtrudeId::default();
        assert_eq!(
            document.rename_extrude(stranger, "Boss"),
            Err(RenameExtrudeError::UnknownExtrude(stranger))
        );
    }

    #[test]
    fn remove_extrude_drops_label() {
        let (mut document, sketch) = doc_with_sketch();
        let id = document.commit_extrude(sample_blind_extrude(sketch));
        document.remove_extrude(id);
        assert_eq!(document.extrude_label(id), None);
    }

    #[test]
    fn commit_after_removing_earlier_extrude_keeps_labels_unique() {
        let (mut document, sketch) = doc_with_sketch();
        let first = document.commit_extrude(sample_blind_extrude(sketch));
        let second = document.commit_extrude(sample_blind_extrude(sketch));
        document.remove_extrude(first);
        let third = document.commit_extrude(sample_blind_extrude(sketch));
        assert_eq!(document.extrude_label(second), Some("Extrude2"));
        assert_eq!(document.extrude_label(third), Some("Extrude3"));
        assert_ne!(
            document.extrude_label(second),
            document.extrude_label(third),
            "default labels track the id, so remove + re-commit cannot collide",
        );
    }

    #[test]
    fn reinsert_existing_extrude_preserves_renamed_label() {
        let (mut document, sketch) = doc_with_sketch();
        let id = document.commit_extrude(sample_blind_extrude(sketch));
        let Ok(()) = document.rename_extrude(id, "Boss") else {
            panic!("rename accepts");
        };
        document.insert_extrude(id, sample_blind_extrude(sketch));
        assert_eq!(document.extrude_label(id), Some("Boss"));
    }
}
