use alloc::format;
use core::marker::PhantomData;
use anyhow::Result;
use battler_data::{
DataStore,
Id,
};
use zone_alloc::{
BorrowError,
ElementRef,
KeyedRegistry,
};
use crate::error::{
ConvertError,
general_error,
};
type DataTable<T> = KeyedRegistry<Id, T>;
pub struct LookupAliasOutput<T> {
pub id: Id,
pub data: T,
}
pub trait ResourceLookup<'d, T> {
fn new(data: &'d dyn DataStore) -> Self;
fn lookup(&self, id: &Id) -> Result<T>;
fn lookup_alias(&self, _alias: &Id, real_id: &Id) -> Result<LookupAliasOutput<T>> {
Ok(LookupAliasOutput {
id: real_id.clone(),
data: self.lookup(real_id)?,
})
}
}
pub trait ResourceWrapper<D, T> {
fn wrap(id: Id, data: D) -> T;
}
pub struct ResourceCache<T> {
cache: DataTable<T>,
}
impl<T> ResourceCache<T> {
pub fn new() -> Self {
Self {
cache: DataTable::new(),
}
}
pub fn is_cached(&self, id: &Id) -> bool {
self.cache.contains_key(id)
}
pub fn get(&self, id: &Id) -> Result<ElementRef<'_, T>, BorrowError> {
self.cache.get(id)
}
pub fn save(&self, id: &Id, data: T) -> bool {
self.cache.register(id.clone(), data)
}
}
pub struct ResourceDex<'d, D, T, L, W> {
data: &'d dyn DataStore,
cache: ResourceCache<T>,
lookup: L,
phantom_data: PhantomData<D>,
phantom_wrapper: PhantomData<W>,
}
impl<'d, D, T, L, W> ResourceDex<'d, D, T, L, W>
where
L: ResourceLookup<'d, D>,
W: ResourceWrapper<D, T>,
{
pub fn new(data: &'d dyn DataStore) -> Self {
Self {
data,
cache: ResourceCache::new().into(),
lookup: L::new(data),
phantom_data: PhantomData,
phantom_wrapper: PhantomData,
}
}
fn cache_data(&self, id: &Id, real_id: &Id) -> Result<()> {
let output = self.lookup_data_by_id(id, real_id)?;
let resource = W::wrap(output.id, output.data);
if !self.cache.save(id, resource) {
Err(general_error(format!(
"failed to save data for {id} in cache"
)))
} else {
Ok(())
}
}
pub fn get(&self, name: &str) -> Result<ElementRef<'_, T>> {
self.get_by_id(&Id::from(name))
}
pub fn get_by_id(&self, id: &Id) -> Result<ElementRef<'_, T>> {
let real_id = self.resolve_alias(id.clone())?;
if self.cache.is_cached(id) {
return self
.cache
.get(id)
.map_err(|err| err.convert_error_with_message(format!("cached resource {id}")));
}
self.cache_data(id, &real_id)?;
self.cache
.get(id)
.map_err(|err| err.convert_error_with_message(format!("cached resource {id}")))
}
fn resolve_alias(&self, mut id: Id) -> Result<Id> {
loop {
match self.data.translate_alias(&id) {
Ok(Some(alias)) => id = alias,
Ok(None) => return Ok(id),
Err(error) => {
return Err(error);
}
}
}
}
fn lookup_data_by_id(&self, id: &Id, real_id: &Id) -> Result<LookupAliasOutput<D>> {
self.lookup.lookup_alias(&id, real_id)
}
}
impl<'d, D, T, L, W> Clone for ResourceDex<'d, D, T, L, W>
where
L: Clone,
{
fn clone(&self) -> Self {
Self {
data: self.data,
cache: ResourceCache::new(),
lookup: self.lookup.clone(),
phantom_data: PhantomData,
phantom_wrapper: PhantomData,
}
}
}
#[derive(Clone)]
pub struct SingleValueDex<'d, T> {
#[allow(unused)]
data: &'d dyn DataStore,
value: T,
}
impl<'d, T> SingleValueDex<'d, T> {
pub fn new(data: &'d dyn DataStore, value: T) -> Self {
Self { data, value }
}
pub fn get(&self) -> &T {
&self.value
}
}
#[cfg(test)]
mod resource_cache_test {
use battler_data::Id;
use crate::dex::ResourceCache;
#[derive(Debug, Clone, PartialEq)]
struct Data {
number: i32,
}
#[test]
fn caches_resources() {
let cache = ResourceCache::<Data>::new();
let id = Id::from("first");
assert!(!cache.is_cached(&id));
cache.save(&id, Data { number: 123 });
assert!(cache.is_cached(&id));
}
#[test]
fn gets_reference_to_cached_resource() {
let cache = ResourceCache::<Data>::new();
let id = Id::from("first");
let data = Data { number: 123 };
let inserted = cache.save(&id, data.clone());
assert!(inserted);
let fetched = cache.get(&id);
assert_eq!(fetched.unwrap().number, 123);
}
}
#[cfg(test)]
mod dex_test {
use core::{
cell::RefCell,
ops::Deref,
};
use anyhow::Result;
use battler_data::{
DataStore,
Id,
};
use battler_test_utils::{
local_data_store,
static_local_data_store,
};
use hashbrown::HashMap;
use rand::random;
use crate::dex::{
ResourceDex,
ResourceLookup,
ResourceWrapper,
};
#[derive(Debug, Clone, PartialEq)]
struct TestData {
id: Id,
numeric_id: u64,
}
#[derive(Debug, Clone, PartialEq)]
struct TestResource {
id: Id,
data: TestData,
}
struct TestDataLookup {
lookup_calls: RefCell<HashMap<Id, u64>>,
}
impl<'d> ResourceLookup<'d, TestData> for TestDataLookup {
fn new(_: &'d dyn DataStore) -> Self {
Self {
lookup_calls: RefCell::new(HashMap::default()),
}
}
fn lookup(&self, id: &Id) -> Result<TestData> {
*self
.lookup_calls
.borrow_mut()
.entry(id.clone())
.or_default() += 1;
Ok(TestData {
id: id.clone(),
numeric_id: random(),
})
}
}
struct TestResourceWrapper;
impl ResourceWrapper<TestData, TestResource> for TestResourceWrapper {
fn wrap(id: Id, data: TestData) -> TestResource {
TestResource { data, id }
}
}
type TestDex<'d> = ResourceDex<'d, TestData, TestResource, TestDataLookup, TestResourceWrapper>;
#[test]
fn finds_and_caches_resource() {
let dex = TestDex::new(static_local_data_store());
let first_resource = dex.get("first").unwrap();
let second_resource = dex.get_by_id(&Id::from("first")).unwrap();
assert_eq!(first_resource.deref(), second_resource.deref());
assert_eq!(
*dex.lookup.lookup_calls.borrow(),
HashMap::from_iter([(Id::from("first"), 1)])
);
}
#[test]
fn resolves_alias() {
let mut data = local_data_store();
data.aliases.insert(Id::from("alias3"), Id::from("alias2"));
data.aliases.insert(Id::from("alias2"), Id::from("alias1"));
data.aliases.insert(Id::from("alias1"), Id::from("native"));
let dex = TestDex::new(&data);
let a = dex.get("alias3");
let b = dex.get("alias3");
let c = dex.get("alias1");
let d = dex.get("native");
assert_eq!(a.unwrap().deref(), b.as_ref().unwrap().deref());
assert_eq!(b.unwrap().data.id, c.as_ref().unwrap().data.id);
assert_eq!(c.unwrap().data.id, d.unwrap().data.id);
assert_eq!(
*dex.lookup.lookup_calls.borrow(),
HashMap::from_iter([(Id::from("native"), 3)])
);
}
}