//! Functions and types relating to audio playback.

use std::io::Cursor;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, Weak};
use std::time::Duration;

use rodio::source::{Buffered, Empty};
use rodio::{Decoder, Device, Sample, Source};

use crate::error::{Result, TetraError};
use crate::fs;
use crate::Context;

pub(crate) struct AudioContext {
    device: Option<Device>,
    master_volume: Arc<Mutex<f32>>,
}

impl AudioContext {
    pub(crate) fn new() -> AudioContext {
        let device = rodio::default_output_device();

        if let Some(active_device) = &device {
            rodio::play_raw(&active_device, Empty::new());
        }

        AudioContext {
            device,
            master_volume: Arc::new(Mutex::new(1.0)),
        }
    }
}

/// Sound data that can be played back.
///
/// Supports WAV, Ogg Vorbis, MP3 and FLAC (in other words, everything that
/// [Rodio](https://github.com/tomaka/rodio) provides support for).
///
/// All of the playback methods on this type return a [`SoundInstance`](./struct.SoundInstance.html) that
/// can be used to control the sound after it has started. If you just want
/// to 'fire and forget' a sound, you can discard it - the sound will
/// continue playing regardless.
///
/// This type acts as a lightweight handle to the associated audio data,
/// and so can be cloned with little overhead.
#[derive(Debug, Clone, PartialEq)]
pub struct Sound {
    pub(crate) data: Arc<[u8]>,
}

impl Sound {
    /// Creates a new sound from the given file.
    ///
    /// Note that the data is not decoded until playback begins, so this function will not
    /// validate that the data being read is formatted correctly.
    ///
    /// # Errors
    ///
    /// * `TetraError::FailedToLoadAsset` will be returned if the file could not be loaded.
    pub fn new<P>(path: P) -> Result<Sound>
    where
        P: AsRef<Path>,
    {
        Ok(Sound {
            data: fs::read(path)?.into(),
        })
    }

    /// Creates a new sound from a slice of binary data, encoded in one of Tetra's supported
    /// file formats.
    ///
    /// This is useful in combination with `include_bytes`, as it allows you to include
    /// your audio data directly in the binary.
    ///
    /// Note that the data is not decoded until playback begins, so this function will not
    /// validate that the data being read is formatted correctly.
    pub fn from_file_data(data: &[u8]) -> Sound {
        Sound { data: data.into() }
    }

    /// Plays the sound.
    ///
    /// # Errors
    ///
    /// * `TetraError::NoAudioDevice` will be returned if no audio device is active.
    /// * `TetraError::InvalidSound` will be returned if the sound data could not be decoded.
    pub fn play(&self, ctx: &Context) -> Result<SoundInstance> {
        self.start_source(ctx, true, false, 1.0, 1.0)
    }

    /// Plays the sound repeatedly.
    ///
    /// # Errors
    ///
    /// * `TetraError::NoAudioDevice` will be returned if no audio device is active.
    /// * `TetraError::InvalidSound` will be returned if the sound data could not be decoded.
    pub fn repeat(&self, ctx: &Context) -> Result<SoundInstance> {
        self.start_source(ctx, true, true, 1.0, 1.0)
    }

    /// Spawns a new instance of the sound that is not playing yet.
    ///
    /// # Errors
    ///
    /// * `TetraError::NoAudioDevice` will be returned if no audio device is active.
    /// * `TetraError::InvalidSound` will be returned if the sound data could not be decoded.
    pub fn spawn(&self, ctx: &Context) -> Result<SoundInstance> {
        self.start_source(ctx, false, false, 1.0, 1.0)
    }

    /// Plays the sound, with the provided settings.
    ///
    /// # Errors
    ///
    /// * `TetraError::NoAudioDevice` will be returned if no audio device is active.
    /// * `TetraError::InvalidSound` will be returned if the sound data could not be decoded.
    pub fn play_with(&self, ctx: &Context, volume: f32, speed: f32) -> Result<SoundInstance> {
        self.start_source(ctx, true, false, volume, speed)
    }

    /// Plays the sound repeatedly, with the provided settings.
    ///
    /// # Errors
    ///
    /// * `TetraError::NoAudioDevice` will be returned if no audio device is active.
    /// * `TetraError::InvalidSound` will be returned if the sound data could not be decoded.
    pub fn repeat_with(&self, ctx: &Context, volume: f32, speed: f32) -> Result<SoundInstance> {
        self.start_source(ctx, true, true, volume, speed)
    }

    /// Spawns a new instance of the sound that is not playing yet, with the provided settings.
    ///
    /// # Errors
    ///
    /// * `TetraError::NoAudioDevice` will be returned if no audio device is active.
    /// * `TetraError::InvalidSound` will be returned if the sound data could not be decoded.
    pub fn spawn_with(&self, ctx: &Context, volume: f32, speed: f32) -> Result<SoundInstance> {
        self.start_source(ctx, false, false, volume, speed)
    }

    fn start_source(
        &self,
        ctx: &Context,
        playing: bool,
        repeating: bool,
        volume: f32,
        speed: f32,
    ) -> Result<SoundInstance> {
        let controls = Arc::new(RemoteControls {
            playing: AtomicBool::new(playing),
            repeating: AtomicBool::new(repeating),
            rewind: AtomicBool::new(false),
            volume: Mutex::new(volume),
            speed: Mutex::new(speed),
        });

        let master_volume = { *ctx.audio.master_volume.lock().unwrap() };

        let data = Decoder::new(Cursor::new(Arc::clone(&self.data)))
            .map_err(TetraError::InvalidSound)?
            .buffered();

        let source = TetraSource {
            repeat_source: data.clone(),
            data,

            remote_master_volume: Arc::clone(&ctx.audio.master_volume),
            remote_controls: Arc::downgrade(&Arc::clone(&controls)),
            time_till_update: 220,

            detached: false,
            playing,
            repeating,
            rewind: false,
            master_volume,
            volume,
            speed,
        };

        rodio::play_raw(
            ctx.audio.device.as_ref().ok_or(TetraError::NoAudioDevice)?,
            source.convert_samples(),
        );
        Ok(SoundInstance { controls })
    }
}

#[derive(Debug)]
pub(crate) struct RemoteControls {
    pub(crate) playing: AtomicBool,
    pub(crate) repeating: AtomicBool,
    pub(crate) rewind: AtomicBool,
    pub(crate) volume: Mutex<f32>,
    pub(crate) speed: Mutex<f32>,
}

/// A handle to a single instance of a [`Sound`](./struct.Sound.html).
///
/// The audio thread will poll this for updates every 220 samples (roughly
/// every 5ms at a 44100hz sample rate).
///
/// Cloning a `SoundInstance` will create a new handle to the same instance,
/// rather than creating a new instance.
///
/// Note that dropping a `SoundInstance` does not stop playback.
#[derive(Debug, Clone)]
pub struct SoundInstance {
    pub(crate) controls: Arc<RemoteControls>,
}

impl SoundInstance {
    /// Plays the sound if it is stopped, or resumes the sound if it is paused.
    pub fn play(&self) {
        self.controls.playing.store(true, Ordering::SeqCst);
    }

    /// Stops the sound, and rewinds it to the beginning.
    pub fn stop(&self) {
        self.controls.playing.store(false, Ordering::SeqCst);
        self.controls.rewind.store(true, Ordering::SeqCst);
    }

    /// Pauses the sound.
    pub fn pause(&self) {
        self.controls.playing.store(false, Ordering::SeqCst);
    }

    /// Sets the volume of the sound.
    ///
    /// The parameter is used as a multiplier - for example, `1.0` would result in the
    /// sound being played back at its original volume.
    pub fn set_volume(&self, volume: f32) {
        *self.controls.volume.lock().unwrap() = volume;
    }

    /// Sets the speed (and by extension, the pitch) of the sound.
    ///
    /// The parameter is used as a multiplier - for example, `1.0` would result in the
    /// sound being played back at its original speed.
    pub fn set_speed(&self, speed: f32) {
        *self.controls.speed.lock().unwrap() = speed;
    }

    /// Sets whether the sound should repeat or not.
    pub fn set_repeating(&self, repeating: bool) {
        self.controls.repeating.store(repeating, Ordering::SeqCst);
    }

    /// Toggles whether the sound should repeat or not.
    pub fn toggle_repeating(&self) {
        if self.controls.repeating.load(Ordering::SeqCst) {
            self.controls.repeating.store(false, Ordering::SeqCst);
        } else {
            self.controls.repeating.store(true, Ordering::SeqCst);
        }
    }
}

/// Sets the master volume for the game.
///
/// The parameter is used as a multiplier - for example, `1.0` would result in
/// sounds being played back at their original volume.
pub fn set_master_volume(ctx: &mut Context, volume: f32) {
    *ctx.audio.master_volume.lock().unwrap() = volume;
}

/// Gets the master volume for the game.
pub fn get_master_volume(ctx: &mut Context) -> f32 {
    *ctx.audio.master_volume.lock().unwrap()
}

type TetraSourceData = Buffered<Decoder<Cursor<Arc<[u8]>>>>;

struct TetraSource {
    data: TetraSourceData,
    repeat_source: TetraSourceData,

    remote_master_volume: Arc<Mutex<f32>>,
    remote_controls: Weak<RemoteControls>,
    time_till_update: u32,

    detached: bool,
    playing: bool,
    repeating: bool,
    rewind: bool,
    master_volume: f32,
    volume: f32,
    speed: f32,
}

impl Iterator for TetraSource {
    type Item = i16;

    #[inline]
    fn next(&mut self) -> Option<i16> {
        // There's a lot of shenanigans in this method where we try to keep the local state and
        // the remote state in sync. I'm not sure if it'd be a better idea to just load data from the
        // controls every sample or whether that'd be too slow...

        self.time_till_update -= 1;

        if self.time_till_update == 0 {
            self.master_volume = *self.remote_master_volume.lock().unwrap();

            if let Some(controls) = self.remote_controls.upgrade() {
                self.playing = controls.playing.load(Ordering::SeqCst);

                // If we're not playing, we don't really care about updating the rest of the state.
                if self.playing {
                    self.repeating = controls.repeating.load(Ordering::SeqCst);
                    self.rewind = controls.rewind.load(Ordering::SeqCst);
                    self.volume = *controls.volume.lock().unwrap();
                    self.speed = *controls.speed.lock().unwrap();
                }
            } else {
                self.detached = true;
            }

            self.time_till_update = 220;
        }

        if !self.playing {
            return if self.detached { None } else { Some(0) };
        }

        if self.rewind {
            self.data = self.repeat_source.clone();
            self.rewind = false;

            if let Some(controls) = self.remote_controls.upgrade() {
                controls.rewind.store(false, Ordering::SeqCst);
            }
        }

        self.data
            .next()
            .or_else(|| {
                if self.repeating {
                    self.data = self.repeat_source.clone();
                    self.data.next()
                } else {
                    None
                }
            })
            .map(|v| v.amplify(self.volume).amplify(self.master_volume))
            .or_else(|| {
                if self.detached {
                    None
                } else {
                    // Report that the sound has finished.
                    if !self.rewind {
                        self.playing = false;
                        self.rewind = true;

                        if let Some(controls) = self.remote_controls.upgrade() {
                            controls.playing.store(false, Ordering::SeqCst);
                            controls.rewind.store(true, Ordering::SeqCst);
                        }
                    }

                    Some(0)
                }
            })
    }

    #[inline]
    fn size_hint(&self) -> (usize, Option<usize>) {
        (0, None)
    }
}

impl Source for TetraSource {
    #[inline]
    fn current_frame_len(&self) -> Option<usize> {
        match self.data.current_frame_len() {
            Some(0) => self.repeat_source.current_frame_len(),
            a => a,
        }
    }

    #[inline]
    fn channels(&self) -> u16 {
        match self.data.current_frame_len() {
            Some(0) => self.repeat_source.channels(),
            _ => self.data.channels(),
        }
    }

    #[inline]
    fn sample_rate(&self) -> u32 {
        match self.data.current_frame_len() {
            Some(0) => (self.repeat_source.sample_rate() as f32 * self.speed) as u32,
            _ => (self.data.sample_rate() as f32 * self.speed) as u32,
        }
    }

    #[inline]
    fn total_duration(&self) -> Option<Duration> {
        None
    }
}
