use super::song::{Curve, Sound, Waveform};
use crate::waveform::{hill, saw, soft_square, square, triangle};
use rodio::{ChannelCount, SampleRate, Source};
use std::{num::NonZero, time::Duration};

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

const MASTER_GAIN: f32 = 0.15;

fn apply_curve(ratio: f32, curve: Curve) -> f32 {
    use std::f32::consts::{FRAC_PI_2, PI};
    match (curve.ease_in, curve.ease_out) {
        (false, false) => ratio,
        (false, true) => (ratio * FRAC_PI_2).sin(),
        (true, false) => 1.0 - (ratio * FRAC_PI_2).cos(),
        (true, true) => 0.5 * (1.0 - (ratio * PI).cos()),
    }
}

/// Bipolar (-1..1) sample of `waveform` at the given phase in radians.
fn wave_sample(waveform: Waveform, phase: f32) -> f32 {
    match waveform {
        Waveform::Sine => phase.sin(),
        Waveform::Triangle => triangle(phase),
        Waveform::SoftSquare => soft_square(phase),
        Waveform::Square => square(phase),
        Waveform::Saw => saw(phase),
        Waveform::Hill => hill(phase),
    }
}

/// A sounding synthesizer voice for one note: sound, frequency, and duration.
///
/// Implements `rodio::Source` (mono, 44100 Hz). The waveform mix, envelope
/// and modulation come from the [`Sound`] definition.
pub struct Voice {
    sample_rate: u32,
    samples_total: usize,
    samples_played: usize,
    phase: f32,
    frequency: f32,
    end_frequency: f32,
    pitch_curve: Curve,
    volume: f32,
    end_volume: f32,
    volume_curve: Curve,
    sound: Sound,
    attack_samples: usize,
    decay_samples: usize,
    noise_state: u32,
}

impl Voice {
    /// Creates a note source with the given sound, fundamental frequency
    /// (Hz), and total duration. Pitch and volume are steady.
    pub fn new(sound: Sound, frequency: f32, duration: Duration) -> Self {
        Self::ramped(
            sound,
            [frequency, frequency],
            Curve::LINEAR,
            [1.0, 1.0],
            Curve::LINEAR,
            duration,
        )
    }

    /// Creates a note that glides frequency `[start, end]` along `pitch_curve`
    /// and ramps volume `[start, end]` along `volume_curve` over its duration.
    pub fn ramped(
        sound: Sound,
        frequency: [f32; 2],
        pitch_curve: Curve,
        volume: [f32; 2],
        volume_curve: Curve,
        duration: Duration,
    ) -> Self {
        let sample_rate = 44100;
        let samples_total = (sample_rate as f64 * duration.as_secs_f64()) as usize;
        let attack_samples = (sample_rate as f32 * sound.attack.duration) as usize;
        let decay_samples = (sample_rate as f32 * sound.decay.duration) as usize;
        let [frequency, end_frequency] = frequency;
        let [volume, end_volume] = volume;
        Self {
            sample_rate,
            samples_total,
            samples_played: 0,
            phase: 0.0,
            frequency,
            end_frequency,
            pitch_curve,
            volume,
            end_volume,
            volume_curve,
            sound,
            attack_samples,
            decay_samples,
            noise_state: 0x1234_5678,
        }
    }

    fn current_volume(&self) -> f32 {
        if self.volume == self.end_volume || self.samples_total == 0 {
            return self.volume;
        }
        let ratio = self.samples_played as f32 / self.samples_total as f32;
        let shaped = apply_curve(ratio, self.volume_curve);
        self.volume + (self.end_volume - self.volume) * shaped
    }

    fn time(&self) -> f32 {
        self.samples_played as f32 / self.sample_rate as f32
    }

    fn next_noise(&mut self) -> f32 {
        self.noise_state = self
            .noise_state
            .wrapping_mul(1103515245)
            .wrapping_add(12345);
        (self.noise_state >> 16) as f32 / 32768.0 - 1.0
    }

    fn current_frequency(&self) -> f32 {
        let base = if self.frequency == self.end_frequency || self.samples_total == 0 {
            self.frequency
        } else {
            let ratio = self.samples_played as f32 / self.samples_total as f32;
            let shaped = apply_curve(ratio, self.pitch_curve);
            self.frequency + (self.end_frequency - self.frequency) * shaped
        };
        let mut semitones = 0.0;
        let slide = self.sound.slide;
        if slide.time > 0.0 {
            let remaining = (1.0 - self.time() / slide.time).max(0.0);
            semitones += slide.amount * remaining;
        }
        let vibrato = self.sound.vibrato;
        if vibrato.depth != 0.0 {
            let phase = std::f32::consts::TAU * vibrato.rate * self.time();
            semitones += vibrato.depth * wave_sample(vibrato.waveform, phase);
        }
        if semitones == 0.0 {
            base
        } else {
            base * 2.0_f32.powf(semitones / 12.0)
        }
    }

    fn envelope(&self) -> f32 {
        let position = self.samples_played;
        let attack_gain = if position >= self.attack_samples {
            1.0
        } else {
            let ratio = position as f32 / self.attack_samples.max(1) as f32;
            apply_curve(ratio, self.sound.attack.curve)
        };
        let decay_start = self.samples_total.saturating_sub(self.decay_samples);
        let decay_gain = if self.decay_samples == 0 || position < decay_start {
            1.0
        } else {
            let into_decay = position - decay_start;
            let ratio = 1.0 - (into_decay as f32 / self.decay_samples as f32);
            apply_curve(ratio, self.sound.decay.curve)
        };
        attack_gain.min(decay_gain) * self.sound.sustain
    }

    fn synthesize(&self, noise: f32) -> f32 {
        let mut clean = 0.0;
        for harmonic in &self.sound.harmonics {
            let scaled_phase = self.phase * harmonic.multiplier;
            clean += wave_sample(harmonic.waveform, scaled_phase) * harmonic.weight;
        }
        clean += noise * self.sound.noise;
        if self.sound.distortion > 0.0 {
            (clean * (1.0 + self.sound.distortion)).tanh()
        } else {
            clean
        }
    }

    fn tremolo_gain(&self) -> f32 {
        let tremolo = self.sound.tremolo;
        if tremolo.depth == 0.0 {
            return 1.0;
        }
        let phase = std::f32::consts::TAU * tremolo.rate * self.time();
        let unipolar = wave_sample(tremolo.waveform, phase) * 0.5 + 0.5;
        1.0 - unipolar * tremolo.depth
    }
}

impl Iterator for Voice {
    type Item = f32;
    fn next(&mut self) -> Option<f32> {
        if self.samples_played >= self.samples_total {
            return None;
        }
        let envelope = self.envelope();
        let tremolo = self.tremolo_gain();
        let noise = if self.sound.noise > 0.0 {
            self.next_noise()
        } else {
            0.0
        };
        let sample = self.synthesize(noise)
            * envelope
            * tremolo
            * self.sound.volume
            * self.current_volume()
            * MASTER_GAIN;
        self.phase += std::f32::consts::TAU * self.current_frequency() / self.sample_rate as f32;
        self.samples_played += 1;
        Some(sample)
    }
}

impl Source for Voice {
    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_total as f64 / self.sample_rate as f64,
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::apply_curve;
    use crate::track::Curve;

    const EASE_IN: Curve = Curve {
        ease_in: true,
        ease_out: false,
    };
    const EASE_OUT: Curve = Curve {
        ease_in: false,
        ease_out: true,
    };
    const EASE_BOTH: Curve = Curve {
        ease_in: true,
        ease_out: true,
    };

    fn approx(a: f32, b: f32) -> bool {
        (a - b).abs() < 1e-5
    }

    #[test]
    fn curves_pin_endpoints() {
        for curve in [Curve::LINEAR, EASE_IN, EASE_OUT, EASE_BOTH] {
            assert!(approx(apply_curve(0.0, curve), 0.0));
            assert!(approx(apply_curve(1.0, curve), 1.0));
        }
    }

    #[test]
    fn linear_is_identity() {
        assert!(approx(apply_curve(0.5, Curve::LINEAR), 0.5));
    }

    #[test]
    fn ease_out_starts_fast_ease_in_starts_slow() {
        // At the midpoint, ease-out is already past halfway, ease-in behind.
        assert!(apply_curve(0.5, EASE_OUT) > 0.5);
        assert!(apply_curve(0.5, EASE_IN) < 0.5);
        // The S-curve is symmetric: exactly 0.5 at the midpoint.
        assert!(approx(apply_curve(0.5, EASE_BOTH), 0.5));
    }

    #[test]
    fn short_note_overlaps_attack_and_decay() {
        use super::Voice;
        use crate::track::{Envelope, Sound};
        use std::time::Duration;

        let ramp = Envelope {
            duration: 0.04,
            curve: Curve::LINEAR,
        };
        let sound = Sound {
            attack: ramp,
            decay: ramp,
            ..Sound::default()
        };
        // Note (50 ms) is shorter than attack + decay (80 ms): they overlap.
        let mut voice = Voice::new(sound, 440.0, Duration::from_millis(50));
        let total = voice.samples_total;
        let mut peak = 0.0f32;
        let mut envelope = Vec::with_capacity(total);
        for position in 0..total {
            voice.samples_played = position;
            let value = voice.envelope();
            peak = peak.max(value);
            envelope.push(value);
        }
        // Attack never fully opens before decay pulls it back → capped triangle.
        assert!(peak < 1.0);
        // Silent at both ends: no click.
        assert!(envelope[0] < 0.05);
        assert!(envelope[total - 1] < 0.1);
    }
}
