using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Linq.Expressions; using System.Reflection; 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 Xamarin.Forms; 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 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() => Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); } public abstract class CoreRepository : CoreRepository, ICoreRepository, IEnumerable where TParent : CoreRepository where TEntity : Entity, IRemotable, IPersistent, new() where TItem : Shell, new() { readonly MultiQuery _query = new(); protected Func> BaseFilter { get; set; } public IModelHost Host { get; set; } public DateTime LastUpdated { get; protected set; } public String FileName { get; set; } protected CoreRepository(IModelHost host, Func> baseFilter) { _items = new CoreObservableCollection(); BindingBase.EnableCollectionSynchronization(_items, null, (collection, context, method, access) => { lock (collection) { method?.Invoke(); } } ); Reset(); Host = host; BaseFilter = baseFilter; } #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 GetImage(Guid id) { ImageSource result = null; if (Images.TryGetValue(id, out byte[] data)) result = ImageSource.FromStream(() => new MemoryStream(data)); return result; } public bool HasImages() => Images.Any(); #endregion protected virtual string FilterTag => typeof(TEntity).EntityName().Split('.').Last(); public CoreFilterDefinitions AvailableFilters() { return string.IsNullOrWhiteSpace(FilterTag) ? new CoreFilterDefinitions() : new GlobalConfiguration(FilterTag).Load(); } 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?.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 = null; _items.Clear(); Images.Clear(); } public bool Loaded { get; protected set; } private void DoRefresh(bool force) { var curselected = _selectedItems.ToArray(); _items.Clear(); _selectedItems.Clear(); if (!Loaded && CoreRepository.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(); private IList _allitems; private CoreTable _table = new CoreTable(); private CoreObservableCollection _items; public IList Items => _items; IEnumerable ICoreRepository.Items => Items; #region Item Selection private CoreObservableCollection _selectedItems = new CoreObservableCollection(); public IEnumerable SelectedItems => _selectedItems; 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 ICoreRepository Search(Func predicate) { SearchPredicate = predicate; 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); _selectedItems = new CoreObservableCollection(items.Where(x=>curselected.Contains(x))); OnPropertyChanged(nameof(SelectedItems)); //_selectedItems.RemoveAll(x => !items.Contains(x)); _items = new CoreObservableCollection(items); //_items.Clear(); //_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), new Columns(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 = new List(_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 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), FileName); 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 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(); } 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 } }