// UltEvents // Copyright 2021 Kybernetik //

#if UNITY_EDITOR

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

namespace UltEvents.Editor
{
    /// <summary>[Editor-Only]
    /// Manages the construction of menus for selecting methods for <see cref="PersistentCall"/>s.
    /// </summary>
    internal static class MethodSelectionMenu
    {
        /************************************************************************************************************************/
        #region Fields
        /************************************************************************************************************************/

        /// <summary>
        /// The drawer state from when the menu was opened which needs to be restored when a method is selected because
        /// menu items are executed after the frame finishes and the drawer state is cleared.
        /// </summary>
        private static readonly DrawerState
            CachedState = new DrawerState();

        private static readonly StringBuilder
            LabelBuilder = new StringBuilder();

        // These fields should really be passed around as parameters, but they make all the method signatures annoyingly long.
        private static MethodBase _CurrentMethod;
        private static BindingFlags _Bindings;
        private static GenericMenu _Menu;

        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/
        #region Entry Point
        /************************************************************************************************************************/

        /// <summary>Opens the menu near the specified `area`.</summary>
        public static void ShowMenu(Rect area)
        {
            CachedState.CopyFrom(DrawerState.Current);

            _CurrentMethod = CachedState.call.GetMethodSafe();
            _Bindings = GetBindingFlags();
            _Menu = new GenericMenu();

            BoolPref.AddDisplayOptions(_Menu);

            Object[] targetObjects;
            var targets = GetObjectReferences(CachedState.TargetProperty, out targetObjects);

            AddCoreItems(targets);

            // Populate the main contents of the menu.
            {
                if (targets == null)
                {
                    var serializedMethodName = CachedState.MethodNameProperty.stringValue;
                    Type declaringType;
                    string methodName;
                    PersistentCall.GetMethodDetails(serializedMethodName, null, out declaringType, out methodName);

                    // If we have no target, but do have a type, populate the menu with that type's statics.
                    if (declaringType != null)
                    {
                        PopulateMenuWithStatics(targetObjects, declaringType);

                        goto ShowMenu;
                    }
                    else// If we have no type either, pretend the inspected objects are the targets.
                    {
                        targets = targetObjects;
                    }
                }

                // Ensure that all targets share the same type.
                var firstTarget = ValidateTargetsAndGetFirst(targets);
                if (firstTarget == null)
                {
                    targets = targetObjects;
                    firstTarget = targets[0];
                }

                // Add menu items according to the type of the target.
                if (firstTarget is GameObject)
                    PopulateMenuForGameObject("", false, targets);
                else if (firstTarget is Component)
                    PopulateMenuForComponent(targets);
                else
                    PopulateMenuForObject(targets);
            }

            ShowMenu:

            _Menu.DropDown(area);

            GC.Collect();
        }

        /************************************************************************************************************************/

        private static BindingFlags GetBindingFlags()
        {
            var bindings = BindingFlags.Public | BindingFlags.Instance;

            if (BoolPref.ShowNonPublicMethods)
                bindings |= BindingFlags.NonPublic;

            if (BoolPref.ShowStaticMethods)
                bindings |= BindingFlags.Static;

            return bindings;
        }

        /************************************************************************************************************************/

        private static void AddCoreItems(Object[] targets)
        {
            _Menu.AddItem(new GUIContent("Null"), _CurrentMethod == null, () =>
            {
                DrawerState.Current.CopyFrom(CachedState);

                if (targets != null)
                {
                    PersistentCallDrawer.SetMethod(null);
                }
                else
                {
                    // For a static method, remove the method name but keep the declaring type.
                    var methodName = CachedState.MethodNameProperty.stringValue;
                    var lastDot = methodName.LastIndexOf('.');
                    if (lastDot < 0)
                        CachedState.MethodNameProperty.stringValue = null;
                    else
                        CachedState.MethodNameProperty.stringValue = methodName.Substring(0, lastDot + 1);

                    CachedState.PersistentArgumentsProperty.arraySize = 0;

                    CachedState.MethodNameProperty.serializedObject.ApplyModifiedProperties();
                }

                DrawerState.Current.Clear();
            });

            var isStatic = _CurrentMethod != null && _CurrentMethod.IsStatic;
            if (targets != null && !isStatic)
            {
                _Menu.AddItem(new GUIContent("Static Method"), isStatic, () =>
                {
                    DrawerState.Current.CopyFrom(CachedState);

                    PersistentCallDrawer.SetTarget(null);

                    DrawerState.Current.Clear();
                });
            }

            _Menu.AddSeparator("");
        }

        /************************************************************************************************************************/

        private static Object[] GetObjectReferences(SerializedProperty property, out Object[] targetObjects)
        {
            targetObjects = property.serializedObject.targetObjects;

            if (property.hasMultipleDifferentValues)
            {
                var references = new Object[targetObjects.Length];
                for (int i = 0; i < references.Length; i++)
                {
                    using (var serializedObject = new SerializedObject(targetObjects[i]))
                    {
                        references[i] = serializedObject.FindProperty(property.propertyPath).objectReferenceValue;
                    }
                }
                return references;
            }
            else
            {
                var target = property.objectReferenceValue;
                if (target != null)
                    return new Object[] { target };
                else
                    return null;
            }

        }

        /************************************************************************************************************************/

        private static Object ValidateTargetsAndGetFirst(Object[] targets)
        {
            var firstTarget = targets[0];
            if (firstTarget == null)
                return null;

            var targetType = firstTarget.GetType();

            // Make sure all targets have the exact same type.
            // Unfortunately supporting inheritance would be more complicated.

            var i = 1;
            for (; i < targets.Length; i++)
            {
                var obj = targets[i];
                if (obj == null || obj.GetType() != targetType)
                {
                    return null;
                }
            }

            return firstTarget;
        }

        /************************************************************************************************************************/

        private static T[] GetRelatedObjects<T>(Object[] objects, Func<Object, T> getRelatedObject)
        {
            var relatedObjects = new T[objects.Length];

            for (int i = 0; i < relatedObjects.Length; i++)
            {
                relatedObjects[i] = getRelatedObject(objects[i]);
            }

            return relatedObjects;
        }

        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/
        #region Populate for Objects
        /************************************************************************************************************************/

        private static void PopulateMenuWithStatics(Object[] targets, Type type)
        {
            var firstTarget = targets[0];
            var component = firstTarget as Component;
            if (!ReferenceEquals(component, null))
            {
                var gameObjects = GetRelatedObjects(targets, (target) => (target as Component).gameObject);
                PopulateMenuForGameObject("", true, gameObjects);
            }
            else
            {
                PopulateMenuForObject(firstTarget.GetType().GetNameCS(BoolPref.ShowFullTypeNames) + " ->/", targets);
            }

            _Menu.AddSeparator("");

            var bindings = BindingFlags.Static | BindingFlags.Public;
            if (BoolPref.ShowNonPublicMethods)
                bindings |= BindingFlags.NonPublic;

            PopulateMenuWithMembers(type, bindings, "", null);
        }

        /************************************************************************************************************************/

        private static void PopulateMenuForGameObject(string prefix, bool putGameObjectInSubMenu, Object[] targets)
        {
            var header = new GUIContent(prefix + "Selected GameObject and its Components");

            var gameObjectPrefix = prefix;
            if (putGameObjectInSubMenu)
            {
                _Menu.AddDisabledItem(header);
                gameObjectPrefix += "GameObject ->/";
            }

            PopulateMenuForObject(gameObjectPrefix, targets);

            if (!putGameObjectInSubMenu)
            {
                _Menu.AddSeparator(prefix);
                _Menu.AddDisabledItem(header);
            }

            var gameObjects = GetRelatedObjects(targets, (target) => target as GameObject);
            PopulateMenuForComponents(prefix, gameObjects);
        }

        /************************************************************************************************************************/

        private static void PopulateMenuForComponents(string prefix, GameObject[] gameObjects)
        {
            var firstGameObject = gameObjects[0];
            var components = firstGameObject.GetComponents<Component>();

            for (int i = 0; i < components.Length; i++)
            {
                var component = components[i];

                var targets = new Object[gameObjects.Length];
                targets[0] = component;

                Type type;
                var typeIndex = GetComponentTypeIndex(component, components, out type);

                int minTypeCount;
                Component unused;
                GetComponent(firstGameObject, type, typeIndex, out minTypeCount, out unused);

                var j = 1;
                for (; j < gameObjects.Length; j++)
                {
                    int typeCount;
                    Component targetComponent;
                    GetComponent(gameObjects[j], type, typeIndex, out typeCount, out targetComponent);
                    if (typeCount <= typeIndex)
                        goto NextComponent;

                    targets[j] = targetComponent;

                    if (minTypeCount > typeCount)
                        minTypeCount = typeCount;
                }

                var name = type.GetNameCS(BoolPref.ShowFullTypeNames) + " ->/";

                if (minTypeCount > 1)
                    name = UltEventUtils.GetPlacementName(typeIndex) + " " + name;

                PopulateMenuForObject(prefix + name, targets);
            }

            NextComponent:;
        }

        private static int GetComponentTypeIndex(Component component, Component[] components, out Type type)
        {
            type = component.GetType();

            var count = 0;

            for (int i = 0; i < components.Length; i++)
            {
                var c = components[i];
                if (c == component)
                    break;
                else if (c.GetType() == type)
                    count++;
            }

            return count;
        }

        private static void GetComponent(GameObject gameObject, Type type, int targetIndex, out int numberOfComponentsOfType, out Component targetComponent)
        {
            numberOfComponentsOfType = 0;
            targetComponent = null;

            var components = gameObject.GetComponents(type);
            for (int i = 0; i < components.Length; i++)
            {
                var component = components[i];
                if (component.GetType() == type)
                {
                    if (numberOfComponentsOfType == targetIndex)
                        targetComponent = component;

                    numberOfComponentsOfType++;
                }
            }
        }

        /************************************************************************************************************************/

        private static void PopulateMenuForComponent(Object[] targets)
        {
            var gameObjects = GetRelatedObjects(targets, (target) => (target as Component).gameObject);

            PopulateMenuForGameObject("", true, gameObjects);
            _Menu.AddSeparator("");

            PopulateMenuForObject(targets);
        }

        /************************************************************************************************************************/

        private static void PopulateMenuForObject(Object[] targets)
        {
            PopulateMenuForObject("", targets);
        }

        private static void PopulateMenuForObject(string prefix, Object[] targets)
        {
            PopulateMenuWithMembers(targets[0].GetType(), _Bindings, prefix, targets);
        }

        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/
        #region Populate for Types
        /************************************************************************************************************************/

        private static void PopulateMenuWithMembers(Type type, BindingFlags bindings, string prefix, Object[] targets)
        {
            var members = GetSortedMembers(type, bindings);
            var previousDeclaringType = type;

            var firstSeparator = true;
            var firstProperty = true;
            var firstMethod = true;
            var firstBaseType = true;
            var nameMatchesNextMethod = false;

            var i = 0;
            while (i < members.Count)
            {
                ParameterInfo[] parameters;
                MethodInfo getter;
                var member = GetNextSupportedMember(members, ref i, out parameters, out getter);

                GotMember:

                if (member == null)
                    return;

                i++;

                if (BoolPref.SubMenuForEachBaseType)
                {
                    if (firstBaseType && member.DeclaringType != type)
                    {
                        if (firstSeparator)
                            firstSeparator = false;
                        else
                            _Menu.AddSeparator(prefix);

                        var baseTypesOf = "Base Types of " + type.GetNameCS();
                        if (BoolPref.SubMenuForBaseTypes)
                        {
                            prefix += baseTypesOf + " ->/";
                        }
                        else
                        {
                            _Menu.AddDisabledItem(new GUIContent(prefix + baseTypesOf));
                        }
                        firstProperty = false;
                        firstMethod = false;
                        firstBaseType = false;
                    }

                    if (previousDeclaringType != member.DeclaringType)
                    {
                        previousDeclaringType = member.DeclaringType;
                        firstProperty = true;
                        firstMethod = true;
                        firstSeparator = true;
                    }
                }

                var property = member as PropertyInfo;
                if (property != null)
                {
                    AppendGroupHeader(prefix, "Properties in ", member.DeclaringType, type, ref firstProperty, ref firstSeparator);

                    AddSelectPropertyItem(prefix, targets, type, property, getter);
                    continue;
                }

                var method = member as MethodBase;
                if (method != null)
                {
                    AppendGroupHeader(prefix, "Methods in ", member.DeclaringType, type, ref firstMethod, ref firstSeparator);

                    // Check if the method name matched the previous or next method to group them.
                    if (BoolPref.GroupMethodOverloads)
                    {
                        var nameMatchedPreviousMethod = nameMatchesNextMethod;

                        ParameterInfo[] nextParameters;
                        MethodInfo nextGetter;
                        var nextMember = GetNextSupportedMember(members, ref i, out nextParameters, out nextGetter);

                        nameMatchesNextMethod = nextMember != null && method.Name == nextMember.Name;

                        if (nameMatchedPreviousMethod || nameMatchesNextMethod)
                        {
                            AddSelectMethodItem(prefix, targets, type, true, method, parameters);

                            if (i < members.Count)
                            {
                                member = nextMember;
                                parameters = nextParameters;
                                getter = nextGetter;
                                goto GotMember;
                            }
                            else
                            {
                                return;
                            }
                        }
                    }

                    // Otherwise just build the label normally.
                    AddSelectMethodItem(prefix, targets, type, false, method, parameters);
                }
            }
        }

        /************************************************************************************************************************/

        private static void AppendGroupHeader(string prefix, string name, Type declaringType, Type currentType, ref bool firstInGroup, ref bool firstSeparator)
        {
            if (firstInGroup)
            {
                LabelBuilder.Length = 0;
                LabelBuilder.Append(prefix);

                if (BoolPref.SubMenuForEachBaseType && declaringType != currentType)
                    AppendDeclaringTypeSubMenu(LabelBuilder, declaringType, currentType);

                if (firstSeparator)
                    firstSeparator = false;
                else
                    _Menu.AddSeparator(LabelBuilder.ToString());

                LabelBuilder.Append(name);

                if (BoolPref.SubMenuForEachBaseType)
                    LabelBuilder.Append(declaringType.GetNameCS());
                else
                    LabelBuilder.Append(currentType.GetNameCS());

                _Menu.AddDisabledItem(new GUIContent(LabelBuilder.ToString()));
                firstInGroup = false;
            }
        }

        private static void AppendDeclaringTypeSubMenu(StringBuilder text, Type declaringType, Type currentType)
        {
            if (BoolPref.SubMenuForEachBaseType)
            {
                if (BoolPref.SubMenuForRootBaseType || declaringType != currentType)
                {
                    text.Append(declaringType.GetNameCS());
                    text.Append(" ->/");
                }
            }
        }

        /************************************************************************************************************************/

        private static void AddSelectPropertyItem(string prefix, Object[] targets, Type currentType, PropertyInfo property, MethodInfo getter)
        {
            var defaultMethod = getter;

            MethodInfo setter = null;
            if (IsSupported(property.PropertyType))
            {
                setter = property.GetSetMethod(true);
                if (setter != null)
                    defaultMethod = setter;
            }

            LabelBuilder.Length = 0;
            LabelBuilder.Append(prefix);

            // Declaring Type.
            AppendDeclaringTypeSubMenu(LabelBuilder, property.DeclaringType, currentType);

            // Non-Public Grouping.
            if (BoolPref.GroupNonPublicMethods && !IsPublic(property))
                LabelBuilder.Append("Non-Public Properties ->/");

            // Property Type and Name.
            LabelBuilder.Append(property.PropertyType.GetNameCS(BoolPref.ShowFullTypeNames));
            LabelBuilder.Append(' ');
            LabelBuilder.Append(property.Name);

            // Get and Set.
            LabelBuilder.Append(" { ");
            if (getter != null) LabelBuilder.Append("get; ");
            if (setter != null) LabelBuilder.Append("set; ");
            LabelBuilder.Append('}');

            var label = LabelBuilder.ToString();
            AddSetCallItem(label, defaultMethod, targets);
        }

        /************************************************************************************************************************/

        private static void AddSelectMethodItem(string prefix, Object[] targets, Type currentType, bool methodNameSubMenu,
            MethodBase method, ParameterInfo[] parameters)
        {
            LabelBuilder.Length = 0;
            LabelBuilder.Append(prefix);

            // Declaring Type.
            AppendDeclaringTypeSubMenu(LabelBuilder, method.DeclaringType, currentType);

            // Non-Public Grouping.
            if (BoolPref.GroupNonPublicMethods && !IsPublic(method))
                LabelBuilder.Append("Non-Public Methods ->/");

            // Overload Grouping.
            if (methodNameSubMenu)
                LabelBuilder.Append(method.Name).Append(" ->/");

            // Method Signature.
            LabelBuilder.Append(GetMethodSignature(method, parameters, true));

            var label = LabelBuilder.ToString();

            AddSetCallItem(label, method, targets);
        }

        /************************************************************************************************************************/

        private static void AddSetCallItem(string label, MethodBase method, Object[] targets)
        {
            _Menu.AddItem(
                new GUIContent(label),
                method == _CurrentMethod,
                (userData) =>
                {
                    DrawerState.Current.CopyFrom(CachedState);

                    var i = 0;
                    CachedState.CallProperty.ModifyValues<PersistentCall>((call) =>
                    {
                        var target = targets != null ? targets[i % targets.Length] : null;
                        call.SetMethod(method, target);
                        i++;
                    }, "Set Persistent Call");

                    DrawerState.Current.Clear();
                },
                null);
        }

        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/
        #region Member Gathering
        /************************************************************************************************************************/

        private static readonly Dictionary<BindingFlags, Dictionary<Type, List<MemberInfo>>>
            MemberCache = new Dictionary<BindingFlags, Dictionary<Type, List<MemberInfo>>>();

        internal static void ClearMemberCache()
        {
            MemberCache.Clear();
        }

        /************************************************************************************************************************/

        private static List<MemberInfo> GetSortedMembers(Type type, BindingFlags bindings)
        {
            // Get the cache for the specified bindings.
            Dictionary<Type, List<MemberInfo>> memberCache;
            if (!MemberCache.TryGetValue(bindings, out memberCache))
            {
                memberCache = new Dictionary<Type, List<MemberInfo>>();
                MemberCache.Add(bindings, memberCache);
            }

            // If the members for the specified type aren't cached for those bindings, gather and sort them.
            List<MemberInfo> members;
            if (!memberCache.TryGetValue(type, out members))
            {
                var properties = type.GetProperties(bindings);
                var methods = type.GetMethods(bindings);

                // When gathering static members, also include instance constructors.
                var constructors = ((bindings & BindingFlags.Static) == BindingFlags.Static) ?
                    type.GetConstructors((bindings & ~BindingFlags.Static) | BindingFlags.Instance) :
                    null;

                var capacity = properties.Length + methods.Length;
                if (constructors != null)
                    capacity += constructors.Length;

                members = new List<MemberInfo>(capacity);
                members.AddRange(properties);
                if (constructors != null)
                    members.AddRange(constructors);
                members.AddRange(methods);

                // If the bindings include static, add static members from each base type.
                if ((bindings & BindingFlags.Static) == BindingFlags.Static && type.BaseType != null)
                {
                    members.AddRange(GetSortedMembers(type.BaseType, bindings & ~BindingFlags.Instance));
                }

                UltEventUtils.StableInsertionSort(members, CompareMembers);

                memberCache.Add(type, members);
            }

            return members;
        }

        /************************************************************************************************************************/

        private static int CompareMembers(MemberInfo a, MemberInfo b)
        {
            if (BoolPref.SubMenuForEachBaseType)
            {
                var result = CompareChildBeforeBase(a.DeclaringType, b.DeclaringType);
                if (result != 0)
                    return result;
            }

            // Compare types (properties before methods).
            if (a is PropertyInfo)
            {
                if (!(b is PropertyInfo))
                    return -1;
            }
            else
            {
                if (b is PropertyInfo)
                    return 1;
            }

            // Non-Public Sub-Menu.
            if (BoolPref.GroupNonPublicMethods)
            {
                if (IsPublic(a))
                {
                    if (!IsPublic(b))
                        return -1;
                }
                else
                {
                    if (IsPublic(b))
                        return 1;
                }
            }

            // Compare names.
            return a.Name.CompareTo(b.Name);
        }

        /************************************************************************************************************************/

        private static int CompareChildBeforeBase(Type a, Type b)
        {
            if (a == b)
                return 0;

            while (true)
            {
                a = a.BaseType;

                if (a == null)
                    return 1;

                if (a == b)
                    return -1;
            }
        }

        /************************************************************************************************************************/

        private static readonly Dictionary<MemberInfo, bool>
            MemberToIsPublic = new Dictionary<MemberInfo, bool>();

        private static bool IsPublic(MemberInfo member)
        {
            bool isPublic;
            if (!MemberToIsPublic.TryGetValue(member, out isPublic))
            {
                switch (member.MemberType)
                {
                    case MemberTypes.Constructor:
                    case MemberTypes.Method:
                        isPublic = (member as MethodBase).IsPublic;
                        break;

                    case MemberTypes.Property:
                        isPublic =
                            (member as PropertyInfo).GetGetMethod() != null ||
                            (member as PropertyInfo).GetSetMethod() != null;
                        break;

                    default:
                        throw new ArgumentException("Unhandled member type", "member");
                }

                MemberToIsPublic.Add(member, isPublic);
            }

            return isPublic;
        }

        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/
        #region Supported Checks
        /************************************************************************************************************************/

        private static bool IsSupported(MethodBase method, out ParameterInfo[] parameters)
        {
            if (method.IsGenericMethod ||
                (method.IsSpecialName && (!method.IsConstructor || method.IsStatic)) ||
                method.Name.Contains("<") ||
                method.IsDefined(typeof(ObsoleteAttribute), true))
            {
                parameters = null;
                return false;
            }

            // Most UnityEngine.Object types shouldn't be constructed directly.
            if (method.IsConstructor)
            {
                if (typeof(Component).IsAssignableFrom(method.DeclaringType) ||
                    typeof(ScriptableObject).IsAssignableFrom(method.DeclaringType))
                {
                    parameters = null;
                    return false;
                }
            }

            parameters = method.GetParameters();
            if (!IsSupported(parameters))
                return false;

            return true;
        }

        private static bool IsSupported(PropertyInfo property, out MethodInfo getter)
        {
            if (property.IsSpecialName ||
                property.IsDefined(typeof(ObsoleteAttribute), true))// Obsolete.
            {
                getter = null;
                return false;
            }

            getter = property.GetGetMethod(true);
            if (getter == null && !IsSupported(property.PropertyType))
                return false;

            return true;
        }

        /************************************************************************************************************************/

        /// <summary>
        /// Returns true if the specified `type` can be represented by a <see cref="PersistentArgument"/>.
        /// </summary>
        public static bool IsSupported(Type type)
        {
            if (PersistentCall.IsSupportedNative(type))
            {
                return true;
            }
            else
            {
                int linkIndex;
                PersistentArgumentType linkType;
                return DrawerState.Current.TryGetLinkable(type, out linkIndex, out linkType);
            }
        }

        /// <summary>
        /// Returns true if the type of each of the `parameters` can be represented by a <see cref="PersistentArgument"/>.
        /// </summary>
        public static bool IsSupported(ParameterInfo[] parameters)
        {
            for (int i = 0; i < parameters.Length; i++)
            {
                if (!IsSupported(parameters[i].ParameterType))
                    return false;
            }

            return true;
        }

        /************************************************************************************************************************/

        private static MemberInfo GetNextSupportedMember(List<MemberInfo> members, ref int startIndex, out ParameterInfo[] parameters, out MethodInfo getter)
        {
            while (startIndex < members.Count)
            {
                var member = members[startIndex];
                var property = member as PropertyInfo;
                if (property != null && IsSupported(property, out getter))
                {
                    parameters = null;
                    return member;
                }

                var method = member as MethodBase;
                if (method != null && IsSupported(method, out parameters))
                {
                    getter = null;
                    return member;
                }

                startIndex++;
            }

            parameters = null;
            getter = null;
            return null;
        }

        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/
        #region Method Signatures
        /************************************************************************************************************************/

        private static readonly Dictionary<MethodBase, string>
            MethodSignaturesWithParameters = new Dictionary<MethodBase, string>();

        /************************************************************************************************************************/
                /************************************************************************************************************************/
                /************************************************************************************************************************/

        private static readonly Dictionary<MethodBase, string>
            MethodSignaturesWithoutParameters = new Dictionary<MethodBase, string>();
        private static readonly StringBuilder
            MethodSignatureBuilder = new StringBuilder();

        /************************************************************************************************************************/

        public static string GetMethodSignature(MethodBase method, ParameterInfo[] parameters, bool includeParameterNames)
        {
            if (method == null)
                return null;

            var signatureCache = includeParameterNames ? MethodSignaturesWithParameters : MethodSignaturesWithoutParameters;

            string signature;
            if (!signatureCache.TryGetValue(method, out signature))
            {
                signature = BuildAndCacheSignature(method, parameters, includeParameterNames, signatureCache);
            }

            return signature;
        }

        public static string GetMethodSignature(MethodBase method, bool includeParameterNames)
        {
            if (method == null)
                return null;

            var signatureCache = includeParameterNames ? MethodSignaturesWithParameters : MethodSignaturesWithoutParameters;

            string signature;
            if (!signatureCache.TryGetValue(method, out signature))
            {
                signature = BuildAndCacheSignature(method, method.GetParameters(), includeParameterNames, signatureCache);
            }

            return signature;
        }

        /************************************************************************************************************************/

        private static string BuildAndCacheSignature(MethodBase method, ParameterInfo[] parameters, bool includeParameterNames,
            Dictionary<MethodBase, string> signatureCache)
        {
            MethodSignatureBuilder.Length = 0;

            var returnType = method.GetReturnType();
            MethodSignatureBuilder.Append(returnType.GetNameCS(false));
            MethodSignatureBuilder.Append(' ');

            MethodSignatureBuilder.Append(method.Name);

            MethodSignatureBuilder.Append(" (");
            for (int i = 0; i < parameters.Length; i++)
            {
                if (i > 0)
                    MethodSignatureBuilder.Append(", ");

                var parameter = parameters[i];

                MethodSignatureBuilder.Append(parameter.ParameterType.GetNameCS(false));
                if (includeParameterNames)
                {
                    MethodSignatureBuilder.Append(' ');
                    MethodSignatureBuilder.Append(parameter.Name);
                }
            }
            MethodSignatureBuilder.Append(')');

            var signature = MethodSignatureBuilder.ToString();
            MethodSignatureBuilder.Length = 0;
            signatureCache.Add(method, signature);

            return signature;
        }

        /************************************************************************************************************************/
        #endregion
        /************************************************************************************************************************/
    }
}

#endif