using AyCode.Core.Helpers; using AyCode.Core.Interfaces; using AyCode.Core.Loggers; using AyCode.Services.Server.SignalRs; using AyCode.Services.SignalRs; using DevExpress.Blazor; using DevExpress.Data.Filtering; using DevExpress.Data.Linq; using DevExpress.Data.Linq.Helpers; using System.Collections; namespace AyCode.Blazor.Components.Components.Grids; #region Models /// /// Sorting information for a single field /// public class SignalRGridSortInfo { public string FieldName { get; set; } = ""; public bool Descending { get; set; } } #endregion /// /// GridCustomDataSource implementation that wraps AcSignalRDataSource. /// Provides instant local filtering for previously seen filter criteria, /// while refreshing data in background using SignalR callback pattern. /// /// Key features: /// - Uses AcSignalRDataSource for caching and background populate /// - Tracks seen filter criteria - if already seen, returns local data instantly /// - Background refresh with callback/populate pattern (no UI blocking) /// - Full GridCustomDataSource support (filter, sort, page, group, summary) /// /// Entity type implementing IId /// ID type (int, Guid, long, etc.) public class MgGridSignalRDataSource : GridCustomDataSource where TDataItem : class, IId where TId : struct { private readonly AcSignalRDataSource> _innerDataSource; private readonly AcLoggerBase? _logger; // DevExpress CriteriaOperator to Expression converter private readonly CriteriaToExpressionConverter _criteriaConverter = new(); // Track filter criteria that have been seen before private readonly HashSet _knownFilterCriteria = new(StringComparer.Ordinal); // Lock for thread-safe operations private readonly object _syncLock = new(); // Event fired when background refresh completes public event Action? OnBackgroundRefreshCompleted; /// /// Creates a new MgGridSignalRDataSource wrapping an existing AcSignalRDataSource /// /// The underlying AcSignalRDataSource that handles caching and SignalR communication /// Optional logger for debugging public MgGridSignalRDataSource( AcSignalRDataSource> innerDataSource, AcLoggerBase? logger = null) { _innerDataSource = innerDataSource ?? throw new ArgumentNullException(nameof(innerDataSource)); _logger = logger; // Subscribe to data source events _innerDataSource.OnDataSourceLoaded += OnInnerDataSourceLoaded; } /// /// Specifies the data item type for the grid /// protected override Type DataItemType => typeof(TDataItem); /// /// Gets the inner AcSignalRDataSource for direct access if needed /// public AcSignalRDataSource> InnerDataSource => _innerDataSource; #region GridCustomDataSource Implementation /// /// Gets the total count of items matching the current filter. /// If filter was seen before, returns local count instantly and refreshes in background. /// public override async Task GetItemCountAsync( GridCustomDataSourceCountOptions options, CancellationToken cancellationToken) { var filterKey = GetFilterKey(options.FilterCriteria); _logger?.Debug($"[MgGridSignalRDataSource] GetItemCountAsync - Filter: {filterKey}"); // If we have local data and this filter was seen before, return local count if (_innerDataSource.Count > 0 && IsKnownFilter(filterKey)) { var localCount = ApplyLocalFilter(_innerDataSource.ToList(), options.FilterCriteria).Count; _logger?.Debug($"[MgGridSignalRDataSource] Returning local count: {localCount}, refreshing in background"); // Refresh in background (fire-and-forget) _ = RefreshInBackgroundAsync(filterKey); return localCount; } // First time seeing this filter - must wait for server _logger?.Debug("[MgGridSignalRDataSource] New filter, waiting for server data"); await LoadFromServerAsync(); MarkFilterAsKnown(filterKey); return ApplyLocalFilter(_innerDataSource.ToList(), options.FilterCriteria).Count; } /// /// Gets items for the current page with filtering and sorting applied. /// If filter was seen before, returns local data instantly and refreshes in background. /// public override async Task GetItemsAsync( GridCustomDataSourceItemsOptions options, CancellationToken cancellationToken) { var filterKey = GetFilterKey(options.FilterCriteria); _logger?.Debug($"[MgGridSignalRDataSource] GetItemsAsync - Skip: {options.StartIndex}, Take: {options.Count}, Filter: {filterKey}"); // If we have local data and this filter was seen before, return local data if (_innerDataSource.Count > 0 && IsKnownFilter(filterKey)) { var localResult = GetLocalItems(options); _logger?.Debug($"[MgGridSignalRDataSource] Returning {localResult.Count} local items, refreshing in background"); // Refresh in background (fire-and-forget) _ = RefreshInBackgroundAsync(filterKey); return localResult; } // First time seeing this filter - must wait for server _logger?.Debug("[MgGridSignalRDataSource] New filter, waiting for server data"); await LoadFromServerAsync(); MarkFilterAsKnown(filterKey); return GetLocalItems(options); } /// /// Gets unique values for a column (used in filter dropdowns). /// Always returns from local data. /// public override Task GetUniqueValuesAsync( GridCustomDataSourceUniqueValuesOptions options, CancellationToken cancellationToken) { _logger?.Debug($"[MgGridSignalRDataSource] GetUniqueValuesAsync - Field: {options.FieldName}"); if (_innerDataSource.Count == 0) return Task.FromResult(Array.Empty()); try { var propertyInfo = typeof(TDataItem).GetProperty(options.FieldName); if (propertyInfo == null) return Task.FromResult(Array.Empty()); var uniqueValues = _innerDataSource .Select(item => propertyInfo.GetValue(item)) .Where(v => v != null) .Distinct() .Cast() .ToArray(); _logger?.Debug($"[MgGridSignalRDataSource] Found {uniqueValues.Length} unique values for {options.FieldName}"); return Task.FromResult(uniqueValues); } catch (Exception ex) { _logger?.Error($"[MgGridSignalRDataSource] GetUniqueValuesAsync failed: {ex.Message}", ex); return Task.FromResult(Array.Empty()); } } /// /// Gets group information for grouped data. /// Currently delegates to base implementation. /// public override async Task> GetGroupInfoAsync( GridCustomDataSourceGroupingOptions options, CancellationToken cancellationToken) { _logger?.Debug("[MgGridSignalRDataSource] GetGroupInfoAsync"); // TODO: Implement local grouping when needed return await base.GetGroupInfoAsync(options, cancellationToken); } /// /// Gets total summary values. /// Calculates from local data. /// public override Task GetTotalSummaryAsync( GridCustomDataSourceTotalSummaryOptions options, CancellationToken cancellationToken) { _logger?.Debug($"[MgGridSignalRDataSource] GetTotalSummaryAsync - Summaries: {options.SummaryInfo?.Count ?? 0}"); if (options.SummaryInfo == null || options.SummaryInfo.Count == 0 || _innerDataSource.Count == 0) return Task.FromResult(new List()); var filteredData = ApplyLocalFilter(_innerDataSource.ToList(), options.FilterCriteria); var summaryValues = new List(); foreach (var summaryInfo in options.SummaryInfo) { var value = CalculateSummary(filteredData, summaryInfo); summaryValues.Add(value); } _logger?.Debug($"[MgGridSignalRDataSource] Calculated {summaryValues.Count} summary values"); return Task.FromResult(summaryValues); } #endregion #region Local Data Operations /// /// Gets items from local cache with filter, sort, and paging applied /// private List GetLocalItems(GridCustomDataSourceItemsOptions options) { var data = _innerDataSource.ToList(); // Apply filter var filtered = ApplyLocalFilter(data, options.FilterCriteria); // Apply sorting var sorted = ApplyLocalSort(filtered, options.SortInfo); // Apply paging var paged = sorted .Skip(options.StartIndex) .Take(options.Count) .ToList(); return paged; } /// /// Applies CriteriaOperator filter to local data using DevExpress CriteriaToExpressionConverter /// private List ApplyLocalFilter(List data, CriteriaOperator? criteria) { if (criteria is null || data.Count == 0) return data; try { // Use DevExpress built-in CriteriaToExpressionConverter var filteredData = data.AsQueryable().AppendWhere(_criteriaConverter, criteria); return filteredData.Cast().ToList(); } catch (Exception ex) { _logger?.Error($"[MgGridSignalRDataSource] Local filter failed: {ex.Message}", ex); return data; } } /// /// Applies sorting to local data /// private List ApplyLocalSort(List data, IReadOnlyList? sortInfo) { if (sortInfo == null || sortInfo.Count == 0 || data.Count == 0) return data; try { IOrderedEnumerable? ordered = null; for (var i = 0; i < sortInfo.Count; i++) { var sort = sortInfo[i]; var propertyInfo = typeof(TDataItem).GetProperty(sort.FieldName); if (propertyInfo == null) continue; Func keySelector = item => propertyInfo.GetValue(item); if (i == 0) { ordered = sort.DescendingSortOrder ? data.OrderByDescending(keySelector) : data.OrderBy(keySelector); } else if (ordered != null) { ordered = sort.DescendingSortOrder ? ordered.ThenByDescending(keySelector) : ordered.ThenBy(keySelector); } } return ordered?.ToList() ?? data; } catch (Exception ex) { _logger?.Error($"[MgGridSignalRDataSource] Local sort failed: {ex.Message}", ex); return data; } } /// /// Calculates a summary value for the given data /// private object? CalculateSummary(List data, GridCustomDataSourceSummaryInfo summaryInfo) { if (data.Count == 0) return null; try { var propertyInfo = typeof(TDataItem).GetProperty(summaryInfo.FieldName); return summaryInfo.SummaryType switch { GridSummaryItemType.Count => data.Count, GridSummaryItemType.Sum when propertyInfo != null => data.Sum(item => Convert.ToDecimal(propertyInfo.GetValue(item) ?? 0)), GridSummaryItemType.Min when propertyInfo != null => data.Min(item => propertyInfo.GetValue(item)), GridSummaryItemType.Max when propertyInfo != null => data.Max(item => propertyInfo.GetValue(item)), GridSummaryItemType.Avg when propertyInfo != null => data.Average(item => Convert.ToDecimal(propertyInfo.GetValue(item) ?? 0)), _ => null }; } catch (Exception ex) { _logger?.Error($"[MgGridSignalRDataSource] Summary calculation failed: {ex.Message}", ex); return null; } } #endregion #region Filter Criteria Tracking /// /// Gets a unique key for the filter criteria /// private string GetFilterKey(CriteriaOperator? criteria) { if (criteria is null) return string.Empty; try { return CriteriaOperator.ToString(criteria); } catch { return string.Empty; } } /// /// Checks if this filter has been seen before /// private bool IsKnownFilter(string filterKey) { lock (_syncLock) { return _knownFilterCriteria.Contains(filterKey); } } /// /// Marks a filter as known (seen before) /// private void MarkFilterAsKnown(string filterKey) { lock (_syncLock) { _knownFilterCriteria.Add(filterKey); } } /// /// Clears the known filter cache /// public void ClearKnownFilters() { lock (_syncLock) { _knownFilterCriteria.Clear(); } _logger?.Debug("[MgGridSignalRDataSource] Known filters cleared"); } #endregion #region Server Communication /// /// Loads data from server synchronously (blocking) /// private Task LoadFromServerAsync() { _logger?.Debug("[MgGridSignalRDataSource] Loading from server (sync)"); return _innerDataSource.LoadDataSource(); } /// /// Refreshes data in background using callback pattern (non-blocking) /// private Task RefreshInBackgroundAsync(string filterKey) { _logger?.Debug($"[MgGridSignalRDataSource] Starting background refresh for filter: {filterKey}"); // Use async callback version - this won't block return _innerDataSource.LoadDataSourceAsync(); } /// /// Called when inner data source finishes loading /// private Task OnInnerDataSourceLoaded() { _logger?.Debug("[MgGridSignalRDataSource] Inner data source loaded, triggering refresh event"); OnBackgroundRefreshCompleted?.Invoke(); return Task.CompletedTask; } #endregion #region Cleanup /// /// Invalidates all caches and clears known filters /// public void InvalidateCache() { ClearKnownFilters(); _logger?.Debug("[MgGridSignalRDataSource] Cache invalidated"); } #endregion }