use std::collections::BTreeMap;

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

use crate::Sketch;

pub mod feature_tree;

pub use feature_tree::{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,
}

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

#[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(header: DocumentHeader, sketches: BTreeMap<SketchId, Sketch>) -> Self {
        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);
    }

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

    pub fn remove_sketch(&mut self, id: SketchId) -> Option<Sketch> {
        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(_) => 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::{Document, RenameSketchError, Sketch, SketchId};
    use bone_types::{DocumentId, 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 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));
    }
}
