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.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;

namespace InABox.DynamicGrid;

public abstract class DynamicGrid : ContentControl
{
    public static readonly DependencyProperty UseWaitCursorProperty =
        DependencyProperty.Register(nameof(UseWaitCursor), typeof(bool), typeof(DynamicGrid<>));

    public bool UseWaitCursor
    {
        get => (bool)GetValue(UseWaitCursorProperty);
        set => SetValue(UseWaitCursorProperty, value);
    }
}

public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParent<T>, IDynamicGrid<T>
    where T : BaseObject, new()
{

    protected enum ClipAction
    {
        Cut,
        Copy
    }

    private IDynamicGridUIComponent<T> UIComponent { get; set; }

    private UIElement? _header;
    private readonly Button Add;

    public bool bRefreshing;
    bool IDynamicGridUIComponentParent<T>.IsRefreshing => bRefreshing;

    private readonly Label ClipboardSpacer;

    private readonly Button Copy;

    private readonly Label Count;

    private readonly Button Cut;
    private readonly Border Disabler;

    private readonly Button Delete;
    private readonly DockPanel Docker;
    private readonly DynamicRowMovementColumn? down;
    private readonly Button Edit;
    private readonly Label EditSpacer;
    private readonly Button Export;
    private readonly Label ExportSpacer;
    private readonly Button DuplicateBtn;
    private readonly Button SwitchViewBtn;

    private readonly Button Help;
    private readonly Button Import;

    private readonly Grid Layout;
    private readonly Label Loading;

    private DoubleAnimation LoadingFader = new DoubleAnimation(1d, 0.2d, new Duration(TimeSpan.FromSeconds(2))) { AutoReverse = true };

    //private readonly Button MultiEdit;
    private readonly Button Paste;

    private readonly Button Print;
    private readonly Label PrintSpacer;

    private readonly StackPanel LeftButtonStack;
    private readonly StackPanel RightButtonStack;

    private readonly DynamicRowMovementColumn? up;

    protected DynamicGridRowStyleSelector<T> RowStyleSelector;

    #region Events

    public event IDynamicGrid.ReconfigureEvent? OnReconfigure;

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

    public event OnPrintData? OnPrintData;
    
    public event BeforeRefreshEventHandler? BeforeRefresh;
    public event AfterRefreshEventHandler? AfterRefresh;

    public event OnDefineFilter? OnDefineFilter;
    public event OnCreateItem? OnCreateItem;
    public event OnAfterCreateItem? OnAfterCreateItem;
    /// <summary>
    /// Called when an item is selected in the grid. It is not called if <see cref="IsReady"/> is not <see langword="true"/>.
    /// </summary>
    /// <remarks>
    /// It is unnecessary to use this if within a grid. Instead, override <see cref="SelectItems(CoreRow[]?)"/>.
    /// </remarks>
    public event SelectItemHandler? OnSelectItem;

    public event OnCellDoubleClick? OnCellDoubleClick;

    public event EventHandler? OnChanged;

    public delegate void BeforeSelectionEvent(CancelEventArgs cancel);
    public event BeforeSelectionEvent? OnBeforeSelection;
    
    protected virtual void Changed()
    {
    }

    public virtual void DoChanged()  
    {
        Changed();
        OnChanged?.Invoke(this, EventArgs.Empty);
    }


    public event EditorValueChangedHandler? OnEditorValueChanged;

    public event OnCustomiseEditor<T>? OnCustomiseEditor;

    public event OnFilterRecord? OnFilterRecord;

    public event OnDoubleClick? OnDoubleClick;

    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 DynamicGridSettings Settings { get; set; }

    public DynamicGrid() : base()
    {
        UseWaitCursor = true;

        Options = new DynamicGridOptions();
        Options.OnChanged += () =>
        {
            _hasLoadedOptions = true;
            OptionsChanged();
        };

        ActionColumns = new DynamicActionColumns();
        ColumnGroupings = new DynamicGridColumnGroupings();
        
        RowStyleSelector = GetRowStyleSelector();
        RowStyleSelector.GetStyle += (row, style) => GetRowStyle(row, style);
        
        IsReady = false;

        Data = new CoreTable();
        MasterColumns = new DynamicGridColumns();
        MasterColumns.ExtractColumns(typeof(T));

        HiddenColumns = new HiddenColumnsList();
        foreach (var column in LookupFactory.RequiredColumns<T>().ColumnNames())
        {
            AddHiddenColumn(column);
        }

        if (ShowSequenceButtons)
        {
            up = new DynamicRowMovementColumn(DynamicRowMovement.Up, SwapRows);
            ActionColumns.Add(up);
            down = new DynamicRowMovementColumn(DynamicRowMovement.Down, SwapRows);
            ActionColumns.Add(down);
            HiddenColumns.Add(x => (x as ISequenceable)!.Sequence);
        }

        VisibleColumns = new DynamicGridColumns();

        UIComponent = CreateUIComponent();
        
        Loading = new Label();
        Loading.Content = "Loading...";
        Loading.Foreground = new SolidColorBrush(Colors.White);
        Loading.VerticalContentAlignment = VerticalAlignment.Center;
        Loading.HorizontalContentAlignment = HorizontalAlignment.Center;
        Loading.Visibility = Visibility.Collapsed;
        Loading.SetValue(Panel.ZIndexProperty, 999);
        Loading.SetValue(Grid.RowProperty, 1);
        Loading.FontSize = 14.0F;
        LoadingFader.Completed += (sender, args) =>
        {
            if (Loading.Visibility == Visibility.Visible)
            {
                //Logger.Send(LogType.Information, this.GetType().EntityName().Split(".").Last(), "Loading Fader Restarting");
                Loading.BeginAnimation(Label.OpacityProperty, LoadingFader);
            }
        };

        Help = CreateButton(Wpf.Resources.help.AsBitmapImage(Color.White));
        Help.Margin = new Thickness(0, 2, 2, 0);
        Help.SetValue(DockPanel.DockProperty, Dock.Right);
        Help.Click += (o, e) => ShowHelp(typeof(T).Name.Split('.').Last().SplitCamelCase().Replace(" ", "_"));

        Add = CreateButton(Wpf.Resources.add.AsBitmapImage(Color.White));
        Add.Margin = new Thickness(0, 2, 2, 0);
        Add.Click += Add_Click;

        Edit = CreateButton(Wpf.Resources.pencil.AsBitmapImage(Color.White));
        Edit.Margin = new Thickness(0, 2, 2, 0);
        Edit.Click += Edit_Click;

        SwitchViewBtn = CreateButton(Wpf.Resources.alter.AsBitmapImage());
        SwitchViewBtn.Margin = new Thickness(0, 2, 2, 0);
        SwitchViewBtn.Click += SwitchView_Click;

        EditSpacer = new Label { Width = 5 };

        Print = CreateButton(Wpf.Resources.print.AsBitmapImage(Color.White));
        Print.Margin = new Thickness(0, 2, 2, 0);
        Print.Click += (o, e) => DoPrint(o);

        PrintSpacer = new Label { Width = 5 };

        Cut = CreateButton(Wpf.Resources.cut.AsBitmapImage(Color.White));
        Cut.Margin = new Thickness(0, 2, 2, 0);
        Cut.Click += Cut_Click;

        Copy = CreateButton(Wpf.Resources.copy.AsBitmapImage(Color.White));
        Copy.Margin = new Thickness(0, 2, 2, 0);
        Copy.Click += Copy_Click;

        Paste = CreateButton(Wpf.Resources.paste.AsBitmapImage(Color.White));
        Paste.Margin = new Thickness(0, 2, 2, 0);
        Paste.Click += Paste_Click;

        ClipboardSpacer = new Label { Width = 5 };

        Export = CreateButton(Wpf.Resources.doc_xls.AsBitmapImage(Color.White), "Export");
        Export.Margin = new Thickness(0, 2, 2, 0);
        Export.Click += Export_Click;

        Import = CreateButton(Wpf.Resources.doc_xls.AsBitmapImage(Color.White), "Import");
        Import.Margin = new Thickness(0, 2, 2, 0);
        Import.Click += Import_Click;

        ExportSpacer = new Label { Width = 5 };
        

        LeftButtonStack = new StackPanel();
        LeftButtonStack.Orientation = Orientation.Horizontal;
        LeftButtonStack.SetValue(DockPanel.DockProperty, Dock.Left);

        LeftButtonStack.Children.Add(Help);
        LeftButtonStack.Children.Add(Add);
        LeftButtonStack.Children.Add(Edit);
        LeftButtonStack.Children.Add(SwitchViewBtn);
        //Stack.Children.Add(MultiEdit);
        LeftButtonStack.Children.Add(EditSpacer);

        LeftButtonStack.Children.Add(Print);
        LeftButtonStack.Children.Add(PrintSpacer);

        LeftButtonStack.Children.Add(Cut);
        LeftButtonStack.Children.Add(Copy);
        LeftButtonStack.Children.Add(Paste);
        LeftButtonStack.Children.Add(ClipboardSpacer);

        LeftButtonStack.Children.Add(Export);
        LeftButtonStack.Children.Add(Import);
        LeftButtonStack.Children.Add(ExportSpacer);

        RightButtonStack = new StackPanel();
        RightButtonStack.Orientation = Orientation.Horizontal;
        RightButtonStack.SetValue(DockPanel.DockProperty, Dock.Right);

        Delete = CreateButton(Wpf.Resources.delete.AsBitmapImage(Color.White));
        Delete.Margin = new Thickness(2, 2, 0, 0);
        Delete.SetValue(DockPanel.DockProperty, Dock.Right);
        Delete.Click += Delete_Click;

        DuplicateBtn = AddButton("Duplicate", Wpf.Resources.paste.AsBitmapImage(Color.White), DoDuplicate);

        Count = new Label();
        Count.Height = 30;
        Count.Margin = new Thickness(0, 2, 0, 0);
        Count.VerticalContentAlignment = VerticalAlignment.Center;
        Count.HorizontalContentAlignment = HorizontalAlignment.Center;
        Count.SetValue(DockPanel.DockProperty, Dock.Left);

        Docker = new DockPanel();

        Docker.SetValue(Grid.RowProperty, 2);
        Docker.SetValue(Grid.ColumnProperty, 0);
        Docker.Children.Add(LeftButtonStack);
        Docker.Children.Add(Delete);
        Docker.Children.Add(RightButtonStack);
        Docker.Children.Add(Count);


        Layout = new Grid();
        Layout.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
        Layout.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Auto) });
        Layout.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
        Layout.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Auto) });

        var control = UIComponent.Control;
        control.SetValue(Grid.RowProperty, 1);

        Layout.Children.Add(control);
        Layout.Children.Add(Loading);
        Layout.Children.Add(Docker);
        
        Disabler = new Border()
        {
            BorderBrush = new SolidColorBrush(Colors.Transparent),
            Background = new SolidColorBrush(Colors.DimGray) { Opacity = 0.2 },
            Visibility = Visibility.Collapsed,
        };
        Disabler.SetValue(Canvas.ZIndexProperty, 99);
        Disabler.SetValue(Grid.RowSpanProperty, 3);

        Layout.Children.Add(Disabler);
        
        //Scroll.ApplyTemplate();

        Content = Layout;

        IsEnabledChanged += (sender, args) =>
        {
            Disabler.Visibility = Equals(args.NewValue, true)
                ? Visibility.Collapsed
                : Visibility.Visible;
        };

        Settings = LoadSettings();
        
        Init();
        Reconfigure();
    }

    #region IDynamicGridUIComponentParent

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

    bool IDynamicGridUIComponentParent<T>.CanSort()
    {
        return !ShowSequenceButtons || !Options.EditRows;
    }

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

    DynamicGridRowStyleSelector<T> IDynamicGridUIComponentParent<T>.RowStyleSelector => RowStyleSelector;

    void IDynamicGridUIComponentParent<T>.BeforeSelection(CancelEventArgs cancel)
    {
        BeforeSelection(cancel);
    }

    void IDynamicGridUIComponentParent<T>.SelectItems(CoreRow[] rows)
    {
        SelectItems(rows);
    }
    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);
    }

    void IDynamicGridUIComponentParent<T>.HandleKey(KeyEventArgs e)
    {
        if (ShowSequenceButtons)
        {
            if (e.Key == Key.X && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
            {
                CutToClipBuffer();
            }
            else if (e.Key == Key.C && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
            {
                CopyToClipBuffer();
            }
            else if (e.Key == Key.V && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
            {
                PasteFromClipBuffer();
            }
            else if (e.Key == Key.Escape)
            {
                ResetClipBuffer();
                InvalidateGrid();
            }
        }
    }

    void IDynamicGridUIComponentParent<T>.DoubleClickCell(CoreRow? row, DynamicColumnBase? column)
    {
        var args = new DynamicGridCellClickEventArgs(row, column);
        if (OnCellDoubleClick is not null && column is DynamicGridColumn col)
        {
            
            OnCellDoubleClick?.Invoke(this, args);
            if (args.Handled)
                return;
        }

        if (row is not null)
            DoDoubleClick(this, args);
    }

    void IDynamicGridUIComponentParent<T>.ExecuteActionColumn(DynamicActionColumn column, CoreRow[]? rows)
    {
        var bRefresh = false;
        if(rows is null)
        {
            bRefresh = column.Action?.Invoke(null) ?? false;
        }
        else
        {
            foreach (var row in rows)
                if (column.Action?.Invoke(row) == true)
                    bRefresh = true;
        }
        if (bRefresh)
            Dispatcher.BeginInvoke(() => { Refresh(true, true); });
    }

    void IDynamicGridUIComponentParent<T>.OpenColumnMenu(DynamicColumnBase column)
    {
        if(column is DynamicMenuColumn menuColumn)
        {
            menuColumn.Action?.Invoke(SelectedRows.FirstOrDefault());
        }
        else if(column is DynamicActionColumn actionColumn)
        {
            var menu = actionColumn?.ContextMenu?.Invoke(SelectedRows);
            if (menu != null && menu.Items.Count > 0)
            {
                menu.IsOpen = true;
            }
        }
    }

    void IDynamicGridUIComponentParent<T>.UpdateRecordCount(int count)
    {
        Count.Content = FormatRecordCount(count);
    }

    protected virtual string FormatRecordCount(int count) => $"{count} Records";
    

    void IDynamicGridUIComponentParent<T>.LoadColumnsMenu(ContextMenu menu)
    {
        menu.AddItem("Select Columns", null, SelectColumnsClick);
        LoadColumnsMenu(menu);
    }


    void IDynamicGridUIComponentParent<T>.DragOver(object sender, DragEventArgs e)
    {
        HandleDragOver(sender, e);
    }

    void IDynamicGridUIComponentParent<T>.Drop(object sender, DragEventArgs e)
    {
        if (!Options.DragTarget)
            return;

        if(DynamicGridUtils.TryGetDropData(e, out var entityType, out var table))
        {
            OnDragEnd(entityType, table, e);
        }
        else
        {
            HandleDragDrop(sender, e);
        }
    }

    void IDynamicGridUIComponentParent<T>.DragStart(object? sender, CoreRow[] rows)
    {
        Logger.Send(LogType.Information, "", "RowDragDropController_DragStart");
        
        if (!Options.DragSource)
            return;
        
        OnRowsDragStart(rows);
    }

    void IDynamicGridUIComponentParent<T>.UIFilterChanged(object sender) => DoFilterChanged();

    protected virtual void DoFilterChanged()
    {
        
    }

    IEnumerable<string>? IDynamicGridUIComponentParent<T>.GetColumnFilterItems(DynamicColumnBase column) => GetColumnFilterItems(column);

    protected virtual IEnumerable<string>? GetColumnFilterItems(DynamicColumnBase column)
    {
        return null;
    }

    #endregion

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

    protected virtual DynamicGridStyle GetRowStyle(CoreRow row, DynamicGridStyle style)
    {
        DynamicGridStyle? result = null;

        if (ClipBuffer != null)
            if (ClipBuffer.Item2.Contains(row))
            {
                var bgbrush = style.Background as SolidColorBrush;
                var bgcolor = bgbrush != null ? bgbrush.Color : Colors.Transparent;

                result = new DynamicGridRowStyle(style);

                result.Background = ClipBuffer.Item1 == ClipAction.Cut
                    ? new SolidColorBrush(bgcolor.MixColors(0.5, Colors.Orchid))
                    : new SolidColorBrush(bgcolor.MixColors(0.5, Colors.LightGreen));

                result.Foreground = new SolidColorBrush(Colors.Gray);
                result.FontStyle = FontStyles.Italic;
            }
        result ??= OnGetRowStyle != null ? OnGetRowStyle(row, style) : style;

        return result;
    }

    protected virtual void BeforeSelection(CancelEventArgs cancel)
    {
        OnBeforeSelection?.Invoke(cancel);
    }

    public bool IsReady { get; protected set; }

    public UIElement? Header
    {
        get => _header;
        set
        {
            if (_header is not null && Layout.Children.Contains(_header))
                Layout.Children.Remove(_header);
            _header = value;
            if (_header is not null)
            {
                _header.SetValue(Grid.RowProperty, 0);
                _header.SetValue(Grid.ColumnProperty, 0);
                _header.SetValue(Grid.ColumnSpanProperty, 2);
                Layout.Children.Add(_header);
            }
        }
    }

    /// <summary>
    /// Represents the data in the grid. This is <see langword="null"/> until <see cref="Refresh(bool, bool)"/> is called.
    /// </summary>
    public CoreTable? MasterData { get; set; }

    public DynamicGridColumns MasterColumns { get; protected set; }
    public DynamicGridColumns VisibleColumns { get; protected set; }
    public DynamicActionColumns ActionColumns { get; protected set; }
    public CoreTable Data { get; set; }

    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 static bool IsSequenced => typeof(T).GetInterfaces().Any(x => x.Equals(typeof(ISequenceable)));

    protected virtual bool ShowSequenceButtons => IsSequenced;

    public double RowHeight
    {
        get => UIComponent.RowHeight;
        set => UIComponent.RowHeight = value;
    }

    public double HeaderHeight
    {
        get => UIComponent.HeaderRowHeight;
        set => UIComponent.HeaderRowHeight = value;
    }

    #region Options

    /// <summary>
    /// Initialise things like custom buttons; called once during construction.
    /// </summary>
    protected abstract void Init();

    protected abstract void DoReconfigure(DynamicGridOptions options);

    private bool _hasLoadedOptions = false;

    protected virtual void OptionsChanged()
    {
        var reloadColumns = false;

        Help.Visibility = Options.ShowHelp ? Visibility.Visible : Visibility.Collapsed;

        Add.Visibility = Options.AddRows ? Visibility.Visible : Visibility.Collapsed;
        Edit.Visibility = Options.EditRows ? Visibility.Visible : Visibility.Collapsed;

        EditSpacer.Visibility = Options.AddRows || Options.EditRows
            ? Visibility.Visible
            : Visibility.Collapsed;

        Print.Visibility = Options.Print ? Visibility.Visible : Visibility.Collapsed;
        PrintSpacer.Visibility = Options.Print ? Visibility.Visible : Visibility.Collapsed;

        Cut.Visibility = ShowSequenceButtons && Options.EditRows ? Visibility.Visible : Visibility.Collapsed;
        Copy.Visibility = ShowSequenceButtons && Options.EditRows ? Visibility.Visible : Visibility.Collapsed;
        Paste.Visibility = ShowSequenceButtons && Options.EditRows ? Visibility.Visible : Visibility.Collapsed;
        ClipboardSpacer.Visibility = ShowSequenceButtons && Options.EditRows ? Visibility.Visible : Visibility.Collapsed;

        Export.Visibility = Options.ExportData ? Visibility.Visible : Visibility.Collapsed;
        Import.Visibility = Options.ImportData ? Visibility.Visible : Visibility.Collapsed;
        ExportSpacer.Visibility = Options.ExportData || Options.ImportData
            ? Visibility.Visible
            : Visibility.Collapsed;

        SwitchViewBtn.Visibility = Options.DirectEdit 
            ? Options.HideDirectEditButton
                ? Visibility.Collapsed 
                : Visibility.Visible 
            : Visibility.Collapsed;

        Count.Visibility = Options.RecordCount ? Visibility.Visible : Visibility.Collapsed;

        Delete.Visibility = Options.DeleteRows ? Visibility.Visible : Visibility.Collapsed;

        if (up != null && down != null)
        {
            var hasSequence = up.Position == DynamicActionColumnPosition.Start;
            var shouldHaveSequence = Options.EditRows;
            if(hasSequence != shouldHaveSequence)
            {
                up.Position = Options.EditRows ? DynamicActionColumnPosition.Start : DynamicActionColumnPosition.Hidden;
                down.Position = Options.EditRows ? DynamicActionColumnPosition.Start : DynamicActionColumnPosition.Hidden;
                reloadColumns = true;
            }
        }

        if (DuplicateBtn != null)
            DuplicateBtn.Visibility = Visibility.Collapsed;

        reloadColumns = reloadColumns || UIComponent.OptionsChanged();

        if(reloadColumns && IsReady)
        {
            Refresh(true, false);
        }
    }

    public bool IsDirectEditMode()
    {
        return Options.DirectEdit
            && (Settings.ViewMode == DynamicGridSettings.DynamicGridViewMode.DirectEdit
                || Settings.ViewMode == DynamicGridSettings.DynamicGridViewMode.Default);
    }

    private void SwitchView_Click(object sender, RoutedEventArgs e)
    {
        Settings.ViewMode = Settings.ViewMode switch
        {
            DynamicGridSettings.DynamicGridViewMode.Default => DynamicGridSettings.DynamicGridViewMode.Normal,
            DynamicGridSettings.DynamicGridViewMode.Normal => DynamicGridSettings.DynamicGridViewMode.DirectEdit,
            DynamicGridSettings.DynamicGridViewMode.DirectEdit or _ => DynamicGridSettings.DynamicGridViewMode.Normal
        };
        SaveSettings(Settings);
        Reconfigure();
    }
    public DynamicGridOptions Options { get; }
    

    protected void OnReconfigureEvent(DynamicGridOptions options)
    {
        OnReconfigure?.Invoke(options);
    }

    /// <summary>
    /// Configure custom buttons and options.
    /// </summary>
    public void Reconfigure(DynamicGridOptions options)
    {
        options.BeginUpdate().Clear();
        DoReconfigure(options);
        OnReconfigureEvent(options);
        options.EndUpdate();
        if (!_hasLoadedOptions)
        {
            _hasLoadedOptions = true;
            OptionsChanged();
        }
    }
    
    public void Reconfigure()
    {
        Reconfigure(Options);
    }
    
    public void Reconfigure(ReconfigureEvent onReconfigure)
    {
        OnReconfigure += onReconfigure;
        Reconfigure();
    }

    #endregion

    protected virtual DynamicGridSettings LoadSettings()
    {
        return new DynamicGridSettings();
    }
    protected virtual void SaveSettings(DynamicGridSettings settings)
    {
    }


    protected virtual void LoadColumnsMenu(ContextMenu menu)
    {
    }

    protected void UpdateCell(int row, string colname, object? value)
    {
        var coreRow = Data.Rows[row];
        coreRow[colname] = value;
        UIComponent.UpdateCell(coreRow, colname, value);
    }

    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)
        {
            row[key] = value;
        }

        UIComponent.UpdateRow(row);
    }

    #region Row Selections

    protected CoreRow[] GetVisibleRows()
    {
        return UIComponent.GetVisibleRows();
    }


    public CoreRow[] SelectedRows
    {
        get => UIComponent.SelectedRows;
        set => UIComponent.SelectedRows = value;
    }

    /// <summary>
    /// Call the <see cref="OnSelectItem"/> event, and do any updating which needs to occur when items are selected.
    /// </summary>
    /// <param name="rows"></param>
    protected virtual void SelectItems(CoreRow[]? rows)
    {
        if (IsReady)
            OnSelectItem?.Invoke(this, new DynamicGridSelectionEventArgs(rows));

        DuplicateBtn.Visibility =
            typeof(T).IsAssignableTo(typeof(IDuplicatable)) && rows != null && rows.Length >= 1 ? Visibility.Visible : Visibility.Collapsed;
    }

    protected virtual void DoDoubleClick(object sender, DynamicGridCellClickEventArgs args)
    {
        if (IsDirectEditMode())
            return;

        //SelectItems(SelectedRows);
        var e = new HandledEventArgs(false);
        OnDoubleClick?.Invoke(sender, e);
        if (e.Handled)
            return;
        if (Options.EditRows)
            DoEdit();
    }

    #endregion

    #region Column Handling

    #region Column Grouping

    public DynamicGridColumnGroupings ColumnGroupings { get; set; }

    /// <summary>
    /// Create a new column header group, and return it for editing.
    /// </summary>
    /// <returns></returns>
    public DynamicGridColumnGrouping AddColumnGrouping()
    {
        var group = new DynamicGridColumnGrouping();
        ColumnGroupings.Add(group);
        return group;
    }

    /// <summary>
    /// Gets the current column header group, and if there is none, create a new one.
    /// </summary>
    /// <returns></returns>
    public DynamicGridColumnGrouping GetColumnGrouping()
    {
        if(ColumnGroupings.Count == 0)
        {
            return AddColumnGrouping();
        }
        return ColumnGroupings[^1];
    }

    #endregion

    protected virtual DynamicGridColumns LoadColumns()
    {
        return GenerateColumns();
    }


    /// <summary>
    /// Provide a set of columns which is the default for this grid.
    /// </summary>
    public virtual DynamicGridColumns GenerateColumns()
    {
        var columns = new DynamicGridColumns();

        var cols = IsDirectEditMode()
            ? new Columns<T>(ColumnTypeFlags.IncludeVisible | ColumnTypeFlags.IncludeForeignKeys)
            : new Columns<T>(ColumnTypeFlags.IncludeVisible | ColumnTypeFlags.IncludeLinked);

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

    private bool SwapRows(int row1, int row2)
    {

        CoreRow[] rows = Data.Rows.Where(x => x.Index.Equals(row1) || x.Index.Equals(row2)).ToArray();
        var items = LoadItems(rows);
        var first = (items.First() as ISequenceable)!;
        var last = (items.Last() as ISequenceable)!;
        var iBuf1 = first.Sequence;
        var iBuf2 = last.Sequence;
        first.Sequence = iBuf2;
        last.Sequence = iBuf1;
        SaveItems(items);
        return true;
    }

    protected virtual void SaveColumns(DynamicGridColumns columns)
    {
    }

    public int DesiredWidth()
    {
        return UIComponent.DesiredWidth();
    }

    /// <summary>
    /// Handle to configure column groups.
    /// </summary>
    /// <remarks>
    /// This is called after <see cref="LoadColumns"/>, so by the time this is called, both <see cref="VisibleColumns"/>
    /// and <see cref="ActionColumns"/> will be loaded, which means one can reference these in the column groups.
    /// <br/>
    /// <b>Note:</b> <see cref="ColumnGroupings"/> is cleared before this function is called.
    /// </remarks>
    protected virtual void ConfigureColumnGroups()
    {
    }

    protected virtual void ConfigureColumns(DynamicGridColumns columns)
    {
    }
    
    private void ReloadColumns()
    {
        ConfigureColumns(MasterColumns);

        VisibleColumns = LoadColumns();
        ConfigureColumns(VisibleColumns);
        
        ColumnGroupings.Clear();
        ConfigureColumnGroups();

        UIComponent.RefreshColumns(VisibleColumns, ActionColumns, ColumnGroupings);
    }

    #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;
    }

    protected virtual bool FilterRecord(CoreRow row)
    {
        var bOK = ActionColumns.All(x =>
        {
            return x.FilterRecord is null
                || ((x.SelectedFilters is null || x.SelectedFilters.Length == 0 || x.FilterRecord.Invoke(row, x.SelectedFilters))
                    && (x.ExcludeFilters is null || x.ExcludeFilters.Length == 0 || !x.FilterRecord.Invoke(row, x.ExcludeFilters)));
        });
        if (bOK && OnFilterRecord is not null)
            bOK = OnFilterRecord(row);
        return bOK;
    }

    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;
    }

    private class RowRange(int rowIdx, int size)
    {
        public int RowIdx { get; set; } = rowIdx;

        public int Size { get; set; } = size;
    }

    private CancellationTokenSource? RefreshCancellationToken;
    public virtual void Refresh(bool reloadcolumns, bool reloaddata)
    {
        if (bRefreshing)
            return;

        if (!DoBeforeRefresh())
            return;

        UIComponent.BeforeRefresh();

        var cursor = UseWaitCursor ? new WaitCursor() : null;

        Loading.Visibility = Visibility.Visible;
        Loading.BeginAnimation(Label.OpacityProperty, LoadingFader);

        bRefreshing = true;

        if (reloadcolumns)
            ReloadColumns();

        if (reloaddata)
        {
            _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");

            RefreshCancellationToken?.Cancel();

            var tokenSource = new CancellationTokenSource();
            RefreshCancellationToken = tokenSource;
            var token = tokenSource.Token;

            Reload(
                criteria, columns, ref sort,
                token,
                (table, exception) =>
                {
                    if(token.IsCancellationRequested) return; // Don't bother even checking exceptions if task was cancelled.

                    if (exception != null)
                    {
                        Dispatcher.Invoke(() =>
                        {
                            MessageWindow.ShowError("Sorry! We couldn't load the data.", exception);
                        });
                    }
                    else if (table is not null)
                    {
                        if(table.Offset == 0 || MasterData is null)
                        {
                            MasterData = table;
                            Dispatcher.Invoke(() =>
                            {
                                ProcessData(null);
                                DoAfterRefresh();
                                bRefreshing = false;
                                IsReady = true;
                            });
                        }
                        else
                        {
                            int idx = MasterData.Rows.Count;
                            MasterData.AddPage(table);
                            Dispatcher.Invoke(() =>
                            {
                                ProcessData(new(idx, table.Rows.Count));
                            });
                        }
                    }
                }
            );
        }
        else
        {
            ProcessData(null);
            DoAfterRefresh();
            bRefreshing = false;
            IsReady = true;

            Loading.BeginAnimation(Label.OpacityProperty, null);
            Loading.Visibility = Visibility.Collapsed;
        }

        if (cursor != null)
        {
            cursor.Dispose();
            cursor = null;
        }
    }

    public void Shutdown()
    {
        RefreshCancellationToken?.Cancel();
    }
    
    protected void NotifyBeforeRefresh(BeforeRefreshEventArgs args) => BeforeRefresh?.Invoke(this, args);
    
    protected void NotifyAfterRefresh(AfterRefreshEventArgs args) => AfterRefresh?.Invoke(this, args);

    protected bool OnBeforeRefresh()
    {
        return true;
    }

    private bool DoBeforeRefresh()
    {
        var result = OnBeforeRefresh();
        if (result)
        {
            var args = new BeforeRefreshEventArgs() { Cancel = false };
            NotifyBeforeRefresh(args);
            result = args.Cancel == false;
        }

        return result;
    }

    protected virtual void OnAfterRefresh()
    {
    }

    protected void DoAfterRefresh()
    {
        OnAfterRefresh();
        NotifyAfterRefresh(new AfterRefreshEventArgs());
    }

    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));
        return columns;
    }


    /// <summary>
    /// Process the data from <see cref="MasterData"/> according to <paramref name="range"/>.
    /// </summary>
    /// <remarks>
    /// Set <paramref name="range"/> to <see langword="null"/> if this is the first page of data to be loaded. This will thus update the grid accordingly,
    /// clearing all current rows, resetting columns, selection, etc. If the <paramref name="range"/> is provided, this will add to the grid the rows
    /// according to the range from <see cref="MasterData"/>.
    /// </remarks>
    /// <param name="initialLoad"></param>
    private void ProcessData(RowRange? range)
    {
        if(range is null)
        {
            Data.Columns.Clear();
            Data.Setters.Clear();
            if (MasterData != null)
                foreach (var column in MasterData.Columns)
                    Data.Columns.Add(column);
        }

        LoadData(range);
    }

    protected readonly Dictionary<CoreRow, CoreRow> _recordmap = new();

    public void UpdateRow<TRow, TType>(CoreRow row, Expression<Func<TRow, TType>> column, TType value, bool refresh = true)
    {
        row.Set(column, value);
        _recordmap[row].Set(column, value);
        if (refresh)
            InvalidateRow(row);
    }

    public void UpdateRow<TType>(CoreRow row, string column, TType value, bool refresh = true)
    {
        row.Set(column, value);
        _recordmap[row].Set(column, value);
        if (refresh)
            InvalidateRow(row);
    }

    public void UpdateRow(CoreRow row, T obj)
    {
        ObjectToRow(obj, row);
        ObjectToRow(obj, _recordmap[row]);
    }


    public void AddRow(CoreRow row)
    {
        if (MasterData is null) return;

        var masterrow = MasterData.NewRow();
        MasterData.FillRow(masterrow, row);
        Refresh(false, false);
    }

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

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

    public void DeleteRow(CoreRow row)
    {
        if (MasterData is null) return;

        var masterrow = _recordmap[row];
        MasterData.Rows.Remove(masterrow);
        Refresh(false, false);
    }

    /// <summary>
    /// Filter all given rows into <paramref name="into"/>, given that they match <paramref name="filter"/> and <see cref="FilterRecord(CoreRow)"/>.
    /// If <paramref name="recordMap"/> is given, also updates the map from <paramref name="from"/> to <paramref name="into"/>.
    /// </summary>
    private IList<CoreRow> FilterRows(
        IEnumerable<CoreRow> from,
        CoreTable into,
        Dictionary<CoreRow, CoreRow>? recordMap = null,
        Func<CoreRow, bool>? filter = null)
    {
        var newRows = new List<CoreRow>();
        foreach (var row in from)
            if (FilterRecord(row) && filter?.Invoke(row) != false)
            {
                var newrow = into.NewRow();
                for (var i = 0; i < into.Columns.Count; i++)
                {
                    var value = i < row.Values.Count ? row.Values[i] : null;
                    if (into.Columns[i].DataType.IsNumeric())
                        value = into.Columns[i].DataType.IsDefault(value) ? null : value;
                    newrow.Values.Add(value);
                }

                newRows.Add(newrow);
                into.Rows.Add(newrow);
                recordMap?.TryAdd(newrow, row);
            }
        return newRows;
    }
    
    private readonly SemaphoreSlim semaphore = new(1,1);

    private void LoadData(RowRange? range)
    {
        if (MasterData is null)
            return;
        if(range is null)
        {
            ResetClipBuffer();
            Data.Rows.Clear();
            _recordmap.Clear();

            FilterRows(MasterData.Rows, Data, _recordmap);

            InvalidateGrid();
            SelectedRows = Array.Empty<CoreRow>();
        }
        else
        {
            var _newRows = FilterRows(Enumerable.Range(range.RowIdx, range.Size).Select(i => MasterData.Rows[i]), Data, _recordmap);
            UIComponent.AddPage(_newRows);
            /*
            semaphore.Wait();
            try
            {
                // This throws errors when overalpping refreshes cause a mismatch between MasterData and range
                // Using try/finally and a Semaphore as a temporary measure until we can isolate the issue
                var _newRows = FilterRows(Enumerable.Range(range.RowIdx, range.Size).Select(i => MasterData.Rows[i]), Data, _recordmap);
                UIComponent.AddPage(_newRows);
            }
            finally
            {
                semaphore.Release();
            }*/
        }
    }

    //IncrementalList<T> _data = null;

    public void InvalidateRow(CoreRow row)
    {
        UIComponent.InvalidateRow(row);
    }

    protected void InvalidateGrid()
    {
        if (RowStyleSelector != null)
            RowStyleSelector.Data = Data;

        UIComponent.RefreshData(Data);

        Loading.BeginAnimation(Label.OpacityProperty, null);
        Loading.Visibility = Visibility.Collapsed;
    }

    public void AddVisualFilter(string column, string value, FilterType filtertype = FilterType.Contains)
    {
        UIComponent.AddVisualFilter(column, value, filtertype);
    }

    protected List<Tuple<string, Func<CoreRow, bool>>> GetFilterPredicates()
    {
        return UIComponent.GetFilterPredicates();
    }

    #endregion

    #region Item Manipulation

    #region Load/Save/Delete

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

    public abstract T LoadItem(CoreRow row);

    public abstract void SaveItem(T item);

    public virtual void SaveItems(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 virtual void DoDelete()
    {
        var rows = SelectedRows.ToArray();

        if (rows.Any())
            if (CanDeleteItems(rows))
                if (MessageBox.Show("Are you sure you wish to delete the selected records?", "Confirm Delete", MessageBoxButton.YesNo) ==
                    MessageBoxResult.Yes)
                {
                    DeleteItems(rows);
                    SelectedRows = Array.Empty<CoreRow>();
                    OnChanged?.Invoke(this, EventArgs.Empty);
                    Refresh(false, true);
                    SelectItems(null);
                }
    }

    private void Delete_Click(object sender, RoutedEventArgs e)
    {
        DoDelete();
    }

    #endregion

    #region Edit

    protected virtual void DoEdit()
    {
        if (SelectedRows.Length == 0)
            return;

        if (AddEditClick(SelectedRows))
        {
            SelectItems(SelectedRows);
        }
    }

    private void Edit_Click(object sender, RoutedEventArgs e)
    {
        DoEdit();
    }

    protected virtual void DoAdd(bool openEditorOnDirectEdit = false)
    {
        //CoreRow row = (SelectedRow > -1) && (SelectedRow < Data.Rows.Count) ?  Data.Rows[this.SelectedRow] : null;

        if (IsDirectEditMode() && !openEditorOnDirectEdit)
        {
            CreateItems(null);
        }
        else if (AddEditClick(null))
        {
            Refresh(false, true);
        }
    }

    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();
        OnChanged?.Invoke(this, EventArgs.Empty);
    }

    private void Add_Click(object sender, RoutedEventArgs e)
    {
        if (CanCreateItems())
            DoAdd();
    }

    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);
        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.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);
    }

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

    protected virtual void CustomiseEditor(T[] items, DynamicGridColumn column, BaseEditor 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);
    }


    public virtual bool EditItems(T[] items, Func<Type, CoreTable?>? PageDataHandler = null, bool PreloadPages = false)
    {
        DynamicEditorForm editor;
        using (var cursor = new WaitCursor())
        {
            editor = new DynamicEditorForm();
            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);
        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(typeof(T), 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(typeof(T), 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 ?? CoreUtils.GetProperty(item.GetType(), column.ColumnName).GetEditor();
    }

    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 virtual bool CanCreateItems()
    {
        return true;
    }

    private bool AddEditClick(CoreRow[]? rows)
    {
        if (!IsEnabled || bRefreshing)
            return false;

        if (rows == null || rows.Length == 0)
        {

            if (!CanCreateItems())
                return false;

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

            // Yea, and this won't work, because we're actually usually showing the description of a linked item,
            // not the id of the link, and we need to set the ID to have it work properly :-(

            //foreach (String key in VisualFilters.Keys)
            //    CoreUtils.SetPropertyValue(item, key, VisualFilters[key]);

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

            return false;
        }

        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 (EditItems(items))
            {
                var sel = SelectedRows;
                for (var i = 0; i < items.Length; i++)
                    UpdateRow(rows[i], items[i]);
                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);
    }

    private bool DoDuplicate(Button button, CoreRow[] rows)
    {
        if (!rows.Any())
        {
            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 virtual void DoPrint(object sender)
    {
        OnPrintData?.Invoke(sender);
    }
    protected virtual void ShowHelp(string slug)
    {
        Process.Start(new ProcessStartInfo("https://prsdigital.com.au/wiki/index.php/" + slug) { UseShellExecute = 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

    private Tuple<ClipAction, CoreRow[]>? ClipBuffer;

    protected void ResetClipBuffer()
    {
        ClipBuffer = null;
    }

    protected void SetClipBuffer(ClipAction action, CoreRow[] rows)
    {
        ClipBuffer = new Tuple<ClipAction, CoreRow[]>(action, rows);
    }

    private void CutToClipBuffer()
    {
        SetClipBuffer(ClipAction.Cut, SelectedRows);
        InvalidateGrid();
    }


    private void CopyToClipBuffer()
    {
        SetClipBuffer(ClipAction.Copy, SelectedRows);
        InvalidateGrid();
    }

    private void PasteFromClipBuffer()
    {
        if (ClipBuffer == null)
            return;

        if (!ShowSequenceButtons)
            return;

        using (new WaitCursor())
        {
            var updates = ClipBuffer.Item2.Select(x => x.ToObject<T>()).ToList();
            if (BeforePaste(updates, ClipBuffer.Item1))
            {
                var currow = SelectedRows.FirstOrDefault()
                    ?? Data.Rows.LastOrDefault();

                var sequence = currow != null ? currow.Get<T, long>(c => ((ISequenceable)c).Sequence) : 0;

                var postrows = Data.Rows.Where(r => !ClipBuffer.Item2.Contains(r) && r.Get<ISequenceable, long>(x => x.Sequence) >= sequence);
                updates.AddRange(LoadItems(postrows.ToArray()));

                foreach (var update in updates)
                {
                    sequence++;
                    ((ISequenceable)update).Sequence = sequence;
                }
            }

            if (updates.Any())
            {
                SaveItems(updates.ToArray());
                Refresh(false, true);
            }
        }
    }

    protected virtual bool BeforePaste(IEnumerable<T> items, ClipAction action)
    {
        return true;
    }
    private void Cut_Click(object sender, RoutedEventArgs e)
    {
        CutToClipBuffer();
    }

    private void Copy_Click(object sender, RoutedEventArgs e)
    {
        CopyToClipBuffer();
    }

    private void Paste_Click(object sender, RoutedEventArgs e)
    {
        PasteFromClipBuffer();
    }

    #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;
    }

    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);
    }

    private void Import_Click(object sender, RoutedEventArgs e)
    {
        DoImport();
    }

    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)
    {
    }

    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 = UIComponent.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 predicates)
        {
            reloadColumns.Add(column);
        }

        var sort = LookupFactory.DefineSort<T>();
        Reload(filters, reloadColumns, ref sort, CancellationToken.None, (data, err) => Dispatcher.Invoke(() =>
        {
            if (data is not null)
            {
                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);
            }
            else if (err is not null)
            {
                Logger.Send(LogType.Error, "", $"Error in export: {CoreUtils.FormatException(err)}");
                MessageBox.Show(err.Message);
            }
        }));
    }

    private void Export_Click(object sender, RoutedEventArgs e)
    {
        DoExport();
    }

    /// <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

    public void ScrollIntoView(CoreRow row)
    {
        UIComponent.ScrollIntoView(row);
    }

    #endregion

    #region Custom Buttons

    private Button CreateButton(BitmapImage? image = null, string? text = null, string? tooltip = null)
    {
        var button = new Button();
        button.SetValue(BorderBrushProperty, new SolidColorBrush(Colors.Gray));
        button.SetValue(BorderThicknessProperty, new Thickness(0.75));
        button.Height = 30;
        UpdateButton(button, image, text, tooltip);
        return button;
    }

    public void UpdateButton(Button button, BitmapImage? image, string? text, string? tooltip = null)
    {
        var stackPnl = new StackPanel();
        stackPnl.Orientation = Orientation.Horizontal;
        //stackPnl.Margin = new Thickness(2);

        if (image != null)
        {
            var img = new Image();
            img.Source = image;
            img.Margin = new Thickness(2);
            img.ToolTip = tooltip;
            stackPnl.Children.Add(img);
        }

        if (!string.IsNullOrEmpty(text))
        {
            button.MaxWidth = double.MaxValue;
            var lbl = new Label();
            lbl.Content = text;
            lbl.VerticalAlignment = VerticalAlignment.Stretch;
            lbl.VerticalContentAlignment = VerticalAlignment.Center;
            lbl.Margin = new Thickness(2, 0, 5, 0);
            lbl.ToolTip = ToolTip;
            stackPnl.Children.Add(lbl);
        }
        else
            button.MaxWidth = 30;

        button.Content = stackPnl;
        button.ToolTip = tooltip;
    }

    private bool bFirstButtonAdded = true;

    private bool AnyButtonsVisible()
    {
        if (Add.Visibility != Visibility.Collapsed)
            return true;
        if (Edit.Visibility != Visibility.Collapsed)
            return true;
        /*if (MultiEdit.Visibility != Visibility.Collapsed)
            return true;*/
        if (Export.Visibility != Visibility.Collapsed)
            return true;
        return false;
    }


    public Button AddButton(string? caption, BitmapImage? image, string? tooltip, DynamicGridButtonClickEvent action, DynamicGridButtonPosition position = DynamicGridButtonPosition.Left)
    {
        var button = CreateButton(image, caption, tooltip);
        button.Margin = position == DynamicGridButtonPosition.Right
            ? new Thickness(2, 2, 0, 0)
            : bFirstButtonAdded && AnyButtonsVisible()
                ? new Thickness(0, 2, 0, 0)
                : new Thickness(0, 2, 2, 0);
        button.Padding = !String.IsNullOrWhiteSpace(caption) ? new Thickness(5, 1, 5, 1) : new Thickness(1);
        button.Tag = action;
        button.Click += Button_Click;
        if (position == DynamicGridButtonPosition.Right)
            RightButtonStack.Children.Add(button);
        else
            LeftButtonStack.Children.Add(button);
        bFirstButtonAdded = false;
        return button;
    }

    public Button AddButton(string? caption, BitmapImage? image, DynamicGridButtonClickEvent action, DynamicGridButtonPosition position = DynamicGridButtonPosition.Left)
    {
        var result = AddButton(caption, image, null, action, position);
        return result;
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        var button = (Button)sender;
        var action = (DynamicGridButtonClickEvent)button.Tag;

        //CoreRow row = (CurrentRow > -1) && (CurrentRow < Data.Rows.Count) ? Data.Rows[this.CurrentRow] : null;
        if (action.Invoke(button, SelectedRows))
            Refresh(false, true);
    }

    #endregion

    #region Header Actions

    private void SelectColumnsClick()
    {
        var editor = new DynamicGridColumnsEditor(typeof(T));
        editor.DirectEdit = IsDirectEditMode();

        editor.Columns.AddRange(VisibleColumns);

        if (editor.ShowDialog().Equals(true))
        {
            VisibleColumns.Clear();
            VisibleColumns.AddRange(editor.Columns);
            SaveColumns(editor.Columns);
            //OnSaveColumns?.Invoke(this, editor.Columns);
            Refresh(true, true);
        }
    }

    #endregion

    #region Drag + Drop

    /// <summary>
    /// Handle a number of rows from a different <see cref="DynamicGrid{T}"/> being dragged into this one.
    /// </summary>
    /// <param name="entity">The type of entity that that the rows of <paramref name="table"/> represent.</param>
    /// <param name="table">The data being dragged.</param>
    /// <param name="e"></param>
    protected virtual void OnDragEnd(Type entity, CoreTable table, DragEventArgs e)
    {
        Logger.Send(LogType.Information,"","OnDragEnd");
    }

    /// <summary>
    /// Handle all types of items being dragged onto this grid that aren't handled by <see cref="OnDragEnd(Type, CoreTable, DragEventArgs)"/>,
    /// i.e., data which is not a <see cref="CoreTable"/> from another <see cref="DynamicGrid{T}"/>
    /// </summary>
    /// <remarks>
    /// Can be used to handle files, for example.
    /// </remarks>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    protected virtual void HandleDragDrop(object sender, DragEventArgs e)
    {
    }

    protected virtual void HandleDragOver(object sender, DragEventArgs e)
    {
    }

    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 virtual 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;
}

public class GridSelectionControllerExt : GridSelectionController
{
    public GridSelectionControllerExt(SfDataGrid datagrid)
        : base(datagrid)
    {
    }

    protected override void ProcessSelectedItemChanged(SelectionPropertyChangedHandlerArgs handle)
    {
        base.ProcessSelectedItemChanged(handle);
        if (handle.NewValue != null)
        {
            //this.DataGrid.ScrollInView(this.CurrentCellManager.CurrentRowColumnIndex);
            //int rowIndex = this.CurrentCellManager.CurrentRowColumnIndex.RowIndex;
            var columnIndex = CurrentCellManager.CurrentRowColumnIndex.ColumnIndex;
            var scrollRowIndex = DataGrid.GetVisualContainer().ScrollRows.LastBodyVisibleLineIndex;
            DataGrid.ScrollInView(new RowColumnIndex(scrollRowIndex, columnIndex));
        }
    }
}

// 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); set => entity = value.EntityName(); }

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