// Reference: https://github.com/PistonDevelopers/interpolation
use std::f32::consts::PI;

#[derive(Clone, Copy)]
pub enum Ease {
    BackIn,
    BackInOut,
    BackOut,
    BounceIn,
    BounceInOut,
    BounceOut,
    ElasticIn,
    ElasticInOut,
    ElasticOut,
    ExpoIn,
    ExpoInOut,
    ExpoOut,
    Linear,
    PowerIn,
    PowerInOut,
    PowerOut,
}

fn clamp(p: f32) -> f32 {
    match () {
        _ if p > 1.0 => 1.0,
        _ if p < 0.0 => 0.0,
        _ => p,
    }
}

pub trait Delta {
    fn delta(self, ease: Ease) -> Self;
}
impl Delta for f32 {
    fn delta(self, ease: Ease) -> f32 {
        match ease {
            Ease::BackIn => {
                let p = clamp(self);
                p * p * p - p * (p * PI).sin()
            }
            Ease::BackInOut => {
                let p = clamp(self);
                if p < 0.5 {
                    let f = 2.0 * p;
                    0.5 * (f * f * f - f * (f * PI).sin())
                } else {
                    let f = 1.0 - (2.0 * p - 1.0);
                    0.5 * (1.0 - (f * f * f - f * (f * PI).sin())) + 0.5
                }
            }
            Ease::BackOut => {
                let p = clamp(self);
                let f = 1.0 - p;
                1.0 - (f * f * f - f * (f * PI).sin())
            }
            Ease::BounceIn => {
                let p = clamp(self);
                1.0 - (1.0 - p).delta(Ease::BounceOut)
            }
            Ease::BounceInOut => {
                let p = clamp(self);
                if p < 0.5 {
                    0.5 * (p * 2.0).delta(Ease::BounceIn)
                } else {
                    0.5 * (p * 2.0 - 1.0).delta(Ease::BounceOut) + 0.5
                }
            }
            Ease::BounceOut => {
                let p = clamp(self);
                if p < 4.0 / 11.0 {
                    (121.0 * p * p) / 16.0
                } else if p < 8.0 / 11.0 {
                    (363.0 / 40.0 * p * p) - (99.0 / 10.0 * p) + 17.0 / 5.0
                } else if p < 9.0 / 10.0 {
                    (4356.0 / 361.0 * p * p) - (35442.0 / 1805.0 * p) + 16061.0 / 1805.0
                } else {
                    (54.0 / 5.0 * p * p) - (513.0 / 25.0 * p) + 268.0 / 25.0
                }
            }
            Ease::ElasticIn => {
                let p = clamp(self);
                (13.0 * PI * 2.0 * p).sin() * (2.0_f32).powf(10.0 * (p - 1.0))
            }
            Ease::ElasticInOut => {
                let p = clamp(self);
                if p < 0.5 {
                    0.5 * (13.0 * PI * 2.0 * (2.0 * p)).sin()
                        * (2.0_f32).powf(10.0 * ((2.0 * p) - 1.0))
                } else {
                    0.5 * ((-13.0 * PI * 2.0 * ((2.0 * p - 1.0) + 1.0)).sin()
                        * (2.0_f32).powf(-10.0 * (2.0 * p - 1.0))
                        + 2.0)
                }
            }
            Ease::ElasticOut => {
                let p = clamp(self);
                (-13.0 * PI * 2.0 * (p + 1.0)).sin() * (2.0_f32).powf(-10.0 * p) + 1.0
            }
            Ease::ExpoIn => {
                if self <= 0.0 {
                    0.0
                } else {
                    2.0_f32.powf(10.0 * (self.min(1.0) - 1.0))
                }
            }
            Ease::ExpoInOut => {
                if self <= 0.0 {
                    return 0.0;
                }
                if self >= 1.0 {
                    return 1.0;
                }
                if self < 0.5 {
                    0.5 * (2.0_f32).powf((20.0 * self) - 10.0)
                } else {
                    -0.5 * (2.0_f32).powf((-20.0 * self) + 10.0) + 1.0
                }
            }
            Ease::ExpoOut => {
                if self >= 1.0 {
                    1.0
                } else {
                    1.0 - (2.0_f32).powf(-10.0 * self.max(0.0))
                }
            }
            Ease::Linear => clamp(self),
            Ease::PowerIn => clamp(self).powf(2.0),
            Ease::PowerInOut => {
                let p = clamp(self);
                if p < 0.5 {
                    2.0 * p * p
                } else {
                    (-2.0 * p * p) + (4.0 * p) - 1.0
                }
            }
            Ease::PowerOut => -(clamp(self) * (clamp(self) - 2.0)),
        }
    }
}

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

    #[test]
    fn back() {
        assert_eq!(0.0.delta(Ease::BackIn), 0.0);
        assert_eq!(0.5.delta(Ease::BackIn), -0.375);
        assert_eq!(1.0.delta(Ease::BackIn), 1.0000001);
        assert_eq!(0.0.delta(Ease::BackInOut), 0.0);
        assert_eq!(0.4.delta(Ease::BackInOut), 0.02088593);
        assert_eq!(0.5.delta(Ease::BackInOut), 0.49999994);
        assert_eq!(0.6.delta(Ease::BackInOut), 0.9791142);
        assert_eq!(1.0.delta(Ease::BackInOut), 1.0);
        assert_eq!(0.0.delta(Ease::BackOut), -1.1920929e-7);
        assert_eq!(0.5.delta(Ease::BackOut), 1.375);
        assert_eq!(1.0.delta(Ease::BackOut), 1.0);
    }

    #[test]
    fn bounce() {
        assert_eq!(0.0.delta(Ease::BounceIn), 0.0);
        assert_eq!(0.5.delta(Ease::BounceIn), 0.28124976);
        assert_eq!(1.0.delta(Ease::BounceIn), 1.0);
        assert_eq!(0.0.delta(Ease::BounceInOut), 0.0);
        assert_eq!(0.4.delta(Ease::BounceInOut), 0.34875);
        assert_eq!(0.5.delta(Ease::BounceInOut), 0.5);
        assert_eq!(0.6.delta(Ease::BounceInOut), 0.65125006);
        assert_eq!(1.0.delta(Ease::BounceInOut), 1.0);
        assert_eq!(0.0.delta(Ease::BounceOut), 0.0);
        assert_eq!(0.5.delta(Ease::BounceOut), 0.71875024);
        assert_eq!(1.0.delta(Ease::BounceOut), 1.0);
    }

    #[test]
    fn elastic() {
        assert_eq!(0.0.delta(Ease::ElasticIn), 0.0);
        assert_eq!(0.5.delta(Ease::ElasticIn), -4.2966086e-8);
        assert_eq!(1.0.delta(Ease::ElasticIn), 2.7498295e-6);
        assert_eq!(0.0.delta(Ease::ElasticInOut), 0.0);
        assert_eq!(0.4.delta(Ease::ElasticInOut), 0.07347279);
        assert_eq!(0.5.delta(Ease::ElasticInOut), 0.9999986);
        assert_eq!(0.6.delta(Ease::ElasticInOut), 1.073474);
        assert_eq!(1.0.delta(Ease::ElasticInOut), 1.0);
        assert_eq!(0.0.delta(Ease::ElasticOut), 0.99999726);
        assert_eq!(0.5.delta(Ease::ElasticOut), 1.0000001);
        assert_eq!(1.0.delta(Ease::ElasticOut), 1.0);
    }

    #[test]
    fn expo() {
        assert_eq!(0.0.delta(Ease::ExpoIn), 0.0);
        assert_eq!(0.5.delta(Ease::ExpoIn), 0.03125);
        assert_eq!(1.0.delta(Ease::ExpoIn), 1.0);
        assert_eq!(0.0.delta(Ease::ExpoInOut), 0.0);
        assert_eq!(0.4.delta(Ease::ExpoInOut), 0.125);
        assert_eq!(0.5.delta(Ease::ExpoInOut), 0.5);
        assert_eq!(0.6.delta(Ease::ExpoInOut), 0.875);
        assert_eq!(1.0.delta(Ease::ExpoInOut), 1.0);
        assert_eq!(0.0.delta(Ease::ExpoOut), 0.0);
        assert_eq!(0.5.delta(Ease::ExpoOut), 0.96875);
        assert_eq!(1.0.delta(Ease::ExpoOut), 1.0);
    }

    #[test]
    fn linear() {
        assert_eq!(0.0.delta(Ease::Linear), 0.0);
        assert_eq!(0.5.delta(Ease::Linear), 0.5);
        assert_eq!(1.0.delta(Ease::Linear), 1.0);
    }

    #[test]
    fn power() {
        assert_eq!(0.0.delta(Ease::PowerIn), 0.0);
        assert_eq!(0.5.delta(Ease::PowerIn), 0.25);
        assert_eq!(1.0.delta(Ease::PowerIn), 1.0);
        assert_eq!(0.0.delta(Ease::PowerInOut), 0.0);
        assert_eq!(0.4.delta(Ease::PowerInOut), 0.32000002);
        assert_eq!(0.5.delta(Ease::PowerInOut), 0.5);
        assert_eq!(0.6.delta(Ease::PowerInOut), 0.68000007);
        assert_eq!(1.0.delta(Ease::PowerInOut), 1.0);
        assert_eq!(0.0.delta(Ease::PowerOut), 0.0);
        assert_eq!(0.5.delta(Ease::PowerOut), 0.75);
        assert_eq!(1.0.delta(Ease::PowerOut), 1.0);
    }
}
