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

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

/// Error returned when a [`SoundSystem`] 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 wrapping rodio.
///
/// Provides methods for playing one-shot sounds, delayed sounds, ambient
/// loops, and creating independent players for continuous sources.
///
/// 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.
#[cfg(not(target_arch = "wasm32"))]
pub struct SoundSystem {
    device_sink: MixerDeviceSink,
    ambient: Player,
}

/// Audio playback system backed by Web Audio (wasm).
#[cfg(target_arch = "wasm32")]
pub struct SoundSystem {
    context: web_sys::AudioContext,
    ambient_gain: web_sys::GainNode,
    ambient: std::cell::RefCell<Option<web_sys::AudioBufferSourceNode>>,
}

/// 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) {}
}

impl SoundSystem {
    /// Creates a new sound system using the default audio output device.
    ///
    /// Returns [`SoundSystemError`] if audio initialization fails.
    #[cfg(not(target_arch = "wasm32"))]
    pub 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,
        })
    }

    /// Creates a new sound system backed by a Web Audio context.
    #[cfg(target_arch = "wasm32")]
    pub 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),
        })
    }

    #[cfg(target_arch = "wasm32")]
    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 as i32);
        }
        Some(buffer)
    }

    /// Plays a sound immediately.
    #[cfg(not(target_arch = "wasm32"))]
    pub fn play<S: Source<Item = f32> + Send + 'static>(&self, source: S) {
        self.device_sink.mixer().add(source);
    }

    /// Plays a sound immediately (rendered to an `AudioBuffer`, scheduled on the
    /// audio thread).
    #[cfg(target_arch = "wasm32")]
    pub 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();
    }

    /// Plays a sound after a delay.
    #[cfg(not(target_arch = "wasm32"))]
    pub fn play_delayed<S: Source<Item = f32> + Send + 'static>(&self, source: S, delay: Duration) {
        self.device_sink.mixer().add(source.delay(delay));
    }

    /// Plays a sound after a delay.
    #[cfg(target_arch = "wasm32")]
    pub 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);
    }

    /// Starts playing an ambient sound, stopping any current ambient.
    #[cfg(not(target_arch = "wasm32"))]
    pub fn play_ambient<S: Source<Item = f32> + Send + 'static>(&self, source: S) {
        self.ambient.stop();
        self.ambient.append(source);
        self.ambient.play();
    }

    /// Starts playing an ambient sound (looped), stopping any current ambient.
    #[cfg(target_arch = "wasm32")]
    pub 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.take_duration(Duration::from_secs(8))) 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);
    }

    /// Stops the currently playing ambient sound.
    #[cfg(not(target_arch = "wasm32"))]
    pub fn stop_ambient(&self) {
        self.ambient.stop();
    }

    /// Stops the currently playing ambient sound.
    #[cfg(target_arch = "wasm32")]
    pub fn stop_ambient(&self) {
        if let Some(node) = self.ambient.borrow_mut().take() {
            let base: &web_sys::AudioScheduledSourceNode = node.as_ref();
            let _ = base.stop();
        }
    }

    /// Sets the ambient volume (0.0 = silent, 1.0 = full).
    #[cfg(not(target_arch = "wasm32"))]
    pub fn set_ambient_volume(&self, volume: f32) {
        self.ambient.set_volume(volume);
    }

    /// Sets the ambient volume (0.0 = silent, 1.0 = full).
    #[cfg(target_arch = "wasm32")]
    pub fn set_ambient_volume(&self, volume: f32) {
        self.ambient_gain.gain().set_value(volume);
    }

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

    /// Creates a new independent player (always `None` on wasm).
    #[cfg(target_arch = "wasm32")]
    pub fn create_sink(&self) -> Option<Player> {
        None
    }
}
