//! 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::{MuteSolo, PlayerError, render_audible_layers, soft_clip},
    song::Song,
};
use std::path::Path;

/// Errors that can happen during audio file export.
#[cfg(all(not(target_arch = "wasm32"), 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 rendered with their effects, scaled by their volume, and summed
/// sample-by-sample, matching live playback. Mute and solo are honoured (this
/// is an editor-style bounce; use [`render_song_stereo_with`] to ignore them).
/// The buffer length covers the last clip end or loop, whichever is longer. The
/// summed signal is soft-clipped: transparent up to 0.8, smoothly limited
/// below 1.0 above, so stacked layers never hard-clip in exports.
pub fn render_song_stereo(song: &Song, base: Option<&Path>) -> Result<Vec<f32>, PlayerError> {
    render_song_stereo_with(song, base, MuteSolo::Honor)
}

/// Like [`render_song_stereo`], but `mute_solo` chooses whether `@mute`/`@solo`
/// layer flags apply ([`MuteSolo::Honor`] for an editor bounce,
/// [`MuteSolo::Ignore`] to bake every layer for a game).
pub fn render_song_stereo_with(
    song: &Song,
    base: Option<&Path>,
    mute_solo: MuteSolo,
) -> Result<Vec<f32>, PlayerError> {
    let tempo = song.tempo_map();
    let mut output = Vec::new();
    let buffers = render_audible_layers(song, base, tempo, mute_solo)?;
    for (layer, buffer) in song.layers.iter().zip(buffers) {
        let Some(buffer) = buffer else { continue };
        if buffer.len() > output.len() {
            output.resize(buffer.len(), 0.0);
        }
        for (out, sample) in output.iter_mut().zip(buffer) {
            *out += sample * layer.volume;
        }
    }

    for sample in &mut output {
        *sample = soft_clip(*sample);
    }

    Ok(output)
}

/// Renders each audible layer separately, parallel to `song.layers` — muted
/// (or, when any layer is solo, non-solo) layers yield empty buffers.
///
/// Layer effects and volume are applied like in the master render and all
/// audible stems are padded to a common length, but no soft-clip runs: stems
/// keep their headroom for external mixing (32-bit float does not clip). Mute
/// and solo are honoured; use [`render_song_stems_with`] to ignore them.
pub fn render_song_stems(song: &Song, base: Option<&Path>) -> Result<Vec<Vec<f32>>, PlayerError> {
    render_song_stems_with(song, base, MuteSolo::Honor)
}

/// Like [`render_song_stems`], but `mute_solo` chooses whether `@mute`/`@solo`
/// layer flags apply.
pub fn render_song_stems_with(
    song: &Song,
    base: Option<&Path>,
    mute_solo: MuteSolo,
) -> Result<Vec<Vec<f32>>, PlayerError> {
    let tempo = song.tempo_map();
    let mut stems = Vec::with_capacity(song.layers.len());
    let mut longest = 0;
    let buffers = render_audible_layers(song, base, tempo, mute_solo)?;
    for (layer, buffer) in song.layers.iter().zip(buffers) {
        let Some(mut buffer) = buffer else {
            stems.push(Vec::new());
            continue;
        };
        for sample in &mut buffer {
            *sample *= layer.volume;
        }
        longest = longest.max(buffer.len());
        stems.push(buffer);
    }
    for stem in &mut stems {
        if !stem.is_empty() {
            stem.resize(longest, 0.0);
        }
    }
    Ok(stems)
}

/// Writes a rendered song as a 32-bit float stereo WAV file.
#[cfg(all(not(target_arch = "wasm32"), feature = "export"))]
pub fn export_wav(song: &Song, base: Option<&Path>, path: &Path) -> Result<(), ExportError> {
    let samples = render_song_stereo(song, base)?;
    write_wav(&samples, path)
}

/// Writes one WAV file per audible layer into `directory` (created if
/// missing), named `<index> <layer name>.wav`, and returns the written paths.
///
/// Stems come from [`render_song_stems`]: equal length, layer effects and
/// volume applied, no soft-clip.
#[cfg(all(not(target_arch = "wasm32"), feature = "export"))]
pub fn export_stems(
    song: &Song,
    base: Option<&Path>,
    directory: &Path,
) -> Result<Vec<std::path::PathBuf>, ExportError> {
    let stems = render_song_stems(song, base)?;
    std::fs::create_dir_all(directory).map_err(ExportError::Create)?;
    let mut written = Vec::new();
    for (index, (layer, samples)) in song.layers.iter().zip(stems).enumerate() {
        if samples.is_empty() {
            continue;
        }
        let name: String = layer
            .name
            .chars()
            .map(|symbol| {
                if matches!(symbol, '/' | '\\') {
                    '-'
                } else {
                    symbol
                }
            })
            .collect();
        let path = directory.join(format!("{:02} {name}.wav", index + 1));
        write_wav(&samples, &path)?;
        written.push(path);
    }
    Ok(written)
}

#[cfg(all(not(target_arch = "wasm32"), feature = "export"))]
fn write_wav(samples: &[f32], path: &Path) -> Result<(), ExportError> {
    use hound::{SampleFormat, WavSpec, WavWriter};
    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(all(not(target_arch = "wasm32"), 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::{MuteSolo, render_song_stereo, render_song_stereo_with};
    use crate::track::parse;

    #[test]
    fn ignore_mute_solo_still_renders_muted_layers() {
        let song = parse("# Sounds\nb: sine 1\n# Layers\n\n## A\n@mute true\nb 0 0 1\n").unwrap();
        // Honoured, the only layer is muted → nothing renders.
        assert!(render_song_stereo(&song, None).unwrap().is_empty());
        // Ignored, the layer plays → audible output (the game-facing default).
        let ignored = render_song_stereo_with(&song, None, MuteSolo::Ignore).unwrap();
        assert!(ignored.iter().any(|sample| sample.abs() > 0.01));
    }

    #[test]
    fn note_uses_default_instrument_without_a_sound_definition() {
        // No `# Sounds`: `sine` resolves to the built-in default instrument.
        let song = parse("# Layers\n\n## A\nsine 0 0 1\n").unwrap();
        assert!(
            render_song_stereo(&song, None)
                .unwrap()
                .iter()
                .any(|sample| sample.abs() > 0.001)
        );
        // A local definition of the same name overrides the default (still renders).
        let overridden = parse("# Sounds\nsine: saw 1\n# Layers\n\n## A\nsine 0 0 1\n").unwrap();
        assert!(!render_song_stereo(&overridden, None).unwrap().is_empty());
    }

    #[test]
    fn render_errors_on_unknown_sound() {
        let song = parse("# Layers\n\n## 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# Layers\n\n## A\nbell 0 0 1\n").unwrap();
        assert!(!render_song_stereo(&song, None).unwrap().is_empty());
    }

    #[test]
    fn soft_clip_is_transparent_below_threshold_and_bounded_above() {
        assert_eq!(super::soft_clip(0.5), 0.5);
        assert_eq!(super::soft_clip(-0.5), -0.5);
        assert!(super::soft_clip(3.0) <= 1.0);
        assert!(super::soft_clip(3.0) > 0.9);
        assert!(super::soft_clip(-3.0) >= -1.0);
    }

    #[cfg(all(not(target_arch = "wasm32"), feature = "export"))]
    #[test]
    fn sampler_notes_render_and_pitch_sets_speed() {
        let dir = std::env::temp_dir().join("game-audio-sampler-test");
        std::fs::create_dir_all(&dir).unwrap();
        let samples: Vec<f32> = (0..4410)
            .map(|index| (index as f32 * 0.05).sin() * 0.5)
            .collect();
        super::write_wav(&samples, &dir.join("beep.wav")).unwrap();

        let render = |pitch: &str| {
            let source = format!(
                "# Sounds\nbeep: sample \"beep.wav\"\n# Layers\n\n## A\n\nbeep 0 {pitch} 1\n"
            );
            render_song_stereo(&parse(&source).unwrap(), Some(&dir)).unwrap()
        };
        let original = render("0");
        let octave_up = render("12");
        assert!(original.iter().any(|sample| sample.abs() > 0.01));
        let ratio = octave_up.len() as f32 / original.len() as f32;
        assert!(
            (ratio - 0.5).abs() < 0.05,
            "octave up should halve the length, ratio {ratio}"
        );
    }

    #[test]
    fn unknown_sample_file_errors() {
        let song =
            parse("# Sounds\nbeep: sample \"missing.wav\"\n# Layers\n\n## A\n\nbeep 0 0 1\n")
                .unwrap();
        assert!(render_song_stereo(&song, None).is_err());
    }

    #[test]
    fn wave_and_arpeggio_render() {
        let source = "\
# Waves

## wobble

0 1
0.5 1 linear
0.5 -1
1 -1 linear

# Sounds

wave: wave wobble 0.8
arp: hard_square 0.6 duty 0.125, arpeggio 0 4 7 rate 20

# Layers

## Lead

wave 0 0 1
arp 1 0 1
";
        let song = parse(source).unwrap();
        let samples = render_song_stereo(&song, None).unwrap();
        assert!(samples.iter().any(|sample| sample.abs() > 0.01));
    }

    #[test]
    fn per_note_arpeggio_overrides_the_sound() {
        let render = |note: &str| {
            let source = format!("# Sounds\nlead: hard_square 0.6\n# Layers\n\n## A\n{note}\n");
            render_song_stereo(&parse(&source).unwrap(), None).unwrap()
        };
        // A note-level arpeggio must reshape the pitch a plain note lacks.
        let plain = render("lead 0 0 1");
        let arpeggiated = render("lead 0 0 1 arpeggio 0 4 7 rate 20");
        assert!(arpeggiated.iter().any(|sample| sample.abs() > 0.01));
        assert_ne!(plain, arpeggiated);
    }

    #[test]
    fn unknown_wave_errors() {
        let song = parse("# Sounds\nwave: wave ghost 1\n# Layers\n\n## A\nwave 0 0 1\n").unwrap();
        assert!(render_song_stereo(&song, None).is_err());
    }

    #[test]
    fn duty_cycle_changes_the_waveform() {
        let narrow =
            parse("# Sounds\np: hard_square 1 duty 0.125\n# Layers\n\n## A\np 0 0 0.5\n").unwrap();
        let square = parse("# Sounds\np: hard_square 1\n# Layers\n\n## A\np 0 0 0.5\n").unwrap();
        let narrow_samples = render_song_stereo(&narrow, None).unwrap();
        let square_samples = render_song_stereo(&square, None).unwrap();
        assert_ne!(narrow_samples, square_samples);
        let positive = narrow_samples
            .iter()
            .filter(|sample| **sample > 0.01)
            .count();
        let negative = narrow_samples
            .iter()
            .filter(|sample| **sample < -0.01)
            .count();
        assert!(positive * 3 < negative, "duty 0.125 should be mostly low");
    }

    #[test]
    fn stems_are_parallel_to_layers_and_equal_length() {
        let source = "\
# Sounds
bell: sine 1
# Layers

## Long
bell 0 0 4
# Layers

## Muted
@mute true
bell 0 0 1
# Layers

## Short
bell 0 0 1
";
        let song = parse(source).unwrap();
        let stems = super::render_song_stems(&song, None).unwrap();
        assert_eq!(stems.len(), 3);
        assert!(stems[1].is_empty());
        assert_eq!(stems[0].len(), stems[2].len());
        assert!(stems[2].iter().any(|sample| sample.abs() > 0.0));
    }

    #[test]
    fn render_output_never_exceeds_unity() {
        let source =
            String::from("# Sounds\nbell: sine 1\n") + &"# Layers\n\n## A\nbell 0 0 1\n".repeat(8);
        let song = parse(&source).unwrap();
        assert_eq!(song.layers.len(), 8);
        let samples = render_song_stereo(&song, None).unwrap();
        assert!(samples.iter().all(|sample| sample.abs() <= 1.0));
        assert!(samples.iter().any(|sample| sample.abs() > 0.8));
    }
}
