using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using InABox.Clients;
using InABox.Core;
using InABox.Wpf;
using InABox.WPF;
using Syncfusion.Data;
using Syncfusion.UI.Xaml.Grid;
using Syncfusion.UI.Xaml.Grid.Helpers;
using static InABox.DynamicGrid.IDynamicGrid;
using Color = System.Drawing.Color;
using Columns = InABox.Core.Columns;
using Image = System.Windows.Controls.Image;
using RowColumnIndex = Syncfusion.UI.Xaml.ScrollAxis.RowColumnIndex;
using SolidColorBrush = System.Windows.Media.SolidColorBrush;
using String = System.String;
using VerticalAlignment = System.Windows.VerticalAlignment;
using VirtualizingCellsControl = Syncfusion.UI.Xaml.Grid.VirtualizingCellsControl;
using System.Threading;
using System.Diagnostics.CodeAnalysis;

namespace InABox.DynamicGrid;


public abstract class DynamicGrid<T> : BaseDynamicGrid, IDynamicGridUIComponentParent<T>, IDynamicGrid<T>,
    IImportDynamicGrid, IExportDynamicGrid, IDuplicateDynamicGrid, IHelpDynamicGrid
    where T : BaseObject, new()
{
    #region Events

    public ValidateEvent<T>? OnValidate { get; set; }

    public event OnDefineFilter? OnDefineFilter;
    public event OnCreateItem? OnCreateItem;
    public event OnAfterCreateItem? OnAfterCreateItem;

    public event EditorValueChangedHandler? OnEditorValueChanged;
    public event OnAfterEditorValueChanged? OnAfterEditorValueChangedEvent;

    private List<(OnCustomiseEditor, OnCustomiseEditor<T>)> _customiseEditorEvents;
    event OnCustomiseEditor? IDynamicGrid.OnCustomiseEditor
    {
        add
        {
            if (value is null) return;
            void dlg(IDynamicEditorForm sender, T[]? items, DynamicGridColumn column, BaseEditor editor)
            {
                value?.Invoke(sender, items, column, editor);
            }
            _customiseEditorEvents ??= new();
            _customiseEditorEvents.Add((value, dlg));
            OnCustomiseEditor += dlg;
        }
        remove
        {
            var (_, dlg) = _customiseEditorEvents.FirstOrDefault(x => x.Item1 == value);
            if(dlg is not null)
            {
                OnCustomiseEditor -= dlg;
            }
        }
    }

    public event OnCustomiseEditor<T>? OnCustomiseEditor;

    public event EntitySaveEvent? OnBeforeSave;
    public event EntitySaveEvent? OnAfterSave;

    public delegate void EditorLoaded(IDynamicEditorForm editor, T[] items);
    public event EditorLoaded? OnEditorLoaded;

    public event OnLoadEditorButtons<T>? OnLoadEditorButtons;

    #endregion

    protected virtual string HelpSlug()
    {
        return typeof(T).Name.Split('.').Last().SplitCamelCase().Replace(" ", "_");
    }
    string IHelpDynamicGrid.HelpSlug() => HelpSlug();

    protected override bool CanDuplicate => typeof(T).IsAssignableTo(typeof(IDuplicatable));

    public DynamicGrid() : base()
    {
    }

    protected sealed override void PreInit()
    {
        MasterColumns = new DynamicGridColumns();
        MasterColumns.ExtractColumns(typeof(T));

        HiddenColumns = new HiddenColumnsList();

        if (typeof(T).IsAssignableTo(typeof(ISequenceable)))
        {
            HiddenColumns.Add(x => (x as ISequenceable)!.Sequence);
        }
    }
    protected override void Init()
    {
    }

    #region IDynamicGridUIComponentParent

    protected override IDynamicGridUIComponent CreateUIComponent()
    {
        return new DynamicGridGridUIComponent<T>
        {
            Parent = this
        };
    }

    T IDynamicGrid<T>.LoadItem(CoreRow row) => LoadItem(row);

    void IDynamicGridUIComponentParent<T>.EntityChanged(T obj, CoreRow row, string changedColumn, Dictionary<string, object?> changes)
        => EntityChanged(obj, row, changedColumn, changes);

    void IDynamicGridUIComponentParent<T>.UpdateData(T obj, CoreRow row, string changedColumn, Dictionary<CoreColumn, object?> updates)
    {
        var result = new Dictionary<string, object?>();

        foreach (var (col, value) in updates)
        {
            UpdateRow(row, col.ColumnName, value, refresh: false);
            DynamicGridUtils.UpdateEditorValue(new BaseObject[] { obj }, col.ColumnName, value, result);
        }

        EntityChanged(obj, row, changedColumn, result);
    }

    #endregion

    protected override DynamicGridRowStyleSelector GetRowStyleSelector()
    {
        return new DynamicGridRowStyleSelector<T, DynamicGridRowStyle>();
    }

    public DynamicGridColumns MasterColumns { get; protected set; }

    public override void OnItemSourceChanged(object value)
    {
        Data = value as CoreTable;
        Refresh(true, true);
    }

    public class HiddenColumnsList
    {
        private List<string> Columns { get; set; } = new();

        public IEnumerable<string> ColumnNames => Columns;

        public void Add(Expression<Func<T, object?>> column) => Add(CoreUtils.GetFullPropertyName(column, "."));

        public void Add(IColumn column) => Add(column.Property);

        public void Add(string column)
        {
            if (!Contains(column))
                Columns.Add(column);
        }

        public bool Contains(string column) => Columns.Contains(column);
    }

    public void AddHiddenColumn(string column) => HiddenColumns.Add(column);

    public HiddenColumnsList HiddenColumns { get; private set; }

    private static bool IsSequenced => typeof(T).GetInterfaces().Any(x => x.Equals(typeof(ISequenceable)));

    #region Options

    protected override void DoReconfigure(DynamicGridOptions options)
    {
        options.ReorderRows = IsSequenced;
    }

    #endregion

    private void EntityChanged(T obj, CoreRow row, string changedColumn, Dictionary<string, object?> changes)
    {
        OnAfterEditorValueChanged(null, [obj], new AfterEditorValueChangedArgs(changedColumn, changes), changes);

        SaveItem(obj);

        foreach (var (key, value) in changes)
        {
            if (row.Table.GetColumnIndex(key) > -1)
                row[key] = value;
        }

        GetUIComponent().UpdateRow(row);
    }

    #region Column Handling

    /// <summary>
    /// Create a set of <see cref="DynamicGridColumns"/> according to the natural default sizes and formats and captions, based on the contents of <paramref name="columns"/>.
    /// </summary>
    /// <param name="columns"></param>
    /// <returns></returns>
    protected DynamicGridColumns ExtractColumns(Columns<T> columns)
    {
        var cols = new DynamicGridColumns();

        foreach (var col in columns)
        {
            var mc = MasterColumns.FirstOrDefault(x => x.ColumnName.Equals(col.Property));
            if (mc != null && mc.Editor is not NullEditor && mc.Editor.Visible != Visible.Hidden)
                cols.Add(mc);
        }

        return cols;
    }
    DynamicGridColumns IDynamicGrid.ExtractColumns(IColumns columns)
    {
        var cols = new DynamicGridColumns();

        foreach (var col in columns)
        {
            var mc = MasterColumns.FirstOrDefault(x => x.ColumnName.Equals(col.Property));
            if (mc != null && mc.Editor is not NullEditor && mc.Editor.Visible != Visible.Hidden)
                cols.Add(mc);
        }

        return cols;
    }

    public override DynamicGridColumns GenerateColumns()
    {
        var defaultCols = DefaultColumns.GetDefaultColumns(typeof(T)).Select(x => x.Property).ToArray();

        Columns<T> cols;
        if(defaultCols.Length == 0)
        {
            cols = IsDirectEditMode()
                ? new Columns<T>(ColumnTypeFlags.IncludeVisible | ColumnTypeFlags.IncludeForeignKeys)
                : new Columns<T>(ColumnTypeFlags.IncludeVisible | ColumnTypeFlags.IncludeLinked);
        }
        else
        {
            IEnumerable<IProperty> propList;
            if (IsDirectEditMode())
            {
                propList = defaultCols
                    .Where(x => x.Parent is null || !x.HasParentEntityLink() || (x.IsForeignKey() && !x.Parent.HasParentEntityLink()));
            }
            else
            {
                // Ignore foreign keys.
                propList = defaultCols.Where(x => !x.IsForeignKey());
            }
            cols = Columns.None<T>().AddRange(propList.Select(x => new Column<T>(x)));
        }
        
        var columns = ExtractColumns(cols);
        OnGenerateColumns?.Invoke(this, new GenerateColumnsEventArgs(columns));
        return columns;
    }
    
    public event GenerateColumnsEvent? OnGenerateColumns;
    public event SaveColumnsEvent? OnSaveColumns;
    public event GetAvailableColumnsEvent? GetAvailableColumns;

    protected override void SaveColumns(DynamicGridColumns columns)
    {
        OnSaveColumns?.Invoke(this, new(columns));
    }

    #endregion

    #region Refresh / Reload

    protected abstract void Reload(
        Filters<T> criteria, Columns<T> columns, ref SortOrder<T>? sort,
        CancellationToken token, Action<CoreTable?, Exception?> action);

    public Filter<T>? DefineFilter()
    {
        if (OnDefineFilter is null)
            return null;
        var result = OnDefineFilter.Invoke(typeof(T)) as Filter<T>;
        return result;
    }

    public IEnumerable<TType> ExtractValues<TType>(Expression<Func<T, TType>> column, Selection selection)
    {
        var result = selection == Selection.None
            ? Enumerable.Empty<TType>()
            : selection == Selection.Selected
                ? SelectedRows.Select(r => r.Get(column))
                : Data.ExtractValues(column);
        return result;
    }

    protected sealed override void ReloadData(CancellationToken token, Action<CoreTable?, Exception?> action)
    {
        _lookupcache.Clear();

        var criteria = new Filters<T>();
        var filter = DefineFilter();
        if (filter != null)
            criteria.Add(filter);

        var columns = DataColumns();

        var sort = LookupFactory.DefineSort<T>();
        if (sort == null && IsSequenced)
            sort = new SortOrder<T>("Sequence");

        Reload(criteria, columns, ref sort, token, action);
    }

    public Columns<T> DataColumns()
    {
        var columns = Columns.None<T>();
        foreach (var column in VisibleColumns)
            columns.Add(column.ColumnName);
        foreach (var column in HiddenColumns.ColumnNames)
            columns.Add(new Column<T>(column));
        if (!Options.ReadOnly)
        {
            foreach (var column in LookupFactory.RequiredColumns<T>())
            {
                columns.Add(column);
            }
        }
        return columns;
    }


    public void UpdateRow(CoreRow row, T obj, bool invalidateRow = true)
    {
        ObjectToRow(obj, row);
        ObjectToRow(obj, _recordmap[row]);
        if (invalidateRow)
        {
            InvalidateRow(row);
        }
    }

    public void UpdateRows(CoreRow[] rows, T[] objs, bool invalidateRows = true)
    {
        for(var i = 0; i < objs.Length; ++i)
        {
            UpdateRow(rows[i], objs[i], invalidateRow: invalidateRows);
        }
    }

    public void AddRow(T data)
    {
        if (MasterData is null) return;

        MasterData.LoadRow(data);
        Refresh(false, false);
    }

    #endregion

    #region Item Manipulation

    #region Load/Save/Delete

    public virtual T[] LoadItems(IList<CoreRow> rows)
    {
        return rows.ToArray(LoadItem);
    }

    public abstract T LoadItem(CoreRow row);

    public abstract void SaveItem(T item);

    public virtual void SaveItems(IEnumerable<T> items)
    {
        foreach (var item in items)
            SaveItem(item);
    }

    protected virtual bool CanDeleteItems(params CoreRow[] rows)
    {
        return true;
    }

    public abstract void DeleteItems(params CoreRow[] rows);

    protected override bool CanDeleteRows(params CoreRow[] rows) => CanDeleteItems(rows);

    public override void DeleteRows(params CoreRow[] rows) => DeleteItems(rows);

    #endregion

    #region Edit

    protected override void NewRow()
    {
        CreateItems(null);
    }

    protected void CreateItems(Func<IEnumerable<T>>? create)
    {
        var newRows = new List<CoreRow>();
        var items = create?.Invoke() ?? CoreUtils.One(CreateItem());
        foreach (var item in items)
        {
            if (!AfterCreate(item))
                return;
            SaveItem(item);

            var datarow = Data.NewRow();
            ObjectToRow(item, datarow);
            Data.Rows.Add(datarow);
            newRows.Add(datarow);

            var masterrow = MasterData.NewRow();
            ObjectToRow(item, masterrow);
            MasterData.Rows.Add(masterrow);

            _recordmap[datarow] = masterrow;
        }

        InvalidateGrid();
        SelectedRows = newRows.ToArray();
        DoChanged();
    }

    public virtual DynamicEditorPages LoadEditorPages(T item)
    {
        DynamicEditorPages pages = new DynamicEditorPages();
        DynamicGridUtils.LoadOneToManyPages(typeof(T), pages);
        DynamicGridUtils.LoadEnclosedListPages(typeof(T), pages);
        DynamicGridUtils.LoadManyToManyPages(typeof(T), pages);
        DynamicGridUtils.LoadCustomEditorPages(typeof(T), pages);
        pages.RemoveAll(x => !x.Visible);
        foreach (var page in pages)
            page.Ready = false;
        return pages;
    }

    public virtual void LoadEditorButtons(T item, DynamicEditorButtons buttons)
    {
        buttons.Clear();
        buttons.Add(
            "",
            Wpf.Resources.help.AsBitmapImage(),
            item,
            (f, i) =>
            {
                Process.Start(new ProcessStartInfo("https://prsdigital.com.au/wiki/index.php/" + typeof(T).Name.SplitCamelCase().Replace(" ", "_"))
                { UseShellExecute = true });
            }
        );

        OnLoadEditorButtons?.Invoke(item, buttons);
    }

    protected virtual void BeforeLoad(IDynamicEditorForm form, T[] items)
    {
        form.BeforeLoad();
    }

    void IDynamicGrid.InitialiseEditorForm(IDynamicEditorForm editor, object[] items, Func<Type, CoreTable>? pageDataHandler, bool preloadPages)
    {
        InitialiseEditorForm(editor, items.Cast<T>().ToArray(), pageDataHandler, preloadPages);
    }
    public virtual bool EditItems(object[] items, Func<Type, CoreTable?>? PageDataHandler = null, bool PreloadPages = false)
    {
        var values = items.Cast<T>().ToArray();
        return EditItems(values, PageDataHandler, PreloadPages);
    }

    public virtual void InitialiseEditorForm(IDynamicEditorForm editor, T[] items, Func<Type, CoreTable?>? pageDataHandler = null, bool preloadPages = false)
    {
        var pages = items.Length == 1 ? LoadEditorPages(items.First()) : new DynamicEditorPages();

        var buttons = new DynamicEditorButtons();
        if (items.Length == 1)
            LoadEditorButtons(items.First(), buttons);

        editor.Setup(items.Any() ? items.First().GetType() : typeof(T), pages, buttons, pageDataHandler, preloadPages);

        editor.OnCustomiseColumns = (sender, columns) =>
        {
            columns.Clear();
            columns.AddRange(MasterColumns);
        };

        editor.OnDefineEditor = (o, c) =>
        {
            var result = GetEditor(o, c);

            if (result != null)
                result = result.CloneEditor();

            return result;
        };

        editor.OnFormCustomiseEditor += DoCustomiseEditor;

        editor.OnDefineFilter = (type, column) => { return DefineLookupFilter(type, column, items); };

        //editor.OnDefineFilter += (o, e) => { return DefineFilter(items, e); };
        editor.OnDefineLookups = editor => DefineLookups(editor, items);

        editor.OnEditorValueChanged += (s, n, v) => EditorValueChanged(editor, items, n, v);

        editor.OnAfterEditorValueChanged += (g, args) => AfterEditorValueChanged(g, items, args);

        editor.OnReconfigureEditors = g => DoReconfigureEditors(g, items);

        editor.OnCreateEditorControl += OnCreateEditorControl;

        editor.OnValidateData += (o, i) => ValidateData(items);

        editor.OnSelectPage += SelectPage;

        editor.OnSaveItem = (o, e) =>
        {
            try
            {
                using var Wait = new WaitCursor();
                DoBeforeSave(editor, items);


                if (items.Length == 1)
                    editor.UnloadEditorPages(false);
                foreach (var item in items)
                    SaveItem(item);
                if (items.Length == 1)
                    editor.UnloadEditorPages(true);

                DoAfterSave(editor, items);

            }
            catch (Exception err)
            {
                MessageBox.Show(err.Message);
                e.Cancel = true;
            }
        };

        BeforeLoad(editor, items);
        editor.Items = items;
        AfterLoad(editor, items);
    }

    protected virtual void OnCreateEditorControl(string column, BaseEditor editor, IDynamicEditorControl control)
    {
    }

    private void DoCustomiseEditor(IDynamicEditorForm sender, object[] items, DynamicGridColumn column, BaseEditor editor)
    {
        CustomiseEditor(sender, (T[])items, column, editor);
        OnCustomiseEditor?.Invoke(sender, (T[])items, column, editor);
    }

    protected virtual void CustomiseEditor(IDynamicEditorForm form, T[] items, DynamicGridColumn column, BaseEditor editor)
    {
        CustomiseEditor(column, editor);
    }

    protected virtual void DoAfterSave(IDynamicEditorForm editor, T[] items)
    {
        OnAfterSave?.Invoke(editor, items);
    }

    protected virtual void DoBeforeSave(IDynamicEditorForm editor, T[] items)
    {
        OnBeforeSave?.Invoke(editor, items);
    }

    /// <summary>
    /// Edit the <paramref name="items"/> with a non-modal editor window, attaching them to the <paramref name="host"/> provided.
    /// </summary>
    /// <param name="items">List of objects to edit.</param>
    /// <param name="callback">A callback for when the items are finished being edited.</param>
    public virtual void EditItemsNonModal(ISubPanelHost host, T[] items, Action<T[], bool> callback, Func<Type, CoreTable?>? PageDataHandler = null, bool PreloadPages = false)
    {
        if (!DynamicGridUtils.TryEdit(items, out var editLock))
        {
            if(editLock.Panel is Window window)
            {
                Task.Delay(100).ContinueWith(task =>
                {
                    window.WindowState = WindowState.Normal;
                    window.Activate();
                }, TaskScheduler.FromCurrentSynchronizationContext());
            }
            else
            {
                MessageWindow.ShowMessage("One or more items are already being edited in an open window. You cannot edit the same entity multiple times simultaneously.", "Simultaneous edit.");
            }
            return;
        }

        DynamicEditorForm editor;
        using (var cursor = new WaitCursor())
        {
            editor = new DynamicEditorForm();
            editor.SetValue(Panel.ZIndexProperty, 999);
            editor.Form.DisableOKIfUnchanged = true;

            InitialiseEditorForm(editor, items, PageDataHandler, PreloadPages);
            OnEditorLoaded?.Invoke(editor, items);
        }
        editLock.Panel = editor;

        host.AddSubPanel(editor);
        editor.SubPanelClosed += o =>
        {
            try
            {
                DynamicGridUtils.FinishEdit(items);
                callback(items, editor.Result == true);
            }
            catch(Exception e)
            {
                MessageWindow.ShowError("Error occurred while closing editor.", e);
            }
        };

        editor.Show();
    }

    /// <summary>
    /// Edit the <paramref name="items"/> with a modal editor window.
    /// </summary>
    /// <param name="items">List of objects to edit.</param>
    /// <returns><see langword="true"/> if the items were saved.</returns>
    public virtual bool EditItems(T[] items, Func<Type, CoreTable?>? PageDataHandler = null, bool PreloadPages = false)
    {
        DynamicEditorForm editor;
        using (var cursor = new WaitCursor())
        {

            editor = new DynamicEditorForm()
            {
                WindowStartupLocation = WindowStartupLocation.CenterScreen
            };
            
            editor.SetValue(Panel.ZIndexProperty, 999);
            
            InitialiseEditorForm(editor, items, PageDataHandler, PreloadPages);
            OnEditorLoaded?.Invoke(editor, items);
        }

        return editor.ShowDialog() == true;
    }

    private Dictionary<String, object?> AfterEditorValueChanged(DynamicEditorGrid grid, T[] items, AfterEditorValueChangedArgs args)
    {
        var changes = new Dictionary<string, object?>();
        OnAfterEditorValueChanged(grid, items, args, changes);
        OnAfterEditorValueChangedEvent?.Invoke(grid, args);
        return changes;
    }

    protected virtual void OnAfterEditorValueChanged(DynamicEditorGrid? grid, T[] items, AfterEditorValueChangedArgs args, Dictionary<String, object?> changes)
    {
    }
    
    protected virtual void DoReconfigureEditors(DynamicEditorGrid grid, T[] items)
    {
        /*if (items.First() is IDimensioned dimensioned)
        {
            UpdateEditor(grid, x => x.Dimensions.Quantity, dimensioned.Dimensions.GetUnit().HasQuantity);
            UpdateEditor(grid, x => x.Dimensions.Length, dimensioned.Dimensions.GetUnit().HasLength);
            UpdateEditor(grid, x => x.Dimensions.Width, dimensioned.Dimensions.GetUnit().HasWidth);
            UpdateEditor(grid, x => x.Dimensions.Height, dimensioned.Dimensions.GetUnit().HasHeight);
            UpdateEditor(grid, x => x.Dimensions.Weight, dimensioned.Dimensions.GetUnit().HasWeight);
        }*/
    }

    private List<string>? ValidateData(T[] items)
    {
        var errors = new List<string>();
        DoValidate(items, errors);
        OnValidate?.Invoke(this, items, errors);
        return errors.Count != 0 ? errors : null;
    }

    protected virtual void DoValidate(T[] items, List<string> errors)
    {
    }

    protected virtual void AfterLoad(IDynamicEditorForm editor, T[] items)
    {
        editor.AfterLoad();
    }

    protected virtual void SelectPage(object sender, BaseObject[]? items)
    {
    }

    protected virtual Dictionary<string, object?> EditorValueChanged(IDynamicEditorForm editor, T[] items, string name, object value)
    {
        var result = DynamicGridUtils.UpdateEditorValue(items, name, value);
        if (OnEditorValueChanged != null)
        {
            var newchanges = OnEditorValueChanged(editor, name, value);
            foreach (var key in newchanges.Keys)
                result[key] = newchanges[key];
        }

        return result;
    }

    private readonly Dictionary<Tuple<Type, Type>, Dictionary<object, object>> _lookupcache = new();

    protected virtual void DefineLookups(ILookupEditorControl sender, T[] items, bool async = true)
    {
        if (sender.EditorDefinition is not ILookupEditor editor)
            return;

        var colname = sender.ColumnName;
        if (async)
        {
            Task.Run(() =>
            {
                try
                {
                    var values = editor.Values(colname, items);
                    Dispatcher.Invoke(
                        () =>
                        {
                            try
                            {
                                //Logger.Send(LogType.Information, typeof(T).Name, "Dispatching Results" + colname);
                                sender.LoadLookups(values);
                            }
                            catch (Exception e2)
                            {
                                Logger.Send(LogType.Information, typeof(T).Name,
                                    "Exception (2) in LoadLookups: " + e2.Message + "\n" + e2.StackTrace);
                            }
                        }
                    );
                }
                catch (Exception e)
                {
                    Logger.Send(LogType.Information, typeof(T).Name,
                        "Exception (1) in LoadLookups: " + e.Message + "\n" + e.StackTrace);
                }
            });
        }
        else
        {
            var values = editor.Values(colname, items);
            sender.LoadLookups(values);
        }
    }

    /// <summary>
    /// Retrieves an editor to display for the given column of <paramref name="item"/>.
    /// </summary>
    /// <param name="item">The object being edited.</param>
    /// <param name="column">The column of the editor.</param>
    /// <returns>A new editor, or <see langword="null"/> if no editor defined and no sensible default exists.</returns>
    protected virtual BaseEditor? GetEditor(object item, DynamicGridColumn column)
    {
        return column.Editor ?? DatabaseSchema.Property(item.GetType(), column.ColumnName)?.Editor;
    }

    protected IFilter? DefineLookupFilter(Type type, string column, T[] items)
    {
        return LookupFactory.DefineLookupFilter(typeof(T), type, column, items);
    }

    protected virtual void SetEditorValue(object item, string name, object value)
    {
        try
        {
            CoreUtils.SetPropertyValue(item, name, value);
        }
        catch (Exception e)
        {
            Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
        }
    }

    protected virtual object? GetEditorValue(object item, string name)
    {
        return CoreUtils.GetPropertyValue(item, name);
    }

    protected override bool CanCreateRows() => CanCreateItems();

    protected virtual bool CanCreateItems()
    {
        return true;
    }

    protected override bool EditRows(CoreRow[]? rows)
    {
        if(rows is null)
        {
            if (!CanCreateItems())
                return false;

            var item = CreateItem();
            
            if (!AfterCreate(item))
                return false;

            if(Options.NonModalEditorHost is not null)
            {
                EditItemsNonModal(Options.NonModalEditorHost, [item], (items, result) =>
                {
                    if (result)
                    {
                        var row = Data.NewRow();
                        ObjectToRow(item, row);
                        Data.Rows.Add(row);

                        var masterrow = MasterData.NewRow();
                        ObjectToRow(item, masterrow);
                        MasterData.Rows.Add(masterrow);

                        _recordmap[row] = masterrow;

                        InvalidateGrid();
                        SelectedRows = [row];
                        DoChanged();
                    }
                });
                return false;
            }
            else
            {
                if (EditItems([item]))
                {
                    //_CurrentRow = Data.Rows.Count;
                    var row = Data.NewRow();
                    ObjectToRow(item, row);
                    Data.Rows.Add(row);

                    var masterrow = MasterData.NewRow();
                    ObjectToRow(item, masterrow);
                    MasterData.Rows.Add(masterrow);

                    _recordmap[row] = masterrow;

                    InvalidateGrid();
                    SelectedRows = [row];
                    DoChanged();
                    return true;
                }

                return false;
            }
        }
        else
        {
            var items = Array.Empty<T>();
            using (new WaitCursor())
            {
                Stopwatch sw = new Stopwatch();
                sw.Start();
                items = LoadItems(rows);
                //Logger.Send(LogType.Information, "DG:LoadItems", String.Format("Loaded Items: {0}ms", sw.ElapsedMilliseconds));
                sw.Stop();
            }

            if (items.Length != 0)
            {
                var snaps = items.ToArray(x => x.TakeSnapshot());

                if(Options.NonModalEditorHost is not null)
                {
                    EditItemsNonModal(Options.NonModalEditorHost, items, (items, result) =>
                    {
                        if (result)
                        {
                            var sel = SelectedRows;
                            UpdateRows(rows, items, invalidateRows: false);
                            InvalidateGrid();
                            SelectedRows = sel;
                            DoChanged();
                        }
                        else
                        {
                            foreach(var snap in snaps)
                            {
                                snap.ResetObject();
                            }
                        }
                    });
                    return false;
                }
                else
                {
                    if (EditItems(items))
                    {
                        var sel = SelectedRows;
                        UpdateRows(rows, items, invalidateRows: false);
                        InvalidateGrid();
                        SelectedRows = sel;
                        DoChanged();
                        return true;
                    }
                    else
                    {
                        foreach(var snap in snaps)
                        {
                            snap.ResetObject();
                        }
                    } 
                }
                return false;
            }

            return false;
        }
    }

    #endregion

    #region Duplicate

    protected virtual IEnumerable<T> LoadDuplicatorItems(CoreRow[] rows)
    {
        return LoadItems(rows);
    }

    bool IDuplicateDynamicGrid.DoDuplicate(CoreRow[] rows) => DoDuplicate(rows);

    protected virtual bool DoDuplicate(CoreRow[] rows)
    {
        if (rows.Length == 0)
        {
            MessageBox.Show("Please select at least one record to duplicate!");
            return false;
        }

        /*var ids = ExtractValues(x => x.ID, Selection.Selected).ToArray();
        if (!ids.Any())
        {
            MessageBox.Show("Please select at least one record to duplicate!");
            return false;
        }*/

        var duplicator = (new T() as IDuplicatable)?.GetDuplicator();
        if (duplicator is null)
        {
            MessageBox.Show($"Cannot duplicate {typeof(T)}");
            return false;
        }

        duplicator.Duplicate(LoadDuplicatorItems(rows));// new Filter<T>(x => x.ID).InList(ids));
        return true;
    }

    #endregion

    public virtual T CreateItem()
    {
        var result = new T();
        OnCreateItem?.Invoke(this, result);
        return result;
    }

    public virtual bool AfterCreate(T item)
    {
        return OnAfterCreateItem?.Invoke(this, item) ?? true;
    }

    protected void ReloadForms<TTargetType, TTargetForm, TSourceForm>(IDynamicEditorForm editor, TTargetType item,
        Expression<Func<TSourceForm, object?>> sourcekey, Guid sourceid)
        where TTargetType : Entity, new()
        where TTargetForm : Entity, IRemotable, IPersistent, IDigitalFormInstance, new()
        where TSourceForm : Entity, IRemotable, IPersistent, IDigitalForm<TTargetType>, new()
    {
        var type = typeof(IDynamicOneToManyGrid<,>).MakeGenericType(typeof(TTargetType), typeof(TTargetForm));
        var page =
            editor.Pages?.FirstOrDefault(x => x.GetType().GetInterfaces().Contains(type)) as IDynamicOneToManyGrid<TTargetType, TTargetForm>;
        if (page != null && item != null)
        {
            if (!page.Ready)
                page.Load(item, null);

            CoreTable table;
            if (sourceid == Guid.Empty)
            {
                table = new CoreTable();
                table.LoadColumns(typeof(TSourceForm));
            }
            else
            {
                table = new Client<TSourceForm>().Query(
                    new Filter<TSourceForm>(sourcekey).IsEqualTo(sourceid).And(x => x.Form.AppliesTo)
                        .IsEqualTo(typeof(TTargetType).EntityName().Split('.').Last())
                );
            }

            var newforms = new List<TTargetForm>();
            foreach (var row in table.Rows)
            {
                var sourceform = row.ToObject<TSourceForm>();
                var targetform = new TTargetForm();

                targetform.Form.ID = sourceform.Form.ID;
                targetform.Form.Synchronise(sourceform.Form);
                newforms.Add(targetform);
            }

            page.Items.Clear();
            page.LoadItems(newforms.ToArray());
        }
    }

    #region ClipBuffer

    protected virtual bool BeforeCopy(IList<T> items)
    {
        return true;
    }

    protected override void MoveRows(CoreRow[] rows, int index, bool isCopy = false)
    {
        if (!Options.ReorderRows || !IsSequenced) return;

        var items = LoadItems(rows);
        if (isCopy)
        {
            if (!BeforeCopy(items))
            {
                return;
            }
        }

        var sequence = index < Data.Rows.Count
            ? Data.Rows[index].Get<ISequenceable, long>(x => x.Sequence)
            : Data.Rows[^1].Get<ISequenceable, long>(x => x.Sequence) + 1;

        var postRows = Data.Rows.Where(r => !rows.Contains(r) && r.Get<ISequenceable, long>(x => x.Sequence) >= sequence);

        var updates = items.Concatenate(LoadItems(postRows.ToArray()));
        foreach (var update in updates)
        {
            ((ISequenceable)update).Sequence = sequence;
            sequence++;
        }

        if (updates.Length != 0)
        {
            SaveItems(updates);
            DoChanged();
            Refresh(false, true);
        }
    }

    #endregion

    protected virtual void ObjectToRow(T obj, CoreRow row)
    {
        row.Table.FillRow(row, obj);
    }

    #region Import / Export

    protected virtual CoreTable LoadImportKeys(String[] fields)
    {
        var result = new CoreTable();
        result.LoadColumns(Columns.None<T>().Add(fields));
        return result;
    }

    protected virtual Guid GetImportID()
    {
        return Guid.Empty;
    }

    protected virtual bool CustomiseImportItem(T item)
    {
        if (IsSequenced)
            ((ISequenceable)item).Sequence = CoreUtils.GenerateSequence();
        return true;
    }

    protected virtual string CustomiseImportFileName(string filename)
    {
        return filename;
    }

    void IImportDynamicGrid.DoImport() => DoImport();

    protected virtual void DoImport()
    {
        var list = new DynamicImportList(
            typeof(T),
            GetImportID()
        );
        list.OnImportItem += o => { return CustomiseImportItem((T)o); };
        list.OnCustomiseImport += (o, args) => { args.FileName = CustomiseImportFileName(args.FileName); };
        list.OnSave += (sender, entity) => SaveItem(entity as T);
        list.OnLoad += (sender, type, fields, id) => LoadImportKeys(fields);
        list.ShowDialog();
        Refresh(false, true);
    }

    protected virtual void CustomiseExportColumns(List<string> columnnames)
    {
    }

    protected virtual string CustomiseExportFileName(string filename)
    {
        return filename;
    }

    protected virtual void CustomiseExportFilters(Filters<T> filters, CoreRow[] visiblerows)
    {
    }

    protected virtual void ApplyExportFilter(CoreTable table, object data)
    {
    }

    void IExportDynamicGrid.DoExport() => DoExport();

    protected virtual void DoExport()
    {
        var columnnames = VisibleColumns.Select(x => x.ColumnName).ToList();
        CustomiseExportColumns(columnnames);
        var form = new DynamicExportForm(typeof(T), columnnames);
        if (form.ShowDialog() != true)
            return;

        var filters = new Filters<T>();
        filters.Add(DefineFilter());

        var predicates = GetUIComponent().GetFilterPredicates();

        var visiblerows = GetVisibleRows();
        CustomiseExportFilters(filters, visiblerows);

        var columns = Columns.None<T>().Add(form.Fields);
        var otherColumns = form.GetChildFields()
            .Select(x => new Tuple<Type, IColumns>(
                x.Key,
                Columns.None(x.Key).Add(x.Value)))
            .Where(x => x.Item2.ColumnNames().Any()).ToList();

        var reloadColumns = Columns.None<T>();
        foreach (var column in columns)
        {
            reloadColumns.Add(column);
        }
        foreach (var column in HiddenColumns.ColumnNames)
        {
            reloadColumns.Add(column);
        }
        foreach (var column in LookupFactory.RequiredColumns<T>())
        {
            columns.Add(column);
        }
        foreach (var (column, _) in predicates)
        {
            reloadColumns.Add(column);
        }

        CoreTable? data = null;

        void LoadExport()
        {
            var newData = new CoreTable();
            newData.LoadColumns(columns);

            FilterRows(data.Rows, newData, filter: row =>
            {
                foreach(var (_, predicate) in predicates)
                {
                    if (!predicate(row))
                    {
                        return false;
                    }
                }
                return true;
            });

            var list = new List<Tuple<Type?, CoreTable>>() { new(typeof(T), newData) };
            list.AddRange(LoadExportTables(filters, otherColumns));
            DoExportTables(list);
        }

        var sort = LookupFactory.DefineSort<T>();
        Reload(filters, reloadColumns, ref sort, CancellationToken.None, (table, err) =>
        {
            if (table is not null)
            {
                if (table.Offset == 0 || data is null)
                {
                    data = table;
                    if (!IsPaging)
                    {
                        Dispatcher.Invoke(LoadExport);
                    }
                }
                else
                {
                    data.AddPage(table);
                    if (!IsPaging)
                    {
                        Dispatcher.Invoke(LoadExport);
                    }
                }
            }
            else if (err is not null)
            {
                Dispatcher.Invoke(() =>
                {
                    MessageWindow.ShowError("Error in export.", err);
                });
            }
        });
    }

    /// <summary>
    /// Loads the child tables for an export, based on the filter of the parent table.
    /// </summary>
    /// <remarks>
    /// If not overriden, defaults to creating empty tables with no records.
    /// </remarks>
    /// <param name="filter">Filter for the parent table.</param>
    /// <param name="tableColumns">A list of the child table types, with columns to load for each</param>
    /// <returns>A list of tables, in the same order as they came in <paramref name="tableColumns"/></returns>
    protected virtual IEnumerable<Tuple<Type?, CoreTable>> LoadExportTables(Filters<T> filter, IEnumerable<Tuple<Type, IColumns>> tableColumns)
    {
        return tableColumns.Select(x =>
        {
            var table = new CoreTable();
            table.LoadColumns(x.Item2);
            return new Tuple<Type?, CoreTable>(x.Item1, table);
        });
    }

    private void DoExportTables(List<Tuple<Type?, CoreTable>> data)
    {
        var filename = CustomiseExportFileName(typeof(T).EntityName().Split('.').Last());
        ExcelExporter.DoExport(data, filename);
    }

    #endregion

    #endregion

    #region Header Actions

    protected override bool SelectColumns([NotNullWhen(true)] out DynamicGridColumns? columns)
    {
        var schema = new DynamicGridObjectColumnSchema(typeof(T));

        var editor = new DynamicGridColumnsEditor(schema, typeof(T)) { WindowStartupLocation = WindowStartupLocation.CenterScreen };
        editor.DirectEdit = IsDirectEditMode();

        editor.Columns.AddRange(VisibleColumns);
        schema.OnProcessColumns += args =>
        {
            GetAvailableColumns?.Invoke(args);
        };

        if (editor.ShowDialog().Equals(true))
        {
            columns = editor.Columns;
            return true;
        }
        else
        {
            columns = null;
            return false;
        }
    }

    #endregion

    #region Drag + Drop

    protected DragDropEffects DragTable(Type entity, CoreTable table)
    {
        Logger.Send(LogType.Information, "", "DragTable");
        var data = new DataObject();
        data.SetData(DynamicGridUtils.DragFormat, new DynamicGridDragFormat(table.ToDataTable(), entity));
        var effect = DragDrop.DoDragDrop(this, data, DragDropEffects.All);
        return effect;
    }

    protected override DragDropEffects OnRowsDragStart(CoreRow[] rows)
    {
        Logger.Send(LogType.Information, "", "OnRowsDragStart");
        var table = new CoreTable();
        table.LoadColumns(Data.Columns);
        table.LoadRows(rows);
        return DragTable(typeof(T), table);
    }

    #endregion
}

#region Styling

public class DynamicGridRowStyle : DynamicGridStyle<VirtualizingCellsControl>
{
    public DynamicGridRowStyle() : base(null)
    {
    }

    public DynamicGridRowStyle(IDynamicGridStyle source) : base(source)
    {
    }

    public override DependencyProperty FontSizeProperty => Control.FontSizeProperty;

    public override DependencyProperty FontStyleProperty => Control.FontStyleProperty;

    public override DependencyProperty FontWeightProperty => Control.FontWeightProperty;

    public override DependencyProperty BackgroundProperty => Control.BackgroundProperty;

    public override DependencyProperty ForegroundProperty => Control.ForegroundProperty;
}

public class DynamicGridCellStyle : DynamicGridStyle<Control>
{
    public DynamicGridCellStyle() : base(null)
    {
    }

    public DynamicGridCellStyle(IDynamicGridStyle source) : base(source)
    {
    }

    public override DependencyProperty FontSizeProperty => Control.FontSizeProperty;

    public override DependencyProperty FontStyleProperty => Control.FontStyleProperty;

    public override DependencyProperty FontWeightProperty => Control.FontWeightProperty;

    public override DependencyProperty BackgroundProperty => Control.BackgroundProperty;

    public override DependencyProperty ForegroundProperty => Control.ForegroundProperty;
}

// Used to render boolean columns (the default "false" value shows what appears to be an intermediate state, which is ugly
// This should show nothing for false, and a tick in a box for true
public class BoolToImageConverter : AbstractConverter<bool, ImageSource?>
{

    public ImageSource TrueValue { get; set; }
    public ImageSource? FalseValue { get; set; }

    public BoolToImageConverter()
    {
        TrueValue = Wpf.Resources.Bullet_Tick.AsBitmapImage();
    }
    
    public override ImageSource? Convert(bool value)
    {
        return value ? TrueValue : FalseValue;
    }

    public override bool Deconvert(ImageSource? value)
    {
        return ImageUtils.IsEqual(value as BitmapImage, TrueValue as BitmapImage);
    }
}

public class StringToColorImageConverter : IValueConverter
{
    private readonly int _height = 50;
    private readonly int _width = 25;
    private readonly Dictionary<string, BitmapImage> cache = new();

    public StringToColorImageConverter(int width, int height)
    {
        _width = width;
        _height = height;
    }

    public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var str = value?.ToString();
        if (str is null)
            return null;

        var colorcode = str.TrimStart('#');

        if (!cache.ContainsKey(colorcode))
        {
            var col = ImageUtils.StringToColor(colorcode);
            var bmp = ImageUtils.BitmapFromColor(col, _width, _height, Color.Black);
            cache[colorcode] = bmp.AsBitmapImage();
        }

        var result = cache[colorcode];
        return result;
    }


    public object? ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return null;
    }
}

public class StringArrayConverter : IValueConverter
{
    object? IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is string[] strArray)
        {
            return string.Join("\n", strArray);
        }
        Logger.Send(LogType.Error, "", $"Attempt to convert an object which is not a string array: {value}.");
        return null;
    }

    object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value;
    }
}

#endregion


[Serializable]
class DynamicGridDragFormat
{
    private string entity;

    public DataTable Table { get; set; }

    public Type Entity
    {
        get => CoreUtils.GetEntity(entity);
        [MemberNotNull(nameof(entity))]
        set => entity = value.EntityName();
    }

    public DynamicGridDragFormat(DataTable table, Type entity)
    {
        Table = table;
        Entity = entity;
    }
}