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<TShell> : EventArgs
    {
        public TShell Item { get; private set; }

        public CoreRepositoryItemCreatedArgs(TShell item)
        {
            Item = item;
        }
    }

    public delegate void CoreRepositoryItemCreatedEvent<TShell>(object sender, CoreRepositoryItemCreatedArgs<TShell> args);

    public abstract class CoreRepository
    {

        
        public static string CacheFileName<T>() => typeof(T).EntityName().Split('.').Last();
        public static string CacheFileName<T>(string postfix)
        {
            var result = CacheFileName<T>();
            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<TParent, TItem, TEntity> : CoreRepository, ICoreRepository, IEnumerable<TItem>
        where TParent : CoreRepository<TParent, TItem, TEntity>
        where TEntity : Entity, IRemotable, IPersistent, new() 
        where TItem : Shell<TParent,TEntity>, new()
    {
        readonly MultiQuery _query = new();
        
        public Func<Filter<TEntity>> Filter { get; set; }

        protected virtual Filter<TEntity> BaseFilter() => null;
        
        public IModelHost Host { get; set; }
        
        public DateTime LastUpdated { get; protected set; }
        
        [CanBeNull] private Func<string> _cacheFileName { get; set; }
        
        protected CoreRepository(IModelHost host, [CanBeNull] Func<Filter<TEntity>> filter = null, [CanBeNull] Func<string> cachefilename = null)
        {
            AllItems = new ObservableRangeCollection<TItem>();
            AllItems.CollectionChanged += (sender, args) => ItemsChanged(AllItems);
            EnableSynchronization(AllItems);
            
            Items = new ObservableRangeCollection<TItem>(); 
            EnableSynchronization(Items);

            SelectedItems = new ObservableRangeCollection<TItem>();
            EnableSynchronization(SelectedItems);
            
            Reset();
            Host = host;
            Filter = filter;
            _cacheFileName = cachefilename;
        }

        [CanBeNull] public override string FileName() => _cacheFileName?.Invoke();


        protected virtual void ItemsChanged(IEnumerable<TItem> 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<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
        {
            if (EqualityComparer<T>.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<Guid, byte[]> Images { get; private set; } = new Dictionary<Guid, byte[]>();

        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<CoreFilterDefinitions>(FilterTag)
                    .Load()
                    .Where(x => x.Visibility == CoreFilterDefinitionVisibility.DesktopAndMobile);
                result.AddRange(mobile);
            }
            return result;
        }

        protected Filter<TEntity> SelectedFilter;
        
        public void SelectFilter(String name)
        {
            var definition = AvailableFilters().FirstOrDefault(x => String.Equals(x.Name, name));
            SelectedFilter = definition?.AsFilter<TEntity>();
        }
        
        protected Filter<TEntity> EffectiveFilter()
        {
            var filters = new Filters<TEntity>();
            filters.Add(BaseFilter());
            filters.Add(Filter?.Invoke());
            filters.Add(SelectedFilter);
            var result = filters.Combine();
            return result;
        }
        
        protected Columns<TOtherEntity> GetColumns<TOtherItem, TOtherEntity>()
            where TOtherItem : Shell<TParent, TOtherEntity>, 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<TEntity> Sort => LookupFactory.DefineSort<TEntity>();

        protected ObservableRangeCollection<TItem> AllItems { get; private set; }
        
        private CoreTable _table = new CoreTable();

        public ObservableRangeCollection<TItem> Items { get; private set; }
        
        IEnumerable ICoreRepository.Items => Items;
        
        #region Item Selection
        
        public ObservableRangeCollection<TItem> SelectedItems { get; private set; }
        
        IEnumerable ICoreRepository.SelectedItems => SelectedItems;
        
        public bool IsSelected(TItem item) => (item != null) && SelectedItems.Contains(item);
        
        public void SetSelectedItems(IEnumerable<TItem> 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<object> items) => SetSelectedItems(items.OfType<TItem>());
        
        #endregion
        
        #region Searching
        
        public Func<TItem, bool> SearchPredicate { get; set; }
        
        public Func<List<TItem>,List<TItem>> SortPredicate { get; set; }

        public ICoreRepository Search(Func<TItem, bool> searchpredicate, Func<List<TItem>,List<TItem>> sortpredicate)
        {
            SortPredicate = sortpredicate;
            SearchPredicate = searchpredicate;
            Search();
            return this;
        }
        
        public ICoreRepository Search(Func<TItem, bool> searchpredicate)
        {
            SearchPredicate = searchpredicate;
            Search();
            return this;
        }

        public ICoreRepository Search()
        {
            var curselected = SelectedItems.ToArray();
            var items = AllItems == null
                ? new List<TItem>()
                : SearchPredicate != null
                    ? new List<TItem>(AllItems.Where(SearchPredicate))
                    : new List<TItem>(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<object,bool> method)
            => Search((o) => method(o as TItem));


        #endregion
        
        protected virtual Expression<Func<TEntity, object>> ImageColumn => null;
        
        #region Loading
        
        
        private void DoBeforeLoad()
        {
            _query.Clear();
            
            _query.Add(
                EffectiveFilter(),
                GetColumns<TItem,TEntity>(),
                Sort
            );
            
            if (ImageColumn != null)
            {
                _query.Add(
                    new Filter<Document>(x => x.ID).InQuery(EffectiveFilter(), ImageColumn),
                    Columns.None<Document>().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<TEntity>();
            AllItems.ReplaceRange(_query.Get<TEntity>().Rows.Select(CreateItem<TItem>));
                    
            if (ImageColumn != null)
            {
                Images.Clear();
                _query.Get<Document>().IntoDictionary<Document, Guid, byte[]>(Images, x => x.ID,
                    r => r.Get<Document, byte[]>(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<String, CoreTable> _data = new Dictionary<string, CoreTable>();

            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<QueryStorage>(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<TItem> ItemAdded;
        
        private T CreateItem<T>(CoreRow row) 
            where T : Shell<TParent,TEntity>, 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<TItem>(row);
            ItemAdded?.Invoke(this, new CoreRepositoryItemCreatedArgs<TItem>(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<TEntity>().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<TItem> IEnumerable<TItem>.GetEnumerator()
        {
            return Items.GetEnumerator();
        }

        public IEnumerator GetEnumerator()
        {
            return Items.GetEnumerator();
        }
        
        #endregion


    }
    

    
}