//! Microphone/line-in recording (feature `record`).
//!
//! [`Recorder::start`] opens the default input device and captures into
//! memory until [`Recorder::stop`] returns the finished [`Recording`] —
//! interleaved f32 samples plus the device's sample rate and channel count,
//! ready to be saved or placed as a sample in a song.

use cpal::{
    SampleFormat, Stream,
    traits::{DeviceTrait, HostTrait, StreamTrait},
};
use std::sync::{Arc, Mutex};

/// Errors while opening or starting the input device.
#[derive(Debug, thiserror::Error)]
pub enum RecordError {
    /// No input device is available.
    #[error("no input device available")]
    NoDevice,
    /// The device reports a sample format this recorder does not handle.
    #[error("unsupported input sample format '{0}'")]
    UnsupportedFormat(SampleFormat),
    /// Opening, configuring or starting the input stream failed.
    #[error("input stream error: {0}")]
    Audio(#[from] cpal::Error),
}

/// Records the default input device into memory until stopped.
///
/// The recorder holds the OS audio stream, so it is bound to the thread that
/// created it. Stream errors after a successful start (e.g. the device
/// disappearing) end the capture silently; the samples taken so far remain.
pub struct Recorder {
    stream: Stream,
    samples: Arc<Mutex<Vec<f32>>>,
    sample_rate: u32,
    channels: u16,
}

impl Recorder {
    /// Opens the default input device with its default configuration and
    /// starts recording immediately.
    pub fn start() -> Result<Self, RecordError> {
        let host = cpal::default_host();
        let device = host.default_input_device().ok_or(RecordError::NoDevice)?;
        let config = device.default_input_config()?;
        let sample_rate = config.sample_rate();
        let channels = config.channels();
        let sample_format = config.sample_format();
        let stream_config: cpal::StreamConfig = config.into();
        let samples = Arc::new(Mutex::new(Vec::new()));

        let sink = Arc::clone(&samples);
        let stream = match sample_format {
            SampleFormat::F32 => device.build_input_stream(
                stream_config,
                move |data: &[f32], _: &cpal::InputCallbackInfo| {
                    if let Ok(mut samples) = sink.lock() {
                        samples.extend_from_slice(data);
                    }
                },
                |_error| {},
                None,
            )?,
            SampleFormat::I16 => device.build_input_stream(
                stream_config,
                move |data: &[i16], _: &cpal::InputCallbackInfo| {
                    if let Ok(mut samples) = sink.lock() {
                        samples.extend(
                            data.iter()
                                .map(|&sample| f32::from(sample) / f32::from(i16::MAX)),
                        );
                    }
                },
                |_error| {},
                None,
            )?,
            SampleFormat::U16 => device.build_input_stream(
                stream_config,
                move |data: &[u16], _: &cpal::InputCallbackInfo| {
                    if let Ok(mut samples) = sink.lock() {
                        samples.extend(data.iter().map(|&sample| {
                            (f32::from(sample) - f32::from(u16::MAX) / 2.0)
                                / (f32::from(u16::MAX) / 2.0)
                        }));
                    }
                },
                |_error| {},
                None,
            )?,
            other => return Err(RecordError::UnsupportedFormat(other)),
        };
        stream.play()?;
        Ok(Self {
            stream,
            samples,
            sample_rate,
            channels,
        })
    }

    /// Sample rate of the recording in Hz.
    pub fn sample_rate(&self) -> u32 {
        self.sample_rate
    }

    /// Number of interleaved channels.
    pub fn channels(&self) -> u16 {
        self.channels
    }

    /// Seconds captured so far.
    pub fn recorded_seconds(&self) -> f32 {
        let samples = self.samples.lock().map_or(0, |samples| samples.len());
        samples as f32 / (self.sample_rate as f32 * f32::from(self.channels.max(1)))
    }

    /// Stops the capture and returns everything recorded.
    pub fn stop(self) -> Recording {
        drop(self.stream);
        let samples = self
            .samples
            .lock()
            .map(|samples| samples.clone())
            .unwrap_or_default();
        Recording {
            samples,
            sample_rate: self.sample_rate,
            channels: self.channels,
        }
    }
}

/// A finished recording: interleaved f32 samples in `-1..1`.
#[derive(Debug, Clone)]
pub struct Recording {
    /// Interleaved samples.
    pub samples: Vec<f32>,
    /// Sample rate in Hz.
    pub sample_rate: u32,
    /// Number of interleaved channels.
    pub channels: u16,
}

impl Recording {
    /// Length of the recording in seconds.
    pub fn seconds(&self) -> f32 {
        self.samples.len() as f32 / (self.sample_rate as f32 * f32::from(self.channels.max(1)))
    }

    /// Writes the recording as a 32-bit float WAV file, ready to be used as
    /// an audio clip or (future) sample sound.
    #[cfg(feature = "export")]
    pub fn save_wav(&self, path: &std::path::Path) -> Result<(), std::io::Error> {
        use hound::{SampleFormat, WavSpec, WavWriter};
        let spec = WavSpec {
            channels: self.channels,
            sample_rate: self.sample_rate,
            bits_per_sample: 32,
            sample_format: SampleFormat::Float,
        };
        let mut writer = WavWriter::create(path, spec).map_err(std::io::Error::other)?;
        for &sample in &self.samples {
            writer.write_sample(sample).map_err(std::io::Error::other)?;
        }
        writer.finalize().map_err(std::io::Error::other)
    }
}
