#if UNITY_EDITOR
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.Assertions;
using Debug = UnityEngine.Debug;
using Object = UnityEngine.Object;
using Type = System.Type;
using UnityEditor;

namespace Jovian.Utilities.Editor {
    /// <summary>
    /// Helper class for serializing objects in the editor
    /// </summary>
    public static class EditorSerializationUtility {

        /// <summary>
        /// Returns the property type for cases when it is needed, like in the case of trying to get the type of SerializedPropertyType.ObjectReference
        /// Solution found on https://answers.unity.com/questions/929293/get-field-type-of-serializedproperty.html
        /// </summary>
        /// <param name="property"></param>
        /// <returns></returns>
        public static Type GetTypeFromProperty(SerializedProperty property) {
            //gets parent type info
            string[] slices = property.propertyPath.Split('.');
            Type type = property.serializedObject.targetObject.GetType();

            for (int i = 0; i < slices.Length; i++) {
                string slice = slices[i];
                if (slice == "Array") {
                    i++; //skips "data[x]"
                    if (type.IsArray) // e.g Type[]
                    {
                        type = type.GetElementType();
                    }
                    else if (type.IsGenericType) // e.g. List<Type>
                    {
                        type = type.GetGenericArguments()[0];
                    }
                    else {
                        throw new NotSupportedException("Unsupported array type. Type[] or List<Type> are only supported array types");
                    }
                }
                else {
                    //gets info on field and its type
                    type = type.GetField(slice, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance)?.FieldType;
                }

                if (type == null) {
                    throw new NullReferenceException(
                        $"Type is null, something is not working correctly. Path={property.propertyPath}, Slice={slice}");
                }
            }

            //type is now the type of the property
            return type;
        }

        private static readonly Regex isArrayElementRegex = new Regex("Array.data\\[\\d+\\]$");

        /// <summary>
        /// Checks if the property is an array element
        /// </summary>
        /// <param name="property"></param>
        /// <returns></returns>
        public static bool IsPropertyAnArrayElement(SerializedProperty property) {
            return isArrayElementRegex.IsMatch(property.propertyPath);
        }

        /// <summary>
        /// Returns the parent array property for the given property, which is a member of the array
        /// </summary>
        /// <param name="property"></param>
        /// <returns></returns>
        public static SerializedProperty GetArrayPropertyWithElementProperty(SerializedProperty property) {
            SerializedObject serializedObject = property.serializedObject;
            string propertyPath = property.propertyPath;
            int arrayDataIndex = propertyPath.LastIndexOf(".Array.data", StringComparison.Ordinal);
            string propertyPathWithoutArray = propertyPath.Substring(0, arrayDataIndex);
            int pathDividerIndex = propertyPathWithoutArray.LastIndexOf(".", StringComparison.Ordinal);

            string parentPropertyName = propertyPathWithoutArray;
            if (pathDividerIndex != -1) {
                parentPropertyName = propertyPathWithoutArray.Substring(pathDividerIndex);
            }

            return serializedObject.FindProperty(parentPropertyName);
        }

        /// <summary>
        /// Will save specifically the property args passed in the form new object[] { "propertyName1", "propertyName2", etc. } to the specified targetObject
        /// </summary>
        /// <param name="targetObject"></param>
        /// <param name="args"></param>
        /// <exception cref="NotSupportedException"></exception>
        /// <exception cref="Exception"></exception>
        public static void SaveObjectProperties(Object targetObject, params object[] args) {
            SerializedObject serializedObject = new SerializedObject(targetObject);

            for (int i = 0; i < args.Length; i += 2) {
                object keyArg = args[i];
                Type keyType = keyArg.GetType();

                if ((keyType == typeof(string)) == false) {
                    throw new NotSupportedException($"Key must be string. {args[i]} is {keyType}");
                }

                SerializedProperty property = serializedObject.FindProperty((string)keyArg);
                object argValue = args[i + 1];

                if (property == null) {
                    throw new Exception($"No property found for key {keyArg}");
                }

                if (argValue == null) {
                    property.objectReferenceValue = null;
                }
                else {
                    SetSerializedPropertyValue(property, argValue);
                }
            }

            serializedObject.ApplyModifiedProperties();
        }

        /// <summary>
        /// Will auto-detect the value type and save it to the property. It only supports the serializable types that the Unity serialization system supports
        /// </summary>
        /// <param name="fromProperty"></param>
        /// <param name="value"></param>
        /// <exception cref="NotSupportedException"></exception>
        public static void SetSerializedPropertyValue(SerializedProperty fromProperty, object value) {
            if (fromProperty == null) {
                Debug.LogError("SetSerializedPropertyValue failed, property is null");
                return;
            }

            // Strings are counted as arrays but should be handled separately
            if (fromProperty.isArray && fromProperty.propertyType != SerializedPropertyType.String) {
                fromProperty.arraySize = 0;
                var argArray = (IEnumerable)value;
                var enumerator = argArray.GetEnumerator();

                int index = 0;
                while (enumerator.MoveNext()) {
                    fromProperty.InsertArrayElementAtIndex(index);
                    var arrayProperty = fromProperty.GetArrayElementAtIndex(index);
                    SetSerializedPropertyValue(arrayProperty, enumerator.Current);
                    index++;
                }
            }
            else {
                switch (fromProperty.propertyType) {
                    case SerializedPropertyType.AnimationCurve:
                        fromProperty.animationCurveValue = (AnimationCurve)value;
                        break;
                    case SerializedPropertyType.Boolean:
                        fromProperty.boolValue = (bool)value;
                        break;
                    case SerializedPropertyType.BoundsInt:
                        fromProperty.boundsIntValue = (BoundsInt)value;
                        break;
                    case SerializedPropertyType.Character:
                        fromProperty.intValue = (int)(char)value;
                        break;
                    case SerializedPropertyType.Color: {
                        if (value is Color32 color32) {
                            fromProperty.colorValue = (Color)color32;
                        }
                        else {
                            fromProperty.colorValue = (Color)value;
                        }

                        break;
                    }
                    case SerializedPropertyType.ExposedReference:
                    case SerializedPropertyType.ObjectReference:
                        fromProperty.objectReferenceValue = (Object)value;
                        break;
                    case SerializedPropertyType.Float:
                        fromProperty.floatValue = (float)value;
                        break;
                    case SerializedPropertyType.Integer:
                        fromProperty.intValue = (int)value;
                        break;
                    case SerializedPropertyType.LayerMask:
                        fromProperty.intValue = ((LayerMask)value).value;
                        break;
                    case SerializedPropertyType.Quaternion:
                        fromProperty.quaternionValue = (Quaternion)value;
                        break;
                    case SerializedPropertyType.Rect:
                        fromProperty.rectValue = (Rect)value;
                        break;
                    case SerializedPropertyType.RectInt:
                        fromProperty.rectIntValue = (RectInt)value;
                        break;
                    case SerializedPropertyType.String:
                        fromProperty.stringValue = (string)value;
                        break;
                    case SerializedPropertyType.Vector2:
                        fromProperty.vector2Value = (Vector2)value;
                        break;
                    case SerializedPropertyType.Vector2Int:
                        fromProperty.vector2IntValue = (Vector2Int)value;
                        break;
                    case SerializedPropertyType.Vector3:
                        fromProperty.vector3Value = (Vector3)value;
                        break;
                    case SerializedPropertyType.Vector3Int:
                        fromProperty.vector3IntValue = (Vector3Int)value;
                        break;
                    case SerializedPropertyType.Vector4:
                        fromProperty.vector4Value = (Vector4)value;
                        break;
                    case SerializedPropertyType.Enum:
                        fromProperty.enumValueIndex = Array.IndexOf(Enum.GetValues(value.GetType()), value);
                        // flags???
                        break;
                    case SerializedPropertyType.Generic:
                        SaveGenericProperty(fromProperty, value);
                        break;
                    default:
                        throw new NotSupportedException($"PropertyType {fromProperty.propertyType} is not supported - yet. Array? {fromProperty.isArray} Path: {fromProperty.propertyPath}");
                }
            }
        }

        /// <summary>
        /// Will copy fromProperty to toProperty. It only supports the serializable types that the Unity serialization system supports
        /// </summary>
        /// <param name="fromProperty"></param>
        /// <param name="toProperty"></param>
        /// <exception cref="NotSupportedException"></exception>
        public static void CopyPropertyValue(SerializedProperty fromProperty, SerializedProperty toProperty) {
            Assert.IsNotNull(fromProperty, "fromProperty == null");
            Assert.IsNotNull(toProperty, "toProperty == null");
            Assert.AreEqual(fromProperty.propertyType, toProperty.propertyType, $"Properties do not match types. fromType={fromProperty.propertyType}, toType={toProperty.propertyType}");
            switch (fromProperty.propertyType) {
                case SerializedPropertyType.AnimationCurve:
                    toProperty.animationCurveValue = fromProperty.animationCurveValue;
                    break;
                case SerializedPropertyType.Boolean:
                    toProperty.boolValue = fromProperty.boolValue;
                    break;
                case SerializedPropertyType.BoundsInt:
                    toProperty.boundsIntValue = fromProperty.boundsIntValue;
                    break;
                case SerializedPropertyType.Character:
                    toProperty.intValue = fromProperty.intValue;
                    break;
                case SerializedPropertyType.Color: {
                    toProperty.colorValue = fromProperty.colorValue;
                    break;
                }
                case SerializedPropertyType.ExposedReference:
                case SerializedPropertyType.ObjectReference:
                    toProperty.objectReferenceValue = fromProperty.objectReferenceValue;
                    break;
                case SerializedPropertyType.Float:
                    toProperty.floatValue = fromProperty.floatValue;
                    break;
                case SerializedPropertyType.Integer:
                    toProperty.intValue = fromProperty.intValue;
                    break;
                case SerializedPropertyType.LayerMask:
                    toProperty.intValue = fromProperty.intValue;
                    break;
                case SerializedPropertyType.Quaternion:
                    toProperty.quaternionValue = fromProperty.quaternionValue;
                    break;
                case SerializedPropertyType.Rect:
                    toProperty.rectValue = fromProperty.rectValue;
                    break;
                case SerializedPropertyType.RectInt:
                    toProperty.rectIntValue = fromProperty.rectIntValue;
                    break;
                case SerializedPropertyType.String:
                    toProperty.stringValue = fromProperty.stringValue;
                    break;
                case SerializedPropertyType.Vector2:
                    toProperty.vector2Value = fromProperty.vector2Value;
                    break;
                case SerializedPropertyType.Vector2Int:
                    toProperty.vector2IntValue = fromProperty.vector2IntValue;
                    break;
                case SerializedPropertyType.Vector3:
                    toProperty.vector3Value = fromProperty.vector3Value;
                    break;
                case SerializedPropertyType.Vector3Int:
                    toProperty.vector3IntValue = fromProperty.vector3IntValue;
                    break;
                case SerializedPropertyType.Vector4:
                    toProperty.vector4Value = fromProperty.vector4Value;
                    break;
                case SerializedPropertyType.Enum:
                    toProperty.intValue = fromProperty.intValue;
                    break;
                case SerializedPropertyType.Generic:
                    CopyGenericProperty(fromProperty, toProperty);
                    break;
                case SerializedPropertyType.ManagedReference:
                    toProperty.managedReferenceValue = Activator.CreateInstance(fromProperty.managedReferenceValue.GetType());
                    CopyGenericProperty(fromProperty, toProperty);
                    break;
                default:
                    throw new NotSupportedException($"PropertyType {fromProperty.propertyType} is not supported - yet. Array? {fromProperty.isArray} Path: {fromProperty.propertyPath}");
            }
        }

        private static void CopyGenericProperty(SerializedProperty fromProperty, SerializedProperty toProperty) {
            IEnumerator fromPropertyEnumerator = fromProperty.GetEnumerator();
            IEnumerator toPropertyEnumerator = toProperty.GetEnumerator();
            while (toPropertyEnumerator.MoveNext() && fromPropertyEnumerator.MoveNext()) {
                if (toPropertyEnumerator.Current is SerializedProperty toChildProperty &&
                    fromPropertyEnumerator.Current is SerializedProperty fromChildProperty) {
                    CopyPropertyValue(fromChildProperty, toChildProperty);
                }
            }
        }

        private static void SaveGenericProperty(SerializedProperty property, object instance) {
            Type type = instance.GetType();
            IEnumerable<FieldInfo> fields = type.GetRuntimeFields();
            foreach (FieldInfo field in fields) {
                if (field.IsNotSerialized || field.IsStatic) {
                    continue;
                }

                SerializedProperty fieldProperty = property.FindPropertyRelative(field.Name);
                if (fieldProperty != null) {
                    SetSerializedPropertyValue(fieldProperty, field.GetValue(instance));
                }
                else {
                    Debug.Log($"SaveGenericProperty cannot field serializedProperty named '{field.Name}'");
                }
            }
        }

        public static bool TryGetAttribute<TAttribute>(
            this SerializedProperty serializedProperty,
            out TAttribute attribute,
            bool includeAttributesFromParentProperties = false,
            bool includeInheritedAttributes = true
        )
            where TAttribute : Attribute {
            if (serializedProperty == null) {
                throw new ArgumentNullException(nameof(serializedProperty));
            }

            Type targetObjectType = serializedProperty.serializedObject.targetObject.GetType();

            if (targetObjectType == null) {
                throw new ArgumentException($"Could not find the {nameof(targetObjectType)} of {nameof(serializedProperty)}");
            }

            string[] pathSegments = includeAttributesFromParentProperties ? serializedProperty.propertyPath.Split('.') : new[] { serializedProperty.propertyPath };

            foreach (string pathSegment in pathSegments) {
                FieldInfo fieldInfo = targetObjectType.GetField(pathSegment, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);
                if (fieldInfo != null) {
                    attribute = fieldInfo.GetCustomAttribute<TAttribute>(includeInheritedAttributes);
                    if (attribute != null) {
                        return true;
                    }
                }

                PropertyInfo propertyInfo = targetObjectType.GetProperty(pathSegment, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);
                if (propertyInfo != null) {
                    attribute = propertyInfo.GetCustomAttribute<TAttribute>(includeInheritedAttributes);
                    if (attribute != null) {
                        return true;
                    }
                }
            }

            attribute = null;
            return false;
        }

        public static bool TryGetAttributes<TAttribute>(
            SerializedProperty serializedProperty,
            out List<TAttribute> attributes,
            bool includeAttributesFromParentProperties = false,
            bool includeInheritedAttributes = true
        )
            where TAttribute : Attribute {
            if (serializedProperty == null) {
                throw new ArgumentNullException(nameof(serializedProperty));
            }

            Type targetObjectType = serializedProperty.serializedObject.targetObject.GetType();

            if (targetObjectType == null) {
                throw new ArgumentException($"Could not find the {nameof(targetObjectType)} of {nameof(serializedProperty)}");
            }

            attributes = new List<TAttribute>();

            string[] pathSegments = includeAttributesFromParentProperties ? serializedProperty.propertyPath.Split('.') : new[] { serializedProperty.propertyPath };

            foreach (string pathSegment in pathSegments) {
                FieldInfo fieldInfo = targetObjectType.GetField(pathSegment, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);
                if (fieldInfo != null) {
                    IEnumerable<TAttribute> foundAttributes = fieldInfo.GetCustomAttributes<TAttribute>(includeInheritedAttributes);
                    foreach (TAttribute foundAttribute in foundAttributes) {
                        attributes.Add(foundAttribute);
                    }
                }

                PropertyInfo propertyInfo = targetObjectType.GetProperty(pathSegment, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);
                if (propertyInfo != null) {
                    IEnumerable<TAttribute> foundAttributes = propertyInfo.GetCustomAttributes<TAttribute>(includeInheritedAttributes);
                    foreach (TAttribute attribute in foundAttributes) {
                        attributes.Add(attribute);
                    }
                }
            }

            return attributes.Count > 0;
        }

    }
}
#endif
