//! Loading a song and every song reachable from it through `@jump`.

use super::{
    format::{ParseError, read_source, resolve_import_path, resolve_imports},
    song::{Refresh, Song},
};
use std::{
    collections::BTreeMap,
    path::{Path, PathBuf},
};

/// A song together with every other song reachable from it via `@jump`.
///
/// Loaded once up front (jump targets are followed and parsed here, not at
/// playback time), so following a jump is a pure state change with no I/O.
#[derive(Debug, Clone)]
pub struct SongGraph {
    /// Resolved path of the entry song (the file that was loaded).
    pub entry: PathBuf,
    /// Every reachable song, keyed by its resolved path.
    pub songs: BTreeMap<PathBuf, Song>,
}

impl SongGraph {
    /// Loads the song at `path` and, following `@jump` targets, every song
    /// reachable from it. Imports are resolved per song. Cycles are fine — each
    /// file is loaded once.
    pub fn load(path: impl AsRef<Path>, refresh: Refresh) -> Result<Self, ParseError> {
        let entry = canonical(path.as_ref());
        let mut songs = BTreeMap::new();
        let mut pending = vec![(entry.clone(), path.as_ref().to_path_buf())];

        while let Some((key, load_path)) = pending.pop() {
            if songs.contains_key(&key) {
                continue;
            }
            let source = read_source(&load_path, refresh == Refresh::Force)?;
            let mut song = super::format::parse(&source)?;
            resolve_imports(&mut song, load_path.parent(), refresh)?;

            for jump in &song.jumps {
                let target = resolve_import_path(load_path.parent(), &jump.target);
                let target_key = canonical(&target);
                if !songs.contains_key(&target_key) {
                    pending.push((target_key, target));
                }
            }
            songs.insert(key, song);
        }

        Ok(Self { entry, songs })
    }

    /// The entry song.
    pub fn entry_song(&self) -> &Song {
        &self.songs[&self.entry]
    }
}

fn canonical(path: &Path) -> PathBuf {
    if path.to_string_lossy().contains("://") {
        path.to_path_buf()
    } else {
        path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::unwrap_used, clippy::expect_used)]
    use super::SongGraph;
    use crate::track::Refresh;
    use std::io::Write;

    fn write(dir: &std::path::Path, name: &str, body: &str) -> std::path::PathBuf {
        let path = dir.join(name);
        let mut file = std::fs::File::create(&path).unwrap();
        file.write_all(body.as_bytes()).unwrap();
        path
    }

    fn temp_dir(tag: &str) -> std::path::PathBuf {
        let dir = std::env::temp_dir().join(format!("game-audio-graph-{tag}"));
        let _ = std::fs::remove_dir_all(&dir);
        std::fs::create_dir_all(&dir).unwrap();
        dir
    }

    #[test]
    fn loads_jump_chain() {
        let dir = temp_dir("chain");
        write(
            &dir,
            "intro.track",
            "@jump loop.track\n# Layer A\nbell 0 0 1\n",
        );
        write(
            &dir,
            "loop.track",
            "# Sounds\nbell: sine 1\n# Layer A\nbell 0 0 2\n",
        );
        let entry = write(
            &dir,
            "intro.track",
            "@jump loop.track\n# Layer A\nbell 0 0 1\n",
        );

        let graph = SongGraph::load(&entry, Refresh::Cached).unwrap();
        assert_eq!(graph.songs.len(), 2);
        assert!(!graph.entry_song().jumps.is_empty());
    }

    #[test]
    fn self_jump_loads_single_song() {
        let dir = temp_dir("selfloop");
        let entry = write(
            &dir,
            "loop.track",
            "@jump loop.track\n# Layer A\nbell 0 0 1\n",
        );
        let graph = SongGraph::load(&entry, Refresh::Cached).unwrap();
        assert_eq!(graph.songs.len(), 1);
    }

    #[test]
    fn cycle_terminates() {
        let dir = temp_dir("cycle");
        write(&dir, "a.track", "@jump b.track\n# Layer A\nbell 0 0 1\n");
        write(&dir, "b.track", "@jump a.track\n# Layer A\nbell 0 0 1\n");
        let entry = write(&dir, "a.track", "@jump b.track\n# Layer A\nbell 0 0 1\n");
        let graph = SongGraph::load(&entry, Refresh::Cached).unwrap();
        assert_eq!(graph.songs.len(), 2);
    }
}
