use super::{
    format::{ParseError, parse, resolve_imports},
    song::{Clip, Effect, PatternStep, Song, Track, pitch_to_frequency},
    synth::Note,
};
use crate::system::SoundSystem;
use rodio::{
    ChannelCount, Decoder, Player, SampleRate, Source, buffer::SamplesBuffer,
    source::UniformSourceIterator,
};
use std::{
    fs::File,
    io::BufReader,
    num::NonZero,
    path::{Path, PathBuf},
    time::Duration,
};

const SAMPLE_RATE: u32 = 44100;
const MONO: ChannelCount = NonZero::new(1).unwrap();
const STEREO: ChannelCount = NonZero::new(2).unwrap();
const RATE: SampleRate = NonZero::new(SAMPLE_RATE).unwrap();

/// Errors that can occur while loading or playing a song.
#[derive(Debug, thiserror::Error)]
pub enum PlayerError {
    /// Failed to allocate a per-track sink on the audio system.
    #[error("could not create sink")]
    SinkCreate,
    /// A note references a sound name that doesn't exist in the song.
    #[error("unknown sound '{0}'")]
    UnknownSound(String),
    /// An audio file referenced by a clip could not be opened.
    #[error("could not open audio file '{path}': {error}")]
    AudioOpen {
        /// Path that failed to open.
        path: PathBuf,
        /// Underlying I/O error.
        error: std::io::Error,
    },
    /// An audio file could not be decoded.
    #[error("could not decode audio file '{path}': {error}")]
    AudioDecode {
        /// Path that failed.
        path: PathBuf,
        /// Underlying decoder error.
        error: rodio::decoder::DecoderError,
    },
    /// Reading a song file from disk failed.
    #[error("read error: {0}")]
    Read(std::io::Error),
    /// Parsing a song file failed.
    #[error("parse error: {0}")]
    Parse(#[from] ParseError),
}

/// Plays a [`Song`] through a [`SoundSystem`].
///
/// Each track is pre-mixed into a stereo buffer and submitted to its own
/// sink. Looping tracks repeat infinitely. The player keeps the sinks alive
/// until [`SongPlayer::stop`] or another `play` call.
#[derive(Default)]
pub struct SongPlayer {
    track_sinks: Vec<Player>,
}

impl SongPlayer {
    /// Creates an idle player with no tracks playing.
    pub fn new() -> Self {
        Self::default()
    }

    /// Loads a `.track` file from disk and starts playing it.
    ///
    /// Resolves `@import` references relative to the file's directory.
    pub fn play_path(
        &mut self,
        system: &SoundSystem,
        path: impl AsRef<Path>,
    ) -> Result<(), PlayerError> {
        let path = path.as_ref();
        let source = std::fs::read_to_string(path).map_err(PlayerError::Read)?;
        let mut song = parse(&source)?;
        resolve_imports(&mut song, path.parent())?;
        self.play(system, &song, path.parent(), 0.0)
    }

    /// Plays a song that has already been loaded.
    ///
    /// `base_directory` is used to resolve relative paths in audio clips.
    /// `offset_beats` skips the start of the song without changing scheduling.
    pub fn play(
        &mut self,
        system: &SoundSystem,
        song: &Song,
        base_directory: Option<&Path>,
        offset_beats: f32,
    ) -> Result<(), PlayerError> {
        self.stop();
        let beat_duration = 60.0 / song.tempo.max(1.0);
        let any_solo = song.tracks.iter().any(|track| track.solo);
        let offset_seconds = (offset_beats * beat_duration).max(0.0);

        for track in &song.tracks {
            if track.muted {
                continue;
            }
            if any_solo && !track.solo {
                continue;
            }

            let mut buffer = render_track(track, song, base_directory, beat_duration)?;
            if buffer.is_empty() {
                continue;
            }
            apply_effects(&mut buffer, &track.effects, SAMPLE_RATE);

            let sink = system.create_sink().ok_or(PlayerError::SinkCreate)?;
            sink.set_volume(track.volume);

            let source = SamplesBuffer::new(STEREO, RATE, buffer);
            if track.loop_length.is_some() {
                sink.append(
                    source
                        .repeat_infinite()
                        .skip_duration(Duration::from_secs_f32(offset_seconds)),
                );
            } else {
                sink.append(source.skip_duration(Duration::from_secs_f32(offset_seconds)));
            }

            sink.play();
            self.track_sinks.push(sink);
        }
        Ok(())
    }

    /// Stops all per-track sinks created by this player.
    pub fn stop(&mut self) {
        for sink in &self.track_sinks {
            sink.stop();
        }
        self.track_sinks.clear();
    }

    /// Returns true while at least one track is still producing sound.
    ///
    /// Looping tracks never finish, so this stays true until [`SongPlayer::stop`].
    pub fn is_playing(&self) -> bool {
        self.track_sinks.iter().any(|sink| !sink.empty())
    }

    /// Sets playback speed for all tracks (1.0 = normal, 2.0 = double).
    ///
    /// Speed also shifts pitch, like a tape player. Applies to tracks
    /// currently playing; a later [`SongPlayer::play`] resets to 1.0.
    pub fn set_speed(&self, speed: f32) {
        for sink in &self.track_sinks {
            sink.set_speed(speed);
        }
    }
}

/// Renders one track into an interleaved stereo `Vec<f32>` at 44100 Hz.
///
/// All notes and audio clips are summed at their beat positions, panned
/// equal-power, and (if the track has a `loop_length`) trimmed to that
/// length so a downstream `repeat_infinite` produces seamless loops.
pub fn render_track(
    track: &Track,
    song: &Song,
    base_directory: Option<&Path>,
    beat_duration: f32,
) -> Result<Vec<f32>, PlayerError> {
    let (gain_left, gain_right) = pan_gains(track.pan);
    let mut buffer: Vec<f32> = Vec::new();

    let track_length_samples = track
        .loop_length
        .map(|length| (length * beat_duration * SAMPLE_RATE as f32) as usize);

    for clip in &track.clips {
        match clip {
            Clip::Note {
                sound,
                start,
                pitch,
                duration,
            } => {
                render_note(
                    song,
                    &mut buffer,
                    sound,
                    *start,
                    *pitch,
                    *duration,
                    beat_duration,
                    gain_left,
                    gain_right,
                    track_length_samples,
                )?;
            }
            Clip::Pattern {
                sound,
                start,
                steps,
                total_duration,
            } => {
                let slot_duration = total_duration / steps.len() as f32;
                let mut current: Option<(f32, f32, f32)> = None;
                for (index, step) in steps.iter().enumerate() {
                    let slot_start = start + index as f32 * slot_duration;
                    match step {
                        PatternStep::Pitch(p) => {
                            if let Some((pitch, note_start, note_duration)) = current.take() {
                                render_note(
                                    song,
                                    &mut buffer,
                                    sound,
                                    note_start,
                                    pitch,
                                    note_duration,
                                    beat_duration,
                                    gain_left,
                                    gain_right,
                                    track_length_samples,
                                )?;
                            }
                            current = Some((*p, slot_start, slot_duration));
                        }
                        PatternStep::Hold => {
                            if let Some(entry) = current.as_mut() {
                                entry.2 += slot_duration;
                            }
                        }
                        PatternStep::Pause => {
                            if let Some((pitch, note_start, note_duration)) = current.take() {
                                render_note(
                                    song,
                                    &mut buffer,
                                    sound,
                                    note_start,
                                    pitch,
                                    note_duration,
                                    beat_duration,
                                    gain_left,
                                    gain_right,
                                    track_length_samples,
                                )?;
                            }
                        }
                    }
                }
                if let Some((pitch, note_start, note_duration)) = current {
                    render_note(
                        song,
                        &mut buffer,
                        sound,
                        note_start,
                        pitch,
                        note_duration,
                        beat_duration,
                        gain_left,
                        gain_right,
                        track_length_samples,
                    )?;
                }
            }
            Clip::Audio { file, start } => {
                let path = resolve_path(base_directory, file);
                let reader = File::open(&path).map_err(|error| PlayerError::AudioOpen {
                    path: path.clone(),
                    error,
                })?;
                let decoder = Decoder::new(BufReader::new(reader)).map_err(|error| {
                    PlayerError::AudioDecode {
                        path: path.clone(),
                        error,
                    }
                })?;
                let normalized = UniformSourceIterator::new(decoder, MONO, RATE);
                let start_sample = (start * beat_duration * SAMPLE_RATE as f32) as usize;
                mix_mono_into_stereo(
                    &mut buffer,
                    normalized,
                    start_sample,
                    gain_left,
                    gain_right,
                    track_length_samples,
                );
            }
        }
    }

    if let Some(loop_samples) = track_length_samples {
        let needed = loop_samples * 2;
        if buffer.len() < needed {
            buffer.resize(needed, 0.0);
        } else {
            buffer.truncate(needed);
        }
    }

    Ok(buffer)
}

fn apply_effects(buffer: &mut [f32], effects: &[Effect], sample_rate: u32) {
    for effect in effects {
        match *effect {
            Effect::Lowpass { cutoff } => apply_lowpass(buffer, cutoff, sample_rate),
            Effect::Highpass { cutoff } => apply_highpass(buffer, cutoff, sample_rate),
            Effect::Echo {
                delay,
                feedback,
                mix,
            } => apply_echo(buffer, delay, feedback, mix, sample_rate),
            Effect::Tremolo { rate, depth } => apply_tremolo(buffer, rate, depth, sample_rate),
            Effect::Bitcrusher { bits } => apply_bitcrusher(buffer, bits),
        }
    }
}

fn apply_lowpass(buffer: &mut [f32], cutoff: f32, sample_rate: u32) {
    let alpha = lowpass_alpha(cutoff, sample_rate);
    let mut y_l = 0.0_f32;
    let mut y_r = 0.0_f32;
    for chunk in buffer.chunks_exact_mut(2) {
        y_l += alpha * (chunk[0] - y_l);
        y_r += alpha * (chunk[1] - y_r);
        chunk[0] = y_l;
        chunk[1] = y_r;
    }
}

fn apply_highpass(buffer: &mut [f32], cutoff: f32, sample_rate: u32) {
    let alpha = lowpass_alpha(cutoff, sample_rate);
    let mut y_l = 0.0_f32;
    let mut y_r = 0.0_f32;
    let mut prev_l = 0.0_f32;
    let mut prev_r = 0.0_f32;
    for chunk in buffer.chunks_exact_mut(2) {
        y_l = (1.0 - alpha) * (y_l + chunk[0] - prev_l);
        y_r = (1.0 - alpha) * (y_r + chunk[1] - prev_r);
        prev_l = chunk[0];
        prev_r = chunk[1];
        chunk[0] = y_l;
        chunk[1] = y_r;
    }
}

fn lowpass_alpha(cutoff: f32, sample_rate: u32) -> f32 {
    let exponent = -std::f32::consts::TAU * cutoff.max(0.0) / sample_rate as f32;
    1.0 - exponent.exp()
}

fn apply_echo(buffer: &mut [f32], delay: f32, feedback: f32, mix: f32, sample_rate: u32) {
    let delay_samples = ((delay.max(0.0) * sample_rate as f32) as usize).max(1);
    let feedback = feedback.clamp(0.0, 0.95);
    let mix = mix.clamp(0.0, 1.0);
    let mut delay_l = vec![0.0_f32; delay_samples];
    let mut delay_r = vec![0.0_f32; delay_samples];
    let mut position = 0;
    for chunk in buffer.chunks_exact_mut(2) {
        let echoed_l = delay_l[position];
        let echoed_r = delay_r[position];
        let new_l = chunk[0] + echoed_l * feedback;
        let new_r = chunk[1] + echoed_r * feedback;
        delay_l[position] = new_l;
        delay_r[position] = new_r;
        chunk[0] = chunk[0] * (1.0 - mix) + echoed_l * mix;
        chunk[1] = chunk[1] * (1.0 - mix) + echoed_r * mix;
        position = (position + 1) % delay_samples;
    }
}

fn apply_tremolo(buffer: &mut [f32], rate: f32, depth: f32, sample_rate: u32) {
    let depth = depth.clamp(0.0, 1.0);
    let phase_step = std::f32::consts::TAU * rate / sample_rate as f32;
    let mut phase = 0.0_f32;
    for chunk in buffer.chunks_exact_mut(2) {
        let envelope = 1.0 - depth + depth * (phase.sin() * 0.5 + 0.5);
        chunk[0] *= envelope;
        chunk[1] *= envelope;
        phase += phase_step;
    }
}

fn apply_bitcrusher(buffer: &mut [f32], bits: u8) {
    let levels = 2.0_f32.powi(bits.clamp(1, 16) as i32);
    for sample in buffer.iter_mut() {
        *sample = (*sample * levels).round() / levels;
    }
}

#[allow(clippy::too_many_arguments)]
fn render_note(
    song: &Song,
    buffer: &mut Vec<f32>,
    sound_name: &str,
    start_beat: f32,
    pitch: f32,
    duration_beats: f32,
    beat_duration: f32,
    gain_left: f32,
    gain_right: f32,
    limit_samples: Option<usize>,
) -> Result<(), PlayerError> {
    let sound_def = song
        .sounds
        .get(sound_name)
        .ok_or_else(|| PlayerError::UnknownSound(sound_name.to_string()))?
        .clone();
    let start_sample = (start_beat * beat_duration * SAMPLE_RATE as f32) as usize;
    let note = Note::new(
        sound_def,
        pitch_to_frequency(pitch),
        Duration::from_secs_f32(duration_beats * beat_duration),
    );
    mix_mono_into_stereo(
        buffer,
        note,
        start_sample,
        gain_left,
        gain_right,
        limit_samples,
    );
    Ok(())
}

fn mix_mono_into_stereo<S: Iterator<Item = f32>>(
    buffer: &mut Vec<f32>,
    source: S,
    start_sample: usize,
    gain_left: f32,
    gain_right: f32,
    limit_samples: Option<usize>,
) {
    for (offset, sample) in source.enumerate() {
        let absolute = start_sample + offset;
        if let Some(limit) = limit_samples
            && absolute >= limit
        {
            break;
        }
        let buffer_index = absolute * 2;
        if buffer_index + 1 >= buffer.len() {
            buffer.resize(buffer_index + 2, 0.0);
        }
        buffer[buffer_index] += sample * gain_left;
        buffer[buffer_index + 1] += sample * gain_right;
    }
}

fn pan_gains(pan: f32) -> (f32, f32) {
    let clamped = pan.clamp(-1.0, 1.0);
    let angle = (clamped + 1.0) * std::f32::consts::FRAC_PI_4;
    (angle.cos(), angle.sin())
}

fn resolve_path(base: Option<&Path>, file: &str) -> PathBuf {
    let candidate = PathBuf::from(file);
    if candidate.is_absolute() {
        return candidate;
    }
    match base {
        Some(base) => base.join(file),
        None => candidate,
    }
}
