using System; using System.Collections; using System.Collections.Generic; using System.Linq; using EcsRx.Entities; using EcsRx.Events.Collections; using EcsRx.Extensions; using EcsRx.Groups.Observable; using EcsRx.MicroRx.Extensions; using EcsRx.MicroRx.Subjects; namespace EcsRx.Plugins.Computeds.Collections { public abstract class ComputedCollectionFromGroup : IComputedCollection, IDisposable { public IDictionary FilteredCache { get; } public List Subscriptions { get; } public IObservable> OnAdded => _onElementAdded; public IObservable> OnRemoved => _onElementChanged; public IObservable> OnUpdated => _onElementChanged; public IObservableGroup InternalObservableGroup { get; } public IEnumerable Value => GetData(); public T this[int index] => FilteredCache[index]; public int Count => FilteredCache.Count; private readonly Subject> _onDataChanged; private readonly Subject> _onElementAdded; private readonly Subject> _onElementChanged; private readonly Subject> _onElementRemoved; private bool _needsUpdate; public ComputedCollectionFromGroup(IObservableGroup internalObservableGroup) { InternalObservableGroup = internalObservableGroup; Subscriptions = new List(); FilteredCache = new Dictionary(); _onDataChanged = new Subject>(); _onElementAdded = new Subject>(); _onElementChanged = new Subject>(); _onElementRemoved = new Subject>(); MonitorChanges(); RefreshData(); } public IDisposable Subscribe(IObserver> observer) { return _onDataChanged.Subscribe(observer); } public void MonitorChanges() { InternalObservableGroup.OnEntityAdded.Subscribe(RequestUpdate).AddTo(Subscriptions); InternalObservableGroup.OnEntityRemoving.Subscribe(RequestUpdate).AddTo(Subscriptions); RefreshWhen().Subscribe(x => RequestUpdate()).AddTo(Subscriptions); } public void RequestUpdate(object _ = null) { _needsUpdate = true; if(_onDataChanged.HasObservers || _onElementAdded.HasObservers || _onElementChanged.HasObservers || _onElementRemoved.HasObservers) { RefreshData(); } } private void ProcessEntity(IEntity entity) { var isApplicable = ShouldTransform(entity); if (!isApplicable) { if (!FilteredCache.ContainsKey(entity.Id)) { return; } RemoveEntity(entity.Id); return; } var transformedData = Transform(entity); if (FilteredCache.ContainsKey(entity.Id)) { ChangeEntity(entity.Id, transformedData); return; } AddEntity(entity.Id, transformedData); } private void AddEntity(int entityId, T transformedData) { FilteredCache.Add(entityId, transformedData); _onElementAdded.OnNext(new CollectionElementChangedEvent { Index = entityId, OldValue = default(T), NewValue = transformedData }); } private void RemoveEntity(int entityId) { var currentValue = FilteredCache[entityId]; FilteredCache.Remove(entityId); _onElementRemoved.OnNext(new CollectionElementChangedEvent { Index = entityId, OldValue = currentValue, NewValue = default(T) }); } private void ChangeEntity(int entityId, T transformedData) { var currentData = FilteredCache[entityId]; FilteredCache[entityId] = transformedData; _onElementChanged.OnNext(new CollectionElementChangedEvent { Index = entityId, OldValue = currentData, NewValue = transformedData }); } public void RefreshData() { var unprocessedIds = FilteredCache.Keys.ToList(); foreach (var entity in InternalObservableGroup) { unprocessedIds.Remove(entity.Id); ProcessEntity(entity); } foreach(var id in unprocessedIds) { RemoveEntity(id);} _onDataChanged.OnNext(FilteredCache.Values); _needsUpdate = false; } /// /// The method to indicate when the listings should be updated /// /// /// If there is no checking required outside of adding/removing this can /// return an empty observable, but common usages would be to refresh every update. /// The bool is throw away, but is a workaround for not having a Unit class /// /// An observable trigger that should trigger when the group should refresh public abstract IObservable RefreshWhen(); /// /// The method to see if this entity should be transformed /// /// The entity to verify /// true if it should transform the entity, false if not public abstract bool ShouldTransform(IEntity entity); /// /// The method to generate given data from the data source /// /// The entity to transform /// The transformed data public abstract T Transform(IEntity entity); /// /// Available as a way to post process the data, i.e order them /// /// Data to transform /// Processed data public virtual IEnumerable PostProcess(IEnumerable data) { return data; } public IEnumerable GetData() { if(_needsUpdate) { RefreshData(); } return PostProcess(FilteredCache.Values); } public IEnumerator GetEnumerator() { return GetData().GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public void Dispose() { Subscriptions.DisposeAll(); _onDataChanged?.Dispose(); _onElementAdded?.Dispose(); _onElementChanged?.Dispose(); _onElementRemoved?.Dispose(); } } }