using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
namespace XNodeEditor {
    ///  Contains reflection-related extensions built for xNode 
    public static class NodeEditorReflection {
        [NonSerialized] private static Dictionary nodeTint;
        [NonSerialized] private static Dictionary nodeWidth;
        ///  All available node types 
        public static Type[] nodeTypes { get { return _nodeTypes != null ? _nodeTypes : _nodeTypes = GetNodeTypes(); } }
        [NonSerialized] private static Type[] _nodeTypes = null;
        ///  Return a delegate used to determine whether window is docked or not. It is faster to cache this delegate than run the reflection required each time. 
        public static Func GetIsDockedDelegate(this EditorWindow window) {
            BindingFlags fullBinding = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
            MethodInfo isDockedMethod = typeof(EditorWindow).GetProperty("docked", fullBinding).GetGetMethod(true);
            return (Func) Delegate.CreateDelegate(typeof(Func), window, isDockedMethod);
        }
        public static Type[] GetNodeTypes() {
            //Get all classes deriving from Node via reflection
            return GetDerivedTypes(typeof(XNode.Node));
        }
        ///  Custom node tint colors defined with [NodeColor(r, g, b)] 
        public static bool TryGetAttributeTint(this Type nodeType, out Color tint) {
            if (nodeTint == null) {
                CacheAttributes(ref nodeTint, x => x.color);
            }
            return nodeTint.TryGetValue(nodeType, out tint);
        }
        ///  Get custom node widths defined with [NodeWidth(width)] 
        public static bool TryGetAttributeWidth(this Type nodeType, out int width) {
            if (nodeWidth == null) {
                CacheAttributes(ref nodeWidth, x => x.width);
            }
            return nodeWidth.TryGetValue(nodeType, out width);
        }
        private static void CacheAttributes(ref Dictionary dict, Func getter) where A : Attribute {
            dict = new Dictionary();
            for (int i = 0; i < nodeTypes.Length; i++) {
                object[] attribs = nodeTypes[i].GetCustomAttributes(typeof(A), true);
                if (attribs == null || attribs.Length == 0) continue;
                A attrib = attribs[0] as A;
                dict.Add(nodeTypes[i], getter(attrib));
            }
        }
        ///  Get FieldInfo of a field, including those that are private and/or inherited 
        public static FieldInfo GetFieldInfo(this Type type, string fieldName) {
            // If we can't find field in the first run, it's probably a private field in a base class.
            FieldInfo field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
            // Search base classes for private fields only. Public fields are found above
            while (field == null && (type = type.BaseType) != typeof(XNode.Node)) field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
            return field;
        }
        ///  Get all classes deriving from baseType via reflection 
        public static Type[] GetDerivedTypes(this Type baseType) {
            List types = new List();
            System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies();
            foreach (Assembly assembly in assemblies) {
                try {
                    types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray());
                } catch (ReflectionTypeLoadException) { }
            }
            return types.ToArray();
        }
        ///  Find methods marked with the [ContextMenu] attribute and add them to the context menu 
        public static void AddCustomContextMenuItems(this GenericMenu contextMenu, object obj) {
            KeyValuePair[] items = GetContextMenuMethods(obj);
            if (items.Length != 0) {
                contextMenu.AddSeparator("");
                List invalidatedEntries = new List();
                foreach (KeyValuePair checkValidate in items) {
                    if (checkValidate.Key.validate && !(bool) checkValidate.Value.Invoke(obj, null)) {
                        invalidatedEntries.Add(checkValidate.Key.menuItem);
                    }
                }
                for (int i = 0; i < items.Length; i++) {
                    KeyValuePair kvp = items[i];
                    if (invalidatedEntries.Contains(kvp.Key.menuItem)) {
                        contextMenu.AddDisabledItem(new GUIContent(kvp.Key.menuItem));
                    } else {
                        contextMenu.AddItem(new GUIContent(kvp.Key.menuItem), false, () => kvp.Value.Invoke(obj, null));
                    }
                }
            }
        }
        ///  Call OnValidate on target 
        public static void TriggerOnValidate(this UnityEngine.Object target) {
            System.Reflection.MethodInfo onValidate = null;
            if (target != null) {
                onValidate = target.GetType().GetMethod("OnValidate", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
                if (onValidate != null) onValidate.Invoke(target, null);
            }
        }
        public static KeyValuePair[] GetContextMenuMethods(object obj) {
            Type type = obj.GetType();
            MethodInfo[] methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
            List> kvp = new List>();
            for (int i = 0; i < methods.Length; i++) {
                ContextMenu[] attribs = methods[i].GetCustomAttributes(typeof(ContextMenu), true).Select(x => x as ContextMenu).ToArray();
                if (attribs == null || attribs.Length == 0) continue;
                if (methods[i].GetParameters().Length != 0) {
                    Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + " has parameters and cannot be used for context menu commands.");
                    continue;
                }
                if (methods[i].IsStatic) {
                    Debug.LogWarning("Method " + methods[i].DeclaringType.Name + "." + methods[i].Name + " is static and cannot be used for context menu commands.");
                    continue;
                }
                for (int k = 0; k < attribs.Length; k++) {
                    kvp.Add(new KeyValuePair(attribs[k], methods[i]));
                }
            }
#if UNITY_5_5_OR_NEWER
            //Sort menu items
            kvp.Sort((x, y) => x.Key.priority.CompareTo(y.Key.priority));
#endif
            return kvp.ToArray();
        }
        ///  Very crude. Uses a lot of reflection. 
        public static void OpenPreferences() {
            try {
#if UNITY_2018_3_OR_NEWER
                SettingsService.OpenUserPreferences("Preferences/Node Editor");
#else
                //Open preferences window
                Assembly assembly = Assembly.GetAssembly(typeof(UnityEditor.EditorWindow));
                Type type = assembly.GetType("UnityEditor.PreferencesWindow");
                type.GetMethod("ShowPreferencesWindow", BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, null);
                //Get the window
                EditorWindow window = EditorWindow.GetWindow(type);
                //Make sure custom sections are added (because waiting for it to happen automatically is too slow)
                FieldInfo refreshField = type.GetField("m_RefreshCustomPreferences", BindingFlags.NonPublic | BindingFlags.Instance);
                if ((bool) refreshField.GetValue(window)) {
                    type.GetMethod("AddCustomSections", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(window, null);
                    refreshField.SetValue(window, false);
                }
                //Get sections
                FieldInfo sectionsField = type.GetField("m_Sections", BindingFlags.Instance | BindingFlags.NonPublic);
                IList sections = sectionsField.GetValue(window) as IList;
                //Iterate through sections and check contents
                Type sectionType = sectionsField.FieldType.GetGenericArguments() [0];
                FieldInfo sectionContentField = sectionType.GetField("content", BindingFlags.Instance | BindingFlags.Public);
                for (int i = 0; i < sections.Count; i++) {
                    GUIContent sectionContent = sectionContentField.GetValue(sections[i]) as GUIContent;
                    if (sectionContent.text == "Node Editor") {
                        //Found contents - Set index
                        FieldInfo sectionIndexField = type.GetField("m_SelectedSectionIndex", BindingFlags.Instance | BindingFlags.NonPublic);
                        sectionIndexField.SetValue(window, i);
                        return;
                    }
                }
#endif
            } catch (Exception e) {
                Debug.LogError(e);
                Debug.LogWarning("Unity has changed around internally. Can't open properties through reflection. Please contact xNode developer and supply unity version number.");
            }
        }
    }
}