/*
<copyright file="BGDBUiToolkitBindersTable.cs" company="BansheeGz">
    Copyright (c) 2018-2021 All Rights Reserved
</copyright>
*/

using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;


namespace BansheeGz.BGDatabase.Editor
{
    public class BGDBUiToolkitBindersTable : BGTableView<BGDBUiToolkitBinderTableAdapter>
    {
        private readonly List<BGDBUiToolkitBinderTableAdapter> reusableList = new List<BGDBUiToolkitBinderTableAdapter>();

        private static float indexWidth = 30;
        private static float nameWidth = 120;
        private static float typeWidth = 100;
        private static float elementFieldWidth = 100;
        private static float binderTypeWidth = 80;
        private static float metaWidth = 80;
        private static float entityWidth = 80;
        private static float fieldWidth = 80;
        private static float templateWidth = 80;
        private static float graphWidth = 40;
        private static float resultWidth = 80;
        private static float liveWidth = 80;

        private readonly BGDBUiToolkitBindersWindow editorWindow;
        private string newBinderName;
        private readonly BGPager<BGDBUiToolkitBinderTableAdapter> pager = new BGPager<BGDBUiToolkitBinderTableAdapter>();

        public BGDBUiToolkitBindersTable(BGDBUiToolkitBindersWindow editorWindow) : base("Binders", null)
        {
            this.editorWindow = editorWindow;
            var toolkit = editorWindow.Toolkit;
            var doc = toolkit.Doc;
            UseScrollView = true;
            Provider = () =>
            {
                reusableList.Clear();
                var binders = toolkit.Binders;
                if (binders != null)
                {
                    for (var i = 0; i < binders.Count; i++) reusableList.Add(new BGDBUiToolkitBinderTableAdapter(binders[i], i));
                    return pager.Filter(reusableList);
                }

                return reusableList;
            };

            OnAdd = () =>
            {
                var menu = new GenericMenu();
                var mousePosition = Event.current.mousePosition;
                foreach (var factory in BGDBUiElementBinderA.EnabledFactories)
                {
                    menu.AddItem(new GUIContent(factory.BinderName + " binder"), false, () => AddBinder(toolkit, factory, mousePosition));
                }

                menu.ShowAsContext();
            };

            Add(new ColumnInt("#", adapter => adapter.Index, null)
            {
                GetWidth = () => indexWidth,
                SetWidth = w => SetWidth(ref indexWidth, w),
            });
            Add(new ColumnWithButton("Unique name", () => BGStyle.CellButton, adapter => adapter.Binder.ElementName, adapter =>
            {
            })
            {
                GetWidth = () => nameWidth,
                SetWidth = w => SetWidth(ref nameWidth, w),
                Gui = (adapter, context) =>
                {
                    var newName = EditorGUILayout.TextField("", adapter.Binder.ElementName, BGStyle.CellLB,
                        BGEditorUtility.GetOptions(context.Width - BGEditorUtility.MinRowHeight, context.Height));
                    if (newName != adapter.Binder.ElementName)
                    {
                        Undo.RecordObject(toolkit, "Changing binder name");
                        adapter.Binder.ElementName = newName;
                    }

                    if (!GUILayout.Button("", BGStyle.Up, BGEditorUtility.OptionsMinRect)) return;
                    BGDBUiToolkitElementsSearcher.Open(editorWindow, element =>
                    {
                        if (!IsElementValid(element, toolkit, adapter.Binder)) return;

                        Undo.RecordObject(toolkit, "change binder name");
                        adapter.Binder.ElementName = element.name;
                        BGDBUiElementBinderA.AssignDefaultField(element, adapter.Binder);

                        editorWindow.Repaint();
                    });
                }
            });
            Add(new Column("Element type")
            {
                GetWidth = () => typeWidth,
                SetWidth = w => SetWidth(ref typeWidth, w),
                Gui = (adapter, context) =>
                {
                    using (BGUsingEditor.DisableGUI(true))
                    {
                        var element = adapter.Binder.GetElement(doc.rootVisualElement);
                        string message;
                        if (element != null) message = element.GetType().Name;
                        else
                        {
                            message = string.IsNullOrEmpty(adapter.Binder.ElementName)
                                ? "Element name is not set"
                                : "Element is not found";
                        }

                        GUILayout.Label(message, element == null ? new GUIStyle(BGStyle.CellLB) { normal = { textColor = Color.red } } : BGStyle.CellLB, context.Options);
                    }
                }
            });

            Add(new Column("Element field")
            {
                GetWidth = () => elementFieldWidth,
                SetWidth = w => SetWidth(ref elementFieldWidth, w),
                Gui = (adapter, context) =>
                {
                    ShowElementFieldButton(toolkit, adapter.Binder, context.Options);
                }
            });
            Add(new ColumnBool("Live", adapter => adapter.Binder.LiveUpdate, (adapter, b) =>
            {
                Undo.RecordObject(toolkit, "Changing binder's live parameter");
                adapter.Binder.LiveUpdate = b;
            })
            {
                GetWidth = () => liveWidth,
                SetWidth = w => SetWidth(ref liveWidth, w),
            });

            Add(new Column("Binder type")
            {
                GetWidth = () => binderTypeWidth,
                SetWidth = w => SetWidth(ref binderTypeWidth, w),
                Gui = (adapter, context) =>
                {
                    var style = new GUIStyle(BGStyle.CellLB)
                    {
                        normal =
                        {
                            textColor = (BGStyle.GuiSkin == BGGuiSkin.Light
                                    ? new Color(0f, 0.05f, 1f)
                                    : new Color(0.64f, 0.75f, 1f)
                                )
                        }
                    };
                    GUILayout.Label(adapter.Binder.Title, style, context.Options);
                }
            });

            Add(new Column("Meta")
            {
                GetWidth = () => metaWidth,
                SetWidth = w => SetWidth(ref metaWidth, w),
                Gui = (adapter, context) =>
                {
                    var supported = adapter.Binder is BGDBUiElementBinderRowBasedA;
                    using (BGUsingEditor.DisableGUI(!supported))
                    {
                        if (!supported) GUILayout.Label("N/A", BGStyle.CellNoValueLB, context.Options);
                        else
                        {
                            var binder = adapter.Binder as BGDBUiElementBinderRowBasedA;
                            ShowMetaButton(toolkit, binder, context.Options);
                        }
                    }
                }
            });

            Add(new Column("Field")
            {
                GetWidth = () => fieldWidth,
                SetWidth = w => SetWidth(ref fieldWidth, w),
                Gui = (adapter, context) =>
                {
                    var supported = adapter.Binder is BGDBUiElementBinderField;
                    using (BGUsingEditor.DisableGUI(!supported))
                    {
                        if (!supported) GUILayout.Label("N/A", BGStyle.CellNoValueLB, context.Options);
                        else
                        {
                            var binder = adapter.Binder as BGDBUiElementBinderField;
                            ShowFieldButton(toolkit, binder, context.Options);
                        }
                    }
                }
            });

            Add(new Column("Entity")
            {
                GetWidth = () => entityWidth,
                SetWidth = w => SetWidth(ref entityWidth, w),
                Gui = (adapter, context) =>
                {
                    var supported = adapter.Binder is BGDBUiElementBinderRowBasedA;
                    using (BGUsingEditor.DisableGUI(!supported))
                    {
                        if (!supported) GUILayout.Label("N/A", BGStyle.CellNoValueLB, context.Options);
                        else
                        {
                            var binder = adapter.Binder as BGDBUiElementBinderRowBasedA;
                            ShowEntityButton(binder, toolkit, context.Options);
                        }
                    }
                }
            });

            Add(new Column("Template")
            {
                GetWidth = () => templateWidth,
                SetWidth = w => SetWidth(ref templateWidth, w),
                Gui = (adapter, context) =>
                {
                    var supported = adapter.Binder is BGDBUiElementBinderTemplate;
                    using (BGUsingEditor.DisableGUI(!supported))
                    {
                        if (!supported) GUILayout.Label("N/A", BGStyle.CellNoValueLB, context.Options);
                        else
                        {
                            var binder = adapter.Binder as BGDBUiElementBinderTemplate;
                            if (!GUILayout.Button(binder.Template, BGStyle.CellButtonLB, context.Options)) return;

                            BGPopup.Popup("Template", 400, 400, popup =>
                            {
                                var newTemplate = EditorGUILayout.TextArea(binder.Template, BGStyle.Editor_textArea, GUILayout.MinHeight(44));
                                if (string.Equals(newTemplate, binder.Template)) return;

                                Undo.RecordObject(toolkit, "Changing binder's field");
                                binder.Template = newTemplate;
                            });
                        }
                    }
                }
            });
            Add(new Column("Graph")
            {
                GetWidth = () => graphWidth,
                SetWidth = w => SetWidth(ref graphWidth, w),
                Gui = (adapter, context) =>
                {
                    var supported = adapter.Binder is BGDBUiElementBinderGraph;
                    using (BGUsingEditor.DisableGUI(!supported))
                    {
                        if (!supported) GUILayout.Label("N/A", BGStyle.CellNoValueLB, context.Options);
                        else
                        {
                            var binder = adapter.Binder as BGDBUiElementBinderGraph;
                            if (!GUILayout.Button("Edit (" + binder.Graph.UnitsCount + ")", BGStyle.CellButtonLB, context.Options)) return;

                            var graph = binder.Graph;
                            graph.ClearOnAnyChange();
                            graph.OnAnyChange += () =>
                            {
                                if (toolkit == null) return;
                                Undo.RecordObject(toolkit, "Changing binder graph");
                                binder.graphContent = Convert.ToBase64String(graph.ToBytes());
                            };
                            BGCalcWindow.OpenWindow(new BGCalcGraphViewContext(() => toolkit==null?null:graph, binder.TypeCode, BGCalcGraphTypeEnum.GraphBinder));
                        }
                    }
                }
            });

            Add(new Column("Value")
            {
                GetWidth = () => resultWidth,
                SetWidth = w => SetWidth(ref resultWidth, w),
                Gui = (adapter, context) =>
                {
                    ShowResult(adapter.Binder, context.Options);
                }
            });

            Add(new ColumnWithButton("D", () => BGStyle.ButtonDelete, adapter => "", adapter =>
            {
                toolkit.Remove(adapter.Binder);
                GUIUtility.ExitGUI();
            })
            {
                GetWidth = () => BGEditorUtility.MinRowHeight
            });
        }

        protected override void AdditionalHeader()
        {
            BGEditorUtility.Horizontal(new GUIStyle(BGStyle.TableBack) { padding = new RectOffset(0, 0, 1, 1) }, () =>
            {
                newBinderName = GUILayout.TextField(newBinderName, BGStyle.Editor_textField, GUILayout.Width(100));
                if (BGEditorUtility.Button("Add binder", 80)) AddBinder();

                pager.Gui();

                var error = editorWindow.Toolkit.Error;
                if (error != null) GUILayout.Label(error, BGStyle.Attention);
                GUILayout.FlexibleSpace();
            });
        }

        private void AddBinder()
        {
            BGEditorUtility.ExitIf(string.IsNullOrEmpty(newBinderName), "Element name can not be null! Paste it to the text field on the left or use 'plus' icon to select from the tree");
            var toolkit = editorWindow.Toolkit;
            var visualElement = toolkit.Doc.rootVisualElement.Q(newBinderName);
            BGEditorUtility.ExitIf(visualElement == null, $"Element with name {newBinderName} can not be found!");
            AddBuilder(toolkit, visualElement);
        }

        public static void AddBuilder(BGDataBinderUiToolkitGo toolkit, VisualElement visualElement)
        {
            var doc = toolkit.Doc;
            BGEditorUtility.ExitIf(doc.visualTreeAsset == null, "VisualTreeAsset field is not set on UIDocument!");
            var menu = new GenericMenu();
            foreach (var factory in BGDBUiElementBinderA.EnabledFactories) menu.AddItem(new GUIContent(factory.BinderName + " binder"), false, () => AddBuilder(factory, toolkit, visualElement));
            menu.ShowAsContext();
        }

        private static void AddBuilder(BGDBUiElementBinderA.Factory factory, BGDataBinderUiToolkitGo toolkit, VisualElement visualElement)
        {
            Undo.RecordObject(toolkit, "Add a binder");

            var binder = factory.Create();
            binder.ElementName = visualElement.name;

            BGDBUiElementBinderA.AssignDefaultField(visualElement, binder);
            toolkit.Add(binder);
        }

        public static void ShowElementFieldButton(BGDataBinderUiToolkitGo toolkit, BGDBUiElementBinderA binder, GUILayoutOption[] options)
        {
            var doc = toolkit.Doc;
            var fieldPath = binder.ElementFieldPath;
            var element = binder.GetElement(doc.rootVisualElement);
            string message;
            if (element == null) message = "[Element not found]";
            else
            {
                if (string.IsNullOrEmpty(fieldPath)) message = "[Field is not set]";
                else
                {
                    var memberInfo = binder.GetElementField(doc.rootVisualElement);
                    if (memberInfo != null) message = fieldPath;
                    else message = fieldPath + " [not found]";
                }
            }

            using (BGUsingEditor.DisableGUI(element == null))
            {
                if (!GUILayout.Button(message, message == fieldPath ? BGStyle.CellButtonLB : BGStyle.CellButtonInvalidLB, options)) return;
            }

            var menu = new GenericMenu();
            menu.AddItem(new GUIContent("[Not set]"), string.IsNullOrEmpty(fieldPath), () => SetPath(toolkit, binder, null, false));
            AddToMenu(toolkit, binder, menu, element.GetType().GetFields(), null);
            AddToMenu(toolkit, binder, menu, element.GetType().GetProperties(), info => ((PropertyInfo)info).GetSetMethod() != null || ((PropertyInfo)info).GetSetMethod(true) != null);
            menu.ShowAsContext();
        }

        public static void ShowFieldButton(BGDataBinderUiToolkitGo toolkit, BGDBUiElementBinderField binder, GUILayoutOption[] options)
        {
            var doc = toolkit.Doc;
            string message;
            GUIStyle style;
            if (doc.visualTreeAsset == null)
            {
                message = "[Visual asset not set]";
                style = BGStyle.CellButtonNoValueLB;
            }
            else if (binder.Meta == null)
            {
                message = "[Meta not set]";
                style = BGStyle.CellButtonNoValueLB;
            }
            else
            {
                if (binder.Field == null)
                {
                    message = "[Field not set]";
                    style = BGStyle.CellButtonNoValueLB;
                }
                else
                {
                    if (!binder.IsValueCompatible(doc.rootVisualElement))
                    {
                        message = binder.Field.Name + " [Not compatible]";
                        style = BGStyle.CellButtonInvalidLB;
                    }
                    else
                    {
                        message = binder.Field.Name;
                        style = BGStyle.CellButtonLB;
                    }
                }
            }

            using (BGUsingEditor.DisableGUI(binder.Meta == null || doc.visualTreeAsset == null))
            {
                if (!GUILayout.Button(message, style, options)) return;
            }

            var fields = new List<BGField>();
            Predicate<BGField> filter = null;
            MemberInfo targetField = null;
            if (!string.IsNullOrEmpty(binder.ElementFieldPath))
            {
                targetField = binder.GetElementField(doc.rootVisualElement);
                if (targetField != null)
                {
                    var fieldType = GetFieldType(targetField);
                    filter = f => fieldType.IsAssignableFrom(f.ValueType);
                }
            }

            binder.Meta.FindFields(fields, filter);
            if (fields.Count == 0)
            {
                BGEditorUtility.ShowInfo("No compatible fields" + (filter == null ? "!" : (" with value type=" + GetFieldType(targetField).Name)), null);
                return;
            }

            var menu = new GenericMenu();
            menu.AddItem(new GUIContent("[Not set]"), binder.Field == null, () =>
            {
                Undo.RecordObject(toolkit, "Changing binder's field");
                binder.Field = null;
            });
            foreach (var field in fields)
            {
                menu.AddItem(new GUIContent(field.Name), Equals(binder.Field, field), () =>
                {
                    Undo.RecordObject(toolkit, "Changing binder's field");
                    binder.Field = field;
                });
            }


            menu.ShowAsContext();
        }

        public static void ShowMetaButton(BGDataBinderUiToolkitGo toolkit, BGDBUiElementBinderRowBasedA binder, GUILayoutOption[] options)
        {
            if (!GUILayout.Button(binder.Meta == null ? "[Not set]" : binder.Meta.Name, binder.Meta == null ? BGStyle.CellButtonNoValueLB : BGStyle.CellButtonLB, options)) return;

            var tree = new BGTreeViewMeta(new BGTreeViewMeta.TreeConfig()
            {
                metaFilter = binder.IsMetaSupported,
                IncludeFields = false
            });

            BGPopup.Popup("Select meta", 450, 400, popup => tree.Gui(), popup =>
            {
                tree.OnSelect = m =>
                {
                    var newMeta = (BGMetaEntity)m;
                    if (Equals(newMeta, binder.Meta)) return;
                    Undo.RecordObject(toolkit, "Changing binder's meta");
                    binder.Meta = newMeta;
                    popup.Close();
                };
            });
        }

        public static void ShowEntityButton(BGDBUiElementBinderRowBasedA binder, BGDataBinderUiToolkitGo toolkit, GUILayoutOption[] options)
        {
            string message;
            GUIStyle style;
            if (binder.Meta == null)
            {
                message = "[Meta not set]";
                style = BGStyle.CellButtonNoValueLB;
            }
            else
            {
                if (binder.Entity == null)
                {
                    message = "[Entity not set]";
                    style = BGStyle.CellButtonNoValueLB;
                }
                else
                {
                    message = binder.Entity.Name;
                    style = BGStyle.CellButtonLB;
                }
            }

            using (BGUsingEditor.DisableGUI(binder.Meta == null))
            {
                var pressed = GUILayout.Button(message, style, options);

                GUI.enabled = true;
                BGEntityDragNDrop.DragArea(GUILayoutUtility.GetLastRect(), newEntity =>
                {
                    if (!binder.IsMetaSupported(newEntity.Meta))
                    {
                        BGEditorUtility.ShowInfo("This entity is not supported by this binder", null);
                        return;
                    }
                    Undo.RecordObject(toolkit, "Changing binder's entity");
                    binder.Entity = newEntity;
                });

                if (!pressed) return;
            }


            var manager = BGMetaManagerA.GetManager(binder.Meta.GetType());
            manager.ChoseObject(binder.Meta, newEntity =>
            {
                Undo.RecordObject(toolkit, "Changing binder's entity");
                binder.Entity = newEntity;
            });
        }


        private static bool IsElementValid(VisualElement element, BGDataBinderUiToolkitGo toolkit, BGDBUiElementBinderA binder)
        {
            if (string.IsNullOrEmpty(element.name))
            {
                BGEditorUtility.ShowInfo("Can not assign a binder to the element without a name!", null);
                return false;
            }

            var existingBinders = toolkit.GetBinders(element.name);
            if (existingBinders != null)
            {
                var has = false;
                foreach (var b in existingBinders)
                {
                    if (b == binder) continue;
                    has = true;
                    break;
                }

                if (has) return BGEditorUtility.Confirm("This element already has a binder! Add a new binder to this element?");
            }

            return true;
        }

        private static Type GetFieldType(MemberInfo targetField) => targetField is PropertyInfo info ? info.PropertyType : ((FieldInfo)targetField).FieldType;

        private static void AddToMenu(BGDataBinderUiToolkitGo toolkit, BGDBUiElementBinderA binder, GenericMenu menu, MemberInfo[] fields, Predicate<MemberInfo> filter)
        {
            if (fields == null || fields.Length == 0) return;
            foreach (var field in fields)
            {
                if (filter != null && !filter(field)) continue;
                menu.AddItem(new GUIContent(field.Name), field.Name == binder.ElementFieldPath, () =>
                {
                    SetPath(toolkit, binder, field.Name, field is PropertyInfo);
                    binder.IsProperty = field is PropertyInfo;
                });
            }
        }

        private static void SetPath(BGDataBinderUiToolkitGo toolkit, BGDBUiElementBinderA binder, string path, bool isProperty)
        {
            Undo.RecordObject(toolkit, "Changing binder's path");
            binder.ElementFieldPath = path;
            binder.IsProperty = isProperty;
        }

        private void SetWidth(ref float param, float w)
        {
            param = w;
            editorWindow.Repaint();
        }

        private void AddBinder(BGDataBinderUiToolkitGo toolkit, BGDBUiElementBinderA.Factory factory, Vector2 offset)
        {
            BGDBUiToolkitElementsSearcher.Open(editorWindow, element =>
            {
                var binder = factory.Create();
                if (!IsElementValid(element, toolkit, binder)) return;
                binder.ElementName = element.name;
                BGDBUiElementBinderA.AssignDefaultField(element, binder);
                Undo.RecordObject(toolkit, "Adding a binder");
                BGEditorUtility.CatchException(() => toolkit.Add(binder));
            }, offset);
        }

        public static void ShowResult(BGDBUiElementBinderA binder, GUILayoutOption[] options = null)
        {
            string value;
            GUIStyle style;
            try
            {
                var binderValue = binder.Value;
                value = binderValue == null ? "" : binderValue.ToString();
                style = BGStyle.Cell;
            }
            catch (Exception e)
            {
                value = "ERROR:" + e.Message;
                style = new GUIStyle(BGStyle.Cell)
                {
                    normal =
                    {
                        textColor = BGStyle.GuiSkin == BGGuiSkin.Light
                            ? new Color(0.63f, 0.01f, 0f)
                            : new Color(1f, 0.54f, 0.15f)
                    }
                };
            }

            GUILayout.Label(new GUIContent(value, value), style, options);
        }
    }
}