using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Linq.Expressions; using System.Runtime.CompilerServices; using System.Threading.Tasks; using InABox.Clients; using InABox.Configuration; using InABox.Core; using InABox.Mobile.Shared; using JetBrains.Annotations; using Syncfusion.SfDataGrid.XForms; using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.Forms; using Columns = InABox.Core.Columns; namespace InABox.Mobile { 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 string CacheFileName() => typeof(T).EntityName().Split('.').Last(); public static string CacheFileName(string postfix) { var result = CacheFileName(); if (!string.IsNullOrWhiteSpace(postfix)) result = $"{result}_{postfix}"; return result; } [CanBeNull] public abstract string FileName(); public bool IsCached() => CoreRepository.IsCached(FileName()); public static bool IsCached(string filename) => !String.IsNullOrWhiteSpace(filename) && File.Exists(CacheFileName(filename)); public static string CacheFileName(string filename) => Path.Combine(CacheFolder(), $"{filename}.cache"); public static string CacheFolder() { var result = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); 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 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; } public DateTime LastUpdated { get; protected set; } [CanBeNull] private Func _cacheFileName { get; set; } protected CoreRepository(IModelHost host, [CanBeNull] Func> filter = null, [CanBeNull] Func cachefilename = null) { AllItems = new ObservableRangeCollection(); AllItems.CollectionChanged += (sender, args) => ItemsChanged(AllItems); EnableSynchronization(AllItems); Items = new ObservableRangeCollection(); EnableSynchronization(Items); SelectedItems = new ObservableRangeCollection(); EnableSynchronization(SelectedItems); Reset(); Host = host; Filter = filter; _cacheFileName = cachefilename; } [CanBeNull] public override string FileName() => _cacheFileName?.Invoke(); 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 = null) { if (EqualityComparer.Default.Equals(field, value)) return false; field = value; OnPropertyChanged(propertyName); return true; } protected void OnPropertyChanged([CallerMemberName] string propertyName = null) => DoPropertyChanged(this, new PropertyChangedEventArgs(propertyName)); #endregion #region Image Lookups public Dictionary Images { get; private set; } = new Dictionary(); public ImageSource GetImageSource(Guid id) { ImageSource result = null; if (Images.TryGetValue(id, out byte[] data)) result = ImageSource.FromStream(() => new MemoryStream(data)); return result; } public byte[] GetImage(Guid id) => Images.GetValueOrDefault(id); public bool HasImages() => Images.Any(); #endregion protected virtual string FilterTag => typeof(TEntity).EntityName().Split('.').Last(); public CoreFilterDefinitions AvailableFilters() { var result = new CoreFilterDefinitions(); if (!string.IsNullOrWhiteSpace(FilterTag)) { var mobile = new GlobalConfiguration(FilterTag) .Load() .Where(x => x.Visibility == CoreFilterDefinitionVisibility.DesktopAndMobile); result.AddRange(mobile); } return result; } protected Filter SelectedFilter; public void SelectFilter(String name) { var definition = AvailableFilters().FirstOrDefault(x => String.Equals(x.Name, name)); SelectedFilter = definition?.AsFilter(); } 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(); SelectedItems.Clear(); Images.Clear(); } public bool Loaded { get; protected set; } private void DoRefresh(bool force) { var curselected = SelectedItems.ToArray(); Items.Clear(); SelectedItems.Clear(); if (!Loaded && IsCached(FileName())) { DoBeforeLoad(); if (LoadFromStorage()) { DoAfterLoad(); SelectedItems.AddRange(Items.Where(x=>curselected.Contains(x))); return; } } if ((force || !Loaded) && Host.IsConnected()) { DoLoad(); SaveToStorage(); SelectedItems.AddRange(Items.Where(x=>curselected.Contains(x))); return; } SelectedItems.AddRange(Items.Where(x=>curselected.Contains(x))); } private void AfterRefresh() { Loaded = true; Search(); NotifyChanged(); } public virtual ICoreRepository Refresh(bool force) { DoRefresh(force); AfterRefresh(); return this; } public void Refresh(bool force, Action loaded) { Task.Run(() => { DoRefresh(force); }) .BeginInvokeOnMainThread((_) => { AfterRefresh(); loaded?.Invoke(); }); } public void Reset() { Initialize(); } public event CoreRepositoryChangedEvent Changed; protected void NotifyChanged() => Changed?.Invoke(this, new CoreRepositoryChangedEventArgs()); public virtual SortOrder Sort => LookupFactory.DefineSort(); protected ObservableRangeCollection AllItems { get; private set; } private CoreTable _table = new CoreTable(); public ObservableRangeCollection Items { get; private set; } IEnumerable ICoreRepository.Items => Items; #region Item Selection public ObservableRangeCollection SelectedItems { get; private set; } IEnumerable ICoreRepository.SelectedItems => SelectedItems; public bool IsSelected(TItem item) => (item != null) && SelectedItems.Contains(item); public void SetSelectedItems(IEnumerable items) { SelectedItems.ReplaceRange(items); Search(); } public void SelectItem([CanBeNull] TItem item) { if ((item != null) && !SelectedItems.Contains(item)) { SelectedItems.Add(item); Search(); } } public void UnselectItem([CanBeNull] TItem item) { if ((item != null) && SelectedItems.Contains(item)) { SelectedItems.Remove(item); Search(); } } public void ToggleSelection(TItem item) { if (IsSelected(item)) UnselectItem(item); else SelectItem(item); } public void SelectNone() { SelectedItems.Clear(); Search(); } public void SelectAll() { SelectedItems.ReplaceRange(Items); 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 curselected = SelectedItems.ToArray(); var items = AllItems == null ? new List() : SearchPredicate != null ? new List(AllItems.Where(SearchPredicate)) : new List(AllItems); if (SortPredicate != null) items = SortPredicate(items); SelectedItems.ReplaceRange(items.Where(x=>curselected.Contains(x))); OnPropertyChanged(nameof(SelectedItems)); Items.ReplaceRange(items); OnPropertyChanged(nameof(Items)); 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 != 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 { DoBeforeLoad(); BeforeLoad(_query); Task.Run(() => { _query.Query(); DoAfterLoad(); }).Wait(); Search(); AfterLoad(_query); 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 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() { if (String.IsNullOrWhiteSpace(FileName())) { InitializeTables(); return true; } var file = CacheFileName(FileName()); if (File.Exists(file)) { LastUpdated = File.GetLastWriteTime(file); using (var stream = new FileStream(file, FileMode.Open)) { QueryStorage storage = Serialization.ReadBinary(stream, BinarySerializationSettings.Latest); var defs = _query.Definitions(); foreach (var key in defs.Keys) { var table = storage.Contains(key.ToString()) ? storage.Get(key.ToString()) : InitializeTable(defs[key]); if (CheckColumns(table, _query.Definitions()[key].Columns)) _query.Set(key, table); else return false; } } } else InitializeTables(); return true; } private bool CheckColumns(CoreTable table, IColumns required) { foreach (var column in required.ColumnNames()) { if (!table.Columns.Any(x => String.Equals(x.ColumnName, column))) return false; } return true; } protected void SaveToStorage() { if (String.IsNullOrWhiteSpace(FileName())) return; QueryStorage storage = new QueryStorage(); var results = _query.Results(); foreach (var key in results.Keys) storage.Set(key.ToString(),results[key]); var data = storage.WriteBinary(BinarySerializationSettings.Latest); try { var file = $"{CacheFileName(FileName())}"; if (!string.IsNullOrWhiteSpace(file)) File.WriteAllBytes(file,data); } catch (Exception e) { MobileLogging.Log(e); } } #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(null); 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(null); NotifyChanged(); } public virtual void Save(string auditMessage) { new Client().Save(Items.Select(x=>x.Entity).Where(x=>x.IsChanged()),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 } }