mkapk-res 0.1.1

Cross-platform audio plugin build and packaging system (Standalone/VST3/AU/AAX) with a shared retained-mode GUI framework
use std::any::Any;
use std::collections::BTreeMap;
use std::ops::Deref;
use std::sync::Arc;
use std::vec::Vec;

use crate::ResourceId;

#[derive(Debug)]
pub struct ResourceHandle<T>(Arc<T>);

impl<T> Clone for ResourceHandle<T> {
    fn clone(&self) -> Self {
        Self(self.0.clone())
    }
}

impl<T> Deref for ResourceHandle<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

impl<T> From<Arc<T>> for ResourceHandle<T> {
    fn from(arc: Arc<T>) -> Self {
        Self(arc)
    }
}

pub trait Resource: Send + Sync + 'static {
    fn decode(bytes: &[u8]) -> Option<Self>
    where
        Self: Sized;
}

#[derive(Clone, Debug, PartialEq)]
pub struct Image(pub Vec<u8>);

impl Resource for Image {
    fn decode(bytes: &[u8]) -> Option<Self> {
        Some(Image(bytes.to_vec()))
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct Svg(pub String);

impl Resource for Svg {
    fn decode(bytes: &[u8]) -> Option<Self> {
        Some(Svg(String::from_utf8(bytes.to_vec()).ok()?))
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct Font(pub Vec<u8>);

impl Resource for Font {
    fn decode(bytes: &[u8]) -> Option<Self> {
        Some(Font(bytes.to_vec()))
    }
}

#[derive(Debug, Default)]
pub struct ResourceRegistry {
    typed: BTreeMap<ResourceId, Arc<dyn Any + Send + Sync>>,
    raw: BTreeMap<ResourceId, &'static [u8]>,
}

impl ResourceRegistry {
    pub fn new() -> Self {
        Self {
            typed: BTreeMap::new(),
            raw: BTreeMap::new(),
        }
    }

    pub fn register_bytes(&mut self, id: ResourceId, bytes: &'static [u8]) {
        self.raw.insert(id, bytes);
    }

    pub fn load<T: Resource>(&mut self, id: ResourceId) -> Option<ResourceHandle<T>> {
        if let Some(handle) = self.get(id) {
            return Some(handle);
        }

        let bytes = self.raw.get(&id).copied()?;
        let decoded = T::decode(bytes)?;
        let arc = Arc::new(decoded);
        self.typed
            .insert(id, arc.clone() as Arc<dyn Any + Send + Sync>);
        Some(ResourceHandle::from(arc))
    }

    pub fn get<T: Resource>(&self, id: ResourceId) -> Option<ResourceHandle<T>> {
        let arc = self.typed.get(&id)?;
        arc.clone().downcast::<T>().ok().map(ResourceHandle::from)
    }

    pub fn evict(&mut self, id: ResourceId) {
        self.typed.remove(&id);
        self.raw.remove(&id);
    }
}

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

    #[test]
    fn load_image_from_raw_bytes() {
        let bytes: &'static [u8] = b"image-bytes";
        let id = ResourceId::new(1);

        let mut registry = ResourceRegistry::new();
        registry.register_bytes(id, bytes);

        let handle = registry.load::<Image>(id).unwrap();
        assert_eq!(&*handle, &Image(bytes.to_vec()));
    }

    #[test]
    fn load_caches_and_reuses_arc() {
        let bytes: &'static [u8] = b"shared-bytes";
        let id = ResourceId::new(2);

        let mut registry = ResourceRegistry::new();
        registry.register_bytes(id, bytes);

        let handle1 = registry.load::<Image>(id).unwrap();
        let handle2 = registry.load::<Image>(id).unwrap();

        assert!(core::ptr::eq(&*handle1, &*handle2));
    }

    #[test]
    fn evict_removes_cached_and_raw() {
        let bytes: &'static [u8] = b"evict-me";
        let id = ResourceId::new(3);

        let mut registry = ResourceRegistry::new();
        registry.register_bytes(id, bytes);
        registry.load::<Image>(id).unwrap();

        registry.evict(id);

        assert!(registry.get::<Image>(id).is_none());
        assert!(registry.load::<Image>(id).is_none());
    }

    #[test]
    fn register_from_embedded_bundle_and_load() {
        let mut registry = ResourceRegistry::new();
        EMBEDDED.register_with(&mut registry);

        let id = ResourceId::from_bytes_le(b"test.png");
        let handle = registry.load::<Image>(id).unwrap();
        assert!(!(*handle).0.is_empty());
    }
}