#if UNITY_EDITOR
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using Object = UnityEngine.Object;

namespace Jovian.InspectorTools.EditorTools {
    public static class MySerializedProperty {
        #region Collections Handling

        /// <summary>
        /// Get array of property childs, if parent property is array
        /// </summary>
        public static SerializedProperty[] AsArray(this SerializedProperty property) {
            List<SerializedProperty> items = new List<SerializedProperty>();
            for(int i = 0; i < property.arraySize; i++)
                items.Add(property.GetArrayElementAtIndex(i));
            return items.ToArray();
        }

        /// <summary>
        /// Get array of property childs casted to specific type
        /// </summary>
        public static T[] AsArray<T>(this SerializedProperty property) {
            var propertiesArray = property.AsArray();
            return propertiesArray.Select(s => s.objectReferenceValue).OfType<T>().ToArray();
        }

        /// <summary>
        /// Get array of property childs, if parent property is array
        /// </summary>
        public static IEnumerable<SerializedProperty> AsIEnumerable(this SerializedProperty property) {
            for(int i = 0; i < property.arraySize; i++)
                yield return property.GetArrayElementAtIndex(i);
        }

        /// <summary>
        /// Replace array contents of SerializedProperty with another array
        /// </summary>
        public static void ReplaceArray(this SerializedProperty property, Object[] newElements) {
            property.arraySize = 0;
            property.serializedObject.ApplyModifiedProperties();
            property.arraySize = newElements.Length;
            for(var i = 0; i < newElements.Length; i++) {
                property.GetArrayElementAtIndex(i).objectReferenceValue = newElements[i];
            }

            property.serializedObject.ApplyModifiedProperties();
        }

        /// <summary>
        /// If property is array, insert new element at the end and get it as a property
        /// </summary>
        public static SerializedProperty NewElement(this SerializedProperty property) {
            int newElementIndex = property.arraySize;
            property.InsertArrayElementAtIndex(newElementIndex);
            return property.GetArrayElementAtIndex(newElementIndex);
        }

        #endregion

        /// <summary>
        /// Property is float, int, vector or int vector
        /// </summary>
        public static bool IsNumerical(this SerializedProperty property) {
            var propertyType = property.propertyType;
            switch(propertyType) {
                case SerializedPropertyType.Float:
                case SerializedPropertyType.Integer:
                case SerializedPropertyType.Vector2:
                case SerializedPropertyType.Vector3:
                case SerializedPropertyType.Vector4:
                case SerializedPropertyType.Vector2Int:
                case SerializedPropertyType.Vector3Int:
                    return true;

                default: return false;
            }
        }

        /// <summary>
        /// Get string representation of serialized property
        /// </summary>
        public static string AsStringValue(this SerializedProperty property) {
            switch(property.propertyType) {
                case SerializedPropertyType.String:
                    return property.stringValue;

                case SerializedPropertyType.Character:
                case SerializedPropertyType.Integer:
                    if(property.type == "char") return Convert.ToChar(property.intValue).ToString();
                    return property.intValue.ToString();

                case SerializedPropertyType.ObjectReference:
                    return property.objectReferenceValue != null ? property.objectReferenceValue.ToString() : "null";

                case SerializedPropertyType.Boolean:
                    return property.boolValue.ToString();

                case SerializedPropertyType.Enum:
                    return property.GetValue().ToString();

                default:
                    return string.Empty;
            }
        }

        /// <summary>
        /// Property path for collection without ".Array.data[x]" in it
        /// </summary>
        public static string GetFixedPropertyPath(this SerializedProperty property) => property.propertyPath.Replace(".Array.data[", "[");


        /// <summary>
        /// Get FieldInfo out of SerializedProperty
        /// </summary>
        public static FieldInfo GetFieldInfo(this SerializedProperty property) {
            var targetObject = property.serializedObject.targetObject;
            var targetType = targetObject.GetType();
            return targetType.GetField(property.propertyPath);
        }

        /// <summary>
        /// Get raw object value out of the SerializedProperty
        /// </summary>
        public static object GetValue(this SerializedProperty property) {
            if(property == null) return null;

            object obj = property.serializedObject.targetObject;
            var elements = property.GetFixedPropertyPath().Split('.');
            foreach(var element in elements) {
                if(element.Contains("[")) {
                    var elementName = element.Substring(0, element.IndexOf("[", StringComparison.Ordinal));
                    var index = Convert.ToInt32(element.Substring(element.IndexOf("[", StringComparison.Ordinal)).Replace("[", "").Replace("]", ""));
                    obj = GetValueByArrayFieldName(obj, elementName, index);
                }
                else obj = GetValueByFieldName(obj, element);
            }
            return obj;


            object GetValueByArrayFieldName(object source, string name, int index) {
                if(!(GetValueByFieldName(source, name) is IEnumerable enumerable)) return null;
                var enumerator = enumerable.GetEnumerator();

                for(var i = 0; i <= index; i++) if(!enumerator.MoveNext()) return null;
                return enumerator.Current;
            }

            // Search "source" object for a field with "name" and get it's value
            object GetValueByFieldName(object source, string name) {
                if(source == null) return null;
                var type = source.GetType();

                while(type != null) {
                    var fieldInfo = type.GetField(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
                    if(fieldInfo != null) return fieldInfo.GetValue(source);

                    var propertyInfo = type.GetProperty(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
                    if(propertyInfo != null) return propertyInfo.GetValue(source, null);

                    type = type.BaseType;
                }
                return null;
            }
        }


        /// <summary>
        /// Set raw object value to the SerializedProperty
        /// </summary>
        public static void SetValue(this SerializedProperty property, object value) {
            GetFieldInfo(property).SetValue(property.serializedObject.targetObject, value);
        }

        /// <summary>
        /// Is specific attribute defined on SerializedProperty
        /// </summary>
        /// <param name="property"></param>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public static bool IsAttributeDefined<T>(this SerializedProperty property) where T : Attribute {
            var fieldInfo = property.GetFieldInfo();
            if(fieldInfo == null) return false;
            return Attribute.IsDefined(fieldInfo, typeof(T));
        }

        #region SerializedProperty Get Parent

        // Found here http://answers.unity.com/answers/425602/view.html
        // Update here https://gist.github.com/AdrienVR/1548a145c039d2fddf030ebc22f915de to support inherited private members.
        /// <summary>
        /// Get parent object of SerializedProperty
        /// </summary>
        public static object GetParent(this SerializedProperty prop) {
            var path = prop.propertyPath.Replace(".Array.data[", "[");
            object obj = prop.serializedObject.targetObject;
            var elements = path.Split('.');
            foreach(var element in elements.Take(elements.Length - 1)) {
                if(element.Contains("[")) {
                    var elementName = element.Substring(0, element.IndexOf("[", StringComparison.Ordinal));
                    var index = Convert.ToInt32(element.Substring(element.IndexOf("[", StringComparison.Ordinal)).Replace("[", "").Replace("]", ""));
                    obj = GetValueAt(obj, elementName, index);
                }
                else {
                    obj = GetValue(obj, element);
                }
            }

            return obj;
        }

        private static object GetValue(object source, string name) {
            if(source == null)
                return null;

            foreach(var type in GetHierarchyTypes(source.GetType())) {
                var f = type.GetField(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
                if(f != null)
                    return f.GetValue(source);

                var p = type.GetProperty(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
                if(p != null)
                    return p.GetValue(source, null);
            }

            return null;
        }

        private static IEnumerable<Type> GetHierarchyTypes(Type sourceType) {
            yield return sourceType;
            while(sourceType.BaseType != null) {
                yield return sourceType.BaseType;
                sourceType = sourceType.BaseType;
            }
        }

        private static object GetValueAt(object source, string name, int index) {
            var enumerable = GetValue(source, name) as IEnumerable;
            if(enumerable == null) return null;

            var enm = enumerable.GetEnumerator();
            while(index-- >= 0)
                enm.MoveNext();
            return enm.Current;
        }

        #endregion
    }
}
#endif
