using System;
using System.Linq;
using UnityEngine;
#if UNITY_EDITOR
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using Jovian.Utilities;
using UnityObject = UnityEngine.Object;
#endif

namespace Jovian.InspectorTools {
    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
    public class ShowInInspectorAttribute : PropertyAttribute {
        public readonly PlayModeVisibility Visibility;
        public readonly bool isEdittingSupported;

        public ShowInInspectorAttribute() {
            Visibility = PlayModeVisibility.PlayMode;
            isEdittingSupported = false;
        }

        public ShowInInspectorAttribute(PlayModeVisibility playModeVisibility, bool isEdittingSupported = false) {
            Visibility = playModeVisibility;
            this.isEdittingSupported = isEdittingSupported;
        }
    }
}

#if UNITY_EDITOR
namespace Jovian.InspectorTools.Internal {
    public class ShowInInspectorAttributeHandler {
        private const int MAX_DEPTH = 10;

        private bool _initialized;
        private readonly UnityObject target;
        private readonly SerializedObject serializedObject;
        private List<Member> showMembers;

        private GUIStyle labelStyle;
        private GUIStyle tooltipStyle;

        private class Member {
            public MemberInfo member;
            public readonly PlayModeVisibility visibility;
            public readonly bool isEditingSupported;
            private readonly object parent;
            public bool supportsExpansion;
            public bool expanded;
            public Member parentMember;
            public int depth;

            public string niceName;
            public string name;

            public bool IsChild => parentMember != null;
            public bool IsParentExpanded => parentMember.expanded;

            public Member(MemberInfo member,
                PlayModeVisibility visibility,
                bool isEditingSupported,
                object parent,
                int depth,
                Member parentMember) {
                this.member = member;
                this.visibility = visibility;
                this.isEditingSupported = isEditingSupported;
                this.parent = parent;
                this.parentMember = parentMember;
                this.depth = depth;
                name = member.Name;
                niceName = ObjectNames.NicifyVariableName(name);
            }

            public bool TryGetValueAndData(out object value) {
                switch (member) {
                    case FieldInfo fieldInfo:
                        value = fieldInfo.GetValue(parent);
                        return true;
                    case PropertyInfo propertyInfo:
                        value = propertyInfo.GetValue(parent);
                        return true;
                    default:
                        value = null;
                        return false;
                }
            }

            public Type GetValueType() {
                return member switch {
                    FieldInfo fieldInfo => fieldInfo.FieldType,
                    PropertyInfo propertyInfo => propertyInfo.PropertyType,
                    _ => throw new NotSupportedException()
                };
            }

            public void SetValue(object value) {
                switch (member) {
                    case FieldInfo fieldInfo:
                        fieldInfo.SetValue(parent, value);
                        break;
                    case PropertyInfo propertyInfo:
                        propertyInfo.SetValue(parent, value);
                        break;
                }
            }
        }

        public bool HasValues => showMembers.Count > 0;

        public ShowInInspectorAttributeHandler(UnityObject target, SerializedObject serializedObject) {
            this.target = target;
            this.serializedObject = serializedObject;
            showMembers = new();
        }

        public void Update() {
            serializedObject.Update();
            Setup();
        }

        private void Setup() {
            if (_initialized) {
                return;
            }

            showMembers.Clear();

            int fieldCount = EditorTypes.GetFields(target, out List<FieldInfo> objectFields);
            for (int i = 0; i < fieldCount; i++) {
                var showAttribute = Attribute.GetCustomAttribute(objectFields[i], typeof(ShowInInspectorAttribute)) as ShowInInspectorAttribute;
                if (showAttribute == null) {
                    continue;
                }

                SetupMember(objectFields[i], objectFields[i].GetValue(target), showAttribute.Visibility, showAttribute.isEdittingSupported, target);
            }

            int propertiesCount = EditorTypes.GetProperties(target, out List<PropertyInfo> objectProperties);
            for (int i = 0; i < propertiesCount; i++) {
                var showAttribute = Attribute.GetCustomAttribute(objectProperties[i], typeof(ShowInInspectorAttribute)) as ShowInInspectorAttribute;
                if (showAttribute == null) {
                    continue;
                }

                SetupMember(objectProperties[i], objectProperties[i].GetValue(target), showAttribute.Visibility, showAttribute.isEdittingSupported, target);
            }

            _initialized = true;
        }

        private void SetupMember(MemberInfo memberInfo,
            object memberValue,
            PlayModeVisibility visibility,
            bool isEditingSupported,
            object parent,
            int depth = 0,
            Member parentMember = null) {
            Member member = new(memberInfo, visibility, isEditingSupported, parent, depth, parentMember);

            if (depth >= MAX_DEPTH) {
                return;
            }

            showMembers.Add(member);
            if (memberValue != null && CanValueExpand(memberValue)) {

                Type memberType = memberValue.GetType();
                foreach (FieldInfo fieldInfo in memberType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) {
                    ShowInInspectorAttribute attribute = fieldInfo.GetCustomAttribute<ShowInInspectorAttribute>();
                    if (attribute != null) {
                        SetupMember(fieldInfo, fieldInfo.GetValue(memberValue), attribute.Visibility, attribute.isEdittingSupported, memberValue, depth + 1, member);
                        member.supportsExpansion = true;
                    }
                }

                foreach (PropertyInfo propertyInfo in memberType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) {
                    ShowInInspectorAttribute attribute = propertyInfo.GetCustomAttribute<ShowInInspectorAttribute>();
                    if (attribute != null) {
                        SetupMember(propertyInfo, propertyInfo.GetValue(memberValue), attribute.Visibility, attribute.isEdittingSupported, memberValue, depth + 1, member);
                        member.supportsExpansion = true;
                    }
                }
            }
        }

        private bool TryGetValueTypeFromEnumerable(object value, out Type type) {
            Type[] arguments = value.GetType().GenericTypeArguments;
            if (arguments.Length > 0) {
                if (value is IDictionary) {
                    type = value.GetType().GenericTypeArguments[1]; // value
                    return true;
                }
                else if (value is IEnumerable) {
                    type = value.GetType().GenericTypeArguments[0];
                    return true;
                }
            }

            type = null;
            return false;
        }

        public void OnInspectorGUI(Editor editor) {
            labelStyle ??= new GUIStyle(EditorStyles.label) {
                alignment = TextAnchor.UpperLeft,
                padding = new RectOffset(0, 0, 2, 2)
            };

            tooltipStyle ??= new GUIStyle(EditorStyles.miniTextField) {
                fixedHeight = 0f,
                stretchHeight = true,
                wordWrap = true,
                stretchWidth = false,
                imagePosition = ImagePosition.TextOnly,
                clipping = TextClipping.Overflow,
                border = new RectOffset(0, 0, 0, 0)
            };

            int indentLevel = EditorGUI.indentLevel;

            foreach (Member entry in showMembers) {
                if (entry.visibility.IsVisible() == false || (entry.IsChild && entry.IsParentExpanded == false)) {
                    continue;
                }

                Type valueType = entry.GetValueType();
                string stringValue = "";
                object value = null;

                EditorGUI.indentLevel = indentLevel + entry.depth;

                if (entry.TryGetValueAndData(out value)) {
                    if (value is IList list) {
                        stringValue = list.ListToString(true);
                    }
                    else if (value is IEnumerable enumerable) {
                        stringValue = enumerable.EnumerableToString(true);
                    }
                    else {
                        stringValue = value?.ToString();
                    }
                }
                else {
                    stringValue = "<NULL>";
                }

                string labelName = entry.niceName;

                if (entry.supportsExpansion) {
                    entry.expanded = EditorGUILayout.Foldout(entry.expanded, labelName, true);
                }
                else {
                    bool isGUIEnabled = GUI.enabled;
                    bool allowEdit = entry.isEditingSupported;
                    GUI.enabled = allowEdit;

                    Rect entryRect;

                    if (value is IEnumerable enumerable && TryGetValueTypeFromEnumerable(value, out Type enumValueType)) {

                        string labelText = $"{labelName}";
                        if (value is ICollection collection) {
                            labelText += $" (Count={collection.Count})";
                        }

                        string collectionInfo = $"({value.GetType().Name}<{enumValueType.Name}>)";

                        GUILayout.BeginHorizontal();
                        entry.expanded = EditorGUILayout.Foldout(entry.expanded, labelText, false);
                        GUILayout.FlexibleSpace();
                        GUILayout.Label(collectionInfo, EditorStyles.miniLabel);
                        GUILayout.EndHorizontal();
                        entryRect = GUILayoutUtility.GetLastRect();

                        if (entry.expanded) {
                            EditorGUI.indentLevel++;
                            int counter = 0;

                            if (enumerable is IDictionary dictionary) {
                                foreach (DictionaryEntry keyValuePair in dictionary) {
                                    object newValue = InspectorGUIUtility.DrawField(keyValuePair.Key.ToString(), enumValueType, keyValuePair.Value);
                                    if (newValue != value && allowEdit) {
                                        entry.SetValue(newValue);
                                    }
                                }
                            }
                            else {
                                foreach (var item in enumerable) {
                                    object newValue = InspectorGUIUtility.DrawField((counter++).ToString(), enumValueType, item);
                                    if (newValue != value && allowEdit) {
                                        entry.SetValue(newValue);
                                    }
                                }
                            }

                            EditorGUI.indentLevel--;
                        }
                    }
                    else {
                        object newValue = InspectorGUIUtility.DrawField(labelName, valueType, value);
                        entryRect = GUILayoutUtility.GetLastRect();
                        if (newValue != value && allowEdit) {
                            entry.SetValue(newValue);
                        }
                    }


                    GUI.enabled = isGUIEnabled;
                    if (entryRect.Contains(Event.current.mousePosition)) {
                        Vector2 tooltipPosition = Event.current.mousePosition;
                        float width = entryRect.width - (tooltipPosition.x - entryRect.x);

                        tooltipStyle.fixedWidth = width;

                        var tooltipContent = new GUIContent(stringValue, stringValue);
                        float height = tooltipStyle.CalcHeight(tooltipContent, width);
                        Vector2 tooltipSize = new(width, height);
                        tooltipPosition.y -= tooltipSize.y;
                        var tooltipRect = new Rect(tooltipPosition, tooltipSize);
                        GUI.Label(tooltipRect, tooltipContent, tooltipStyle);

                        if (Event.current.type == EventType.ContextClick) {
                            GenericMenu menu = new();
                            menu.AddItem(new GUIContent("Copy"), false, () => { EditorGUIUtility.systemCopyBuffer = stringValue; });
                            menu.ShowAsContext();
                            Event.current.Use();
                        }

                        editor.Repaint();
                    }
                }
            }

            EditorGUI.indentLevel = indentLevel;
        }

        private static readonly Type[] NonExpandedTypes = new Type[] {
            typeof(string),
            typeof(decimal),
            typeof(DateTime),
            typeof(DateTimeOffset),
            typeof(TimeSpan),
            typeof(Guid)
        };

        // Use this to stop expansion of types we dont want, including Unity Objects - they just link to the object.
        private static bool CanValueExpand(object value) {
            if (value == null) {
                return false;
            }

            Type type = value.GetType();
            if (IsSimpleType(type)) {
                return false;
            }

            if (type == typeof(UnityEngine.Object) || type.IsSubclassOf(typeof(UnityEngine.Object))) {
                return false;
            }

            return true;
        }

        //https://stackoverflow.com/a/32337906/584774
        private static bool IsSimpleType(Type type) {
            return type.IsPrimitive || type.IsEnum || NonExpandedTypes.Contains(type) || Convert.GetTypeCode(type) != TypeCode.Object ||
                   (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>) && IsSimpleType(type.GetGenericArguments()[0]));

        }
    }
}
#endif
