using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using InABox.Clients;
using InABox.Configuration;
using InABox.Core;
using InABox.Wpf;
using InABox.WPF;
using Expression = System.Linq.Expressions.Expression;

namespace InABox.DynamicGrid;

public interface IDynamicDataGrid : IDynamicGrid
{
    /// <summary>
    /// The tag the the DynamicGridColumns are stored against. If set to <see langword="null"/>,
    /// the name of <typeparamref name="TEntity"/> is used as a default.
    /// </summary>
    string? ColumnsTag { get; set; }

    IColumns LoadEditorColumns();
    
    Type DataType { get; }
}

public class DynamicDataGrid<TEntity> : DynamicGrid<TEntity>, IDynamicDataGrid where TEntity : Entity, IRemotable, IPersistent, new()
{

    public delegate void OnReloadEventHandler(object sender, Filters<TEntity> criteria, Columns<TEntity> columns, ref SortOrder<TEntity>? sortby);

    private readonly int ChunkSize = 500;
    private Button MergeBtn = null!; //Late-initialised

    public DynamicGridFilterButtonComponent<TEntity> FilterComponent;
    protected DynamicGridCustomColumnsComponent<TEntity> ColumnsComponent;

    private Column<TEntity>[] FilterColumns;

    public Type DataType => typeof(TEntity);

    public DynamicDataGrid() : base()
    {
        var fields = DatabaseSchema.Properties(typeof(TEntity));

        foreach (var field in fields)
            if (!MasterColumns.Any(x => x.ColumnName == field.Name))
                MasterColumns.Add(new DynamicGridColumn(field));

        var cols = LookupFactory.DefineColumns<TEntity>();

        // Minimum Columns for Lookup values
        foreach (var col in cols)
            HiddenColumns.Add(col);
        
        if (typeof(TEntity).GetInterfaces().Contains(typeof(IProblems)))
        {
            HiddenColumns.Add(x => (x as IProblems)!.Problem.Notes);
            HiddenColumns.Add(x=>(x as IProblems)!.Problem.Resolved);
            var coltype = typeof(DynamicProblemsColumn<>).MakeGenericType(typeof(TEntity));
            var column = Activator.CreateInstance(coltype, this);
            var dac = (column as DynamicActionColumn)!;
            ActionColumns.Add(dac);
        }

        SetupFilterColumns();
    }

    protected override void Init()
    {
        base.Init();
        FilterComponent = new(this,
            new GlobalConfiguration<CoreFilterDefinitions>(GetTag()),
            new UserConfiguration<CoreFilterDefinitions>(GetTag()));
        FilterComponent.OnFilterRefresh += () => Refresh(false, true);

        ColumnsComponent = new DynamicGridCustomColumnsComponent<TEntity>(this, GetTag());

        MergeBtn = AddButton("Merge", Wpf.Resources.merge.AsBitmapImage(Color.White), MergeClick);
    }
    protected override void DoReconfigure(DynamicGridOptions options)
    {
        base.DoReconfigure(options);

        if (Security.CanEdit<TEntity>())
        {
            options.AddRows = true;
            options.EditRows = true;
        }
        if (Security.CanDelete<TEntity>())
            options.DeleteRows = true;
        if (Security.CanImport<TEntity>() && typeof(TEntity).HasInterface<IImportable>())
            options.ImportData = true;
        if (Security.CanExport<TEntity>() && typeof(TEntity).HasInterface<IExportable>())
            options.ExportData = true;
        if (Security.CanMerge<TEntity>())
            options.MultiSelect = true;
    }

    [MemberNotNull(nameof(FilterColumns))]
    private void SetupFilterColumns()
    {
        if (typeof(TEntity).GetCustomAttribute<AutoEntity>() is AutoEntity auto)
        {
            if (auto.Generator is not null)
            {
                var columns = auto.Generator.IDColumns;
                FilterColumns = columns.Select(x => new Column<TEntity>(x.Property)).ToArray();
            }
            else
            {
                FilterColumns = Array.Empty<Column<TEntity>>();
            }
        }
        else
        {
            FilterColumns = new[] { new Column<TEntity>(x => x.ID) };
        }

        foreach (var column in FilterColumns)
        {
            AddHiddenColumn(column.Property);
        }
    }

    protected override void BeforeLoad(IDynamicEditorForm form, TEntity[] items)
    {
        form.ReadOnly = form.ReadOnly || !Security.CanEdit<TEntity>();
        base.BeforeLoad(form, items);
    }

    private string? _columnsTag;
    public string? ColumnsTag
    {
        get => _columnsTag;
        set
        {
            _columnsTag = value;
            ColumnsComponent.Tag = GetTag();
        }
    }

    protected override void OptionsChanged()
    {
        base.OptionsChanged();
        if (MergeBtn != null)
            MergeBtn.Visibility = Visibility.Collapsed;

        FilterComponent.ShowFilterList = Options.FilterRows && !Options.HideDatabaseFilters;
    }

    protected override void SelectItems(CoreRow[]? rows)
    {
        base.SelectItems(rows);
        MergeBtn.Visibility = Options.MultiSelect && typeof(TEntity).IsAssignableTo(typeof(IMergeable)) && Security.CanMerge<TEntity>() && rows != null && rows.Length > 1
            ? Visibility.Visible
            : Visibility.Collapsed;
    }
    
    public event OnReloadEventHandler? OnReload;

    protected override string FormatRecordCount(int count)
    {
        return IsPaging 
            ? $"{base.FormatRecordCount(count)} (loading..)"
            : base.FormatRecordCount(count);
    }

    protected override void Reload(
        Filters<TEntity> criteria, Columns<TEntity> columns, ref SortOrder<TEntity>? sort,
        CancellationToken token, Action<CoreTable?, Exception?> action)
    {
        criteria.Add(FilterComponent.GetFilter());

        OnReload?.Invoke(this, criteria, columns, ref sort);
        if(Options.PageSize > 0)
        {
            var inSort = sort;
            Task.Run(() =>
            {
                
                var page = CoreRange.Database(Options.PageSize);
                var filter = criteria.Combine();
                
                IsPaging = true;
                while (!token.IsCancellationRequested)
                {
                    try
                    {
                        var data = Client.Query(filter, columns, inSort, page);
                        data.Offset = page.Offset;
                        IsPaging = data.Rows.Count == page.Limit;

                        if (token.IsCancellationRequested)
                        {
                            break;
                        }

                        action(data, null);
                        if (!IsPaging)
                            break;
                        
                        // Proposal - Let's slow it down a bit to enhance UI responsiveness?
                        Thread.Sleep(100);
                        
                        page.Next();
                    }
                    catch (Exception e)
                    {
                        action(null, e);
                        break;
                    }
                }
            }, token);
        }
        else
        {
            Client.Query(criteria.Combine(), columns, sort, action);
        }
    }

    private CoreRow[]? SelectedBeforeRefresh;
    protected override void OnAfterRefresh()
    {
        base.OnAfterRefresh();

        if (SelectedBeforeRefresh is not null)
        {
            var selectedValues = SelectedBeforeRefresh
                .Select(x => FilterColumns.Select(c => x[c.Property]).ToList())
                .ToList();

            SelectedBeforeRefresh = null;

            var selectedRows = Data.Rows.Where(r =>
            {
                return selectedValues.Any(v =>
                {
                    for (int i = 0; i < v.Count; ++i)
                    {
                        if (v[i]?.Equals(r[FilterColumns[i].Property]) != true)
                            return false;
                    }
                    return true;
                });
            }).ToArray();

            SelectedRows = selectedRows;
            SelectItems(selectedRows);
        }
    }

    public override void Refresh(bool reloadcolumns, bool reloaddata)
    {
        SelectedBeforeRefresh = SelectedRows;
        base.Refresh(reloadcolumns, reloaddata);
    }

    IColumns IDynamicDataGrid.LoadEditorColumns() => LoadEditorColumns();

    public Columns<TEntity> LoadEditorColumns()
    {
        return DynamicGridUtils.LoadEditorColumns(DataColumns());
    }

    private Filter<TEntity>? GetRowFilter(CoreRow row)
    {
        var newFilters = new Filters<TEntity>();
        foreach (var column in FilterColumns)
        {
            newFilters.Add(new Filter<TEntity>(column.Property).IsEqualTo(row[column.Property]));
        }
        return newFilters.Combine();
    }

    public override TEntity[] LoadItems(IList<CoreRow> rows)
    {
        Filter<TEntity>? filter = null;
        var results = new List<TEntity>(rows.Count);
        for (var i = 0; i < rows.Count; i += ChunkSize)
        {
            var chunk = rows.Skip(i).Take(ChunkSize);
            foreach (var row in chunk)
            {
                var newFilter = GetRowFilter(row);

                if (newFilter is not null)
                {
                    if (filter is null)
                        filter = newFilter;
                    else
                        filter = filter.Or(newFilter);
                }
            }

            var columns = LoadEditorColumns();
            var data = Client.Query(filter, columns);
            results.AddRange(data.ToObjects<TEntity>());
        }

        return results.ToArray();
    }

    public override TEntity LoadItem(CoreRow row)
    {
        var id = row.Get<TEntity, Guid>(x => x.ID);
        var filter = GetRowFilter(row);
        return Client.Query(filter, LoadEditorColumns()).ToObjects<TEntity>().FirstOrDefault()
            ?? throw new Exception($"No {typeof(TEntity).Name} with ID {id}");
    }

    public override void SaveItem(TEntity item)
    {
        Client.Save(item, "Edited by User");
    }

    public override void SaveItems(IEnumerable<TEntity> items)
    {
        Client.Save(items, "Edited by User");
    }

    public override void DeleteItems(params CoreRow[] rows)
    {
        var deletes = new List<TEntity>();
        foreach (var row in rows)
        {
            var delete = new TEntity();
            foreach (var column in FilterColumns)
            {
                CoreUtils.SetPropertyValue(delete, column.Property, row[column.Property]);
            }

            deletes.Add(delete);
        }

        Client.Delete(deletes, "Deleted on User Request");
        DoChanged();
    }

    private object GetPropertyValue(Expression member, object obj)
    {
        var objectMember = Expression.Convert(member, typeof(object));

        var getterLambda = Expression.Lambda<Func<object>>(objectMember);

        var getter = getterLambda.Compile();

        return getter();
    }

    protected override void ObjectToRow(TEntity obj, CoreRow row)
    {
        foreach (var column in Data.Columns)
        {
            var prop = DatabaseSchema.Property(obj.GetType(), column.ColumnName);
            if (prop is StandardProperty)
                row.Set(column.ColumnName, CoreUtils.GetPropertyValue(obj, prop.Name));
            else if (prop is CustomProperty)
                row.Set(column.ColumnName, obj.UserProperties[column.ColumnName]);
        }
    }

    public override void LoadEditorButtons(TEntity item, DynamicEditorButtons buttons)
    {
        base.LoadEditorButtons(item, buttons);
        if (ClientFactory.IsSupported<AuditTrail>())
            buttons.Add("Audit Trail", Wpf.Resources.view.AsBitmapImage(), item, AuditTrailClick);
    }

    private void AuditTrailClick(object sender, TEntity entity)
    {
        var window = new AuditWindow(entity.ID);
        window.ShowDialog();
    }

    protected override DynamicGridColumns LoadColumns()
    {
        return ColumnsComponent.LoadColumns();
    }

    protected override void SaveColumns(DynamicGridColumns columns)
    {
        ColumnsComponent.SaveColumns(columns);
    }

    protected override void LoadColumnsMenu(ContextMenu menu)
    {
        base.LoadColumnsMenu(menu);
        ColumnsComponent.LoadColumnsMenu(menu);
    }

    public string GetTag()
    {
        var tag = typeof(TEntity).Name;
        if (!string.IsNullOrWhiteSpace(ColumnsTag))
            tag = string.Format("{0}.{1}", tag, ColumnsTag);
        return tag;
    }

    protected override DynamicGridSettings LoadSettings()
    {
        var tag = GetTag();

        var user = Task.Run(() => new UserConfiguration<DynamicGridSettings>(tag).Load());
        user.Wait();

        //var global = Task.Run(() => new GlobalConfiguration<DynamicGridSettings>(tag).Load());
        //global.Wait();
        //Task.WaitAll(user, global);
        //var columns = user.Result.Any() ? user.Result : global.Result;

        return user.Result;
    }
    protected override void SaveSettings(DynamicGridSettings settings)
    {
        var tag = GetTag();
        new UserConfiguration<DynamicGridSettings>(tag).Save(settings);
    }

    protected override BaseEditor? GetEditor(object item, DynamicGridColumn column)
    {
        var prop = DatabaseSchema.Properties(typeof(TEntity)).FirstOrDefault(x => x.Name == column.ColumnName);
        if (prop != null)
            return prop.Editor;
        return base.GetEditor(item, column);
    }

    protected override object? GetEditorValue(object item, string name)
    {
        if (item is TEntity entity)
        {
            var prop = DatabaseSchema.Properties(typeof(TEntity)).FirstOrDefault(x => x.Name == name);
            if (prop is CustomProperty)
            {
                if (entity.UserProperties.ContainsKey(name))
                    return entity.UserProperties[name];
                return null;
            }
        }

        return base.GetEditorValue(item, name);
    }

    protected override void SetEditorValue(object item, string name, object value)
    {
        var prop = DatabaseSchema.Properties(typeof(TEntity)).FirstOrDefault(x => x.Name == name);
        if (prop is CustomProperty && item is TEntity entity)
        {
            entity.UserProperties[name] = value;
        }
        else
        {
            base.SetEditorValue(item, name, value);
        }
    }

    protected bool Duplicate(
        CoreRow row,
        Expression<Func<TEntity, object?>> codefield,
        Type[] childtypes)
    {
        var id = row.Get<TEntity, Guid>(x => x.ID);

        var code = row.Get(codefield) as string;

        var tasks = new List<Task>();

        var itemtask = Task.Run(() =>
        {
            var filter = new Filter<TEntity>(x => x.ID).IsEqualTo(id);
            var result = new Client<TEntity>().Load(filter).FirstOrDefault()
                ?? throw new Exception("Entity does not exist!");
            return result;
        });
        tasks.Add(itemtask);

        Task<List<string?>>? codetask = null;
        if (!string.IsNullOrWhiteSpace(code))
        {
            codetask = Task.Run(() =>
            {
                var columns = Columns.None<TEntity>().Add(codefield);
                //columns.Add<String>(codefield);
                var filter = new Filter<TEntity>(codefield).BeginsWith(code);
                var table = new Client<TEntity>().Query(filter, columns);
                var result = table.Rows.Select(x => x.Get(codefield) as string).ToList();
                return result;
            });
            tasks.Add(codetask);
        }

        var children = new Dictionary<Type, CoreTable>();
        foreach (var childtype in childtypes)
        {
            var childtask = Task.Run(() =>
            {
                var prop = childtype.GetProperties().FirstOrDefault(x =>
                    x.PropertyType.GetInterfaces().Contains(typeof(IEntityLink)) &&
                    x.PropertyType.GetInheritedGenericTypeArguments().FirstOrDefault() == typeof(TEntity));

                if (prop is not null)
                {
                    var filter = Core.Filter.Create(childtype);
                    filter.Property = prop.Name + ".ID";
                    filter.Operator = Operator.IsEqualTo;
                    filter.Value = id;
                    var sort = LookupFactory.DefineSort(childtype);
                    var client = ClientFactory.CreateClient(childtype);
                    var table = client.Query(filter, null, sort);
                    foreach (var r in table.Rows)
                    {
                        r["ID"] = Guid.Empty;
                        r[prop.Name + ".ID"] = Guid.Empty;
                    }

                    children[childtype] = table;
                }
                else
                {
                    Logger.Send(LogType.Error, "", $"DynamicDataGrid<{typeof(TEntity)}>.Duplicate(): No parent property found for child type {childtype}");
                }
            });
            tasks.Add(childtask);
        }

        Task.WaitAll(tasks.ToArray());

        var item = itemtask.Result;
        item.ID = Guid.Empty;

        if (codetask != null)
        {
            var codes = codetask.Result;
            var i = 1;

            while (codes.Contains(string.Format("{0} ({1})", code, i)))
                i++;
            var codeprop = CoreUtils.GetFullPropertyName(codefield, ".");
            CoreUtils.SetPropertyValue(item, codeprop, string.Format("{0} ({1})", code, i));
        }

        var grid = new DynamicDataGrid<TEntity>();
        return grid.EditItems(new[] { item }, t => children.ContainsKey(t) ? children[t] : null, true);
    }

    private bool MergeClick(Button arg1, CoreRow[] rows)
    {
        if (rows == null || rows.Length <= 1)
            return false;
        return DoMerge(rows);
    }

    protected virtual bool DoMerge(CoreRow[] rows)
    {
        var targetid = rows.Last().Get<TEntity, Guid>(x => x.ID);
        var target = rows.Last().ToObject<TEntity>().ToString();
        var otherids = rows.Select(r => r.Get<TEntity, Guid>(x => x.ID)).Where(x => x != targetid).ToArray();
        string[] others = rows.Where(r => otherids.Contains(r.Get<Guid>("ID"))).Select(x => x.ToObject<TEntity>().ToString()!).ToArray();
        var nRows = rows.Length;
        if (!MessageWindow.ShowYesNo(
                $"This will merge the following items:\n\n" +
                $"- {string.Join("\n- ", others)}\n\n" +
                $" into:\n\n" +
                $"- {target}\n\n" +
                $"After this, the items will be permanently removed.\n" +
                $"Are you sure you wish to do this?",
                "Merge Items Warning"))
            return false;

        using (new WaitCursor())
        {
            var types = CoreUtils.Entities.Where(
                x =>
                    !x.IsGenericType
                    && x.IsSubclassOf(typeof(Entity))
                    && !x.Equals(typeof(AuditTrail))
                    && !x.Equals(typeof(TEntity))
                    && x.GetCustomAttribute<AutoEntity>() == null
                    && x.HasInterface<IRemotable>()
                    && x.HasInterface<IPersistent>()
            ).ToArray();

            foreach (var type in types)
            {
                var props = CoreUtils.PropertyList(
                    type,
                    x =>
                        x.PropertyType.GetInterfaces().Contains(typeof(IEntityLink))
                        && x.PropertyType.GetInheritedGenericTypeArguments().Contains(typeof(TEntity))
                );
                foreach (var prop in DatabaseSchema.LocalProperties(type))
                {
                    if(prop.Parent is null
                        || prop.Parent.PropertyType.GetInterfaceDefinition(typeof(IEntityLink<>)) is not Type intDef
                        || intDef.GenericTypeArguments[0] != typeof(TEntity))
                    {
                        continue;
                    }
                    var filter = Filter.Create(type, prop.Name).InList(otherids);
                    var columns = Columns.None(type).Add("ID").Add(prop.Name);
                    var updates = ClientFactory.CreateClient(type).Query(filter, columns).Rows.ToArray(r => r.ToObject(type));
                    if (updates.Length != 0)
                    {
                        foreach (var update in updates)
                            prop.Setter()(update, targetid);
                        ClientFactory.CreateClient(type).Save(updates, $"Merged {typeof(TEntity).Name} Records");
                    }
                }
            }

            var histories = new Client<AuditTrail>()
                .Query(
                    new Filter<AuditTrail>(x => x.EntityID).InList(otherids),
                    Columns.None<AuditTrail>()
                        .Add(x => x.ID).Add(x => x.EntityID))
                .ToArray<AuditTrail>();
            foreach (var history in histories)
                history.EntityID = targetid;
            if (histories.Length != 0)
                new Client<AuditTrail>().Save(histories, "");

            var deletes = new List<object>();
            foreach (var otherid in otherids)
            {
                var delete = new TEntity();
                CoreUtils.SetPropertyValue(delete, "ID", otherid);
                deletes.Add(delete);
            }

            ClientFactory.CreateClient(typeof(TEntity))
                .Delete(deletes, string.Format("Merged {0} Records", typeof(TEntity).EntityName().Split('.').Last()));
            return true;
        }
    }

    protected override IEnumerable<TEntity> LoadDuplicatorItems(CoreRow[] rows)
    {
        return rows.Select(x => x.ToObject<TEntity>());
    }

    protected override bool BeforeCopy(IList<TEntity> items)
    {
        if (!base.BeforeCopy(items)) return false;

        for(int i = 0; i < items.Count; ++i)
        {
            var newItem = items[i].Clone();
            newItem.ID = Guid.Empty;
            items[i] = newItem;
        }
        return true;
    }

    protected override void CustomiseExportFilters(Filters<TEntity> filters, CoreRow[] visiblerows)
    {
        base.CustomiseExportFilters(filters, visiblerows);

        /*if (IsEntity)
        {
            var ids = visiblerows.Select(r => r.Get<TEntity, Guid>(c => c.ID)).ToArray();
            filters.Add(new Filter<TEntity>(x => x.ID).InList(ids));
        }
        else
        {
            Filter<TEntity>? filter = null;

            foreach (var row in visiblerows)
            {
                var rowFilter = GetRowFilter(row);
                if(rowFilter is not null)
                {
                    if (filter is null)
                    {
                        filter = rowFilter;
                    }
                    else
                    {
                        filter.Or(rowFilter);
                    }
                }
            }
            filters.Add(filter);
        }*/
    }

    protected override IEnumerable<Tuple<Type?, CoreTable>> LoadExportTables(Filters<TEntity> filter, IEnumerable<Tuple<Type, IColumns>> tableColumns)
    {
        var queries = new Dictionary<string, IQueryDef>();
        var columns = tableColumns.ToList();
        foreach (var table in columns)
        {
            var tableType = table.Item1;

            PropertyInfo? property = null;

            var m2m = CoreUtils.GetManyToMany(tableType, typeof(TEntity));

            IFilter? queryFilter = null;
            if (m2m != null)
            {
                property = CoreUtils.GetManyToManyThisProperty(tableType, typeof(TEntity));
            }
            else
            {
                var o2m = CoreUtils.GetOneToMany(tableType, typeof(TEntity));
                if (o2m != null)
                {
                    property = CoreUtils.GetOneToManyProperty(tableType, typeof(TEntity));
                }
            }

            if (property != null)
            {
                var subQuery = new SubQuery<TEntity>();
                subQuery.Filter = filter.Combine();
                subQuery.Column = new Column<TEntity>(x => x.ID);

                queryFilter = (Activator.CreateInstance(typeof(Filter<>).MakeGenericType(tableType)) as IFilter)!;
                queryFilter.Property = property.Name + ".ID";
                queryFilter.InQuery(subQuery);

                queries[tableType.Name] = new QueryDef(tableType)
                {
                    Filter = queryFilter,
                    Columns = table.Item2
                };
            }
        }

        var results = Client.QueryMultiple(queries);

        return columns.Select(x => new Tuple<Type?, CoreTable>(x.Item1, results[x.Item1.Name]));
    }

    protected override CoreTable LoadImportKeys(String[] fields)
    {
        return Client.Query(null, Columns.None<TEntity>().Add(fields));
    }
    
}