//! Rendering and file export for complete songs.
//!
//! `render_song_stereo` is always available. WAV and MP3 writing live behind
//! the `export` feature so that the library stays dependency-light when only
//! playback is needed.

use super::{
    player::{PlayerError, render_layer},
    song::Song,
};
use std::path::Path;

/// Errors that can happen during audio file export.
#[cfg(feature = "export")]
#[derive(Debug, thiserror::Error)]
pub enum ExportError {
    /// Could not create the output file.
    #[error("could not create output file")]
    Create(std::io::Error),
    /// Could not write audio data.
    #[error("could not write audio data")]
    Write,
    /// MP3 encoder setup or encoding failed.
    #[error("mp3 encoder error")]
    Mp3Encoder,
    /// A layer could not be rendered (e.g. an unknown sound name).
    #[error("could not render song: {0}")]
    Render(#[from] PlayerError),
}

/// Renders the entire song into an interleaved stereo f32 vector (44100 Hz),
/// returning an error if any layer fails to render (e.g. an unknown sound).
///
/// Layers are summed sample-by-sample. Mute and solo are honoured. The buffer
/// length covers the last clip end or loop, whichever is longer.
pub fn render_song_stereo(song: &Song, base: Option<&Path>) -> Result<Vec<f32>, PlayerError> {
    let beat_duration = 60.0 / song.tempo.max(1.0);
    let any_solo = song.layers.iter().any(|layer| layer.solo);

    let mut output = Vec::new();

    for layer in &song.layers {
        if layer.muted {
            continue;
        }
        if any_solo && !layer.solo {
            continue;
        }
        let buffer = render_layer(layer, song, base, beat_duration)?;
        if buffer.len() > output.len() {
            output.resize(buffer.len(), 0.0);
        }
        for (out, sample) in output.iter_mut().zip(buffer) {
            *out += sample;
        }
    }

    Ok(output)
}

/// Writes a rendered song as a 32-bit float stereo WAV file.
#[cfg(feature = "export")]
pub fn export_wav(song: &Song, base: Option<&Path>, path: &Path) -> Result<(), ExportError> {
    use hound::{SampleFormat, WavSpec, WavWriter};
    let samples = render_song_stereo(song, base)?;
    let spec = WavSpec {
        channels: 2,
        sample_rate: 44100,
        bits_per_sample: 32,
        sample_format: SampleFormat::Float,
    };
    let mut writer = WavWriter::create(path, spec).map_err(|_| ExportError::Write)?;
    for sample in samples {
        writer
            .write_sample(sample)
            .map_err(|_| ExportError::Write)?;
    }
    writer.finalize().map_err(|_| ExportError::Write)?;
    Ok(())
}

/// Encodes a rendered song as an MP3 file.
#[cfg(feature = "export")]
pub fn export_mp3(song: &Song, base: Option<&Path>, path: &Path) -> Result<(), ExportError> {
    let samples = render_song_stereo(song, base)?;
    let data_left: Vec<i16> = samples
        .chunks(2)
        .map(|pair| {
            (pair.first().copied().unwrap_or(0.0) * i16::MAX as f32)
                .clamp(i16::MIN as f32, i16::MAX as f32) as i16
        })
        .collect();
    let data_right: Vec<i16> = samples
        .chunks(2)
        .map(|pair| {
            (pair.get(1).copied().unwrap_or(0.0) * i16::MAX as f32)
                .clamp(i16::MIN as f32, i16::MAX as f32) as i16
        })
        .collect();

    use mp3lame_encoder::{Builder, FlushNoGap};
    use std::io::Write;

    let mut encoder = Builder::new().ok_or(ExportError::Mp3Encoder)?;
    encoder
        .set_num_channels(2)
        .map_err(|_| ExportError::Mp3Encoder)?;
    encoder
        .set_sample_rate(44100)
        .map_err(|_| ExportError::Mp3Encoder)?;
    encoder
        .set_quality(mp3lame_encoder::Quality::Best)
        .map_err(|_| ExportError::Mp3Encoder)?;
    let mut encoder = encoder.build().map_err(|_| ExportError::Mp3Encoder)?;

    let interleaved: Vec<i16> = data_left
        .iter()
        .zip(data_right.iter())
        .flat_map(|(&left, &right)| [left, right])
        .collect();
    let input = mp3lame_encoder::InterleavedPcm(&interleaved);
    let buffer_size = mp3lame_encoder::max_required_buffer_size(interleaved.len() / 2);
    let mut output = vec![std::mem::MaybeUninit::uninit(); buffer_size];
    let encoded_size = encoder
        .encode(input, &mut output)
        .map_err(|_| ExportError::Mp3Encoder)?;
    let flush_size = encoder
        .flush::<FlushNoGap>(&mut output[encoded_size..])
        .map_err(|_| ExportError::Mp3Encoder)?;
    let total = encoded_size + flush_size;
    let output = unsafe { std::slice::from_raw_parts(output.as_ptr().cast::<u8>(), total) };
    let mut file = std::fs::File::create(path).map_err(ExportError::Create)?;
    file.write_all(output).map_err(|_| ExportError::Write)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    #![allow(clippy::unwrap_used)]
    use super::render_song_stereo;
    use crate::track::parse;

    #[test]
    fn render_errors_on_unknown_sound() {
        let song = parse("# Layer A\nghost 0 0 1\n").unwrap();
        assert!(render_song_stereo(&song, None).is_err());
    }

    #[test]
    fn render_sums_layers() {
        let song = parse("# Sounds\nbell: sine 1\n# Layer A\nbell 0 0 1\n").unwrap();
        assert!(!render_song_stereo(&song, None).unwrap().is_empty());
    }
}
