use cpal::traits::DeviceTrait;

pub(crate) trait F32OutputSource {
    fn fill<T>(&mut self, output: &mut [T], channels: usize, convert: fn(f32) -> T)
    where
        T: Default + Copy;
}

pub(crate) fn build_f32_output_stream<S, E>(
    device: &cpal::Device,
    config: &cpal::StreamConfig,
    sample_format: cpal::SampleFormat,
    source: S,
    error_callback: E,
    unsupported_context: &str,
    build_context: &str,
) -> Result<cpal::Stream, String>
where
    S: F32OutputSource + Send + 'static,
    E: FnMut(cpal::StreamError) + Send + 'static,
{
    match sample_format {
        cpal::SampleFormat::F32 => build_f32_output_stream_as::<f32, _, _>(
            device,
            config,
            source,
            error_callback,
            clamp_f32_sample,
            build_context,
        ),
        cpal::SampleFormat::U8 => build_f32_output_stream_as::<u8, _, _>(
            device,
            config,
            source,
            error_callback,
            f32_sample_to_u8,
            build_context,
        ),
        cpal::SampleFormat::I16 => build_f32_output_stream_as::<i16, _, _>(
            device,
            config,
            source,
            error_callback,
            f32_sample_to_i16,
            build_context,
        ),
        cpal::SampleFormat::U16 => build_f32_output_stream_as::<u16, _, _>(
            device,
            config,
            source,
            error_callback,
            f32_sample_to_u16,
            build_context,
        ),
        other => Err(format!(
            "unsupported {unsupported_context} sample format: {other:?}"
        )),
    }
}

fn build_f32_output_stream_as<T, S, E>(
    device: &cpal::Device,
    config: &cpal::StreamConfig,
    mut source: S,
    error_callback: E,
    convert: fn(f32) -> T,
    build_context: &str,
) -> Result<cpal::Stream, String>
where
    T: cpal::SizedSample + Default + Copy + 'static,
    S: F32OutputSource + Send + 'static,
    E: FnMut(cpal::StreamError) + Send + 'static,
{
    let channels = usize::from(config.channels.max(1));
    device
        .build_output_stream(
            config,
            move |output: &mut [T], _| source.fill(output, channels, convert),
            error_callback,
            None,
        )
        .map_err(|error| format!("{build_context} output stream build failed: {error}"))
}

pub(crate) fn clamp_f32_sample(sample: f32) -> f32 {
    sample.clamp(-1.0, 1.0)
}

pub(crate) fn f32_sample_to_u8(sample: f32) -> u8 {
    ((clamp_f32_sample(sample) + 1.0) * 0.5 * f32::from(u8::MAX)).round() as u8
}

pub(crate) fn f32_sample_to_i16(sample: f32) -> i16 {
    (clamp_f32_sample(sample) * f32::from(i16::MAX)).round() as i16
}

pub(crate) fn f32_sample_to_u16(sample: f32) -> u16 {
    ((clamp_f32_sample(sample) + 1.0) * 0.5 * f32::from(u16::MAX)).round() as u16
}

#[cfg(test)]
mod tests {
    use super::{f32_sample_to_i16, f32_sample_to_u8, f32_sample_to_u16};

    #[test]
    fn converts_f32_samples_to_integer_output_ranges() {
        assert_eq!(f32_sample_to_u8(1.0), u8::MAX);
        assert_eq!(f32_sample_to_u8(-1.0), 0);
        assert_eq!(f32_sample_to_i16(1.0), i16::MAX);
        assert_eq!(f32_sample_to_i16(-1.0), -i16::MAX);
        assert_eq!(f32_sample_to_u16(1.0), u16::MAX);
        assert_eq!(f32_sample_to_u16(-1.0), 0);
    }
}
