use super::{
    format::{ParseError, parse},
    song::{
        AudioClip, Compare, Condition, ConditionSource, Curve, Effect, Envelope, Jump, Layer,
        Modulation, MotifInstance, MotifNote, Note, NoteOverride, Slide, Song, Sound,
        TimeSignature, Waveform,
    },
};
use std::{
    fmt::{self, Display, Write as _},
    str::FromStr,
};

impl Display for Song {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(&render(self))
    }
}

impl FromStr for Song {
    type Err = ParseError;
    fn from_str(source: &str) -> Result<Self, Self::Err> {
        parse(source)
    }
}

fn render(song: &Song) -> String {
    let mut out = String::new();
    let _ = writeln!(out, "@tempo {}", trim_float(song.tempo));
    if song.signature != TimeSignature::default() {
        let _ = writeln!(
            out,
            "@signature {} {}",
            song.signature.beats_per_bar, song.signature.beat_unit
        );
    }
    for import in &song.imports {
        let refresh = if import.refresh { " refresh" } else { "" };
        let _ = writeln!(out, "@import {}{refresh}", import.path);
    }
    for (name, default) in &song.params {
        let _ = writeln!(out, "@param {name} {}", trim_float(*default));
    }
    if let Some(length) = song.length {
        let _ = writeln!(out, "@length {}", trim_float(length));
    }
    for jump in &song.jumps {
        let _ = writeln!(out, "{}", format_jump(jump));
    }
    out.push('\n');

    let local_sounds: Vec<(&String, &Sound)> = song
        .sounds
        .iter()
        .filter(|(name, _)| song.local_sound_names.contains(name.as_str()))
        .collect();
    if !local_sounds.is_empty() {
        out.push_str("# Sounds\n\n");
        for (name, sound) in local_sounds {
            let _ = writeln!(out, "{}: {}", name, sound_parameters(sound));
        }
        out.push('\n');
    }

    for (name, tuning) in &song.tunings {
        if song.tuning_origins.contains_key(name.as_str()) {
            continue;
        }
        let _ = writeln!(out, "# Tuning {name}\n");
        if (tuning.period - 2.0).abs() > f32::EPSILON {
            let _ = writeln!(out, "period {}", trim_float(tuning.period));
        }
        let degrees: Vec<String> = tuning
            .degrees
            .iter()
            .map(|degree| trim_float(*degree))
            .collect();
        let _ = writeln!(out, "{}", degrees.join(" "));
        out.push('\n');
    }

    for (name, notes) in &song.motifs {
        if song.motif_origins.contains_key(name.as_str()) {
            continue;
        }
        let _ = writeln!(out, "# Motif {name}\n");
        for note in notes {
            let _ = writeln!(out, "{}", format_motif_note(note));
        }
        out.push('\n');
    }

    for layer in &song.layers {
        let _ = writeln!(out, "# Layer {}", layer.name);
        let directives = layer_directives(layer);
        if !directives.is_empty() {
            let _ = writeln!(out, "{directives}");
        }
        out.push('\n');
        let mut notes: Vec<&Note> = layer.notes.iter().collect();
        notes.sort_by(|a, b| a.start.total_cmp(&b.start));
        for note in notes {
            let _ = writeln!(out, "{}", format_note(note));
        }
        for instance in &layer.motif_instances {
            let _ = writeln!(out, "{}", format_motif_instance(instance));
            for note_override in &instance.overrides {
                let _ = writeln!(out, "  {}", format_note_override(note_override));
            }
            if let Some(trim) = instance.trim {
                let _ = writeln!(out, "  trim {} {}", trim.from, trim.count);
            }
        }
        for clip in &layer.audio {
            let _ = writeln!(out, "{}", format_audio(clip));
        }
        out.push('\n');
    }

    for section in &song.unknown {
        let _ = writeln!(out, "# {}", section.header);
        for line in &section.lines {
            let _ = writeln!(out, "{line}");
        }
        out.push('\n');
    }

    out
}

fn format_jump(jump: &Jump) -> String {
    let mut line = format!("@jump {}", jump.target);
    if jump.skip != 0.0 {
        let _ = write!(line, " {}", trim_float(jump.skip));
    }
    if let Some(condition) = &jump.condition {
        let _ = write!(line, " if {}", format_condition(condition));
    }
    line
}

fn format_condition(condition: &Condition) -> String {
    format!(
        "{} {} {}",
        condition_source_token(&condition.source),
        compare_token(condition.compare),
        trim_float(condition.value)
    )
}

fn condition_source_token(source: &ConditionSource) -> String {
    match source {
        ConditionSource::Plays => "plays".to_string(),
        ConditionSource::Random => "random".to_string(),
        ConditionSource::Param(name) => name.clone(),
    }
}

const fn compare_token(compare: Compare) -> &'static str {
    match compare {
        Compare::Less => "<",
        Compare::LessEqual => "<=",
        Compare::Greater => ">",
        Compare::GreaterEqual => ">=",
        Compare::Equal => "==",
        Compare::NotEqual => "!=",
    }
}

fn append_note_options(
    line: &mut String,
    glide: f32,
    pitch_curve: Curve,
    volume: f32,
    volume_ramp: f32,
    volume_curve: Curve,
) {
    if glide != 0.0 {
        let _ = write!(
            line,
            " glide {}{}",
            trim_float(glide),
            curve_suffix(pitch_curve)
        );
    }
    if (volume - 1.0).abs() > f32::EPSILON {
        let _ = write!(line, " volume {}", trim_float(volume));
    }
    if volume_ramp != 0.0 {
        let _ = write!(
            line,
            " vol-ramp {}{}",
            trim_float(volume_ramp),
            curve_suffix(volume_curve)
        );
    }
}

fn format_note(note: &Note) -> String {
    let mut line = format!(
        "{} {} {} {}",
        note.sound,
        trim_float(note.start),
        trim_float(note.pitch),
        trim_float(note.duration)
    );
    append_note_options(
        &mut line,
        note.glide,
        note.pitch_curve,
        note.volume,
        note.volume_ramp,
        note.volume_curve,
    );
    line
}

fn format_motif_note(note: &MotifNote) -> String {
    let mut line = format!(
        "{} {} {}",
        trim_float(note.start),
        trim_float(note.pitch),
        trim_float(note.duration)
    );
    append_note_options(
        &mut line,
        note.glide,
        note.pitch_curve,
        note.volume,
        note.volume_ramp,
        note.volume_curve,
    );
    line
}

fn format_motif_instance(instance: &MotifInstance) -> String {
    let mut line = format!(
        "motif {} {} {}",
        instance.name,
        trim_float(instance.start),
        instance.sound
    );
    if instance.transpose != 0.0 {
        let _ = write!(line, " transpose {}", trim_float(instance.transpose));
    }
    if let Some(tuning) = &instance.tuning {
        let _ = write!(line, " tuning {tuning}");
    }
    line
}

fn format_note_override(note_override: &NoteOverride) -> String {
    let mut line = format!("note {}", note_override.index);
    for (token, value) in [
        ("pitch", note_override.pitch),
        ("start", note_override.start),
        ("duration", note_override.duration),
        ("glide", note_override.glide),
        ("volume", note_override.volume),
    ] {
        if let Some(value) = value {
            let _ = write!(line, " {token} {}", trim_float(value));
        }
    }
    line
}

const fn curve_suffix(curve: Curve) -> &'static str {
    match (curve.ease_in, curve.ease_out) {
        (true, true) => "",
        (false, false) => " linear",
        (true, false) => " ease-in",
        (false, true) => " ease-out",
    }
}

fn format_audio(clip: &AudioClip) -> String {
    format!("{} {}", clip.file, trim_float(clip.start))
}

fn sound_parameters(sound: &Sound) -> String {
    let mut parts: Vec<String> = sound
        .harmonics
        .iter()
        .map(|harmonic| {
            let waveform = waveform_token(harmonic.waveform);
            if (harmonic.multiplier - 1.0).abs() < f32::EPSILON {
                format!("{waveform} {}", trim_float(harmonic.weight))
            } else {
                format!(
                    "{waveform}@{} {}",
                    trim_float(harmonic.multiplier),
                    trim_float(harmonic.weight)
                )
            }
        })
        .collect();
    if sound.distortion != 0.0 {
        parts.push(format!("distortion {}", trim_float(sound.distortion)));
    }
    if sound.attack.duration > 0.0 {
        parts.push(format_envelope("attack", sound.attack));
    }
    if sound.decay.duration > 0.0 {
        parts.push(format_envelope("decay", sound.decay));
    }
    if (sound.sustain - 1.0).abs() > f32::EPSILON {
        parts.push(format!("sustain {}", trim_float(sound.sustain)));
    }
    if sound.noise != 0.0 {
        parts.push(format!("noise {}", trim_float(sound.noise)));
    }
    if sound.vibrato.depth > 0.0 {
        parts.push(format_modulation("vibrato", sound.vibrato));
    }
    if sound.tremolo.depth > 0.0 {
        parts.push(format_modulation("tremolo", sound.tremolo));
    }
    if sound.slide.time > 0.0 {
        parts.push(format_slide(sound.slide));
    }
    if (sound.volume - 1.0).abs() > f32::EPSILON {
        parts.push(format!("volume {}", trim_float(sound.volume)));
    }
    if parts.is_empty() {
        "sine 1.0".to_string()
    } else {
        parts.join(", ")
    }
}

fn format_modulation(name: &str, modulation: Modulation) -> String {
    let mut text = format!(
        "{name} {} {}",
        trim_float(modulation.rate),
        trim_float(modulation.depth)
    );
    if modulation.waveform != Waveform::Sine {
        let _ = write!(text, " {}", waveform_token(modulation.waveform));
    }
    text
}

fn format_slide(slide: Slide) -> String {
    format!(
        "slide {} {}",
        trim_float(slide.amount),
        trim_float(slide.time)
    )
}

const fn waveform_token(waveform: Waveform) -> &'static str {
    match waveform {
        Waveform::Sine => "sine",
        Waveform::Triangle => "triangle",
        Waveform::SoftSquare => "soft_square",
        Waveform::Square => "hard_square",
        Waveform::Saw => "saw",
        Waveform::Hill => "hill",
    }
}

fn layer_directives(layer: &Layer) -> String {
    let mut parts = Vec::new();
    if layer.volume != 1.0 {
        parts.push(format!("@volume {}", trim_float(layer.volume)));
    }
    if layer.pan != 0.0 {
        parts.push(format!("@pan {}", trim_float(layer.pan)));
    }
    if layer.muted {
        parts.push("@mute true".to_string());
    }
    if layer.solo {
        parts.push("@solo true".to_string());
    }
    if let Some(length) = layer.loop_length {
        if layer.loop_skip == 0.0 {
            parts.push(format!("@loop {}", trim_float(length)));
        } else {
            parts.push(format!(
                "@loop {} skip {}",
                trim_float(length),
                trim_float(layer.loop_skip)
            ));
        }
    }
    if let Some(tuning) = &layer.tuning {
        parts.push(format!("@tuning {tuning}"));
    }
    if layer.root != 440.0 {
        parts.push(format!("@root {}", trim_float(layer.root)));
    }
    for effect in &layer.effects {
        parts.push(format!("@effect {}", format_effect(effect)));
    }
    parts.join(" ")
}

fn format_effect(effect: &Effect) -> String {
    match effect {
        Effect::Lowpass { cutoff } => format!("lowpass {}", trim_float(*cutoff)),
        Effect::Highpass { cutoff } => format!("highpass {}", trim_float(*cutoff)),
        Effect::Echo {
            delay,
            feedback,
            mix,
        } => format!(
            "echo {} {} {}",
            trim_float(*delay),
            trim_float(*feedback),
            trim_float(*mix)
        ),
        Effect::Tremolo { rate, depth } => {
            format!("tremolo {} {}", trim_float(*rate), trim_float(*depth))
        }
        Effect::Bitcrusher { bits } => format!("bitcrusher {bits}"),
    }
}

fn format_envelope(name: &str, envelope: Envelope) -> String {
    format!(
        "{name} {}{}",
        trim_float(envelope.duration),
        curve_suffix(envelope.curve)
    )
}

fn trim_float(value: f32) -> String {
    if value.fract() == 0.0 {
        format!("{}", value as i64)
    } else {
        format!("{value}")
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::unwrap_used)]
    use super::{Curve, Waveform};
    use crate::track::{Song, parse};

    #[test]
    fn full_feature_round_trip_is_idempotent() {
        let source = "\
@tempo 96
@signature 3 4
@param combat 0
@length 32
@jump loop.track 4 if combat > 0
@jump end.track

# Sounds

lead: saw 0.5, attack 0.01 linear, decay 0.2, noise 0.1, vibrato 5 0.3 triangle, slide -2 0.1

# Tuning minor

period 2
1 1.2 1.5

# Motif hook

0 0 1
1 7 1 glide 3 volume 0.4 vol-ramp 0.5 ease-in

# Layer Bass
@volume 0.8 @pan -0.3 @tuning minor @root 220 @effect lowpass 800

lead 0 0 2 glide 5 volume 0.7 vol-ramp -0.2 linear
motif hook 4 lead transpose 12 tuning minor
kick.wav 8
";
        let song = parse(source).unwrap();
        let first = song.to_string();
        let reparsed = parse(&first).unwrap();
        let second = reparsed.to_string();
        assert_eq!(first, second, "serialize is not idempotent");
        assert_eq!(reparsed.signature.beats_per_bar, 3);
        assert_eq!(reparsed.sounds["lead"].slide.amount, -2.0);
        assert_eq!(reparsed.sounds["lead"].vibrato.waveform, Waveform::Triangle);
    }

    #[test]
    fn motif_overrides_and_trim_round_trip() {
        let source = "\
@tempo 120

# Sounds

bell: sine 1

# Motif hook

0 0 1
1 7 1

# Layer A

motif hook 0 bell transpose 2
  note 1 pitch 12 volume 0.5
  note 0 start 0.5
  trim 0 2
";
        let song = parse(source).unwrap();
        let again = parse(&song.to_string()).unwrap();
        let instance = &again.layers[0].motif_instances[0];
        assert_eq!(instance.overrides.len(), 2);
        assert_eq!(instance.overrides[0].index, 1);
        assert_eq!(instance.overrides[0].pitch, Some(12.0));
        assert_eq!(instance.overrides[0].volume, Some(0.5));
        assert_eq!(instance.overrides[1].start, Some(0.5));
        assert_eq!(
            instance.trim.map(|trim| (trim.from, trim.count)),
            Some((0, 2))
        );
    }

    #[test]
    fn smooth_curve_is_default_and_omitted() {
        let source = "@tempo 120\n# Sounds\nbell: sine 1\n# Layer A\nbell 0 0 2 glide 5\n";
        let serialized = parse(source).unwrap().to_string();
        assert!(serialized.contains("glide 5\n"), "{serialized}");
        assert!(!serialized.contains("ease"));
    }

    #[test]
    fn canonical_source_is_a_fixpoint() {
        // A canonically-formatted file must come back byte-for-byte unchanged
        // after parse → serialize, so importing then exporting is a no-op.
        let source = "@tempo 120\n\n# Sounds\n\nbell: sine 1\n\n# Layer A\n\nbell 0 0 1\n\n";
        assert_eq!(parse(source).unwrap().to_string(), source);
    }

    #[test]
    fn canonical_source_with_directives_is_a_fixpoint() {
        let source = "@tempo 96\n@signature 3 4\n\n# Sounds\n\nbell: sine 1, decay 0.2\n\n# Layer Melody\n@volume 0.8 @pan -0.3\n\nbell 0 0 2 glide 5 linear\n\n";
        assert_eq!(parse(source).unwrap().to_string(), source);
    }

    #[test]
    fn from_str_parses() {
        let song: Song = "@tempo 90\n# Layer A\n".parse().unwrap();
        assert_eq!(song.tempo, 90.0);
    }

    #[test]
    fn linear_curve_round_trips() {
        let source = "@tempo 120\n# Sounds\nbell: sine 1\n# Layer A\nbell 0 0 2 glide 5 linear\n";
        let again = parse(&parse(source).unwrap().to_string()).unwrap();
        assert_eq!(again.layers[0].notes[0].pitch_curve, Curve::LINEAR);
    }
}
