use super::{AUDIO_BUFFER_SIZE, AUDIO_SAMPLE_RATE};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use std::{
    collections::VecDeque,
    sync::{
        Arc, Mutex, MutexGuard,
        atomic::{AtomicBool, AtomicU32, Ordering},
    },
    time::{Duration, Instant},
};
use thiserror::Error;

#[derive(Debug, Error)]
#[error("shared audio context error")]
pub enum ContextError {
    #[error("mutex poisoned")]
    MutexPoisoned,
    #[error("clock not started")]
    ClockNotStarted,
}

/// Audio clock which is updated by the audio callback.
/// It keeps track of the audio time elapsed and the real time elapsed.
pub struct AudioClock {
    pts_offset: Option<f64>,
    started_at: Option<Instant>,
    // total time spent paused
    accumulated_paused: Duration,
    pause_started_at: Option<Instant>,
    started: bool,
}

impl AudioClock {
    pub fn started(&self) -> bool {
        self.started
    }

    pub fn start_playback(&mut self) {
        self.started = true;
        self.started_at = Some(Instant::now());
        self.accumulated_paused = Duration::ZERO;
    }

    pub fn set_offset(&mut self, offset: f64) {
        self.pts_offset = Some(offset);
    }

    pub fn has_offset(&self) -> bool {
        self.pts_offset.is_some()
    }

    pub fn pause(&mut self) {
        self.pause_started_at = Some(Instant::now());
    }

    pub fn resume(&mut self) {
        if let Some(ps) = self.pause_started_at.take() {
            self.accumulated_paused += ps.elapsed();
        }
    }

    pub fn reset(&mut self) {
        self.pts_offset = None;
        self.started_at = None;
        self.accumulated_paused = Duration::ZERO;
        self.pause_started_at = None;
        self.started = false;
    }
}

/// `SharedAudioContextInner` is a struct that holds the inner state of the `SharedAudioContext`.
/// This struct is required to ensure atomic operations for [`AudioClock`] and the internal buffer `VecDeque<i16>`.
pub(crate) struct SharedAudioContextInner {
    pub(crate) buffer: VecDeque<i16>,
    pub(crate) clock: AudioClock,
}

/// The context can be safly shared around
pub struct SharedAudioContext {
    inner: Arc<Mutex<SharedAudioContextInner>>,
    audio_buffer_size: u32,
    volume: Arc<AtomicU32>, // f32 bits, range 0.0..=1.0
    muted: Arc<AtomicBool>,
}

impl SharedAudioContext {
    pub fn new(audio_buffer_size: u32) -> Self {
        let clock = AudioClock {
            pts_offset: None,
            started_at: None,
            accumulated_paused: Duration::ZERO,
            pause_started_at: None,
            started: false,
        };

        let inner = Arc::new(Mutex::new(SharedAudioContextInner {
            buffer: VecDeque::new(),
            clock,
        }));

        Self {
            inner,
            audio_buffer_size,
            volume: Arc::new(AtomicU32::new(1f32.to_bits())),
            muted: Arc::new(AtomicBool::new(false)),
        }
    }

    pub fn clone(&self) -> Self {
        Self {
            inner: Arc::clone(&self.inner),
            audio_buffer_size: self.audio_buffer_size,
            volume: Arc::clone(&self.volume),
            muted: Arc::clone(&self.muted),
        }
    }

    pub fn set_volume(&self, volume: f32) {
        let v = volume.clamp(0.0, 1.0);
        self.volume.store(v.to_bits(), Ordering::Relaxed);
    }

    pub fn volume(&self) -> f32 {
        let volume = self.volume.load(Ordering::Relaxed);
        f32::from_bits(volume)
    }

    pub fn set_muted(&self, muted: bool) {
        self.muted.store(muted, Ordering::Relaxed);
    }

    pub fn muted(&self) -> bool {
        self.muted.load(Ordering::Relaxed)
    }

    /// Returns the heuristic time of audio passed.
    pub fn real_time(&self) -> Result<f64, ContextError> {
        let inner = self.inner.lock().map_err(|_| ContextError::MutexPoisoned)?;
        if !inner.clock.started {
            return Err(ContextError::ClockNotStarted);
        }

        let start = inner.clock.started_at.unwrap(); // SAFETY: started=true implies that this is set
        let total_elapsed = start.elapsed();
        let paused = inner
            .clock
            .pause_started_at
            .map(|ps| ps.elapsed())
            .unwrap_or(Duration::ZERO)
            + inner.clock.accumulated_paused;

        let playing_time = (total_elapsed - paused).as_secs_f64();

        Ok(inner.clock.pts_offset.unwrap_or(0.0) + playing_time)
    }

    pub fn inner(&self) -> Result<MutexGuard<'_, SharedAudioContextInner>, ContextError> {
        let inner = self.inner.lock().map_err(|_| ContextError::MutexPoisoned)?;
        Ok(inner)
    }

    /// Clears only the sample buffer (silences output). Does not reset the clock.
    pub fn clear_buffer(&self) -> Result<(), ContextError> {
        let mut inner = self.inner.lock().map_err(|_| ContextError::MutexPoisoned)?;
        inner.buffer.clear();
        Ok(())
    }

    /// Clears the sample buffer and resets the clock.
    pub fn clear(&self) -> Result<(), ContextError> {
        let mut inner = self.inner.lock().map_err(|_| ContextError::MutexPoisoned)?;
        inner.buffer.clear();
        inner.clock.reset();
        Ok(())
    }
}

#[derive(Debug, Error)]
#[error("iced audio output error")]
pub enum AudioOutputError {
    #[error("output device not found")]
    OutputDeviceNotFound,
    #[error("default stream config: {0}")]
    DefaultConfig(#[from] cpal::DefaultStreamConfigError),
    BuildStream(#[from] cpal::BuildStreamError),
    PlayStream(#[from] cpal::PlayStreamError),
    PauseStream(#[from] cpal::PauseStreamError),
}

pub(crate) struct AudioOutput {
    stream: cpal::Stream,
}

impl AudioOutput {
    /// Query the default output device's channel count.
    pub(crate) fn device_channels() -> Result<u16, AudioOutputError> {
        let host = cpal::default_host();
        let device = host
            .default_output_device()
            .ok_or(AudioOutputError::OutputDeviceNotFound)?;
        let config = device.default_output_config()?;
        Ok(config.channels())
    }

    pub(crate) fn new(
        audio_ctx: SharedAudioContext,
        channels: u16,
    ) -> Result<Self, AudioOutputError> {
        let host = cpal::default_host();
        let device = host
            .default_output_device()
            .ok_or(AudioOutputError::OutputDeviceNotFound)?;

        let audio_config = cpal::StreamConfig {
            channels,
            sample_rate: AUDIO_SAMPLE_RATE,
            buffer_size: cpal::BufferSize::Fixed(AUDIO_BUFFER_SIZE as u32),
        };

        let stream = device.build_output_stream(
            &audio_config,
            move |data: &mut [i16], _| {
                let mut audio = audio_ctx.inner().expect("audio context lock");
                let n = data.len().min(audio.buffer.len());

                let muted = audio_ctx.muted();
                let volume = audio_ctx.volume();

                let gain = if muted {
                    0.0
                } else {
                    let v = volume;
                    v * v
                };

                // write what we have into the stream buffer
                for b in data.iter_mut().take(n) {
                    let s = audio.buffer.pop_front().unwrap_or(0);
                    *b = ((s as f32) * gain).clamp(i16::MIN as f32, i16::MAX as f32) as i16;
                }

                // zero-out the remaining stream buffer
                for b in data.iter_mut().skip(n) {
                    *b = 0;
                }

                if !audio.clock.started() && n > 0 {
                    audio.clock.start_playback();
                }
            },
            |err| log::error!("cpal error: {err}"),
            None,
        )?;

        stream.play()?;

        Ok(Self { stream: stream })
    }

    pub(crate) fn pause(&self) -> Result<(), AudioOutputError> {
        self.stream.pause()?;
        Ok(())
    }

    pub(crate) fn resume(&self) -> Result<(), AudioOutputError> {
        self.stream.play()?;
        Ok(())
    }
}
