use crate::extrude::{ExtrudeOperation, FeatureScope, ThinWall};
use crate::revolve::RevolveAxis;
use bone_types::dimensioned_serde;
use bone_types::{Angle, BodyRef, EdgeLabel, PositiveLength, SketchId, radian};
use serde::{Deserialize, Serialize};

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum SweepPath {
    Sketch(SketchId),
    Edge(EdgeLabel),
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum SweepProfileKind {
    #[default]
    Sketch,
    Circular {
        diameter: PositiveLength,
    },
    Solid {
        tool: BodyRef,
    },
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SweepOrientation {
    #[default]
    FollowPath,
    KeepNormalConstant,
    FollowPathAndFirstGuide,
    FollowFirstAndSecondGuide,
}

impl SweepOrientation {
    #[must_use]
    pub fn needs_guides(self) -> usize {
        match self {
            Self::FollowPath | Self::KeepNormalConstant => 0,
            Self::FollowPathAndFirstGuide => 1,
            Self::FollowFirstAndSecondGuide => 2,
        }
    }
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SweepDirection {
    #[default]
    DirectionOne,
    DirectionTwo,
    Bidirectional,
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SweepTangency {
    #[default]
    None,
    PathTangent,
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub enum SweepAlignment {
    #[default]
    MinimumTwist,
    NonePlanar,
    DirectionVector {
        axis: RevolveAxis,
    },
    AllFaces,
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TwistUnit {
    #[default]
    Degrees,
    Radians,
    Revolutions,
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SweepTwist {
    #[serde(with = "dimensioned_serde::angle_si")]
    pub angle: Angle,
    #[serde(default)]
    pub unit: TwistUnit,
}

impl SweepTwist {
    #[must_use]
    pub fn new(angle: Angle, unit: TwistUnit) -> Self {
        Self { angle, unit }
    }

    #[must_use]
    pub fn radians(self) -> f64 {
        self.angle.get::<radian>()
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SweepFeature {
    pub profile: SketchId,
    pub path: SweepPath,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub guides: Vec<SweepPath>,
    #[serde(default)]
    pub profile_kind: SweepProfileKind,
    #[serde(default)]
    pub orientation: SweepOrientation,
    #[serde(default)]
    pub direction: SweepDirection,
    #[serde(default)]
    pub start_tangency: SweepTangency,
    #[serde(default)]
    pub end_tangency: SweepTangency,
    #[serde(default)]
    pub alignment: SweepAlignment,
    #[serde(default)]
    pub twist: Option<SweepTwist>,
    #[serde(default)]
    pub thin_wall: Option<ThinWall>,
    #[serde(default)]
    pub flip_side: bool,
    #[serde(default)]
    pub operation: ExtrudeOperation,
    #[serde(default)]
    pub scope: FeatureScope,
}

impl SweepFeature {
    #[must_use]
    pub fn boss(profile: SketchId, path: SweepPath) -> Self {
        Self {
            profile,
            path,
            guides: Vec::new(),
            profile_kind: SweepProfileKind::Sketch,
            orientation: SweepOrientation::FollowPath,
            direction: SweepDirection::DirectionOne,
            start_tangency: SweepTangency::None,
            end_tangency: SweepTangency::None,
            alignment: SweepAlignment::MinimumTwist,
            twist: None,
            thin_wall: None,
            flip_side: false,
            operation: ExtrudeOperation::default(),
            scope: FeatureScope::default(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{SweepFeature, SweepOrientation, SweepPath, SweepProfileKind};
    use bone_types::SketchId;
    use slotmap::Key;

    #[test]
    fn sweep_feature_ron_round_trips_variants_and_fills_defaults()
    -> Result<(), Box<dyn std::error::Error>> {
        use bone_types::{Angle, BodyOrdinal, BodyRef, FeatureId, radian};
        let base = || SweepFeature::boss(SketchId::null(), SweepPath::Sketch(SketchId::null()));
        [
            base(),
            SweepFeature {
                orientation: super::SweepOrientation::KeepNormalConstant,
                direction: super::SweepDirection::DirectionTwo,
                twist: Some(super::SweepTwist::new(
                    Angle::new::<radian>(1.25),
                    super::TwistUnit::Revolutions,
                )),
                ..base()
            },
            SweepFeature {
                profile_kind: SweepProfileKind::Solid {
                    tool: BodyRef::new(FeatureId::default(), BodyOrdinal::new(3)),
                },
                operation: crate::extrude::ExtrudeOperation::Cut,
                ..base()
            },
        ]
        .into_iter()
        .try_for_each(|feature| -> Result<(), Box<dyn std::error::Error>> {
            let text = ron::to_string(&feature)?;
            let back = ron::from_str::<SweepFeature>(&text)?;
            assert_eq!(feature, back);
            Ok(())
        })?;

        let profile = ron::to_string(&SketchId::null())?;
        let text = format!("(profile:{profile},path:Sketch({profile}))");
        let defaulted = ron::from_str::<SweepFeature>(&text)?;
        assert_eq!(defaulted.profile_kind, SweepProfileKind::Sketch);
        assert_eq!(defaulted.orientation, SweepOrientation::FollowPath);
        assert_eq!(defaulted.direction, super::SweepDirection::DirectionOne);
        assert_eq!(defaulted.start_tangency, super::SweepTangency::None);
        assert_eq!(defaulted.end_tangency, super::SweepTangency::None);
        assert_eq!(defaulted.alignment, super::SweepAlignment::MinimumTwist);
        assert_eq!(defaulted.twist, None);
        assert_eq!(defaulted.thin_wall, None);
        assert!(!defaulted.flip_side);
        Ok(())
    }
}
