use core::time::Duration;

use bone_types::CubicEasing;
use serde::{Deserialize, Serialize};

#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Easing {
    Standard,
    Accelerate,
    Decelerate,
    Linear,
}

impl Easing {
    #[must_use]
    pub const fn cubic_bezier(self) -> CubicEasing {
        match self {
            Self::Standard => CubicEasing::STANDARD,
            Self::Accelerate => CubicEasing::new_unchecked(0.3, 0.0, 1.0, 1.0),
            Self::Decelerate => CubicEasing::new_unchecked(0.0, 0.0, 0.0, 1.0),
            Self::Linear => CubicEasing::LINEAR,
        }
    }
}

impl core::fmt::Display for Easing {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        let name = match self {
            Self::Standard => "standard",
            Self::Accelerate => "accelerate",
            Self::Decelerate => "decelerate",
            Self::Linear => "linear",
        };
        f.write_str(name)
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MotionToken {
    pub duration: Duration,
    pub easing: Easing,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Motion {
    pub fast: MotionToken,
    pub quick: MotionToken,
    pub normal: MotionToken,
    pub relaxed: MotionToken,
    pub slow: MotionToken,
    pub glacial: MotionToken,
}

impl Motion {
    pub const STANDARD: Self = Self {
        fast: MotionToken {
            duration: Duration::from_millis(80),
            easing: Easing::Standard,
        },
        quick: MotionToken {
            duration: Duration::from_millis(120),
            easing: Easing::Standard,
        },
        normal: MotionToken {
            duration: Duration::from_millis(180),
            easing: Easing::Standard,
        },
        relaxed: MotionToken {
            duration: Duration::from_millis(260),
            easing: Easing::Decelerate,
        },
        slow: MotionToken {
            duration: Duration::from_millis(400),
            easing: Easing::Decelerate,
        },
        glacial: MotionToken {
            duration: Duration::from_millis(700),
            easing: Easing::Decelerate,
        },
    };
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn every_easing_curve_passes_cubic_validation() {
        [
            Easing::Standard,
            Easing::Accelerate,
            Easing::Decelerate,
            Easing::Linear,
        ]
        .into_iter()
        .for_each(|easing| {
            let curve = easing.cubic_bezier();
            let Ok(validated) = CubicEasing::new(curve.x1(), curve.y1(), curve.x2(), curve.y2())
            else {
                panic!("{easing} must define a valid cubic easing");
            };
            assert_eq!(
                validated, curve,
                "{easing} control points must survive validation unchanged",
            );
        });
    }
}
