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

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

namespace BansheeGz.BGDatabase
{
    [Serializable]
    public abstract class BGDBUiElementBinderA
    {
        //=========================================== Static
        private static readonly List<Factory> factories = new List<Factory>();

        private static readonly Dictionary<Type, Tuple<string, bool>> ElementType2FieldPath = new Dictionary<Type, Tuple<string, bool>>()
        {
            { typeof(Label), new Tuple<string, bool>("text", true) },
            { typeof(Button), new Tuple<string, bool>("text", true) },
            { typeof(Toggle), new Tuple<string, bool>("label", true) },
            // { typeof(Scroller), new Tuple<string, bool>("value", true) }, //<- does not seem to make sense 
            { typeof(TextField), new Tuple<string, bool>("label", true) },
            { typeof(Foldout), new Tuple<string, bool>("text", true) },
            { typeof(Slider), new Tuple<string, bool>("label", true) },
            { typeof(SliderInt), new Tuple<string, bool>("label", true) },
            { typeof(RadioButton), new Tuple<string, bool>("label", true) },
            { typeof(ProgressBar), new Tuple<string, bool>("title", true) },
        };

        public static List<Factory> Factories
        {
            get
            {
                if (factories.Count > 0) return factories;
                var subtypes = BGUtil.GetAllSubTypes(typeof(BGDBUiElementBinderA));
                foreach (var type in subtypes)
                {
                    if (type.GetCustomAttribute(typeof(IgnoreBinderAttribute)) != null) continue;
                    factories.Add(new Factory(type));
                }

                var providers = BGUtil.GetAllImplementations(typeof(FactoryProvider));
                foreach (var provider in providers)
                {
                    try
                    {
                        var providerInstance = (FactoryProvider)Activator.CreateInstance(provider);
                        var providerFactories = providerInstance.Factories;
                        if (providerFactories != null && providerFactories.Count > 0)
                        {
                            foreach (var factory in providerFactories)
                            {
                                factories.Add(factory);
                            }
                        }
                    }
                    catch
                    {
                        //ignore
                    }
                }

                return factories;
            }
        }

        public static List<Factory> EnabledFactories
        {
            get
            {
                var list = new List<Factory>(Factories);
                for (var i = list.Count - 1; i >= 0; i--)
                {
                    var factory = list[i];
                    if (!factory.Enabled) list.RemoveAt(i);
                }

                return list;
            }
        }

        public class IgnoreBinderAttribute : Attribute
        {
        }

        public class Factory
        {
            private readonly Type binderType;
            private readonly string binderName;
            private BGDBUiElementBinderA instance;

            public virtual string BinderName => Instance.Title;

            public bool Enabled => Instance.Enabled;

            public BGDBUiElementBinderA Instance
            {
                get
                {
                    if (instance != null) return instance;
                    instance = Create();
                    return instance;
                }
            }

            internal Factory(Type binderType) => this.binderType = binderType;

            public virtual BGDBUiElementBinderA Create() => (BGDBUiElementBinderA)Activator.CreateInstance(binderType);

            public override string ToString() => BinderName;
        }

        public interface FactoryProvider
        {
            List<Factory> Factories { get; }
        }


        //=========================================== Serialized
        [SerializeField] private string elementName;
        [SerializeField] private string elementFieldPath;
        [SerializeField] private bool isProperty;
        [SerializeField] private bool liveUpdate;

        //=========================================== NON Serialized
        public event Action<BGDBUiElementBinderA, string> OnNameChange;

        public abstract object Value { get; }
        public abstract Type ValueType { get; }
        public virtual bool Enabled => true;
        public abstract string Title { get; }

        private MemberInfo memberInfo;

        public string ElementName
        {
            get => elementName;
            set
            {
                if (elementName == value) return;
                var oldValue = elementName;
                elementName = value;
                memberInfo = null;
                OnNameChange?.Invoke(this, oldValue);
            }
        }

        public string ElementFieldPath
        {
            get => elementFieldPath;
            set
            {
                if (string.Equals(elementFieldPath, value)) return;
                elementFieldPath = value;
                memberInfo = null;
            }
        }

        public bool IsProperty
        {
            get => isProperty;
            set
            {
                if (isProperty == value) return;
                isProperty = value;
                memberInfo = null;
            }
        }

        public bool LiveUpdate
        {
            get => liveUpdate;
            set
            {
                if (liveUpdate == value) return;
                liveUpdate = value;
            }
        }

        //=========================================== Methods
        public virtual string GetError(UIDocument doc)
        {
            try
            {
                var val = Value;
                var targetElement = GetElement(doc);
                BindViaReflection(targetElement, val, true);
            }
            catch (Exception e)
            {
                return e.Message;
            }

            return null;
        }

        public VisualElement GetElement(VisualElement root)
        {
            if (root == null) return null;
            if (string.IsNullOrEmpty(ElementName)) return null;
            var element = root.Q(ElementName);
            return element;
        }

        public MemberInfo GetElementField(VisualElement root)
        {
            if (root == null) return null;
            if (string.IsNullOrEmpty(ElementName)) return null;
            if (string.IsNullOrEmpty(ElementFieldPath)) return null;
            var element = root.Q(ElementName);
            if (element == null) return null;
            MemberInfo result;
            if (isProperty) result = element.GetType().GetProperty(ElementFieldPath);
            else result = element.GetType().GetField(ElementFieldPath);
            return result;
        }

        public bool IsValueCompatible(VisualElement root)
        {
            var valueType = ValueType;
            if (valueType == null) return true;

            var targetProperty = GetElementField(root);
            if (targetProperty == null) return true;
            var memberType = targetProperty is PropertyInfo ? ((PropertyInfo)targetProperty).PropertyType : ((FieldInfo)targetProperty).FieldType;
            if (memberType == typeof(string)) return true;

            return memberType.IsAssignableFrom(valueType);
        }


        public void Bind(UIDocument doc)
        {
            var targetElement = GetElement(doc);
            var value = Value;
            switch (targetElement)
            {
                //for the sake of performance
                case Label label:
                    if ("text".Equals(elementFieldPath))
                    {
                        label.text = ConvertToString(value);
                        return;
                    }

                    break;
                case Button button:
                    if ("text".Equals(elementFieldPath))
                    {
                        button.text = ConvertToString(value);
                        return;
                    }

                    break;
                case Toggle toggle:
                    if ("text".Equals(elementFieldPath))
                    {
                        toggle.text = ConvertToString(value);
                        return;
                    }

                    break;
            }

            BindViaReflection(targetElement, value);
        }

        private void CheckCastToString(object value)
        {
            if (value == null) return;
            if (!(value is string)) throw new Exception("Value of type " + value.GetType().FullName + " can not be cast to string!");
        }

        private void BindViaReflection(VisualElement targetElement, object value, bool testOnly = false)
        {
            var memberInfo = GetFieldInfo(targetElement);

            if (isProperty)
            {
                var propertyInfo = memberInfo as PropertyInfo;
                if (value != null) value = Convert(propertyInfo.PropertyType, value, true);
                if (!testOnly) propertyInfo.SetValue(targetElement, value);
            }
            else
            {
                var fieldInfo = memberInfo as FieldInfo;
                if (value != null) value = Convert(fieldInfo.FieldType, value, true);
                if (!testOnly) fieldInfo.SetValue(targetElement, value);
            }
        }

        private object Convert(Type targetType, object value, bool isProperty)
        {
            if (value == null) return value;
            var isString = targetType == typeof(string);
            if (!isString)
            {
                if (!targetType.IsInstanceOfType(value))
                    throw new Exception((isProperty ? "Property" : "Field") + $" {memberInfo.Name} type mismatch {targetType.FullName} != {value.GetType().FullName}");
            }
            else value = ConvertToString(value);

            return value;
        }

        private static string ConvertToString(object value)
        {
            if (value is string s) return s;
            return value.ToString();
        }

        protected VisualElement GetElement(UIDocument doc)
        {
            if (doc == null) throw new Exception("UIDocument is null");
            var root = doc.rootVisualElement;
            if (root == null) throw new Exception("UIDocument root is null");
            var targetElement = root.Q(elementName);
            if (targetElement == null) throw new Exception($"Can not find target element with name {elementName}");
            return targetElement;
        }

        protected MemberInfo GetFieldInfo(VisualElement element)
        {
            if (memberInfo != null) return memberInfo;
            if (isProperty)
            {
                memberInfo = element.GetType().GetProperty(elementFieldPath);
                if (memberInfo == null) throw new Exception($"Can not find a property with name {elementFieldPath} at class {element.GetType().Name}");
            }
            else
            {
                memberInfo = element.GetType().GetField(elementFieldPath);
                if (memberInfo == null) throw new Exception($"Can not find a field with name {elementFieldPath} at class {element.GetType().Name}");
            }

            return memberInfo;
        }

        public abstract void ReverseBind(UIDocument doc);

        public static void AssignDefaultField(VisualElement element, BGDBUiElementBinderA binder)
        {
            if (!ElementType2FieldPath.TryGetValue(element.GetType(), out var pathTuple)) return;

            binder.ElementFieldPath = pathTuple.Item1;
            binder.IsProperty = pathTuple.Item2;
        }
    }
}