//! Controls volume transitions for Audio Channels

use std::time::Duration;

/// Faders are attached to channels with initial and target volumes as well as a
/// transition time.
#[derive(PartialEq, Clone, Copy)]
pub struct Fader {
    length: Duration,
    running_time: Duration,
    volume_from: f32,
    volume_to: f32,
    is_running: bool,
}
/// Enables quick lookup of whether a fader is increasing or decreasing the
/// channel volume
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum FadeDirection {
    In,
    Out,
}

fn lerp(t: f32, a: f32, b: f32) -> f32 { (1.0 - t) * a + t * b }

impl Fader {
    pub fn fade(length: Duration, volume_from: f32, volume_to: f32) -> Self {
        Self {
            length,
            running_time: Duration::default(),
            volume_from,
            volume_to,
            is_running: true,
        }
    }

    pub fn fade_in(time: Duration, volume_to: f32) -> Self { Self::fade(time, 0.0, volume_to) }

    pub fn fade_out(time: Duration, volume_from: f32) -> Self { Self::fade(time, volume_from, 0.0) }

    /// Used to update the `target` volume of the fader when the max or min
    /// volume changes. This occurs when the player changes their in-game
    /// volume setting during a fade. Updating the target in this case prevents
    /// the final fade volume from falling outside of the newly configured
    /// volume range.
    pub fn update_target_volume(&mut self, volume: f32) {
        match self.direction() {
            FadeDirection::In => {
                self.volume_to = volume;
            },
            FadeDirection::Out => {
                if self.get_volume() > volume {
                    self.volume_from = volume;
                }
            },
        }
    }

    pub fn direction(&self) -> FadeDirection {
        if self.volume_to < self.volume_from {
            FadeDirection::Out
        } else {
            FadeDirection::In
        }
    }

    /// Called each tick to update the volume and state
    pub fn update(&mut self, dt: std::time::Duration) {
        if self.is_running {
            self.running_time += dt;
            if self.running_time >= self.length {
                self.running_time = self.length;
                self.is_running = false;
            }
        }
    }

    pub fn get_volume(&self) -> f32 {
        lerp(
            self.running_time.as_nanos() as f32 / self.length.as_nanos() as f32,
            self.volume_from,
            self.volume_to,
        )
    }

    pub fn is_finished(&self) -> bool { self.running_time >= self.length || !self.is_running }
}

/// Returns a stopped fader with no running duration
impl Default for Fader {
    fn default() -> Self {
        Self {
            length: Duration::default(),
            running_time: Duration::default(),
            volume_from: 0.0,
            volume_to: 1.0,
            is_running: false,
        }
    }
}

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

    #[test]
    fn fade_direction_in() {
        let fader = Fader::fade_in(Duration::from_secs(10), 0.0);

        assert_eq!(fader.direction(), FadeDirection::In);
    }

    #[test]
    fn fade_direction_out() {
        let fader = Fader::fade_out(Duration::from_secs(10), 1.0);

        assert_eq!(fader.direction(), FadeDirection::Out);
    }

    #[test]
    #[allow(clippy::float_cmp)] // TODO: Pending review in #587
    fn fade_out_completes() {
        let mut fader = Fader::fade_out(Duration::from_secs(10), 1.0);

        // Run for the full duration
        fader.update(Duration::from_secs(10));

        assert_eq!(fader.get_volume(), 0.0);
        assert!(fader.is_finished());
    }

    #[test]
    fn update_target_volume_fading_out_when_currently_above() {
        let mut fader = Fader::fade_out(Duration::from_secs(20), 1.0);

        // After 0.1s, the fader should still be close to 1.0
        fader.update(Duration::from_millis(100));

        // Reduce volume to 0.4. We are currently above that.
        fader.update_target_volume(0.4);

        // The volume should immediately reduce to < 0.4 on the next update
        fader.update(Duration::from_millis(100));

        assert!(fader.get_volume() < 0.4)
    }

    #[test]
    fn update_target_volume_fading_out_when_currently_below() {
        let mut fader = Fader::fade_out(Duration::from_secs(10), 0.8);

        // After 9s, the fader should be close to 0
        fader.update(Duration::from_secs(9));

        // Notify of a volume increase to 1.0. We are already far below that.
        fader.update_target_volume(1.0);

        // The fader should be unaffected by the new value, and continue dropping
        fader.update(Duration::from_millis(100));

        assert!(fader.get_volume() < 0.2);
    }

    #[test]
    #[allow(clippy::float_cmp)] // TODO: Pending review in #587
    fn update_target_volume_fading_in_when_currently_above() {
        let mut fader = Fader::fade_in(Duration::from_secs(10), 1.0);

        // After 9s, the fader should be close to 1.0
        fader.update(Duration::from_secs(9));

        // Reduce volume to 0.4. We are currently above that.
        fader.update_target_volume(0.4);

        // Run out the fader. It's volume should be 0.4
        fader.update(Duration::from_secs(1));

        assert_eq!(fader.get_volume(), 0.4);
    }

    #[test]
    #[allow(clippy::float_cmp)] // TODO: Pending review in #587
    fn update_target_volume_fading_in_when_currently_below() {
        let mut fader = Fader::fade_in(Duration::from_secs(20), 1.0);

        // After 0.1s, the fader should still be close to 0.0
        fader.update(Duration::from_millis(100));

        // Reduce volume to 0.4. The volume_to should be reduced accordingly.
        fader.update_target_volume(0.4);

        assert_eq!(fader.volume_to, 0.4);
    }
}
