use bone_kernel::{BrepReattach, BrepSolid};
use bone_types::SolidKey;
use serde::{Deserialize, Serialize};

use super::ron_io::{self, RonError};

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LabelSidecar {
    solid: SolidKey,
    reattach: BrepReattach,
}

impl LabelSidecar {
    #[must_use]
    pub fn capture(solid: &BrepSolid) -> Self {
        Self {
            solid: solid.content_key(),
            reattach: solid.reattach_data().clone(),
        }
    }

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

    #[must_use]
    pub fn reattach(&self) -> &BrepReattach {
        &self.reattach
    }

    #[must_use]
    pub fn matches(&self, key: SolidKey) -> bool {
        self.solid == key
    }

    pub fn to_ron(&self) -> Result<String, RonError> {
        ron_io::to_string(self)
    }

    pub fn from_ron(text: &str) -> Result<Self, RonError> {
        ron_io::from_str(text)
    }
}

#[cfg(test)]
mod tests {
    use super::LabelSidecar;
    use bone_types::SolidKey;

    fn cube() -> bone_kernel::BrepSolid {
        use bone_kernel::{
            Curve2Kind, ExtrudeFeature, ExtrudeProfile, Line2, ProfileEdge, ProfileLoop,
            evaluate_extrude,
        };
        use bone_types::{
            FeatureId, Length, Plane3, Point2, PositiveLength, SketchEntityId, SketchId, Tolerance,
            UnitVec3, millimeter,
        };
        use slotmap::{Key, SlotMap};

        let tol = Tolerance::new(1.0e-9);
        let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
        let mut features: SlotMap<FeatureId, ()> = SlotMap::with_key();
        let point = |x: f64, y: f64| Point2::from_mm(x, y);
        let line = |a: Point2, b: Point2| {
            let Ok(segment) = Line2::new(a, b, tol) else {
                panic!("distinct endpoints");
            };
            Curve2Kind::Line(segment)
        };
        let corners = [
            point(0.0, 0.0),
            point(2.0, 0.0),
            point(2.0, 3.0),
            point(0.0, 3.0),
        ];
        let edges = (0..4)
            .map(|index| {
                ProfileEdge::new(
                    line(corners[index], corners[(index + 1) % 4]),
                    entities.insert(()),
                    entities.insert(()),
                )
            })
            .collect();
        let Ok(plane) = Plane3::new(
            bone_types::Point3::origin(),
            UnitVec3::x_axis(),
            UnitVec3::y_axis(),
            tol,
        ) else {
            panic!("orthonormal axes");
        };
        let profile = ExtrudeProfile::new(plane, vec![ProfileLoop::Open(edges)]);
        let Ok(depth) = PositiveLength::new(Length::new::<millimeter>(5.0)) else {
            panic!("positive depth");
        };
        let feature = ExtrudeFeature::blind(SketchId::null(), depth);
        let Ok(solid) = evaluate_extrude(features.insert(()), &profile, &feature) else {
            panic!("cube extrudes");
        };
        solid
    }

    #[test]
    fn ron_round_trips() {
        let solid = cube();
        let sidecar = LabelSidecar::capture(&solid);
        let Ok(text) = sidecar.to_ron() else {
            panic!("serialize sidecar");
        };
        let Ok(back) = LabelSidecar::from_ron(&text) else {
            panic!("deserialize sidecar");
        };
        assert_eq!(sidecar, back);
    }

    #[test]
    fn matches_only_its_own_solid() {
        let solid = cube();
        let sidecar = LabelSidecar::capture(&solid);
        assert!(sidecar.matches(solid.content_key()));
        assert!(!sidecar.matches(SolidKey::from_bytes([0u8; 16])));
    }

    #[test]
    fn reattach_payload_round_trips_a_solid() {
        let solid = cube();
        let sidecar = LabelSidecar::capture(&solid);
        let Ok(blob) = solid.to_blob() else {
            panic!("blob serializes");
        };
        let Ok(restored) = bone_kernel::BrepSolid::from_blob(&blob, sidecar.reattach()) else {
            panic!("sidecar reattaches");
        };
        assert_eq!(sidecar.solid(), restored.content_key());
    }
}
