using System.Collections; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Runtime.CompilerServices; using Avalonia.Controls.Selection; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using InABox.Clients; using InABox.Configuration; using InABox.Core; using JetBrains.Annotations; using NotNullAttribute = JetBrains.Annotations.NotNullAttribute; namespace InABox.Avalonia { public class CoreRepositoryItemCreatedArgs : EventArgs { public TShell Item { get; private set; } public CoreRepositoryItemCreatedArgs(TShell item) { Item = item; } } public delegate void CoreRepositoryItemCreatedEvent(object sender, CoreRepositoryItemCreatedArgs args); public abstract class CoreRepository { public static bool IsCached(string? filename) => !String.IsNullOrWhiteSpace(filename) && File.Exists(CacheFileName(filename)); public static string CacheFileName(string filename) => Path.Combine(CacheFolder(), filename); public static string CacheFolder() { var result = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); if (OperatingSystem.IsWindows()) { var assembly = Path.GetFileNameWithoutExtension(System.Diagnostics.Process.GetCurrentProcess().MainModule.ModuleName); result = Path.Combine(result, assembly); } if (CacheID != Guid.Empty) result = Path.Combine(result,CacheID.ToString()); if (!Directory.Exists(result)) Directory.CreateDirectory(result); return result; } public static Guid CacheID { get; set; } } public partial class CoreRepositoryFilter : ObservableObject { [ObservableProperty] private string? _name; [ObservableProperty] private string? _filter; [ObservableProperty] private bool _selected; public override string ToString() => Name ?? ""; } public abstract class CoreRepository : CoreRepository, ICoreRepository, IEnumerable where TParent : CoreRepository where TEntity : Entity, IRemotable, IPersistent, new() where TItem : Shell, new() { readonly MultiQuery _query = new(); public Func> Filter { get; set; } protected virtual Filter? BaseFilter() => null; public IModelHost Host { get; set; } private DateTime _lastUpdated = DateTime.MinValue; public DateTime LastUpdated { get => _lastUpdated; protected set { _lastUpdated = value; OnPropertyChanged(); } } public Func? FileName { get; } private string DataFileName() => FileName != null ? $"{FileName.Invoke()}.data" : string.Empty; private string FilterFileName() => !string.IsNullOrWhiteSpace(FilterTag) && FileName != null ? $"{FileName.Invoke()}.filter" : string.Empty; protected CoreRepository(IModelHost host, Func> filter, Func? filename = null) { AllItems = new CoreObservableCollection(); AllItems.CollectionChanged += (sender, args) => ItemsChanged(AllItems); // EnableSynchronization(AllItems); Items = new CoreObservableCollection(); // EnableSynchronization(Items); Reset(); Host = host; Filter = filter; FileName = filename; } protected virtual void ItemsChanged(IEnumerable items) { } // private void EnableSynchronization(IEnumerable items) // { // // BindingBase.EnableCollectionSynchronization(items, null, // // (collection, context, method, access) => // // { // // lock (collection) // // { // // method?.Invoke(); // // } // // } // // ); // } #region INotifyPropertyChanged public event PropertyChangedEventHandler? PropertyChanged; protected void DoPropertyChanged(object sender, PropertyChangedEventArgs args) { PropertyChanged?.Invoke(sender, args); } protected bool SetProperty(ref T field, T value, [CallerMemberName] string propertyName = "") { if (EqualityComparer.Default.Equals(field, value)) return false; field = value; OnPropertyChanged(propertyName); return true; } protected void OnPropertyChanged([CallerMemberName] string propertyName = "") => DoPropertyChanged(this, new PropertyChangedEventArgs(propertyName)); #endregion #region Image Lookups public Dictionary Images { get; private set; } = new Dictionary(); public byte[]? GetImageSource(Guid id) { return Images.GetValueOrDefault(id); } public byte[]? GetImage(Guid id) => Images.GetValueOrDefault(id); public bool HasImages() => Images.Any(); #endregion protected virtual string FilterTag => typeof(TEntity).EntityName().Split('.').Last(); public CoreObservableCollection AvailableFilters { get; } = new(); IEnumerable ICoreRepository.AvailableFilters => AvailableFilters; protected Filter? SelectedFilter => Serialization.Deserialize>(AvailableFilters.FirstOrDefault(x => x.Selected)?.Filter); public bool FiltersVisible => AvailableFilters.Any(); public string? SelectedFilterName => AvailableFilters.FirstOrDefault(x => x.Selected)?.Name; public void SelectFilter(String? name) { var definition = AvailableFilters.FirstOrDefault(x => String.Equals(x.Name, name)); foreach (var availableFilter in AvailableFilters) availableFilter.Selected = definition == availableFilter; OnPropertyChanged(nameof(AvailableFilters)); } protected Filter? EffectiveFilter() { var filters = new Filters(); filters.Add(BaseFilter()); filters.Add(Filter?.Invoke()); filters.Add(SelectedFilter); var result = filters.Combine(); return result; } protected Columns GetColumns() where TOtherItem : Shell, new() where TOtherEntity : Entity, IRemotable, IPersistent, new() { return new TOtherItem().Columns.Columns; } protected virtual void Initialize() { Loaded = false; AllItems.Clear(); Items.Clear(); Images.Clear(); } public bool Loaded { get; protected set; } private void DoRefresh(bool force) { // force = force || Host.Status == ConnectionStatus.Connected; var selectedIDs = SelectedItems.Select(x => x.ID).ToHashSet(); Items.Clear(); var dataFileName = DataFileName(); if (!force && !Loaded && CoreRepository.IsCached(dataFileName)) { DoBeforeLoad(); if (LoadFromStorage()) { DoAfterLoad(); foreach(var item in Items) { item.IsSelected = selectedIDs.Contains(item.ID); } return; } } if ((force || !Loaded) && (Host.Status == ConnectionStatus.Connected)) { DoLoad(); SaveToStorage(); foreach(var item in Items) { item.IsSelected = selectedIDs.Contains(item.ID); } return; } foreach(var item in Items) { item.IsSelected = selectedIDs.Contains(item.ID); } } private void AfterRefresh() { Loaded = true; Dispatcher.UIThread.Invoke(Search); NotifyChanged(); } public virtual ICoreRepository Refresh(bool force) { DoRefresh(force); AfterRefresh(); return this; } public void Refresh(bool force, Action loaded) { Task.Run( () => { DoRefresh(force); Dispatcher.UIThread.Post( () => { AfterRefresh(); loaded?.Invoke(); } ); } ); } public Task RefreshAsync(bool force) { return Task.Run(() => Refresh(force)); } public void Reset() { Initialize(); } public event CoreRepositoryChangedEvent? Changed; protected void NotifyChanged() => Changed?.Invoke(this, new CoreRepositoryChangedEventArgs()); public virtual SortOrder? Sort => LookupFactory.DefineSort(); protected CoreObservableCollection AllItems { get; private set; } private CoreTable _table = new CoreTable(); public CoreObservableCollection Items { get; private set; } public int ItemCount => Items.Count; IEnumerable ICoreRepository.Items => Items; #region Item Selection IEnumerable ICoreRepository.SelectedItems => SelectedItems; IEnumerable SelectedItems => Items.Where(x => x.IsSelected); public bool IsSelected(TItem? item) => item is not null && item.IsSelected; public void SetSelectedItems(IEnumerable items) { var selectedItems = items.Select(x => x.ID).ToHashSet(); foreach(var item in Items) { item.IsSelected = selectedItems.Contains(item.ID); } Search(); } public void SelectItem([CanBeNull] TItem? item) { if ((item != null) && !item.IsSelected) { item.IsSelected = true; Search(); } } public void UnselectItem([CanBeNull] TItem? item) { if ((item != null) && item.IsSelected) { item.IsSelected = false; Search(); } } public void ToggleSelection(TItem? item) { if (IsSelected(item)) UnselectItem(item); else SelectItem(item); } public void SelectNone() { foreach(var item in AllItems) { item.IsSelected = false; } Search(); } public void SelectAll() { foreach(var item in AllItems) { item.IsSelected = true; } Search(); } void ICoreRepository.SelectItem(object item) => SelectItem(item as TItem); void ICoreRepository.UnselectItem(object item) => UnselectItem(item as TItem); void ICoreRepository.ToggleSelection(object item) => ToggleSelection(item as TItem); bool ICoreRepository.IsSelected(object item) => IsSelected(item as TItem); void ICoreRepository.SetSelectedItems(IEnumerable items) => SetSelectedItems(items.OfType()); #endregion #region Searching public Func? SearchPredicate { get; set; } public Func,List>? SortPredicate { get; set; } public ICoreRepository Search(Func searchpredicate, Func,List> sortpredicate) { SortPredicate = sortpredicate; SearchPredicate = searchpredicate; Search(); return this; } public ICoreRepository Search(Func? searchpredicate) { SearchPredicate = searchpredicate; Search(); return this; } public ICoreRepository Search() { var items = AllItems == null ? new List() : SearchPredicate != null ? new List(AllItems.Where(SearchPredicate)) : new List(AllItems); if (SortPredicate != null) items = SortPredicate(items); Items.ReplaceRange(items); OnPropertyChanged(nameof(Items)); OnPropertyChanged(nameof(ItemCount)); return this; } ICoreRepository ICoreRepository.Search(Func method) => Search((o) => method(o as TItem)); #endregion protected virtual Expression>? ImageColumn => null; #region Loading private void DoBeforeLoad() { _query.Clear(); _query.Add( EffectiveFilter(), GetColumns(), Sort ); if (ImageColumn is not null) { _query.Add( new Filter(x => x.ID).InQuery(EffectiveFilter(), ImageColumn), Columns.None().Add(x => x.ID) .Add(x => x.Data) ); } } protected virtual void BeforeLoad(MultiQuery query) { } protected virtual void AfterLoad(MultiQuery query) { } protected void DoLoad() { try { var selected = AvailableFilters.FirstOrDefault(x => x.Selected)?.Name; if (!string.IsNullOrWhiteSpace(FilterTag)) { var filters = new GlobalConfiguration(FilterTag).Load() .Where(x => x.Visibility == CoreFilterDefinitionVisibility.DesktopAndMobile) .Select(x => new CoreRepositoryFilter() { Name = x.Name, Filter = x.Filter, Selected = string.Equals(x.Name,selected) }) .ToList(); if (filters.Any()) filters.Insert(0, new CoreRepositoryFilter() { Name="All", Filter = "", Selected = !filters.Any(x => string.Equals(x.Name, selected)) }); AvailableFilters.ReplaceRange(filters); Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(FiltersVisible))); } DoBeforeLoad(); BeforeLoad(_query); Task.Run(() => { _query.Query(); DoAfterLoad(); }).Wait(); AfterLoad(_query); Search(); LastUpdated = DateTime.Now; } catch (Exception e) { MobileLogging.Log(e,"CoreRepository"); } } protected void DoAfterLoad() { _table = _query.Get(); AllItems.ReplaceRange(_query.Get().Rows.Select(CreateItem)); if (ImageColumn != null) { Images.Clear(); _query.Get().IntoDictionary(Images, x => x.ID, r => r.Get(x => x.Data)); } } #endregion #region Persistent Storage protected void InitializeTables() { var defs = _query.Definitions(); foreach (var def in defs) { var table = InitializeTable(def.Value); _query.Set(def.Key, table); } } protected CoreTable InitializeTable(IQueryDef def) { var table = new CoreTable(); if (def.Columns != null) table.LoadColumns(def.Columns); else table.LoadColumns(def.Type); return table; } protected class QueryStorage : ISerializeBinary { private readonly Dictionary _data = new Dictionary(); public CoreTable Get([NotNull] String key) => _data[key]; public void Set([NotNull] String key, CoreTable table) => _data[key] = table; public bool Contains([NotNull] String key) => _data.ContainsKey(key); public bool TryGet(string key, [NotNullWhen(true)] out CoreTable? table) { return _data.TryGetValue(key, out table); } public void SerializeBinary(CoreBinaryWriter writer) { writer.Write(_data.Count); foreach (var key in _data.Keys) { writer.Write(key); _data[key].SerializeBinary(writer); } } public void DeserializeBinary(CoreBinaryReader reader) { int count = reader.ReadInt32(); for (int i = 0; i < count; i++) { String key = reader.ReadString(); CoreTable table = new CoreTable(); table.DeserializeBinary(reader); _data[key] = table; } } } protected bool LoadFromStorage() { var filterFileName = FilterFileName(); if (!filterFileName.IsNullOrWhiteSpace()) { if(CacheManager.TryLoadJSON>(filterFileName, out var filters, out var _)) { AvailableFilters.ReplaceRange(filters); Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(FiltersVisible))); } } var dataFileName = DataFileName(); if (dataFileName.IsNullOrWhiteSpace()) { InitializeTables(); return true; } if(CacheManager.TryLoadBinary(dataFileName, out var storage, out var lastUpdated)) { LastUpdated = lastUpdated; var defs = _query.Definitions(); foreach (var key in defs.Keys) { var keyStr = key.ToString() ?? ""; if(!storage.TryGet(keyStr, out var table)) { table = InitializeTable(defs[key]); } var queryDef = _query.Definitions()[key]; if (CheckColumns(table, queryDef.Type, queryDef.Columns)) _query.Set(key, table); else return false; } } else { InitializeTables(); } return true; } private bool CheckColumns(CoreTable table, Type T, IColumns? required) { required = CoreUtils.GetColumns(T, required); foreach (var column in required.ColumnNames()) { if (!table.Columns.Any(x => String.Equals(x.ColumnName, column))) return false; } return true; } protected void SaveToStorage() { var filterFileName = FilterFileName(); if (!string.IsNullOrWhiteSpace(filterFileName)) { CacheManager.SaveJSON(filterFileName, AvailableFilters, DateTime.MaxValue); } var dataFileName = DataFileName(); if (dataFileName.IsNullOrWhiteSpace()) return; QueryStorage storage = new QueryStorage(); var results = _query.Results(); foreach (var key in results.Keys) storage.Set(key.ToString() ?? "", results[key]); CacheManager.SaveBinary(dataFileName, storage, DateTime.MaxValue); } #endregion #region CRUD Operations public event CoreRepositoryItemCreatedEvent? ItemAdded; private T CreateItem(CoreRow row) where T : Shell, new() { var result = new T() { Row = row, Parent = (TParent)this }; result.PropertyChanged += (_, args) => DoPropertyChanged(result, args); return result; } public virtual TItem CreateItem() { CoreRow row = _table.NewRow(); var entity = new TEntity(); _table.FillRow(row,entity); var result = CreateItem(row); ItemAdded?.Invoke(this, new CoreRepositoryItemCreatedArgs(result)); return result; } public virtual void CommitItem(TItem item) { _table.Rows.Add(item.Row); AllItems.Add(item); Search(); NotifyChanged(); } public virtual TItem AddItem() { var result = CreateItem(); CommitItem(result); return result; } public virtual void DeleteItem(TItem item) { _table.Rows.Remove(item.Row); AllItems.Remove(item); Search(); NotifyChanged(); } public virtual void Save(string auditMessage) { new Client().Save(Items.Select(x=>x.Entity).Where(x=>x.IsChanged()),auditMessage); } public virtual Task SaveAsync(string auditMessage) { return Task.Run(() => Save(auditMessage)); } object ICoreRepository.CreateItem() => this.CreateItem(); void ICoreRepository.CommitItem(object item) { if (item is TItem titem) CommitItem(titem); } object ICoreRepository.AddItem() => this.AddItem(); void ICoreRepository.DeleteItem(object item) { if (item is TItem titem) DeleteItem(titem); } #endregion #region IEnumerable Interface IEnumerator IEnumerable.GetEnumerator() { return Items.GetEnumerator(); } public IEnumerator GetEnumerator() { return Items.GetEnumerator(); } #endregion } }