using System.Collections; using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using AyCode.Core.Enums; using AyCode.Core.Extensions; using AyCode.Core.Helpers; using AyCode.Core.Interfaces; using AyCode.Services.SignalRs; using AyCode.Utils.Extensions; namespace AyCode.Blazor.Components.Services { public class TrackingItem(TrackingState trackingState, T currentValue, T? originalValue = null) where T : class, IId { public TrackingState TrackingState { get; internal set; } = trackingState; public T CurrentValue { get; internal set; } = currentValue; public T? OriginalValue { get; init; } = originalValue; internal TrackingItem UpdateItem(TrackingState trackingState, T newValue) { CurrentValue = newValue; if (TrackingState != TrackingState.Add) TrackingState = trackingState; return this; } } public class ChangeTracking /*: IEnumerable>*/ where T : class, IId { private readonly List> _trackingItems = []; //TODO: Dictionary... - J. internal TrackingItem? AddTrackingItem(TrackingState trackingState, T newValue, T? originalValue = null) { if (newValue.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(newValue), $@"currentValue.Id.IsNullOrEmpty()"); var itemIndex = _trackingItems.FindIndex(x => x.CurrentValue.Id == newValue.Id); TrackingItem? trackingItem = null; if (itemIndex > -1) { trackingItem = _trackingItems[itemIndex]; if (trackingState == TrackingState.Remove && trackingItem.TrackingState == TrackingState.Add) { _trackingItems.RemoveAt(itemIndex); return null; } return trackingItem.UpdateItem(trackingState, newValue); } if (originalValue != null && Equals(newValue, originalValue)) originalValue = TrackingItemHelpers.JsonClone(originalValue); trackingItem = new TrackingItem(trackingState, newValue, originalValue); _trackingItems.Add(trackingItem); return trackingItem; } public int Count => _trackingItems.Count; internal void Clear() => _trackingItems.Clear(); public List> ToList() => _trackingItems.ToList(); public bool TryGetTrackingItem(Guid id, [NotNullWhen(true)] out TrackingItem? trackingItem) { trackingItem = _trackingItems.FirstOrDefault(x => x.CurrentValue.Id == id); return trackingItem != null; } internal void Remove(TrackingItem trackingItem) => _trackingItems.Remove(trackingItem); //public IEnumerator> GetEnumerator() //{ // return _trackingItems.GetEnumerator(); //} //IEnumerator IEnumerable.GetEnumerator() //{ // return GetEnumerator(); //} } [Serializable] [DebuggerDisplay("Count = {Count}")] public class AcSignalRDataSource : IList, IList, IReadOnlyList where T : class, IId { private readonly object _syncRoot = new(); protected List InnerList = []; //TODO: Dictionary??? - J. protected readonly ChangeTracking TrackingItems = new(); public List? ContextIds; public string? FilterText { get; set; } public AcSignalRClientBase SignalRClient; protected readonly SignalRCrudTags SignalRCrudTags; public Func, Task>? OnDataSourceItemChanged; public Func? OnDataSourceLoaded; public AcSignalRDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, IEnumerable? contextIds = null) { if (contextIds != null) (ContextIds = new List()).AddRange(contextIds); SignalRCrudTags = signalRCrudTags; SignalRClient = signalRClient; } public bool IsSynchronized => true; public object SyncRoot => _syncRoot; public bool IsFixedSize => false; private object[]? GetContextParams() { var parameters = new List(); if (ContextIds != null) parameters.AddRange(ContextIds.Cast()); if (!FilterText.IsNullOrWhiteSpace()) parameters.Add(FilterText); if (parameters.Count == 0) parameters = null; return parameters?.ToArray(); } /// /// GetAllMessageTag /// /// /// public async Task LoadDataSource(bool clearChangeTracking = true) { if (SignalRCrudTags.GetAllMessageTag == AcSignalRTags.None) throw new ArgumentException($"SignalRCrudTags.GetAllMessageTag == SignalRTags.None"); var resultList = (await SignalRClient.GetAllAsync>(SignalRCrudTags.GetAllMessageTag, GetContextParams())) ?? throw new NullReferenceException(); await LoadDataSource(resultList); } public Task LoadDataSourceAsync(bool clearChangeTracking = true) { if (SignalRCrudTags.GetAllMessageTag == AcSignalRTags.None) throw new ArgumentException($"SignalRCrudTags.GetAllMessageTag == SignalRTags.None"); return SignalRClient.GetAllAsync>(SignalRCrudTags.GetAllMessageTag, result=> { if (result.Status != SignalResponseStatus.Success || result.ResponseData == null) throw new NullReferenceException($"LoadDataSourceAsync; result.Status != SignalResponseStatus.Success || result.ResponseData == null; Status: {SignalResponseStatus.Success}"); return LoadDataSource(result.ResponseData); }, GetContextParams()); } public async Task LoadDataSource(IList fromSource, bool clearChangeTracking = true) { Monitor.Enter(_syncRoot); try { Clear(clearChangeTracking); if (fromSource is List fromSourceCasted) InnerList = fromSourceCasted; else InnerList.AddRange(fromSource); //foreach (var item in fromSource) //{ // InnerList.Add(item); // var eventArgs = new ItemChangedEventArgs(item, TrackingState.GetAll); // if (OnDataSourceItemChanged != null) await OnDataSourceItemChanged.Invoke(eventArgs); //} } finally { Monitor.Exit(_syncRoot); } if (OnDataSourceLoaded != null) await OnDataSourceLoaded.Invoke(); } public async Task LoadItem(Guid id) { if (SignalRCrudTags.GetItemMessageTag == AcSignalRTags.None) throw new ArgumentException($"SignalRCrudTags.GetItemMessageTag == SignalRTags.None"); T? resultitem = null; Monitor.Enter(_syncRoot); try { resultitem = await SignalRClient.GetByIdAsync(SignalRCrudTags.GetItemMessageTag, [id]); if (resultitem == null) return null; if (TryGetIndex(id, out var index)) InnerList[index] = resultitem; else InnerList.Add(resultitem); var eventArgs = new ItemChangedEventArgs(resultitem, TrackingState.Get); if (OnDataSourceItemChanged != null) await OnDataSourceItemChanged.Invoke(eventArgs); } finally { Monitor.Exit(_syncRoot); } return resultitem; } /// /// set: UpdateMessageTag /// /// /// /// public T this[int index] { get { if ((uint)index >= (uint)Count) throw new ArgumentOutOfRangeException(nameof(index)); Monitor.Enter(_syncRoot); try { return InnerList[index]; } finally { Monitor.Exit(_syncRoot); } } set { Monitor.Enter(_syncRoot); try { UpdateUnsafe(index, value); } finally { Monitor.Exit(_syncRoot); } } } public void Add(T newValue) { if (newValue.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(newValue), @"Add->newValue.Id.IsNullOrEmpty()"); Monitor.Enter(_syncRoot); try { if (Contains(newValue)) throw new ArgumentException($@"It already contains this Id! Id: {newValue.Id}", nameof(newValue)); UnsafeAdd(newValue); } finally { Monitor.Exit(_syncRoot); } } /// /// AddMessageTag /// /// /// /// public async Task Add(T newValue, bool autoSave) { Monitor.Enter(_syncRoot); try { Add(newValue); return autoSave ? await SaveItem(newValue, TrackingState.Add) : newValue; } finally { Monitor.Exit(_syncRoot); } } /// /// AddMessageTag or UpdateMessageTag /// /// /// /// public async Task AddOrUpdate(T newValue, bool autoSave) { if (newValue.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(newValue), @"AddOrUpdate->newValue.Id.IsNullOrEmpty()"); Monitor.Enter(_syncRoot); try { var index = IndexOf(newValue); return index > -1 ? await Update(index, newValue, autoSave) : await Add(newValue, autoSave); } finally { Monitor.Exit(_syncRoot); } } //public void AddRange(IEnumerable collection) //{ // lock (_syncRoot) // { // } //} protected void UnsafeAdd(T newValue) { TrackingItems.AddTrackingItem(TrackingState.Add, newValue); InnerList.Add(newValue); } /// /// AddMessageTag /// /// /// /// /// /// public void Insert(int index, T newValue) { if (newValue.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(newValue), @"Insert->newValue.Id.IsNullOrEmpty()"); Monitor.Enter(_syncRoot); try { if (Contains(newValue)) throw new ArgumentException($@"Insert; It already contains this Id! Id: {newValue.Id}", nameof(newValue)); TrackingItems.AddTrackingItem(TrackingState.Add, newValue); InnerList.Insert(index, newValue); } finally { Monitor.Exit(_syncRoot); } } public async Task Insert(int index, T newValue, bool autoSave) { Monitor.Enter(_syncRoot); try { Insert(index, newValue); return autoSave ? await SaveItem(newValue, TrackingState.Add) : newValue; } finally { Monitor.Exit(_syncRoot); } } /// /// UpdateMessageTag /// /// /// public Task Update(T newItem, bool autoSave) => Update(IndexOf(newItem), newItem, autoSave); /// /// UpdateMessageTag /// /// /// /// /// /// /// /// /// /// public async Task Update(int index, T newValue, bool autoSave) { Monitor.Enter(_syncRoot); try { UpdateUnsafe(index, newValue); return autoSave ? await SaveItem(newValue, TrackingState.Update) : newValue; } finally { Monitor.Exit(_syncRoot); } } private void UpdateUnsafe(int index, T newValue) { if (default(T) != null && newValue == null) throw new NullReferenceException(nameof(newValue)); if (newValue.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(newValue), @"UpdateUnsafe->newValue.Id.IsNullOrEmpty()"); if ((uint)index >= (uint)Count) throw new ArgumentOutOfRangeException(nameof(index)); Monitor.Enter(_syncRoot); try { var currentItem = InnerList[index]; if (currentItem.Id != newValue.Id) throw new ArgumentException($@"UpdateUnsafe; currentItem.Id != item.Id! Id: {newValue.Id}", nameof(newValue)); TrackingItems.AddTrackingItem(TrackingState.Update, newValue, currentItem); InnerList[index] = newValue; } finally { Monitor.Exit(_syncRoot); } } /// /// RemoveMessageTag /// /// /// public bool Remove(T item) { Monitor.Enter(_syncRoot); try { var index = IndexOf(item); if (index < 0) return false; RemoveAt(index); return true; } finally { Monitor.Exit(_syncRoot); } } public async Task Remove(T item, bool autoSave) { Monitor.Enter(_syncRoot); try { var result = Remove(item); if (!autoSave || !result) return result; await SaveItem(item, TrackingState.Remove); return true; } finally { Monitor.Exit(_syncRoot); } } /// /// /// /// /// /// public bool TryRemove(Guid id, out T? item) { Monitor.Enter(_syncRoot); try { return TryGetValue(id, out item) && Remove(item); } finally { Monitor.Exit(_syncRoot); } } /// /// RemoveMessageTag /// /// /// /// /// /// public void RemoveAt(int index) { Monitor.Enter(_syncRoot); try { var currentItem = InnerList[index]; if (currentItem.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(currentItem), $@"RemoveAt->item.Id.IsNullOrEmpty(); index: {index}"); TrackingItems.AddTrackingItem(TrackingState.Remove, currentItem, currentItem); InnerList.RemoveAt(index); } finally { Monitor.Exit(_syncRoot); } } public async Task RemoveAt(int index, bool autoSave) { Monitor.Enter(_syncRoot); try { var currentItem = InnerList[index]; RemoveAt(index); if (autoSave) { await SaveItem(currentItem, TrackingState.Remove); } } finally { Monitor.Exit(_syncRoot); } } /// /// /// /// public List> GetTrackingItems() { Monitor.Enter(_syncRoot); try { return TrackingItems.ToList(); } finally { Monitor.Exit(_syncRoot); } } public void SetTrackingStateToUpdate(T item) { Monitor.Enter(_syncRoot); try { if (TrackingItems.TryGetTrackingItem(item.Id, out var trackingItem)) { if (trackingItem.TrackingState != TrackingState.Add) trackingItem.TrackingState = TrackingState.Update; return; } if (!TryGetValue(item.Id, out var originalItem)) return; TrackingItems.AddTrackingItem(TrackingState.Update, item, originalItem); } finally { Monitor.Exit(_syncRoot); } } /// /// /// /// /// /// public bool TryGetTrackingItem(Guid id, [NotNullWhen(true)] out TrackingItem? trackingItem) { Monitor.Enter(_syncRoot); try { return TrackingItems.TryGetTrackingItem(id, out trackingItem); } finally { Monitor.Exit(_syncRoot); } } /// /// /// /// Unsaved items public async Task>> SaveChanges() { Monitor.Enter(_syncRoot); try { foreach (var trackingItem in TrackingItems.ToList()) { try { await SaveTrackingItemUnsafe(trackingItem); } catch(Exception ex) { TryRollbackItem(trackingItem.CurrentValue.Id, out _); } } return TrackingItems.ToList(); } finally { Monitor.Exit(_syncRoot); } } public async Task SaveChangesAsync() { Monitor.Enter(_syncRoot); try { foreach (var trackingItem in TrackingItems.ToList()) { try { await SaveTrackingItemUnsafeAsync(trackingItem); } catch(Exception ex) { TryRollbackItem(trackingItem.CurrentValue.Id, out _); } } } finally { Monitor.Exit(_syncRoot); } } /// /// /// /// /// public async Task SaveItem(Guid id) { Monitor.Enter(_syncRoot); try { T resultItem = null!; if (TryGetTrackingItem(id, out var trackingItem)) resultItem = await SaveTrackingItemUnsafe(trackingItem); if (resultItem == null) throw new NullReferenceException($"SaveItem; resultItem == null"); return resultItem; } finally { Monitor.Exit(_syncRoot); } } public async Task SaveItem(Guid id, TrackingState trackingState) { //Monitor.Enter(_syncRoot); try { T resultItem = null!; if (TryGetValue(id, out var item)) resultItem = await SaveItem(item, trackingState); if (resultItem == null) throw new NullReferenceException($"SaveItem; resultItem == null"); return resultItem; } finally { //Monitor.Exit(_syncRoot); } } public Task SaveItem(T item, TrackingState trackingState) => SaveItemUnsafe(item, trackingState); protected Task SaveTrackingItemUnsafe(TrackingItem trackingItem) => SaveItemUnsafe(trackingItem.CurrentValue, trackingItem.TrackingState); protected Task SaveTrackingItemUnsafeAsync(TrackingItem trackingItem) => SaveItemUnsafeAsync(trackingItem.CurrentValue, trackingItem.TrackingState); protected Task SaveItemUnsafe(T item, TrackingState trackingState) { var messageTag = SignalRCrudTags.GetMessageTagByTrackingState(trackingState); if (messageTag == AcSignalRTags.None) throw new ArgumentException($"SaveItemUnsafe; messageTag == SignalRTags.None"); return SignalRClient.PostDataAsync(messageTag, item).ContinueWith(x => { if (x.Result == null) { if (TryRollbackItem(item.Id, out _)) return item; throw new NullReferenceException($"SaveItemUnsafe; result == null"); } ProcessSavedResponseItem(x.Result, trackingState); return x.Result; }); } protected Task SaveItemUnsafeAsync(T item, TrackingState trackingState) { var messageTag = SignalRCrudTags.GetMessageTagByTrackingState(trackingState); if (messageTag == AcSignalRTags.None) return Task.CompletedTask; return SignalRClient.PostDataAsync(messageTag, item, response => { Monitor.Enter(_syncRoot); try { if (response.Status != SignalResponseStatus.Success || response.ResponseData == null) { if (TryRollbackItem(item.Id, out _)) return Task.CompletedTask; throw new NullReferenceException($"SaveItemUnsafeAsync; result.Status != SignalResponseStatus.Success || result.ResponseData == null; Status: {SignalResponseStatus.Success}"); } return ProcessSavedResponseItem(response.ResponseData, trackingState); } finally { Monitor.Exit(_syncRoot); } }); } private Task ProcessSavedResponseItem(T? resultItem, TrackingState trackingState) { if (resultItem == null) return Task.CompletedTask; if (TryGetTrackingItem(resultItem.Id, out var trackingItem)) TrackingItems.Remove(trackingItem); if (TryGetIndex(resultItem.Id, out var index)) InnerList[index] = resultItem; var eventArgs = new ItemChangedEventArgs(resultItem, trackingState); if (OnDataSourceItemChanged != null) return OnDataSourceItemChanged.Invoke(eventArgs); return Task.CompletedTask; } protected void RollbackItemUnsafe(TrackingItem trackingItem) { if (TryGetIndex(trackingItem.CurrentValue.Id, out var index)) { if (trackingItem.TrackingState == TrackingState.Add) InnerList.RemoveAt(index); else InnerList[index] = trackingItem.OriginalValue!; } else if (trackingItem.TrackingState != TrackingState.Add) InnerList.Add(trackingItem.OriginalValue!); TrackingItems.Remove(trackingItem); } public bool TryRollbackItem(Guid id, out T? originalValue) { Monitor.Enter(_syncRoot); try { if (TryGetTrackingItem(id, out var trackingItem)) { originalValue = trackingItem.OriginalValue; RollbackItemUnsafe(trackingItem); return true; } originalValue = null; return false; } finally { Monitor.Exit(_syncRoot); } } public void Rollback() { Monitor.Enter(_syncRoot); try { foreach (var trackingItem in TrackingItems.ToList()) RollbackItemUnsafe(trackingItem); } finally { Monitor.Exit(_syncRoot); } } public int Count { get { Monitor.Enter(_syncRoot); try { return InnerList.Count; } finally { Monitor.Exit(_syncRoot); } } } public void Clear() => Clear(true); public void Clear(bool clearChangeTracking) { Monitor.Enter(_syncRoot); try { if (clearChangeTracking) TrackingItems.Clear(); InnerList.Clear(); } finally { Monitor.Exit(_syncRoot); } } public int IndexOf(Guid id) { Monitor.Enter(_syncRoot); try { return InnerList.FindIndex(x => x.Id == id); } finally { Monitor.Exit(_syncRoot); } } public int IndexOf(T item) => IndexOf(item.Id); public bool TryGetIndex(Guid id, out int index) => (index = IndexOf(id)) > -1; public bool Contains(T item) => IndexOf(item) > -1; public bool TryGetValue(Guid id, [NotNullWhen(true)] out T? item) { Monitor.Enter(_syncRoot); try { item = InnerList.FirstOrDefault(x => x.Id == id); return item != null; } finally { Monitor.Exit(_syncRoot); } } public void CopyTo(T[] array) => CopyTo(array, 0); public void CopyTo(T[] array, int arrayIndex) { Monitor.Enter(_syncRoot); try { InnerList.CopyTo(array, arrayIndex); } finally { Monitor.Exit(_syncRoot); } } public int BinarySearch(int index, int count, T item, IComparer? comparer) { throw new NotImplementedException($"BinarySearch"); if (index < 0) throw new ArgumentOutOfRangeException(nameof(index)); if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); if (Count - index < count) throw new ArgumentException("Invalid length"); //Monitor.Enter(_syncRoot); //try //{ // return InnerList.BinarySearch(index, count, item, comparer); //} //finally //{ // Monitor.Exit(_syncRoot); //} } public int BinarySearch(T item) => BinarySearch(0, Count, item, null); public int BinarySearch(T item, IComparer? comparer) => BinarySearch(0, Count, item, comparer); public IEnumerator GetEnumerator() { Monitor.Enter(_syncRoot); try { //return InnerList.ToList().GetEnumerator(); return InnerList.GetEnumerator(); } finally { Monitor.Exit(_syncRoot); } } public ReadOnlyCollection AsReadOnly() => new(this); private static bool IsCompatibleObject(object? value) => (value is T) || (value == null && default(T) == null); #region IList, ICollection bool IList.IsReadOnly => false; object? IList.this[int index] { get => this[index]; set { if (default(T) != null && value == null) throw new NullReferenceException(nameof(value)); try { this[index] = (T)value!; } catch (InvalidCastException) { throw new InvalidCastException(nameof(value)); } } } int IList.Add(object? item) { if (default(T) != null && item == null) throw new NullReferenceException(nameof(item)); try { Add((T)item!); } catch (InvalidCastException) { throw new InvalidCastException(nameof(item)); } return Count - 1; } void IList.Clear() => Clear(true); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); bool IList.Contains(object? item) => IsCompatibleObject(item) && Contains((T)item!); int IList.IndexOf(object? item) => (IsCompatibleObject(item)) ? IndexOf((T)item!) : -1; void IList.Insert(int index, object? item) { if (default(T) != null && item == null) throw new NullReferenceException(nameof(item)); try { Insert(index, (T)item!); } catch (InvalidCastException) { throw new InvalidCastException(nameof(item)); } } void IList.Remove(object? item) { if (IsCompatibleObject(item)) Remove((T)item!); } void ICollection.Clear() => Clear(true); void ICollection.CopyTo(Array array, int arrayIndex) { if ((array != null) && (array.Rank != 1)) { throw new ArgumentException(); } try { Monitor.Enter(_syncRoot); try { //TODO: _list.ToArray() - ez nem az igazi... - J. Array.Copy(InnerList.ToArray(), 0, array!, arrayIndex, InnerList.Count); } finally { Monitor.Exit(_syncRoot); } } catch (ArrayTypeMismatchException) { throw new ArrayTypeMismatchException(); } } int ICollection.Count => Count; int ICollection.Count => Count; bool ICollection.IsReadOnly => false; void IList.RemoveAt(int index) => RemoveAt(index); int IReadOnlyCollection.Count => Count; #endregion IList, ICollection } public class ItemChangedEventArgs where T : IId { internal ItemChangedEventArgs(T item, TrackingState trackingState) { Item = item; TrackingState = trackingState; } public T Item { get; } public TrackingState TrackingState { get; } } }