using InABox.Clients;
using InABox.Core;
using InABox.WPF;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Media.Imaging;

namespace InABox.DynamicGrid;

public abstract class DynamicManyToManyCrossTab<TManyToMany, TRow, TColumn> : DynamicGrid<TRow>
    where TManyToMany : Entity, IRemotable, IPersistent, new()
    where TRow : Entity, IRemotable, IPersistent, new()
    where TColumn : Entity, IRemotable, IPersistent, new()
{
    private static readonly BitmapImage tick = Wpf.Resources.tick.AsBitmapImage();

    /// <summary>
    /// Property on <typeparamref name="TManyToMany"/> which is an <see cref="EntityLink{T}"/> for <typeparamref name="TRow"/>.
    /// </summary>
    private readonly PropertyInfo rowProperty;
    /// <summary>
    /// Property on <typeparamref name="TManyToMany"/> which is an <see cref="EntityLink{T}"/> for <typeparamref name="TColumn"/>.
    /// </summary>
    private readonly PropertyInfo columnProperty;

    private CoreTable? ColumnData;
    /// <summary>
    /// {ColumnID : { RowID }}
    /// </summary>
    private Dictionary<Guid, Dictionary<Guid, TManyToMany>>? ManyToManyData;

    public DynamicManyToManyCrossTab()
    {
        rowProperty = CoreUtils.GetManyToManyThisProperty(typeof(TManyToMany), typeof(TRow));
        columnProperty = CoreUtils.GetManyToManyThisProperty(typeof(TManyToMany), typeof(TColumn));

        HeaderHeight = 125;
    }

    protected override void DoReconfigure(DynamicGridOptions options)
    {
        options.Clear();
    }

    /// <summary>
    /// Load the required columns for <typeparamref name="TRow"/>.
    /// </summary>
    protected abstract DynamicGridColumns LoadRowColumns();

    protected abstract Columns<TColumn>? LoadColumnColumns();

    protected abstract SortOrder<TColumn>? LoadColumnSort();

    protected abstract string FormatColumnHeader(CoreRow row);

    protected virtual Filter<TRow>? RowFilter() => null;

    protected virtual Filter<TColumn>? ColumnFilter() => null;

    protected override DynamicGridColumns LoadColumns()
    {
        var columns = LoadRowColumns();

        var client = Client.Create(typeof(TColumn));

        var columnColumns = Columns.None<TColumn>().Add(x => x.ID);
        if(LoadColumnColumns() is Columns<TColumn> extra)
        {
            foreach(var col in extra)
            {
                columnColumns.Add(col);
            }
        }

        ColumnData = Client.Query(ColumnFilter(), columnColumns, LoadColumnSort());
        ActionColumns.Clear();
        foreach(var columnRow in ColumnData.Rows)
        {
            var colID = columnRow.Get<TColumn, Guid>(x => x.ID);
            ActionColumns.Add(new DynamicImageColumn(
                (row) =>
                {
                    if (row is null || ManyToManyData is null) return null;
                    if(ManyToManyData.TryGetValue(colID, out var rowSet))
                    {
                        var rowID = row.Get<TRow, Guid>(x => x.ID);
                        if (rowSet.ContainsKey(rowID))
                        {
                            return tick;
                        }
                    }
                    return null;
                },
                (row) =>
                {
                    if (row is null) return false;
                    var rowID = row.Get<TRow, Guid>(x => x.ID);
                    return CellClick(colID, rowID);
                }
            )
            {
                HeaderText = FormatColumnHeader(columnRow),
                ContextMenu = (rows) =>
                {
                    var row = rows?.FirstOrDefault();
                    if (row is null) return null;
                    var rowID = row.Get<TRow, Guid>(x => x.ID);
                    return CellMenu(colID, rowID);
                }
            });
        }

        return columns;
    }

    private ContextMenu? CellMenu(Guid colID, Guid rowID)
    {
        if (ManyToManyData is null) return null;

        var menu = new ContextMenu();

        if (ManyToManyData.TryGetValue(colID, out var rowSet)
            && rowSet.TryGetValue(rowID, out var obj))
        {
            if (Security.CanEdit<TManyToMany>() && CanEditCell(obj))
            {
                menu.AddItem("Edit Item", Wpf.Resources.pencil, obj, (obj) =>
                {
                    if (EditCell(obj))
                    {
                        Refresh(false, true);
                    }
                });
            }
            if (Security.CanDelete<TManyToMany>())
            {
                menu.AddItem("Delete Item", Wpf.Resources.delete, obj, DeleteCell);
            }
        }
        else
        {
            if (Security.CanEdit<TManyToMany>())
            {
                menu.AddItem("Create Item", Wpf.Resources.add, (colID, rowID), CreateCell);
            }
        }

        if(menu.Items.Count > 0)
        {
            return menu;
        }
        else
        {
            return null;
        }
    }

    private void CreateCell((Guid colID, Guid rowID) obj)
    {
        var manyToMany = CreateManyToMany(obj.rowID, obj.colID);
        if (SaveManyToMany(manyToMany))
        {
            Refresh(false, true);
        }
    }

    private void DeleteCell(TManyToMany obj)
    {
        if (DeleteManyToMany(obj))
        {
            Refresh(false, true);
        }
    }

    protected virtual bool CanEditCell(TManyToMany obj) => false;

    /// <summary>
    /// Code to edit a <typeparamref name="TManyToMany"/> cell; note that this <b>must not</b> allow for changing either
    /// the <typeparamref name="TColumn"/> or the <typeparamref name="TRow"/>, otherwise we would easily get duplicate <typeparamref name="TManyToMany"/>s.
    /// </summary>
    /// <remarks>
    /// This method should also save the object.
    /// </remarks>
    /// <param name="obj"></param>
    protected virtual bool EditCell(TManyToMany obj)
    {
        return false;
    }

    private bool CellClick(Guid columnID, Guid rowID)
    {
        if (ManyToManyData is null) return false;
        if (ManyToManyData.TryGetValue(columnID, out var rowSet)
            && rowSet.TryGetValue(rowID, out var obj))
        {
            if (Security.CanDelete<TManyToMany>())
            {
                return DeleteManyToMany(obj);
            }
            else
            {
                return false;
            }
        }
        else
        {
            if (Security.CanEdit<TManyToMany>())
            {
                obj = CreateManyToMany(rowID, columnID);
                return SaveManyToMany(obj);
            }
            else
            {
                return false;
            }
        }
    }

    protected virtual TManyToMany CreateManyToMany(Guid rowID, Guid columnID)
    {
        var item = new TManyToMany();
        (rowProperty.GetValue(item) as IEntityLink)!.ID = rowID;
        (columnProperty.GetValue(item) as IEntityLink)!.ID = columnID;
        return item;
    }

    protected virtual bool SaveManyToMany(TManyToMany obj)
    {
        Client.Save(obj, "Edited by user");
        return true;
    }

    protected virtual bool DeleteManyToMany(TManyToMany obj)
    {
        Client.Delete(obj, "Deleted by user");
        return true;
    }

    protected override void Reload(
        Filters<TRow> criteria, Columns<TRow> columns, ref SortOrder<TRow>? sort, 
        CancellationToken token, Action<CoreTable?, Exception?> action)
    {
        var filter = criteria.Add(RowFilter()).Combine();

        var manyToManyColumns = Columns.None<TManyToMany>().Add(x => x.ID);
        manyToManyColumns.Add(rowProperty.Name + ".ID").Add(columnProperty.Name + ".ID");

        Client.QueryMultiple(
            (results, e) =>
            {
                if(e is not null)
                {
                    action(null, e);
                }
                else
                {
                    var manyToManyTable = results!.Get<TManyToMany>();

                    ManyToManyData = new Dictionary<Guid, Dictionary<Guid, TManyToMany>>();
                    foreach(var row in manyToManyTable.Rows)
                    {
                        var obj = row.ToObject<TManyToMany>();
                        var rowID = (rowProperty.GetValue(obj) as IEntityLink)!.ID;
                        var colID = (columnProperty.GetValue(obj) as IEntityLink)!.ID;
                        var rowSet = ManyToManyData.GetValueOrAdd(colID);
                        rowSet[rowID] = obj;
                    }

                    action(results!.Get<TRow>(), null);
                }
            },
            new KeyedQueryDef<TRow>(filter, columns, sort),
            new KeyedQueryDef<TManyToMany>(
                new Filter<TManyToMany>(rowProperty.Name + ".ID").InQuery(filter, x => x.ID)
                    .And(columnProperty.Name + ".ID").InQuery(ColumnFilter(), x => x.ID),
                manyToManyColumns));
    }

    public override void SaveItem(TRow item)
    {
        // Never should get called.
    }

    public override void DeleteItems(params CoreRow[] rows)
    {
        // Never should get called.
    }

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