use super::{Cache, CombineOperator};
use crate::cache::{ChangeAwareCache, ChangeReason, ChangeSet, ICache, IQuery};
use ferroui_reactive::disposables::CompositeDisposable;
use ferroui_reactive::{Observable, ObservableExt};
use ferroui_system::{IDisposable, IObservable, IObserver};
use std::cell::RefCell;
use std::hash::Hash;
use std::rc::Rc;
pub(crate) type SameReference<T> = Rc<dyn Fn(&T, &T) -> bool>;
pub(crate) fn combiner<T: Clone + 'static, K: Eq + Hash + Clone + 'static>(
sources: Vec<Rc<dyn IObservable<ChangeSet<T, K>>>>,
kind: CombineOperator,
same_reference: SameReference<T>,
) -> Rc<dyn IObservable<ChangeSet<T, K>>> {
Observable::create(move |observer: Rc<dyn IObserver<ChangeSet<T, K>>>| {
let combiner = Rc::new(Combiner {
kind,
same_reference: same_reference.clone(),
updated_callback: observer,
combined_cache: RefCell::new(ChangeAwareCache::new()),
source_caches: RefCell::new(Vec::new()),
});
Combiner::subscribe(&combiner, &sources)
})
}
struct Combiner<T, K> {
kind: CombineOperator,
same_reference: SameReference<T>,
updated_callback: Rc<dyn IObserver<ChangeSet<T, K>>>,
combined_cache: RefCell<ChangeAwareCache<T, K>>,
source_caches: RefCell<Vec<Cache<T, K>>>,
}
impl<T: Clone + 'static, K: Eq + Hash + Clone + 'static> Combiner<T, K> {
fn subscribe(this: &Rc<Self>, source: &[Rc<dyn IObservable<ChangeSet<T, K>>>]) -> Rc<dyn IDisposable> {
let disposable = CompositeDisposable::new();
this.source_caches.borrow_mut().extend((0..source.len()).map(|_| Cache::new()));
for (index, item) in source.iter().enumerate() {
let combiner = this.clone();
let subscription = item.subscribe_next(move |updates: ChangeSet<T, K>| combiner.update(index, &updates));
disposable.add(subscription);
}
disposable
}
fn matches_constraint(&self, key: &K) -> bool {
let caches = self.source_caches.borrow();
let has_key = |cache: &Cache<T, K>| cache.dictionary().contains_key(key);
match self.kind {
CombineOperator::And => caches.iter().all(has_key),
CombineOperator::Or => caches.iter().any(has_key),
CombineOperator::Xor => caches.iter().filter(|&cache| has_key(cache)).count() == 1,
CombineOperator::Except => {
let first = caches.iter().take(1).any(has_key);
let others = caches.iter().skip(1).any(has_key);
first && !others
}
}
}
fn update(&self, index: usize, updates: &ChangeSet<T, K>) {
self.source_caches.borrow_mut()[index].clone_changes(updates);
let notifications = self.update_combined(updates);
if notifications.count() != 0 {
self.updated_callback.on_next(notifications);
}
}
fn update_combined(&self, updates: &ChangeSet<T, K>) -> ChangeSet<T, K> {
let mut combined_cache = self.combined_cache.borrow_mut();
for update in updates {
let key = update.key();
match update.reason() {
ChangeReason::Add | ChangeReason::Update => {
let cached = combined_cache.lookup(key);
let matches = self.matches_constraint(key);
if matches {
match cached {
Some(cached) => {
if !(self.same_reference)(update.current(), &cached) {
combined_cache.add_or_update(update.current().clone(), key.clone());
}
}
None => combined_cache.add_or_update(update.current().clone(), key.clone()),
}
} else if cached.is_some() {
combined_cache.remove(key);
}
}
ChangeReason::Remove => {
let cached = combined_cache.lookup(key);
let should_be_included = self.matches_constraint(key);
if should_be_included {
let first_one = self
.source_caches
.borrow()
.iter()
.find_map(|cache| cache.lookup(key))
.expect("a source holds the key that the constraint matches");
match cached {
None => combined_cache.add_or_update(first_one, key.clone()),
Some(cached) => {
if !(self.same_reference)(&first_one, &cached) {
combined_cache.add_or_update(first_one, key.clone());
}
}
}
} else if cached.is_some() {
combined_cache.remove(key);
}
}
ChangeReason::Refresh => combined_cache.refresh(key),
ChangeReason::Moved => {}
}
}
combined_cache.capture_changes()
}
}