use std::collections::BTreeMap;

use bone_kernel::ExtrudeFeature;
use bone_types::{DocumentId, ExtrudeId, FeatureId, SchemaHeader, SketchId};
use serde::{Deserialize, Serialize};

use crate::Sketch;

pub mod feature_tree;

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

#[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(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(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub extrudes: BTreeMap<ExtrudeId, ExtrudeFeature>,
}

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

    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)]
pub struct Document {
    header: DocumentHeader,
    sketches: BTreeMap<SketchId, Sketch>,
}

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

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

    #[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);
    }

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

    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(_) => 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(_) => None,
        }
    }
}

#[must_use]
pub fn sketch_filename(id: SketchId) -> String {
    use slotmap::Key;
    format!("{:016x}.ron", id.data().as_ffi())
}

#[cfg(test)]
mod tests {
    use super::feature_tree::sample_blind_extrude;
    use super::{Document, 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,
            }]
        );
    }
}
