blinc_platform 0.5.1

Platform abstraction layer for Blinc UI - unified windowing, input, and lifecycle
Documentation
//! Cross-platform asset loading
//!
//! This module provides platform-agnostic asset loading for resources
//! like images, fonts, and data files that may be stored differently
//! on each platform:
//!
//! - **Desktop**: Regular filesystem paths
//! - **Android**: APK assets via AssetManager
//! - **iOS**: App bundle resources (planned)
//! - **Web**: HTTP fetch from server (planned)
//!
//! # Example
//!
//! ```ignore
//! use blinc_platform::assets::{AssetLoader, AssetPath};
//!
//! // Create platform-specific loader
//! let loader = create_asset_loader();
//!
//! // Load an asset - path is interpreted per-platform
//! let data = loader.load("images/logo.png")?;
//! ```

use crate::error::{PlatformError, Result};
use std::path::Path;

/// Asset path that can be resolved differently per platform
///
/// On desktop, this is typically relative to the executable or a resource directory.
/// On Android, this refers to files in the APK's assets folder.
/// On iOS, this refers to files in the app bundle.
#[derive(Debug, Clone)]
pub enum AssetPath {
    /// Path relative to asset root (platform-interpreted)
    /// - Desktop: relative to executable or resource dir
    /// - Android: relative to assets/ in APK
    /// - iOS: relative to app bundle
    Relative(String),

    /// Absolute filesystem path (desktop only)
    /// Falls back to relative on mobile platforms
    Absolute(String),

    /// Embedded asset by name (for compile-time embedded resources)
    Embedded(&'static str),
}

impl AssetPath {
    /// Create a relative asset path
    pub fn relative(path: impl Into<String>) -> Self {
        Self::Relative(path.into())
    }

    /// Create an absolute filesystem path
    pub fn absolute(path: impl Into<String>) -> Self {
        Self::Absolute(path.into())
    }

    /// Create an embedded asset reference
    pub const fn embedded(name: &'static str) -> Self {
        Self::Embedded(name)
    }
}

impl<S: Into<String>> From<S> for AssetPath {
    fn from(s: S) -> Self {
        let s = s.into();
        // Detect if this looks like an absolute path
        if s.starts_with('/') || (cfg!(windows) && s.chars().nth(1) == Some(':')) {
            Self::Absolute(s)
        } else {
            Self::Relative(s)
        }
    }
}

/// Platform-agnostic asset loader trait
///
/// Each platform implements this trait to provide asset loading
/// that works with that platform's storage mechanisms.
pub trait AssetLoader: Send + Sync {
    /// Load an asset as raw bytes
    ///
    /// # Arguments
    /// * `path` - Asset path (relative to assets root or absolute)
    ///
    /// # Returns
    /// * `Ok(Vec<u8>)` - Asset data
    /// * `Err(PlatformError)` - If asset cannot be loaded
    fn load(&self, path: &AssetPath) -> Result<Vec<u8>>;

    /// Check if an asset exists
    fn exists(&self, path: &AssetPath) -> bool;

    /// Load an asset as a UTF-8 string
    fn load_string(&self, path: &AssetPath) -> Result<String> {
        let bytes = self.load(path)?;
        String::from_utf8(bytes)
            .map_err(|e| PlatformError::AssetLoad(format!("Invalid UTF-8: {}", e)))
    }

    /// Get the platform name for this loader
    fn platform_name(&self) -> &'static str;
}

/// Default filesystem-based asset loader for desktop platforms
///
/// This loader reads assets directly from the filesystem.
/// It supports both relative paths (resolved from the current directory
/// or a configured base path) and absolute paths.
#[derive(Debug, Clone)]
pub struct FilesystemAssetLoader {
    /// Base directory for relative paths
    base_path: Option<std::path::PathBuf>,
}

impl FilesystemAssetLoader {
    /// Create a new filesystem loader with no base path
    /// (relative paths resolved from current directory)
    pub fn new() -> Self {
        Self { base_path: None }
    }

    /// Create a filesystem loader with a base path for relative assets
    pub fn with_base_path(base: impl AsRef<Path>) -> Self {
        Self {
            base_path: Some(base.as_ref().to_path_buf()),
        }
    }

    /// Set the base path for relative assets
    pub fn set_base_path(&mut self, base: impl AsRef<Path>) {
        self.base_path = Some(base.as_ref().to_path_buf());
    }

    fn resolve_path(&self, path: &AssetPath) -> std::path::PathBuf {
        match path {
            AssetPath::Relative(rel) => {
                if let Some(ref base) = self.base_path {
                    base.join(rel)
                } else {
                    std::path::PathBuf::from(rel)
                }
            }
            AssetPath::Absolute(abs) => std::path::PathBuf::from(abs),
            AssetPath::Embedded(name) => {
                // Embedded assets not supported in filesystem loader
                // Try as relative path
                if let Some(ref base) = self.base_path {
                    base.join(name)
                } else {
                    std::path::PathBuf::from(*name)
                }
            }
        }
    }
}

impl Default for FilesystemAssetLoader {
    fn default() -> Self {
        Self::new()
    }
}

impl AssetLoader for FilesystemAssetLoader {
    fn load(&self, path: &AssetPath) -> Result<Vec<u8>> {
        let resolved = self.resolve_path(path);
        std::fs::read(&resolved).map_err(|e| {
            PlatformError::AssetLoad(format!("Failed to load '{}': {}", resolved.display(), e))
        })
    }

    fn exists(&self, path: &AssetPath) -> bool {
        let resolved = self.resolve_path(path);
        resolved.exists()
    }

    fn platform_name(&self) -> &'static str {
        "filesystem"
    }
}

/// Global asset loader instance
///
/// This is set by the platform during initialization and provides
/// a way for libraries like blinc_image to load assets without
/// needing direct platform knowledge.
static GLOBAL_LOADER: std::sync::OnceLock<Box<dyn AssetLoader>> = std::sync::OnceLock::new();

/// Set the global asset loader
///
/// This should be called once during platform initialization.
/// Returns an error if a loader was already set.
pub fn set_global_asset_loader(loader: Box<dyn AssetLoader>) -> Result<()> {
    GLOBAL_LOADER.set(loader).map_err(|_| {
        PlatformError::InitFailed("Global asset loader already initialized".to_string())
    })
}

/// Get a reference to the global asset loader
///
/// Returns None if no loader has been set yet.
pub fn global_asset_loader() -> Option<&'static dyn AssetLoader> {
    GLOBAL_LOADER.get().map(|b| b.as_ref())
}

/// Load an asset using the global loader
///
/// This is the simplest way to load assets in a cross-platform manner.
///
/// # Example
///
/// ```ignore
/// let image_data = blinc_platform::assets::load_asset("images/logo.png")?;
/// ```
pub fn load_asset(path: impl Into<AssetPath>) -> Result<Vec<u8>> {
    let loader = global_asset_loader()
        .ok_or_else(|| PlatformError::AssetLoad("No asset loader configured".to_string()))?;
    loader.load(&path.into())
}

/// Check if an asset exists using the global loader
pub fn asset_exists(path: impl Into<AssetPath>) -> bool {
    global_asset_loader()
        .map(|l| l.exists(&path.into()))
        .unwrap_or(false)
}

/// Load an asset as a string using the global loader
pub fn load_asset_string(path: impl Into<AssetPath>) -> Result<String> {
    let loader = global_asset_loader()
        .ok_or_else(|| PlatformError::AssetLoad("No asset loader configured".to_string()))?;
    loader.load_string(&path.into())
}

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

    #[test]
    fn test_filesystem_loader() {
        // Create a temp file
        let temp_dir = std::env::temp_dir();
        let test_file = temp_dir.join("blinc_test_asset.txt");
        let mut f = std::fs::File::create(&test_file).unwrap();
        f.write_all(b"Hello, Blinc!").unwrap();

        // Test loading with absolute path
        let loader = FilesystemAssetLoader::new();
        let path = AssetPath::Absolute(test_file.to_string_lossy().to_string());
        let data = loader.load(&path).unwrap();
        assert_eq!(data, b"Hello, Blinc!");

        // Test exists
        assert!(loader.exists(&path));

        // Cleanup
        std::fs::remove_file(test_file).unwrap();
    }

    #[test]
    fn test_asset_path_from_string() {
        let relative: AssetPath = "images/logo.png".into();
        assert!(matches!(relative, AssetPath::Relative(_)));

        let absolute: AssetPath = "/absolute/path.png".into();
        assert!(matches!(absolute, AssetPath::Absolute(_)));
    }
}