//! Sung notes via `phoneme-synth` (feature `speech`).
//!
//! A note's `text` holds one or more syllables: spaces separate words and
//! `'` separates syllables within a word (`"hal lo"` sings two words,
//! `"hal'lo"` one word of two syllables). A leading `'` attaches the note to
//! the previous note on the same sound, continuing its phrase and word — the
//! whole phrase is rendered with a single [`phoneme_synth::sing`] call, so
//! junctions morph smoothly instead of restarting the voice per note.
//! Syllable timing comes from [`phoneme_synth::syllable`] (consonants stay
//! short at the edges, vowels stretch); pitch is then reshaped along the
//! note's glide curve.

use super::{
    player::{PlayerError, SAMPLE_RATE},
    song::{
        Articulation as NoteArticulation, Curve, Sound, VoiceKnob, WavePoint, pitch_to_frequency,
        sample_envelope,
    },
    synth::apply_curve,
};
use phoneme_synth::{
    Articulation, Ensemble, Note as SungNote, Voice, sing, sing_ensemble, syllable,
};
use rodio::{ChannelCount, SampleRate, Source};
use std::{num::NonZero, time::Duration};

const RATE: SampleRate = NonZero::new(44100).unwrap();

/// One note of a phrase, with pitch and timing already resolved to Hz and
/// seconds (tuning and root applied by the caller).
pub(super) struct PhraseNote {
    pub text: String,
    pub articulation: NoteArticulation,
    /// Pitch trajectory in Hz over normalized note time (`x` 0..1), sampled
    /// with [`sample_envelope`]. A single point is a steady pitch; more points
    /// bend through them across the note's syllables.
    pub pitch_hz: Vec<WavePoint>,
    pub start_seconds: f32,
    pub seconds: f32,
    pub volume: [f32; 2],
    pub volume_curve: Curve,
}

/// Renders a phrase of notes on one voice sound into a mono buffer starting
/// at the first note's position. Gaps between attached notes stretch into the
/// preceding note's syllables.
pub(super) fn render_phrase(
    sound_name: &str,
    sound: &Sound,
    notes: &[PhraseNote],
) -> Result<Vec<f32>, PlayerError> {
    let voice = resolve_voice(sound)?;

    let mut sung: Vec<SungNote> = Vec::new();
    let mut note_ranges: Vec<[usize; 2]> = Vec::new();
    let mut elapsed = 0.0;
    for (index, note) in notes.iter().enumerate() {
        let mut syllables = split_syllables(&note.text);
        if syllables.is_empty() {
            return Err(PlayerError::MissingText(sound_name.to_string()));
        }
        if note.articulation == NoteArticulation::Legato
            && let Some(first) = syllables.first_mut()
        {
            first.1 = Articulation::Legato;
        }
        let gap = notes.get(index + 1).map_or(0.0, |next| {
            (next.start_seconds - (note.start_seconds + note.seconds)).max(0.0)
        });
        let syllable_seconds = (note.seconds + gap) / syllables.len() as f32;

        let note_start = elapsed;
        let frequency_at = |time: f32| {
            sample_envelope(
                &note.pitch_hz,
                ((time - note_start) / note.seconds).clamp(0.0, 1.0),
            )
        };
        let endpoints = [
            frequency_at(note_start),
            frequency_at(note_start + note.seconds),
        ];

        let range_start = (elapsed * SAMPLE_RATE as f32) as usize;
        for (text, articulation) in &syllables {
            let mut segments = syllable(text, endpoints, syllable_seconds)
                .map_err(|error| PlayerError::InvalidText(error.to_string()))?;
            if let Some(first) = segments.first_mut() {
                first.articulation = *articulation;
            }
            for segment in &mut segments {
                segment.pitch_hz = [
                    frequency_at(elapsed),
                    frequency_at(elapsed + segment.duration_seconds),
                ];
                elapsed += segment.duration_seconds;
            }
            sung.extend(segments);
        }
        note_ranges.push([range_start, (elapsed * SAMPLE_RATE as f32) as usize]);
    }

    let mut samples = if sound.voice_ensemble > 1 {
        let ensemble = Ensemble::from_voice(voice, sound.voice_ensemble);
        sing_ensemble(&sung, &ensemble, SAMPLE_RATE)
    } else {
        sing(&sung, voice, SAMPLE_RATE)
    };
    let total = samples.len();
    for (note, range) in notes.iter().zip(note_ranges) {
        let [range_start, range_end] = [range[0].min(total), range[1].min(total)];
        let length = (range_end - range_start).max(1);
        let [start_volume, end_volume] = note.volume;
        for (offset, sample) in samples[range_start..range_end].iter_mut().enumerate() {
            let ratio = apply_curve(offset as f32 / length as f32, note.volume_curve);
            *sample *= (start_volume + (end_volume - start_volume) * ratio) * sound.volume;
        }
    }
    Ok(samples)
}

fn resolve_voice(sound: &Sound) -> Result<Voice, PlayerError> {
    let mut voice: Voice = sound
        .voice
        .parse()
        .map_err(|_| PlayerError::UnknownVoice(sound.voice.clone()))?;
    for voice_override in &sound.voice_overrides {
        let target = match voice_override.knob {
            VoiceKnob::FormantShift => &mut voice.formant_shift,
            VoiceKnob::Tilt => &mut voice.tilt,
            VoiceKnob::Breathiness => &mut voice.breathiness,
            VoiceKnob::Vibrato => &mut voice.vibrato,
        };
        *target = voice_override.value;
    }
    Ok(voice)
}

/// Splits note text into syllables: whitespace separates words (the first
/// syllable of a word starts [`Separate`](Articulation::Separate)), `'`
/// separates syllables within a word (they continue
/// [`Legato`](Articulation::Legato)). A leading `'` therefore marks the
/// note's first syllable as a word continuation.
fn split_syllables(text: &str) -> Vec<(String, Articulation)> {
    let mut syllables = Vec::new();
    for word in text.split_whitespace() {
        for (index, part) in word.split('\'').enumerate() {
            if part.is_empty() {
                continue;
            }
            let articulation = if index == 0 {
                Articulation::Separate
            } else {
                Articulation::Legato
            };
            syllables.push((part.to_string(), articulation));
        }
    }
    syllables
}

/// A rendered sung note as a one-shot playable mono source (44.1 kHz).
///
/// Created by [`sing_note`]; pass it to `SoundSystem::play` to audition a
/// voice sound (e.g. from an editor's piano input).
pub struct SungClip {
    samples: Vec<f32>,
    position: usize,
}

impl Iterator for SungClip {
    type Item = f32;
    fn next(&mut self) -> Option<f32> {
        let sample = self.samples.get(self.position).copied()?;
        self.position += 1;
        Some(sample)
    }
}

impl Source for SungClip {
    fn current_span_len(&self) -> Option<usize> {
        None
    }
    fn channels(&self) -> ChannelCount {
        ChannelCount::MIN
    }
    fn sample_rate(&self) -> SampleRate {
        RATE
    }
    fn total_duration(&self) -> Option<Duration> {
        Some(Duration::from_secs_f64(
            self.samples.len() as f64 / SAMPLE_RATE as f64,
        ))
    }
}

/// Renders one sung note on a voice sound for auditioning: `text` syllables
/// at `pitch` semitones (12-tone equal temperament, 0 = A4 = 440 Hz) held
/// for `seconds`.
pub fn sing_note(
    sound: &Sound,
    text: &str,
    pitch: f32,
    seconds: f32,
) -> Result<SungClip, PlayerError> {
    let frequency = pitch_to_frequency(pitch);
    let note = PhraseNote {
        text: text.trim_start_matches('\'').to_string(),
        articulation: NoteArticulation::Separate,
        pitch_hz: vec![WavePoint {
            x: 0.0,
            y: frequency,
            curve: Curve::default(),
        }],
        start_seconds: 0.0,
        seconds,
        volume: [1.0; 2],
        volume_curve: Curve::default(),
    };
    let samples = render_phrase(&sound.voice, sound, &[note])?;
    Ok(SungClip {
        samples,
        position: 0,
    })
}

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

    fn phrase_note(text: &str, start_seconds: f32, seconds: f32) -> PhraseNote {
        PhraseNote {
            text: text.trim_start_matches('\'').into(),
            articulation: if text.starts_with('\'') {
                NoteArticulation::Legato
            } else {
                NoteArticulation::Separate
            },
            pitch_hz: vec![WavePoint {
                x: 0.0,
                y: 440.0,
                curve: Curve::default(),
            }],
            start_seconds,
            seconds,
            volume: [1.0, 1.0],
            volume_curve: Curve::default(),
        }
    }

    fn voice_sound(preset: &str) -> Sound {
        Sound {
            voice: preset.into(),
            ..Sound::default()
        }
    }

    #[test]
    fn splits_words_and_syllables() {
        let syllables = split_syllables("hal'lo du");
        let texts: Vec<&str> = syllables.iter().map(|(text, _)| text.as_str()).collect();
        assert_eq!(texts, ["hal", "lo", "du"]);
        assert_eq!(syllables[0].1, Articulation::Separate);
        assert_eq!(syllables[1].1, Articulation::Legato);
        assert_eq!(syllables[2].1, Articulation::Separate);
    }

    #[test]
    fn leading_apostrophe_continues_the_word() {
        let syllables = split_syllables("'lo");
        assert_eq!(syllables.len(), 1);
        assert_eq!(syllables[0].1, Articulation::Legato);
    }

    #[test]
    fn renders_multi_syllable_note() {
        let sound = voice_sound("female");
        let notes = [phrase_note("hal'lo du", 0.0, 1.5)];
        let samples = render_phrase("choir", &sound, &notes).unwrap();
        let expected = (1.5 * SAMPLE_RATE as f32) as usize;
        assert!(samples.len() as f32 > expected as f32 * 0.9);
        assert!(samples.iter().any(|sample| sample.abs() > 0.01));
    }

    #[test]
    fn phrase_spans_all_notes() {
        let sound = voice_sound("female");
        let notes = [
            phrase_note("ha", 0.0, 0.5),
            phrase_note("'lo", 0.5, 0.5),
            phrase_note("du", 1.0, 0.5),
        ];
        let samples = render_phrase("choir", &sound, &notes).unwrap();
        let expected = (1.5 * SAMPLE_RATE as f32) as usize;
        assert!(samples.len() as f32 > expected as f32 * 0.9);
        let late = &samples[expected * 9 / 10..];
        assert!(late.iter().any(|sample| sample.abs() > 0.001));
    }

    #[test]
    fn multi_point_pitch_bend_differs_from_a_steady_note() {
        let sound = voice_sound("female");
        let bend = |points: Vec<WavePoint>| {
            let note = PhraseNote {
                pitch_hz: points,
                ..phrase_note("la", 0.0, 0.6)
            };
            render_phrase("choir", &sound, &[note]).unwrap()
        };
        let steady = bend(vec![WavePoint {
            x: 0.0,
            y: 440.0,
            curve: Curve::default(),
        }]);
        // A mid-note peak up to 660 Hz must reshape the middle, not just the ends.
        let arch = bend(vec![
            WavePoint {
                x: 0.0,
                y: 440.0,
                curve: Curve::default(),
            },
            WavePoint {
                x: 0.5,
                y: 660.0,
                curve: Curve::default(),
            },
            WavePoint {
                x: 1.0,
                y: 440.0,
                curve: Curve::default(),
            },
        ]);
        assert_ne!(steady, arch);
    }

    #[test]
    fn voice_overrides_change_the_sound() {
        let plain = voice_sound("female");
        let mut breathy = voice_sound("female");
        breathy.voice_overrides.push(crate::track::VoiceOverride {
            knob: VoiceKnob::Breathiness,
            value: 0.9,
        });
        let notes = [phrase_note("la", 0.0, 0.5)];
        let plain_samples = render_phrase("choir", &plain, &notes).unwrap();
        let breathy_samples = render_phrase("choir", &breathy, &notes).unwrap();
        assert_ne!(plain_samples, breathy_samples);
    }

    #[test]
    fn sing_note_is_a_playable_source() {
        let sound = voice_sound("female");
        let clip = sing_note(&sound, "la", 0.0, 0.4).unwrap();
        assert_eq!(clip.channels(), ChannelCount::MIN);
        assert!(clip.total_duration().unwrap().as_secs_f32() > 0.3);
        let samples: Vec<f32> = clip.collect();
        assert!(samples.iter().any(|sample| sample.abs() > 0.01));
    }

    #[test]
    fn unknown_voice_errors() {
        let sound = voice_sound("kraken");
        let notes = [phrase_note("la", 0.0, 0.5)];
        assert!(render_phrase("choir", &sound, &notes).is_err());
    }

    #[test]
    fn missing_text_errors() {
        let sound = voice_sound("male");
        for text in ["", "'"] {
            let notes = [phrase_note(text, 0.0, 0.5)];
            assert!(render_phrase("choir", &sound, &notes).is_err());
        }
    }

    #[test]
    fn invalid_phoneme_errors() {
        let sound = voice_sound("male");
        let notes = [phrase_note("l3", 0.0, 0.5)];
        assert!(render_phrase("choir", &sound, &notes).is_err());
    }
}
