use std::{path::Path, time::Duration};

use cpal::{
    BuildStreamError, Device, FromSample, SizedSample, StreamConfig,
    traits::{DeviceTrait, StreamTrait},
};
use hound::{WavSpec, WavWriter};

use crate::{LegatoApp, runtime::Runtime};

use assert_no_alloc::*;

pub fn render(mut app: LegatoApp, path: &Path, time: Duration) -> Result<(), hound::Error> {
    let config = app.get_config();

    let dur_in_samples = (time.as_secs_f32() * config.sample_rate as f32) as usize;

    let channels = config.channels;

    let spec = WavSpec {
        channels: channels as u16,
        sample_rate: config.sample_rate as u32,
        bits_per_sample: 32,
        sample_format: hound::SampleFormat::Float,
    };

    let mut writer = WavWriter::create(path, spec).unwrap();

    let block_size = config.block_size;

    let mut count = 0_usize;

    while count < dur_in_samples {
        let block_view = app.next_block(None);
        let block = &block_view.channels[0..block_view.chans];

        for n in 0..block_size {
            for block_chan in block {
                writer.write_sample(block_chan[n]).unwrap();
            }
        }
        count += block_size;
    }

    writer.finalize().unwrap();

    Ok(())
}

#[inline(always)]
fn write_runtime_data_cpal<T>(output: &mut [T], config: &StreamConfig, runtime: &mut Runtime)
where
    T: SizedSample + FromSample<f64>,
{
    let next_block_view = runtime.next_block(None);

    let next_block = &next_block_view.channels[0..next_block_view.chans];

    let chans = config.channels as usize;

    for (frame_index, frame) in output.chunks_mut(chans).enumerate() {
        for (channel, sample) in frame.iter_mut().enumerate() {
            let pipeline_next_frame = &next_block[channel];
            *sample = T::from_sample(pipeline_next_frame[frame_index] as f64);
        }
    }
}

pub fn start_runtime_audio_thread(
    device: &Device,
    config: StreamConfig,
    mut runtime: Runtime,
) -> Result<(), BuildStreamError> {
    let stream = device.build_output_stream(
        &config.clone(),
        move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
            assert_no_alloc(|| write_runtime_data_cpal(data, &config, &mut runtime))
        },
        |err| eprintln!("An output stream error occurred: {}", err),
        None,
    )?;

    stream.play().unwrap();

    std::thread::park();

    Ok(())
}

#[inline(always)]
fn write_runtime_data_cpal_app<T>(output: &mut [T], config: &StreamConfig, app: &mut LegatoApp)
where
    T: SizedSample + FromSample<f64>,
{
    let next_block_view = app.next_block(None);
    let next_block = &next_block_view.channels[0..next_block_view.chans];

    let chans = config.channels as usize;

    for (frame_index, frame) in output.chunks_mut(chans).enumerate() {
        for (channel, sample) in frame.iter_mut().enumerate() {
            let pipeline_next_frame = &next_block[channel];
            *sample = T::from_sample(pipeline_next_frame[frame_index] as f64);
        }
    }
}

pub fn start_application_audio_thread(
    device: &Device,
    config: StreamConfig,
    mut app: LegatoApp,
) -> Result<(), BuildStreamError> {
    let stream = device.build_output_stream(
        &config.clone(),
        move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
            assert_no_alloc(|| write_runtime_data_cpal_app(data, &config, &mut app))
        },
        |err| eprintln!("An output stream error occurred: {}", err),
        None,
    )?;

    stream.play().unwrap();

    std::thread::park();

    Ok(())
}
