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

const MASTER_GAIN: f32 = 0.15;

fn apply_curve(ratio: f32, curve: Curve) -> f32 {
    match curve {
        Curve::Linear => ratio,
        Curve::Sine => (ratio * std::f32::consts::FRAC_PI_2).sin(),
    }
}

/// Synthesized note with the given sound, frequency, and duration.
///
/// Implements `rodio::Source` (mono, 44100 Hz). The waveform mix and ADSR
/// envelope come from the [`Sound`] definition.
pub struct Note {
    sample_rate: u32,
    samples_total: usize,
    samples_played: usize,
    phase: f32,
    frequency: f32,
    sound: Sound,
    attack_samples: usize,
    decay_samples: usize,
}

impl Note {
    /// Creates a note source with the given sound, fundamental frequency
    /// (Hz), and total duration.
    pub fn new(sound: Sound, frequency: f32, duration: Duration) -> Self {
        let sample_rate = 44100;
        let samples_total = (sample_rate as f64 * duration.as_secs_f64()) as usize;
        let attack_samples = sound.attack.map_or(0, |envelope| {
            (sample_rate as f32 * envelope.duration) as usize
        });
        let raw_decay = sound.decay.map_or(0, |envelope| {
            (sample_rate as f32 * envelope.duration) as usize
        });
        let decay_samples = raw_decay.min(samples_total.saturating_sub(attack_samples));
        Self {
            sample_rate,
            samples_total,
            samples_played: 0,
            phase: 0.0,
            frequency,
            sound,
            attack_samples,
            decay_samples,
        }
    }

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

    fn synthesize(&self) -> f32 {
        let mut clean = 0.0;
        for harmonic in &self.sound.harmonics {
            let scaled_phase = self.phase * harmonic.multiplier;
            let value = match harmonic.waveform {
                Waveform::Sine => scaled_phase.sin(),
                Waveform::Triangle => triangle(scaled_phase),
                Waveform::SoftSquare => soft_square(scaled_phase),
            };
            clean += value * harmonic.weight;
        }
        if self.sound.distortion > 0.0 {
            (clean * (1.0 + self.sound.distortion)).tanh()
        } else {
            clean
        }
    }
}

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

impl Source for Note {
    fn current_span_len(&self) -> Option<usize> {
        None
    }
    fn channels(&self) -> ChannelCount {
        NonZero::new(1).unwrap()
    }
    fn sample_rate(&self) -> SampleRate {
        NonZero::new(self.sample_rate).unwrap()
    }
    fn total_duration(&self) -> Option<Duration> {
        Some(Duration::from_secs_f64(
            self.samples_total as f64 / self.sample_rate as f64,
        ))
    }
}
