use game_audio::track::{Import, Layer, Note, Song};
use midly::{MetaMessage, MidiMessage, Smf, Timing, TrackEventKind};
use std::{collections::HashMap, path::PathBuf, process::ExitCode};

const KNOWN_SOUNDS: &[&str] = &[
    "lead",
    "marimba",
    "glock",
    "pluck",
    "pad",
    "bass",
    "drone",
    "reed",
    "flute",
    "sax",
    "piano",
    "panflute",
    "pulse1",
    "pulse2",
    "tri_bass",
    "blip",
    "arp",
    "lead_saw",
    "noise_hit",
    "kick",
    "tom",
    "snare",
    "clap",
    "rim",
    "hihat",
    "openhat",
];

fn program_sound(program: u8) -> &'static str {
    match program {
        0..=7 => "piano",
        8..=15 => "glock",
        16..=23 => "reed",
        24..=31 => "pluck",
        32..=39 => "bass",
        40..=55 => "pad",
        56..=63 => "reed",
        64..=71 => "sax",
        72..=79 => "flute",
        80..=87 => "lead",
        88..=103 => "pad",
        104..=111 => "pluck",
        112..=119 => "marimba",
        _ => "lead",
    }
}

fn drum_sound(key: u8) -> &'static str {
    match key {
        35 | 36 => "kick",
        37 => "rim",
        38 | 40 => "snare",
        39 => "clap",
        41 | 43 | 45 | 47 | 48 | 50 => "tom",
        42 | 44 => "hihat",
        46 => "openhat",
        _ => "hihat",
    }
}

fn note_sound(channel: u8, key: u8, program: u8, track_name: Option<&str>) -> String {
    if channel == 9 {
        return drum_sound(key).to_string();
    }
    track_name
        .map(str::to_ascii_lowercase)
        .filter(|name| KNOWN_SOUNDS.contains(&name.as_str()))
        .unwrap_or_else(|| program_sound(program).to_string())
}

fn build_note(
    channel: u8,
    key: u8,
    velocity: u8,
    start_tick: u32,
    end_tick: u32,
    ticks_per_beat: f32,
    program: u8,
    track_name: Option<&str>,
) -> Note {
    let is_drum = channel == 9;
    let held = end_tick.saturating_sub(start_tick) as f32 / ticks_per_beat;
    Note {
        sound: note_sound(channel, key, program, track_name),
        pitch: if is_drum { 0.0 } else { f32::from(key) - 69.0 },
        start: start_tick as f32 / ticks_per_beat,
        duration: if is_drum { 0.25 } else { held.max(0.0625) },
        volume: (f32::from(velocity) / 127.0 * 100.0).round() / 100.0,
        ..Note::default()
    }
}

fn convert_track(track: &[midly::TrackEvent], ticks_per_beat: f32) -> (Option<String>, Vec<Note>) {
    let mut absolute_tick: u32 = 0;
    let mut track_name: Option<String> = None;
    let mut programs = [0u8; 16];
    let mut pending: HashMap<(u8, u8), (u32, u8)> = HashMap::new();
    let mut notes: Vec<Note> = Vec::new();

    for event in track {
        absolute_tick += event.delta.as_int();
        match event.kind {
            TrackEventKind::Meta(
                MetaMessage::TrackName(bytes) | MetaMessage::InstrumentName(bytes),
            ) if track_name.is_none() => {
                let name = String::from_utf8_lossy(bytes).trim().to_string();
                if !name.is_empty() {
                    track_name = Some(name);
                }
            }
            TrackEventKind::Midi { channel, message } => {
                let channel = channel.as_int();
                match message {
                    MidiMessage::ProgramChange { program } => {
                        programs[channel as usize] = program.as_int();
                    }
                    MidiMessage::NoteOn { key, vel } if vel.as_int() > 0 => {
                        pending.insert((channel, key.as_int()), (absolute_tick, vel.as_int()));
                    }
                    MidiMessage::NoteOn { key, .. } | MidiMessage::NoteOff { key, .. } => {
                        if let Some((start_tick, velocity)) =
                            pending.remove(&(channel, key.as_int()))
                        {
                            notes.push(build_note(
                                channel,
                                key.as_int(),
                                velocity,
                                start_tick,
                                absolute_tick,
                                ticks_per_beat,
                                programs[channel as usize],
                                track_name.as_deref(),
                            ));
                        }
                    }
                    _ => {}
                }
            }
            _ => {}
        }
    }
    (track_name, notes)
}

fn first_tempo(smf: &Smf) -> (Option<u32>, usize) {
    let mut microseconds = None;
    let mut count = 0;
    for track in &smf.tracks {
        for event in track {
            if let TrackEventKind::Meta(MetaMessage::Tempo(value)) = event.kind {
                if microseconds.is_none() {
                    microseconds = Some(value.as_int());
                }
                count += 1;
            }
        }
    }
    (microseconds, count)
}

fn main() -> ExitCode {
    let mut args = std::env::args().skip(1);
    let Some(input) = args.next().map(PathBuf::from) else {
        eprintln!("Usage: midi-import <input.mid> [output.track]");
        return ExitCode::FAILURE;
    };
    let output = args
        .next()
        .map_or_else(|| input.with_extension("track"), PathBuf::from);

    let data = match std::fs::read(&input) {
        Ok(data) => data,
        Err(error) => {
            eprintln!("Reading {}: {error}", input.display());
            return ExitCode::FAILURE;
        }
    };
    let smf = match Smf::parse(&data) {
        Ok(smf) => smf,
        Err(error) => {
            eprintln!("Parsing {}: {error}", input.display());
            return ExitCode::FAILURE;
        }
    };
    let ticks_per_beat = match smf.header.timing {
        Timing::Metrical(value) => f32::from(value.as_int()),
        Timing::Timecode(..) => {
            eprintln!("SMPTE timecode timing is not supported");
            return ExitCode::FAILURE;
        }
    };

    let mut layers: Vec<Layer> = Vec::new();
    for (index, track) in smf.tracks.iter().enumerate() {
        let (name, notes) = convert_track(track, ticks_per_beat);
        if notes.is_empty() {
            continue;
        }
        let mut layer = Layer::new(name.unwrap_or_else(|| format!("Track {}", index + 1)));
        layer.notes = notes;
        layers.push(layer);
    }

    if layers.is_empty() {
        eprintln!("No notes found in {}", input.display());
        return ExitCode::FAILURE;
    }

    let (tempo_microseconds, tempo_count) = first_tempo(&smf);
    if tempo_count > 1 {
        eprintln!(
            "Warning: {tempo_count} tempo events; using the first, tempo changes are dropped"
        );
    }

    let layer_count = layers.len();
    let song = Song {
        tempo: tempo_microseconds.map_or(120.0, |microseconds| 60_000_000.0 / microseconds as f32),
        imports: vec![Import {
            path: "sounds/all.track".to_string(),
            refresh: false,
        }],
        layers,
        ..Song::default()
    };

    match std::fs::write(&output, song.to_string()) {
        Ok(()) => {
            eprintln!(
                "Imported {} → {} ({layer_count} layers)",
                input.display(),
                output.display()
            );
            ExitCode::SUCCESS
        }
        Err(error) => {
            eprintln!("Writing {}: {error}", output.display());
            ExitCode::FAILURE
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{build_note, note_sound};

    #[test]
    fn melodic_note_maps_pitch_program_and_velocity() {
        // A4 (key 69) on a piano program, one quarter note at 480 ppq.
        let note = build_note(0, 69, 127, 0, 480, 480.0, 0, None);
        assert_eq!(note.sound, "piano");
        assert_eq!(note.pitch, 0.0);
        assert_eq!(note.duration, 1.0);
        assert_eq!(note.volume, 1.0);
    }

    #[test]
    fn track_name_overrides_program_when_known() {
        assert_eq!(note_sound(0, 60, 0, Some("Flute")), "flute");
        assert_eq!(note_sound(0, 60, 0, Some("Verse 1")), "piano");
    }

    #[test]
    fn channel_nine_is_percussion() {
        let note = build_note(9, 36, 100, 0, 10, 480.0, 0, None);
        assert_eq!(note.sound, "kick");
        assert_eq!(note.pitch, 0.0);
        assert_eq!(note.duration, 0.25);
    }
}
