//! The [`ScriptAsset`] — a loaded script's source — and its [`ScriptLoader`].
//!
//! A script lives on disk as a `.rhai` file under the project's `assets/scripts/`
//! folder. Loading one yields a [`ScriptAsset`], which is just the source text
//! plus its origin path; turning that text into something executable (a compiled
//! `rhai` AST) is the job of the [`ScriptEngine`](crate::ScriptEngine), done at
//! run time so a live edit can recompile without touching the asset plumbing.
//!
//! Keeping the asset as plain source (rather than a pre-compiled AST) is what
//! makes hot-reload cheap: the file watcher swaps in fresh source on change and
//! the host recompiles, with no engine-specific data baked into the asset cache.

use std::path::Path;

use oxide_engine::asset::{AssetError, AssetLoader};

/// A loaded script: its source text and the path it came from.
///
/// This is the *asset* a [`Script`](crate::Script) component points at via an
/// [`AssetRef<ScriptAsset>`](oxide_engine::asset::AssetRef). It is deliberately
/// inert — holding source, not behaviour — so the same file can be recompiled on
/// every live reload.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScriptAsset {
    /// The script's source code.
    pub source: String,
    /// A human-readable name for diagnostics (the file stem, when loaded from
    /// disk), used in error messages and the script console.
    pub name: String,
}

impl ScriptAsset {
    /// Builds an asset from in-memory source with the given diagnostic `name`.
    pub fn from_source(name: impl Into<String>, source: impl Into<String>) -> Self {
        Self {
            source: source.into(),
            name: name.into(),
        }
    }
}

/// The [`AssetServer`](oxide_engine::asset::AssetServer) loader for `.rhai`
/// scripts.
///
/// Registered by the [`ScriptModule`](crate::ScriptModule) (handles `.rhai`), so
/// `assets.load::<ScriptAsset>("scripts/spin.rhai")` works once the module is
/// added. It reads the file as UTF-8 and records the file stem as the asset's
/// diagnostic name.
pub struct ScriptLoader;

impl AssetLoader for ScriptLoader {
    type Asset = ScriptAsset;

    fn extensions(&self) -> &'static [&'static str] {
        &["rhai"]
    }

    fn load(&self, path: &Path) -> Result<ScriptAsset, AssetError> {
        let source = std::fs::read_to_string(path).map_err(|err| AssetError::Load {
            path: path.to_path_buf(),
            message: err.to_string(),
        })?;
        let name = path
            .file_stem()
            .and_then(|s| s.to_str())
            .unwrap_or("script")
            .to_string();
        Ok(ScriptAsset { source, name })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn from_source_keeps_name_and_text() {
        let a = ScriptAsset::from_source("spin", "let x = 1;");
        assert_eq!(a.name, "spin");
        assert_eq!(a.source, "let x = 1;");
    }

    #[test]
    fn loader_reads_a_file_and_uses_the_stem_as_name() {
        let dir = std::env::temp_dir().join("oxide-script-loader-test");
        std::fs::create_dir_all(&dir).unwrap();
        let path = dir.join("hello.rhai");
        std::fs::write(&path, "print(\"hi\");").unwrap();

        let asset = ScriptLoader.load(&path).unwrap();
        assert_eq!(asset.name, "hello");
        assert!(asset.source.contains("print"));

        std::fs::remove_file(&path).ok();
    }

    #[test]
    fn loader_claims_the_rhai_extension() {
        assert_eq!(ScriptLoader.extensions(), &["rhai"]);
    }
}
