use crate::KernelError;
use bone_types::dimensioned_serde;
use bone_types::{
    Angle, BodyId, BrepFaceId, BrepVertexId, FeatureId, Length, PositiveLength, SketchId, UnitVec3,
    degree, radian,
};
use serde::{Deserialize, Serialize};

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ExtrudeSense {
    Forward,
    Reverse,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PlaneRef {
    DatumPlane(FeatureId),
    PlanarFace(BrepFaceId),
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum ExtrudeDirection {
    Normal { sense: ExtrudeSense },
    AlongAxis(UnitVec3),
    BetweenReferences { from: PlaneRef, to: PlaneRef },
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum ExtrudeEndCondition {
    Blind {
        depth: PositiveLength,
    },
    MidPlane {
        depth: PositiveLength,
    },
    ThroughAll,
    UpToNext,
    UpToVertex {
        vertex: BrepVertexId,
    },
    UpToSurface {
        face: BrepFaceId,
    },
    OffsetFromSurface {
        face: BrepFaceId,
        #[serde(with = "dimensioned_serde::length_si")]
        offset: Length,
    },
    UpToBody {
        body: BodyId,
    },
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DraftDirection {
    Inward,
    Outward,
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "f64", into = "f64")]
pub struct DraftMagnitude(Angle);

impl DraftMagnitude {
    pub fn new(angle: Angle) -> Result<Self, KernelError> {
        let degrees = angle.get::<degree>();
        if (0.0..90.0).contains(&degrees) {
            Ok(Self(angle))
        } else {
            Err(KernelError::DraftAngleOutOfRange(degrees))
        }
    }

    #[must_use]
    pub fn get(self) -> Angle {
        self.0
    }
}

impl From<DraftMagnitude> for f64 {
    fn from(value: DraftMagnitude) -> Self {
        value.0.get::<radian>()
    }
}

impl TryFrom<f64> for DraftMagnitude {
    type Error = KernelError;

    fn try_from(value: f64) -> Result<Self, Self::Error> {
        Self::new(Angle::new::<radian>(value))
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DraftAngle {
    angle: DraftMagnitude,
    direction: DraftDirection,
}

impl DraftAngle {
    #[must_use]
    pub const fn new(angle: DraftMagnitude, direction: DraftDirection) -> Self {
        Self { angle, direction }
    }

    #[must_use]
    pub const fn angle(self) -> DraftMagnitude {
        self.angle
    }

    #[must_use]
    pub const fn direction(self) -> DraftDirection {
        self.direction
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ThinWallDirection {
    Inward,
    Outward,
    MidPlane,
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ThinWall {
    pub thickness: PositiveLength,
    pub direction: ThinWallDirection,
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MergeResult {
    #[default]
    Merge,
    Separate,
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExtrudeFeature {
    pub sketch: SketchId,
    pub direction: ExtrudeDirection,
    pub end_condition: ExtrudeEndCondition,
    #[serde(default)]
    pub draft: Option<DraftAngle>,
    #[serde(default)]
    pub thin_wall: Option<ThinWall>,
    #[serde(default)]
    pub merge_result: MergeResult,
}

#[cfg(test)]
mod tests {
    use super::{
        DraftAngle, DraftDirection, DraftMagnitude, ExtrudeDirection, ExtrudeEndCondition,
        ExtrudeFeature, ExtrudeSense, MergeResult, ThinWall, ThinWallDirection,
    };
    use bone_types::{
        Angle, BrepFaceId, Length, PositiveLength, SketchId, degree, millimeter, radian,
    };
    use slotmap::Key;
    use uom::si::length::meter;

    fn pos_mm(value: f64) -> PositiveLength {
        let Ok(length) = PositiveLength::new(Length::new::<millimeter>(value)) else {
            panic!("{value} mm is a positive length");
        };
        length
    }

    fn draft_deg(value: f64) -> DraftMagnitude {
        let Ok(magnitude) = DraftMagnitude::new(Angle::new::<degree>(value)) else {
            panic!("{value} deg is a valid draft angle");
        };
        magnitude
    }

    fn sample() -> ExtrudeFeature {
        ExtrudeFeature {
            sketch: SketchId::null(),
            direction: ExtrudeDirection::Normal {
                sense: ExtrudeSense::Forward,
            },
            end_condition: ExtrudeEndCondition::Blind {
                depth: pos_mm(10.0),
            },
            draft: Some(DraftAngle::new(draft_deg(3.0), DraftDirection::Outward)),
            thin_wall: Some(ThinWall {
                thickness: pos_mm(2.0),
                direction: ThinWallDirection::Outward,
            }),
            merge_result: MergeResult::default(),
        }
    }

    #[test]
    fn extrude_feature_ron_round_trip() {
        let feature = sample();
        let Ok(text) = ron::to_string(&feature) else {
            panic!("serialize extrude feature");
        };
        let Ok(back) = ron::from_str::<ExtrudeFeature>(&text) else {
            panic!("deserialize extrude feature");
        };
        assert_eq!(feature, back);
    }

    #[test]
    fn blind_depth_serializes_in_meters() {
        let blind = ExtrudeEndCondition::Blind {
            depth: pos_mm(10.0),
        };
        let Ok(text) = ron::to_string(&blind) else {
            panic!("serialize end condition");
        };
        let Ok(back) = ron::from_str::<ExtrudeEndCondition>(&text) else {
            panic!("deserialize end condition");
        };
        let ExtrudeEndCondition::Blind { depth } = back else {
            panic!("round-trips back to a blind depth");
        };
        assert!((depth.get().get::<meter>() - 0.01).abs() < f64::EPSILON);
        assert!(text.contains("0.01"));
    }

    #[test]
    fn end_condition_rejects_unknown_field() {
        let blind = ExtrudeEndCondition::Blind { depth: pos_mm(5.0) };
        let Ok(text) = ron::to_string(&blind) else {
            panic!("serialize end condition");
        };
        let Some(idx) = text.find("Blind(") else {
            panic!("expected Blind variant in {text}");
        };
        let mut munged = text.clone();
        munged.insert_str(idx + "Blind(".len(), "bogus:42,");
        assert!(ron::from_str::<ExtrudeEndCondition>(&munged).is_err());
    }

    #[test]
    fn blind_rejects_non_positive_depth() {
        assert!(ron::from_str::<ExtrudeEndCondition>("Blind(depth:0.0)").is_err());
        assert!(ron::from_str::<ExtrudeEndCondition>("Blind(depth:-1.0)").is_err());
        assert!(ron::from_str::<ExtrudeEndCondition>("Blind(depth:inf)").is_err());
        assert!(ron::from_str::<ExtrudeEndCondition>("Blind(depth:-inf)").is_err());
        assert!(ron::from_str::<ExtrudeEndCondition>("Blind(depth:NaN)").is_err());
    }

    #[test]
    fn feature_fills_defaults() {
        let Ok(sketch) = ron::to_string(&SketchId::null()) else {
            panic!("serialize sketch id");
        };
        let text = format!(
            "(sketch:{sketch},direction:Normal(sense:Forward),end_condition:Blind(depth:0.01))"
        );
        let Ok(feature) = ron::from_str::<ExtrudeFeature>(&text) else {
            panic!("deserialize feature with a defaulted tail in {text}");
        };
        assert_eq!(feature.draft, None);
        assert_eq!(feature.thin_wall, None);
        assert_eq!(feature.merge_result, MergeResult::Merge);
    }

    #[test]
    fn draft_angle_serializes_in_radians() {
        let Ok(magnitude) = DraftMagnitude::new(Angle::new::<radian>(0.5)) else {
            panic!("0.5 rad is a valid draft angle");
        };
        let draft = DraftAngle::new(magnitude, DraftDirection::Inward);
        let Ok(text) = ron::to_string(&draft) else {
            panic!("serialize draft angle");
        };
        let Ok(back) = ron::from_str::<DraftAngle>(&text) else {
            panic!("deserialize draft angle");
        };
        assert_eq!(draft, back);
        assert!((back.angle().get().get::<radian>() - 0.5).abs() < f64::EPSILON);
        assert_eq!(back.direction(), DraftDirection::Inward);
        assert!(text.contains("0.5"));
    }

    #[test]
    fn draft_magnitude_rejects_out_of_range() {
        assert!(DraftMagnitude::new(Angle::new::<degree>(0.0)).is_ok());
        assert!(DraftMagnitude::new(Angle::new::<degree>(45.0)).is_ok());
        assert!(DraftMagnitude::new(Angle::new::<degree>(-5.0)).is_err());
        assert!(DraftMagnitude::new(Angle::new::<degree>(90.0)).is_err());
        assert!(DraftMagnitude::new(Angle::new::<degree>(200.0)).is_err());
        assert!(DraftMagnitude::new(Angle::new::<radian>(f64::INFINITY)).is_err());
        assert!(DraftMagnitude::new(Angle::new::<radian>(f64::NAN)).is_err());
    }

    #[test]
    fn draft_angle_deserialize_rejects_out_of_range() {
        assert!(ron::from_str::<DraftAngle>("(angle:0.5,direction:Outward)").is_ok());
        assert!(ron::from_str::<DraftAngle>("(angle:3.5,direction:Outward)").is_err());
    }

    #[test]
    fn offset_from_surface_allows_negative_offset() {
        let condition = ExtrudeEndCondition::OffsetFromSurface {
            face: BrepFaceId::null(),
            offset: Length::new::<meter>(-0.005),
        };
        let Ok(text) = ron::to_string(&condition) else {
            panic!("serialize offset end condition");
        };
        let Ok(back) = ron::from_str::<ExtrudeEndCondition>(&text) else {
            panic!("deserialize offset end condition");
        };
        assert_eq!(condition, back);
        assert!(text.contains("-0.005"));
    }
}
