use crate::KernelError;
use bone_types::dimensioned_serde;
use bone_types::{
    Angle, FaceLabel, FeatureId, Length, PositiveLength, SketchEntityId, SketchId, UnitVec3,
    VertexLabel, 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, Serialize, Deserialize)]
pub enum PlaneRef {
    DatumPlane(FeatureId),
    PlanarFace(FaceLabel),
}

#[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,
    },
    ThroughAll,
    ThroughAllBoth,
    MidPlane {
        depth: PositiveLength,
    },
    UpToNext,
    UpToVertex {
        vertex: VertexLabel,
    },
    UpToSurface {
        surface: FaceLabel,
    },
    OffsetFromSurface {
        surface: FaceLabel,
        #[serde(with = "dimensioned_serde::length_si")]
        offset: Length,
        #[serde(default)]
        reverse_offset: bool,
        #[serde(default)]
        translate_surface: bool,
    },
    UpToBody {
        body: FeatureId,
    },
}

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

    #[must_use]
    pub fn signed_tangent(self) -> f64 {
        let magnitude = self.angle.get().get::<radian>().tan();
        match self.direction {
            DraftDirection::Outward => magnitude,
            DraftDirection::Inward => -magnitude,
        }
    }
}

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

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ThinWall {
    pub thickness: PositiveLength,
    pub direction: ThinWallDirection,
    #[serde(default)]
    pub thickness2: Option<PositiveLength>,
    #[serde(default)]
    pub cap_ends: bool,
    #[serde(default)]
    pub auto_fillet: Option<PositiveLength>,
}

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

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ExtrudeOperation {
    Boss(MergeResult),
    Cut,
}

impl Default for ExtrudeOperation {
    fn default() -> Self {
        Self::Boss(MergeResult::Merge)
    }
}

impl ExtrudeOperation {
    #[must_use]
    pub const fn is_cut(self) -> bool {
        matches!(self, Self::Cut)
    }
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum FromCondition {
    #[default]
    SketchPlane,
    Offset {
        #[serde(with = "dimensioned_serde::length_si")]
        distance: Length,
    },
    Surface {
        surface: FaceLabel,
    },
    Vertex {
        vertex: VertexLabel,
    },
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Direction2 {
    pub end_condition: ExtrudeEndCondition,
    #[serde(default)]
    pub draft: Option<DraftAngle>,
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum FeatureScope {
    AllBodies,
    #[default]
    AutoSelect,
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ContourSelection {
    #[default]
    All,
    Contour(SketchEntityId),
}

#[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 from: FromCondition,
    #[serde(default)]
    pub direction2: Option<Direction2>,
    #[serde(default)]
    pub draft: Option<DraftAngle>,
    #[serde(default)]
    pub thin_wall: Option<ThinWall>,
    #[serde(default)]
    pub flip_side: bool,
    #[serde(default)]
    pub operation: ExtrudeOperation,
    #[serde(default)]
    pub scope: FeatureScope,
    #[serde(default)]
    pub contours: ContourSelection,
}

impl ExtrudeFeature {
    #[must_use]
    pub fn blind(sketch: SketchId, depth: PositiveLength) -> Self {
        Self {
            sketch,
            direction: ExtrudeDirection::Normal {
                sense: ExtrudeSense::Forward,
            },
            end_condition: ExtrudeEndCondition::Blind { depth },
            from: FromCondition::SketchPlane,
            direction2: None,
            draft: None,
            thin_wall: None,
            flip_side: false,
            operation: ExtrudeOperation::Boss(MergeResult::Merge),
            scope: FeatureScope::AutoSelect,
            contours: ContourSelection::All,
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct DirectionPlan {
    pub distance_mm: f64,
    pub draft: Option<DraftAngle>,
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ThinPlan {
    pub outer_mm: f64,
    pub inner_mm: f64,
    pub cap_ends: bool,
    pub auto_fillet_mm: Option<f64>,
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ResolvedSweep {
    pub base_offset_mm: f64,
    pub forward: Option<DirectionPlan>,
    pub backward: Option<DirectionPlan>,
    pub thin: Option<ThinPlan>,
}

#[cfg(test)]
mod tests {
    use super::{
        DraftAngle, DraftDirection, DraftMagnitude, ExtrudeDirection, ExtrudeEndCondition,
        ExtrudeFeature, ExtrudeOperation, ExtrudeSense, FromCondition, MergeResult, ThinWall,
        ThinWallDirection,
    };
    use bone_types::{Angle, 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),
            },
            from: FromCondition::SketchPlane,
            direction2: None,
            draft: Some(DraftAngle::new(draft_deg(3.0), DraftDirection::Outward)),
            thin_wall: Some(ThinWall {
                thickness: pos_mm(2.0),
                direction: ThinWallDirection::Outward,
                thickness2: None,
                cap_ends: false,
                auto_fillet: None,
            }),
            flip_side: false,
            operation: ExtrudeOperation::default(),
            scope: super::FeatureScope::default(),
            contours: super::ContourSelection::All,
        }
    }

    #[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.direction2, None);
        assert_eq!(feature.from, FromCondition::SketchPlane);
        assert!(!feature.flip_side);
        assert_eq!(
            feature.operation,
            ExtrudeOperation::Boss(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 signed_tangent_follows_direction() {
        let outward = DraftAngle::new(draft_deg(45.0), DraftDirection::Outward);
        let inward = DraftAngle::new(draft_deg(45.0), DraftDirection::Inward);
        assert!((outward.signed_tangent() - 1.0).abs() < 1.0e-9);
        assert!((inward.signed_tangent() + 1.0).abs() < 1.0e-9);
    }
}
