using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
using UnityObject = UnityEngine.Object;

namespace Unity.VisualScripting
{
    public static class SerializedPropertyUtility
    {
        static SerializedPropertyUtility()
        {
            CustomPropertyDrawer_m_Type = typeof(CustomPropertyDrawer).GetField("m_Type", BindingFlags.Instance | BindingFlags.NonPublic);
            CustomPropertyDrawer_m_UseForChildren = typeof(CustomPropertyDrawer).GetField("m_UseForChildren", BindingFlags.Instance | BindingFlags.NonPublic);
        }

        private static readonly FieldInfo CustomPropertyDrawer_m_Type; // internal Type CustomPropertyDrawer.m_Type
        private static readonly FieldInfo CustomPropertyDrawer_m_UseForChildren; // internal bool CustomPropertyDrawer.m_UseForChildren

        private static HashSet<CustomPropertyDrawer> _drawerTypeAttributes;

        private static HashSet<CustomPropertyDrawer> drawerTypeAttributes
        {
            get
            {
                if (_drawerTypeAttributes == null)
                {
                    _drawerTypeAttributes = Codebase.types
                       .Select(type => type.GetAttribute<CustomPropertyDrawer>())
                       .NotNull()
                       .ToHashSet();
                }

                return _drawerTypeAttributes;
            }
        }

        public static bool HasCustomDrawer(Type type)
        {
            // Exclude:
            //  - Invisible types (internal, non-public nested)
            //  - Open-constructed generics, which Unity can't serialize
            //  - Attributes, which have a drawer but can't provide a property
            if (!type.IsVisible || type.IsGenericType || typeof(Attribute).IsAssignableFrom(type))
            {
                return false;
            }

            return drawerTypeAttributes.Any(attribute =>
            {
                var drawnType = (Type)CustomPropertyDrawer_m_Type.GetValueOptimized(attribute);
                var useForChildren = (bool)CustomPropertyDrawer_m_UseForChildren.GetValueOptimized(attribute);

                return useForChildren ? drawnType.IsAssignableFrom(type) : drawnType == type;
            });
        }

        #region Temporary Properties

        public static SerializedProperty CreateTemporaryProperty(Type type)
        {
            Ensure.That(nameof(type)).IsNotNull(type);

            if (!SerializedPropertyProviderProvider.instance.HasDecorator(type))
            {
                throw new InvalidOperationException($"Missing property provider for '{type}'.\nDid you generate serialized property providers?");
            }

            return new SerializedObject((ScriptableObject)SerializedPropertyProviderProvider.instance.GetDecorator(type)).FindPropertyOrFail("item");
        }

        public static void DestroyTemporaryProperty(SerializedProperty property)
        {
            if (property == null)
            {
                throw new ArgumentNullException(nameof(property));
            }

            if (!(property.serializedObject.targetObject is ISerializedPropertyProvider))
            {
                throw new InvalidOperationException("Trying to destroy a non-temporary property.");
            }

            UnityObject.DestroyImmediate(property.serializedObject.targetObject);
            property.serializedObject.Dispose();
            property.Dispose();
        }

        #endregion

        #region Helpers

        public static int GetPropertyHash(SerializedProperty property)
        {
            return HashUtility.GetHashCode(property.serializedObject.targetObject, property.propertyPath);
        }

        public static SerializedProperty FindPropertyRelativeOrFail(this SerializedProperty property, string path)
        {
            var subProperty = property.FindPropertyRelative(path);

            if (subProperty == null)
            {
                throw new InvalidOperationException($"Failed to find property '{SerializedObjectLabel(property.serializedObject)}.{property.propertyPath}.{path}'.");
            }

            return subProperty;
        }

        public static SerializedProperty FindPropertyOrFail(this SerializedObject serializedObject, string path)
        {
            var property = serializedObject.FindProperty(path);

            if (property == null)
            {
                throw new InvalidOperationException($"Failed to find property '{SerializedObjectLabel(serializedObject)}.{path}'.");
            }

            return property;
        }

        public static string FixedPropertyPath(this SerializedProperty property)
        {
            // Unity structures array paths like "fieldName.Array.data[i]".
            // Fix that quirk and directly go to index, i.e. "fieldName[i]".
            return property.propertyPath.Replace(".Array.data[", "[");
        }

        public static string[] PropertyPathParts(this SerializedProperty property)
        {
            return property.FixedPropertyPath().Split('.');
        }

        public static bool IsPropertyIndexer(string propertyPart, out string fieldName, out int index)
        {
            var regex = new Regex(@"(.+)\[(\d+)\]");
            var match = regex.Match(propertyPart);

            if (match.Success) // Property refers to an array or list
            {
                fieldName = match.Groups[1].Value;
                index = int.Parse(match.Groups[2].Value);
                return true;
            }
            else
            {
                fieldName = propertyPart;
                index = -1;
                return false;
            }
        }

        #endregion

        #region Reflection

        private static void EnsureReflectable(SerializedProperty property)
        {
            if (property == null)
            {
                throw new ArgumentNullException(nameof(property));
            }

            if (property.serializedObject.isEditingMultipleObjects)
            {
                throw new NotSupportedException($"Attempting to reflect property '{property.propertyPath}' on multiple objects.");
            }

            if (property.serializedObject.targetObject == null)
            {
                throw new NotSupportedException($"Attempting to reflect property '{property.propertyPath}' on a null object.");
            }
        }

        private static string SerializedObjectLabel(SerializedObject serializedObject)
        {
            return serializedObject.isEditingMultipleObjects ? "[Multiple]" : serializedObject.targetObject.GetType().Name;
        }

        public static object GetUnderlyingValue(this SerializedProperty property)
        {
            EnsureReflectable(property);

            object parent = property.serializedObject.targetObject;
            var parts = PropertyPathParts(property);

            for (var i = 0; i < parts.Length; i++)
            {
                var part = parts[i];

                if (parent == null)
                {
                    throw new NullReferenceException($"Parent of '{SerializedObjectLabel(property.serializedObject)}.{string.Join(".", parts, 0, i + 1)}' is null.");
                }

                parent = GetPropertyPartValue(part, parent);
            }

            return parent;
        }

        public static Type GetUnderlyingType(this SerializedProperty property)
        {
            EnsureReflectable(property);

            var type = property.serializedObject.targetObject.GetType();

            foreach (var part in PropertyPathParts(property))
            {
                type = GetPropertyPartType(part, type);
            }

            return type;
        }

        public static FieldInfo GetUnderlyingField(this SerializedProperty property)
        {
            EnsureReflectable(property);

            var type = property.serializedObject.targetObject.GetType();
            var parts = PropertyPathParts(property);

            for (var i = 0; i < parts.Length - 1; i++)
            {
                type = GetPropertyPartType(parts[i], type);
            }

            string fieldName;
            int index;
            IsPropertyIndexer(parts[parts.Length - 1], out fieldName, out index);

            return GetSerializedFieldInfo(type, fieldName);
        }

        public static void SetUnderlyingValue(this SerializedProperty property, object value)
        {
            EnsureReflectable(property);

            // Serialize so we don't overwrite other modifications with our deserialization later
            property.serializedObject.ApplyModifiedPropertiesWithoutUndo();

            object parent = property.serializedObject.targetObject;
            var parts = PropertyPathParts(property);

            for (var i = 0; i < parts.Length - 1; i++)
            {
                var part = parts[i];

                if (parent == null)
                {
                    throw new NullReferenceException($"Parent of '{SerializedObjectLabel(property.serializedObject)}.{string.Join(".", parts, 0, i + 1)}' is null.");
                }

                parent = GetPropertyPartValue(part, parent);
            }

            string fieldName;
            int index;
            IsPropertyIndexer(parts[parts.Length - 1], out fieldName, out index);

            var field = GetSerializedFieldInfo(parent.GetType(), fieldName);

            var previousValue = field.GetValue(parent);
            field.SetValue(parent, value);

            // Since we're using reflection to set the value of the serialized objects directly,
            // we should dirty the object if the value is changed by our action. 
            // Otherwise, any external code has no way of knowing the data was changed and that may break caching. 
            // An example: LocalizedAudioClip property drawer caches parts of TableReference and only updates if dirty.
            if (!previousValue.Equals(value))
            {
                EditorUtility.SetDirty(property.serializedObject.targetObject);
            }

            // Deserialize the object for continued operations after this call
            property.serializedObject.Update();
        }

        private static object GetPropertyPartValue(string propertyPathPart, object parent)
        {
            string fieldName;
            int index;

            if (IsPropertyIndexer(propertyPathPart, out fieldName, out index))
            {
                var list = (IList)GetSerializedFieldInfo(parent.GetType(), fieldName).GetValue(parent);

                return list[index];
            }
            else
            {
                return GetSerializedFieldInfo(parent.GetType(), fieldName).GetValue(parent);
            }
        }

        private static Type GetPropertyPartType(string propertyPathPart, Type type)
        {
            string fieldName;
            int index;

            if (IsPropertyIndexer(propertyPathPart, out fieldName, out index))
            {
                var listType = GetSerializedFieldInfo(type, fieldName).FieldType;

                if (listType.IsArray)
                {
                    return listType.GetElementType();
                }
                else // List<T> is the only other Unity-serializable collection
                {
                    return listType.GetGenericArguments()[0];
                }
            }
            else
            {
                return GetSerializedFieldInfo(type, fieldName).FieldType;
            }
        }

        private static FieldInfo GetSerializedFieldInfo(Type type, string name)
        {
            var field = type.GetFieldUnambiguous(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

            if (field == null)
            {
                throw new MissingMemberException(type.FullName, name);
            }

            return field;
        }

        #endregion
    }
}