use rodio::Source;
use std::time::Duration;

#[cfg(target_arch = "wasm32")]
use std::cell::{Cell, RefCell};

#[cfg(not(target_arch = "wasm32"))]
use rodio::{DeviceSinkBuilder, MixerDeviceSink, Player};

/// Error returned when a [`SoundSystem`] backend cannot be initialized.
#[derive(Debug, thiserror::Error)]
pub enum SoundSystemError {
    /// The default audio output device could not be opened.
    #[error("audio device initialization failed: {0}")]
    Device(String),
    /// The Web Audio context or graph could not be created.
    #[cfg(target_arch = "wasm32")]
    #[error("WebAudio initialization failed")]
    WebAudio,
}

/// Audio playback system.
///
/// [`SoundSystem::new`] never fails: if no audio backend can be initialized
/// (no device, `WebAudio` unavailable) it returns a silent system whose play
/// methods are no-ops. Callers that need to know whether audio is live use
/// [`SoundSystem::try_new`].
///
/// On wasm playback goes through Web Audio directly: rodio's cpal backend is
/// not compiled, so there is no `setTimeout` mixing loop competing for the main
/// thread. One-shot and ambient sources are rendered into `AudioBuffer`s and
/// scheduled on the audio thread.
pub struct SoundSystem(Option<Backend>);

impl SoundSystem {
    /// Creates a sound system, falling back to a silent no-op system if no
    /// audio backend is available.
    pub fn new() -> Self {
        Self(Backend::new().ok())
    }

    /// Creates a sound system, returning [`SoundSystemError`] if the audio
    /// backend cannot be initialized.
    pub fn try_new() -> Result<Self, SoundSystemError> {
        Backend::new().map(|backend| Self(Some(backend)))
    }

    /// Returns true if a live audio backend is active (false for the silent
    /// fallback created by [`SoundSystem::new`]).
    pub fn is_active(&self) -> bool {
        self.0.is_some()
    }

    /// Plays a sound immediately.
    pub fn play<S: Source<Item = f32> + Send + 'static>(&self, source: S) {
        if let Some(backend) = &self.0 {
            backend.play(source);
        }
    }

    /// Plays a sound after a delay.
    pub fn play_delayed<S: Source<Item = f32> + Send + 'static>(&self, source: S, delay: Duration) {
        if let Some(backend) = &self.0 {
            backend.play_delayed(source, delay);
        }
    }

    /// Loops a finite ambient sound, stopping any current ambient.
    ///
    /// The source is repeated endlessly on every platform; choose the loop
    /// length by shaping the source (e.g. `texture.take_duration(…)`).
    /// Infinite sources hang on wasm (the whole loop is rendered up front),
    /// so pass finite ones.
    pub fn play_ambient<S: Source<Item = f32> + Send + 'static>(&self, source: S) {
        if let Some(backend) = &self.0 {
            backend.play_ambient(source);
        }
    }

    /// Stops the currently playing ambient sound.
    pub fn stop_ambient(&self) {
        if let Some(backend) = &self.0 {
            backend.stop_ambient();
        }
    }

    /// Sets the ambient volume (0.0 = silent, 1.0 = full).
    pub fn set_ambient_volume(&self, volume: f32) {
        if let Some(backend) = &self.0 {
            backend.set_ambient_volume(volume);
        }
    }

    /// Estimated latency between a sample being fed to the audio output and it
    /// becoming audible, derived from the output buffer size.
    ///
    /// Best-effort: it covers the ring buffer rodio requests, not the extra
    /// buffering a backend (`PulseAudio`, `PipeWire`) may add, and cpal cannot
    /// report the latter reliably. Use it to seed a playback-position
    /// compensation, then let the host calibrate the rest. Returns
    /// [`Duration::ZERO`] for the silent system and on wasm.
    pub fn output_latency(&self) -> Duration {
        self.0
            .as_ref()
            .map_or(Duration::ZERO, Backend::output_latency)
    }

    /// Creates a new independent player for continuous sounds (e.g. engine).
    ///
    /// The caller manages the player's lifetime, volume, and speed. Returns
    /// `None` for the silent system and on wasm (continuous Web Audio sources
    /// are not supported).
    pub fn create_sink(&self) -> Option<Player> {
        #[cfg(not(target_arch = "wasm32"))]
        {
            self.0.as_ref().map(Backend::create_sink)
        }
        #[cfg(target_arch = "wasm32")]
        {
            let _ = &self.0;
            None
        }
    }

    /// Creates a looping ambient bed on its own player at the given volume.
    ///
    /// Convenience for `create_sink` + `append` + `set_volume` + `play`.
    /// Returns `None` when no independent player is available (silent system,
    /// wasm); use [`SoundSystem::play_ambient`] there instead.
    pub fn ambient_bed<S: Source<Item = f32> + Send + 'static>(
        &self,
        source: S,
        volume: f32,
    ) -> Option<Player> {
        let sink = self.create_sink()?;
        sink.append(source);
        sink.set_volume(volume);
        sink.play();
        Some(sink)
    }

    /// Plays a pre-rendered interleaved-stereo song buffer through Web Audio and
    /// returns a [`WebSong`] transport handle. `base_offset` is the song time
    /// (seconds) the buffer starts at; `loop_start`, if set, loops the buffer
    /// from that buffer-relative second. Returns `None` for the silent system.
    #[cfg(target_arch = "wasm32")]
    pub fn play_song(
        &self,
        samples: &[f32],
        sample_rate: u32,
        base_offset: f64,
        loop_start: Option<f64>,
    ) -> Option<WebSong> {
        self.0
            .as_ref()
            .and_then(|backend| backend.play_song(samples, sample_rate, base_offset, loop_start))
    }
}

impl Default for SoundSystem {
    fn default() -> Self {
        Self::new()
    }
}

/// Independent continuous-sound player (no-op stub on wasm).
#[cfg(target_arch = "wasm32")]
pub struct Player;

#[cfg(target_arch = "wasm32")]
impl Player {
    /// Appends a source (ignored on wasm).
    pub fn append<S: Source<Item = f32> + Send + 'static>(&self, _source: S) {}
    /// Sets the playback volume (ignored on wasm).
    pub fn set_volume(&self, _volume: f32) {}
    /// Sets the playback speed (ignored on wasm).
    pub fn set_speed(&self, _speed: f32) {}
    /// Starts playback (ignored on wasm).
    pub fn play(&self) {}
    /// Stops playback (ignored on wasm).
    pub fn stop(&self) {}
}

#[cfg(not(target_arch = "wasm32"))]
struct Backend {
    device_sink: MixerDeviceSink,
    ambient: Player,
}

#[cfg(not(target_arch = "wasm32"))]
impl Backend {
    fn new() -> Result<Self, SoundSystemError> {
        let device_sink = DeviceSinkBuilder::from_default_device()
            .and_then(DeviceSinkBuilder::open_stream)
            .map_err(|error| SoundSystemError::Device(error.to_string()))?;
        let ambient = Player::connect_new(device_sink.mixer());
        Ok(Self {
            device_sink,
            ambient,
        })
    }

    fn play<S: Source<Item = f32> + Send + 'static>(&self, source: S) {
        self.device_sink.mixer().add(source);
    }

    fn play_delayed<S: Source<Item = f32> + Send + 'static>(&self, source: S, delay: Duration) {
        self.device_sink.mixer().add(source.delay(delay));
    }

    fn play_ambient<S: Source<Item = f32> + Send + 'static>(&self, source: S) {
        self.ambient.stop();
        self.ambient.append(source.repeat_infinite());
        self.ambient.play();
    }

    fn stop_ambient(&self) {
        self.ambient.stop();
    }

    fn set_ambient_volume(&self, volume: f32) {
        self.ambient.set_volume(volume);
    }

    fn create_sink(&self) -> Player {
        Player::connect_new(self.device_sink.mixer())
    }

    fn output_latency(&self) -> Duration {
        let config = self.device_sink.config();
        let frames = match config.buffer_size() {
            rodio::cpal::BufferSize::Fixed(frames) => *frames,
            rodio::cpal::BufferSize::Default => 0,
        };
        Duration::from_secs_f64(f64::from(frames) / f64::from(config.sample_rate().get()))
    }
}

#[cfg(target_arch = "wasm32")]
struct Backend {
    context: web_sys::AudioContext,
    ambient_gain: web_sys::GainNode,
    ambient: std::cell::RefCell<Option<web_sys::AudioBufferSourceNode>>,
}

#[cfg(target_arch = "wasm32")]
impl Backend {
    fn new() -> Result<Self, SoundSystemError> {
        let context = web_sys::AudioContext::new().map_err(|_| SoundSystemError::WebAudio)?;
        let ambient_gain = context
            .create_gain()
            .map_err(|_| SoundSystemError::WebAudio)?;
        let _ = ambient_gain.connect_with_audio_node(&context.destination());
        Ok(Self {
            context,
            ambient_gain,
            ambient: std::cell::RefCell::new(None),
        })
    }

    fn render(&self, source: impl Source<Item = f32>) -> Option<web_sys::AudioBuffer> {
        let sample_rate = source.sample_rate().get();
        let channels = u32::from(source.channels().get());
        let samples: Vec<f32> = source.collect();
        let frames = samples.len() as u32 / channels;
        if frames == 0 {
            return None;
        }
        let buffer = self
            .context
            .create_buffer(channels, frames, sample_rate as f32)
            .ok()?;
        for channel in 0..channels {
            let data: Vec<f32> = (0..frames)
                .map(|frame| samples[(frame * channels + channel) as usize])
                .collect();
            let _ = buffer.copy_to_channel(&data, channel.cast_signed());
        }
        Some(buffer)
    }

    fn play<S: Source<Item = f32> + Send + 'static>(&self, source: S) {
        let _ = self.context.resume();
        let Some(buffer) = self.render(source) else {
            return;
        };
        let Ok(node) = self.context.create_buffer_source() else {
            return;
        };
        node.set_buffer(Some(&buffer));
        let _ = node.connect_with_audio_node(&self.context.destination());
        let _ = node.start();
    }

    fn play_delayed<S: Source<Item = f32> + Send + 'static>(&self, source: S, delay: Duration) {
        let _ = self.context.resume();
        let Some(buffer) = self.render(source) else {
            return;
        };
        let Ok(node) = self.context.create_buffer_source() else {
            return;
        };
        node.set_buffer(Some(&buffer));
        let _ = node.connect_with_audio_node(&self.context.destination());
        let when = self.context.current_time() + delay.as_secs_f64();
        let _ = node.start_with_when(when);
    }

    fn play_ambient<S: Source<Item = f32> + Send + 'static>(&self, source: S) {
        let _ = self.context.resume();
        if let Some(node) = self.ambient.borrow_mut().take() {
            let base: &web_sys::AudioScheduledSourceNode = node.as_ref();
            let _ = base.stop();
        }
        let Some(buffer) = self.render(source) else {
            return;
        };
        let Ok(node) = self.context.create_buffer_source() else {
            return;
        };
        node.set_buffer(Some(&buffer));
        node.set_loop(true);
        let _ = node.connect_with_audio_node(&self.ambient_gain);
        let _ = node.start();
        *self.ambient.borrow_mut() = Some(node);
    }

    fn stop_ambient(&self) {
        if let Some(node) = self.ambient.borrow_mut().take() {
            let base: &web_sys::AudioScheduledSourceNode = node.as_ref();
            let _ = base.stop();
        }
    }

    fn set_ambient_volume(&self, volume: f32) {
        self.ambient_gain.gain().set_value(volume);
    }

    fn play_song(
        &self,
        samples: &[f32],
        sample_rate: u32,
        base_offset: f64,
        loop_start: Option<f64>,
    ) -> Option<WebSong> {
        let _ = self.context.resume();
        let channels = 2u32;
        let frames = samples.len() as u32 / channels;
        if frames == 0 {
            return None;
        }
        let buffer = self
            .context
            .create_buffer(channels, frames, sample_rate as f32)
            .ok()?;
        for channel in 0..channels {
            let data: Vec<f32> = (0..frames)
                .map(|frame| samples[(frame * channels + channel) as usize])
                .collect();
            let _ = buffer.copy_to_channel(&data, channel.cast_signed());
        }
        let gain = self.context.create_gain().ok()?;
        let _ = gain.connect_with_audio_node(&self.context.destination());
        let song = WebSong::new(self.context.clone(), buffer, gain, base_offset, loop_start);
        song.play_from(0.0);
        Some(song)
    }

    fn output_latency(&self) -> Duration {
        let _ = self;
        Duration::ZERO
    }
}

/// Transport handle for a song playing through Web Audio: a pre-rendered buffer
/// on a controllable buffer-source node with its own gain. Position is tracked
/// against the audio context clock; pause/resume/seek recreate the node (buffer
/// sources are single-use) on the same buffer.
#[cfg(target_arch = "wasm32")]
pub struct WebSong {
    context: web_sys::AudioContext,
    buffer: web_sys::AudioBuffer,
    gain: web_sys::GainNode,
    node: RefCell<Option<web_sys::AudioBufferSourceNode>>,
    start_time: Cell<f64>,
    grain: Cell<f64>,
    duration: f64,
    base_offset: f64,
    loop_start: Option<f64>,
    paused: Cell<bool>,
}

#[cfg(target_arch = "wasm32")]
impl WebSong {
    fn new(
        context: web_sys::AudioContext,
        buffer: web_sys::AudioBuffer,
        gain: web_sys::GainNode,
        base_offset: f64,
        loop_start: Option<f64>,
    ) -> Self {
        let duration = buffer.duration();
        Self {
            context,
            buffer,
            gain,
            node: RefCell::new(None),
            start_time: Cell::new(0.0),
            grain: Cell::new(0.0),
            duration,
            base_offset,
            loop_start,
            paused: Cell::new(false),
        }
    }

    fn play_from(&self, grain: f64) {
        self.stop_node();
        let Ok(node) = self.context.create_buffer_source() else {
            return;
        };
        node.set_buffer(Some(&self.buffer));
        if let Some(loop_start) = self.loop_start {
            node.set_loop(true);
            node.set_loop_start(loop_start);
            node.set_loop_end(self.duration);
        }
        let _ = node.connect_with_audio_node(&self.gain);
        let now = self.context.current_time();
        let _ = node.start_with_when_and_grain_offset(now, grain);
        self.start_time.set(now);
        self.grain.set(grain);
        self.paused.set(false);
        *self.node.borrow_mut() = Some(node);
    }

    fn stop_node(&self) {
        if let Some(node) = self.node.borrow_mut().take() {
            let base: &web_sys::AudioScheduledSourceNode = node.as_ref();
            let _ = base.stop();
        }
    }

    fn buffer_position(&self) -> f64 {
        if self.paused.get() {
            return self.grain.get();
        }
        let elapsed = self.grain.get() + (self.context.current_time() - self.start_time.get());
        match self.loop_start {
            Some(loop_start) if elapsed >= self.duration && self.duration > loop_start => {
                loop_start + (elapsed - loop_start) % (self.duration - loop_start)
            }
            _ => elapsed.min(self.duration),
        }
    }

    /// Song-time playback position.
    pub fn position(&self) -> Duration {
        Duration::from_secs_f64((self.base_offset + self.buffer_position()).max(0.0))
    }

    /// Pauses playback, remembering the position.
    pub fn pause(&self) {
        if self.paused.get() {
            return;
        }
        let position = self.buffer_position();
        self.stop_node();
        self.grain.set(position);
        self.paused.set(true);
    }

    /// Resumes from the paused position.
    pub fn resume(&self) {
        if self.paused.get() {
            self.play_from(self.grain.get());
        }
    }

    /// True while paused.
    pub fn is_paused(&self) -> bool {
        self.paused.get()
    }

    /// Stops playback.
    pub fn stop(&self) {
        self.stop_node();
        self.paused.set(false);
    }

    /// True while a node is scheduled and the buffer has not run out (a looping
    /// song plays until stopped).
    pub fn is_playing(&self) -> bool {
        !self.paused.get()
            && self.node.borrow().is_some()
            && (self.loop_start.is_some() || self.buffer_position() < self.duration)
    }

    /// Seeks to a song-time position, restarting the node when playing.
    pub fn seek(&self, position: Duration) {
        let grain = (position.as_secs_f64() - self.base_offset).clamp(0.0, self.duration);
        if self.paused.get() {
            self.grain.set(grain);
        } else {
            self.play_from(grain);
        }
    }

    /// Sets the master gain (0.0 = silent, 1.0 = full).
    pub fn set_master_volume(&self, volume: f32) {
        self.gain.gain().set_value(volume);
    }
}
