//! Minimal audio playback via `rodio` (behind the `audio` feature).
//!
//! Deliberately tiny: load a sound, play it. This matches the course-scaffold
//! goal of "one-line audio" so students (or you) never touch the audio stack to
//! make a noise. Mixing controls, music streaming, and spatial audio are out of
//! scope for v0.1.
//!
//! rodio 0.22 API notes (verified against the pinned crate source):
//! - `OutputStream::try_default()` is gone. You open the default device with
//!   `DeviceSinkBuilder::open_default_sink()`, which yields a
//!   `MixerDeviceSink`; its `.mixer()` is where sources get added.
//! - `Sink` was renamed `Player`; construct with `Player::connect_new(&mixer)`.

use crate::error::{MawidaError, Result};
use std::io::Cursor;
use std::path::Path;

/// Owns the audio output. Keep one alive for the lifetime of the app; dropping
/// it stops all sound.
///
/// RUST NOTE: this is RAII in action. The OS audio stream lives exactly as
/// long as this struct; there is no `close()` to forget. The classic gotcha
/// (`let _ = Audio::new()` dropping the stream immediately, producing silence)
/// is the same one the guides call out for rodio's own types.
pub struct Audio {
    /// The device stream. Field order note: struct fields drop in declaration
    /// order; nothing here depends on it, but sounds die with this stream.
    stream: rodio::MixerDeviceSink,
}

impl Audio {
    pub fn new() -> Result<Self> {
        let stream = rodio::DeviceSinkBuilder::open_default_sink()
            .map_err(|e| MawidaError::Other(format!("audio init: {e}")))?;
        Ok(Self { stream })
    }

    /// Fire-and-forget play of a sound file (WAV/OGG/FLAC/MP3 per rodio
    /// features). Returns a [`Sound`] handle you can keep to control or stop it.
    pub fn play_file(&self, path: impl AsRef<Path>) -> Result<Sound> {
        let bytes = std::fs::read(path)?;
        self.play_bytes(bytes)
    }

    /// Play already-loaded encoded audio bytes.
    pub fn play_bytes(&self, bytes: Vec<u8>) -> Result<Sound> {
        // Player replaces the old Sink; it connects to the device mixer.
        let player = rodio::Player::connect_new(self.stream.mixer());
        let decoder = rodio::Decoder::new(Cursor::new(bytes))
            .map_err(|e| MawidaError::Other(format!("audio decode: {e}")))?;
        player.append(decoder);
        Ok(Sound { player })
    }
}

/// A playing (or playable) sound. Dropping it lets the sound finish; call
/// [`Sound::stop`] to cut it short.
pub struct Sound {
    player: rodio::Player,
}

impl Sound {
    /// RUST NOTE: takes `self` by value; using a stopped Sound afterward is a
    /// compile error, not a runtime bug. Ownership as an API guarantee.
    pub fn stop(self) {
        self.player.stop();
    }

    pub fn set_volume(&self, volume: f32) {
        self.player.set_volume(volume);
    }

    pub fn is_finished(&self) -> bool {
        self.player.empty()
    }

    /// Detach so the sound keeps playing without holding this handle.
    pub fn detach(self) {
        self.player.detach();
    }
}
