/// <summary>
        /// Draws a search field and (if results have been returned) search results.
        /// </summary>
        /// <returns>True if search results are being displayed.</returns>
        public static bool DrawSearchInterface(UnityEngine.Object target)
        {
            if (target == null)
            {
                return(false);
            }

            bool drewSearchGUI = false;

            if (target != activeTarget)
            {
                activeTarget = target;
                config       = new SearchConfig();
                prevConfig   = new SearchConfig();
                searchResults.Clear();
            }

            using (new EditorGUILayout.HorizontalScope())
            {
                EditorGUILayout.LabelField("Search For: ", GUILayout.MaxWidth(70));
                string searchString = SessionState.GetString(searchDisplaySearchFieldKey, string.Empty);
                config.SearchFieldString = EditorGUILayout.TextField(SessionState.GetString(searchDisplaySearchFieldKey, string.Empty), GUILayout.ExpandWidth(true));
                if (GUILayout.Button("Clear", EditorStyles.miniButton, GUILayout.MaxWidth(50)))
                {
                    config.SearchFieldString = string.Empty;
                }
            }

            config.RequireAllKeywords = SessionState.GetBool(searchDisplayRequireAllKeywordsKey, true);
            config.SearchTooltips     = SessionState.GetBool(searchDisplaySearchTooltipsKey, true);
            config.SearchFieldContent = SessionState.GetBool(searchDisplaySearchFieldContentKey, false);

            if (!string.IsNullOrEmpty(config.SearchFieldString))
            {
                // If we're searching for something, draw the search GUI
                DrawSearchResultInterface(target);
                drewSearchGUI = true;
            }

            // Store search settings
            SessionState.SetString(searchDisplaySearchFieldKey, config.SearchFieldString);
            SessionState.SetBool(searchDisplayRequireAllKeywordsKey, config.RequireAllKeywords);
            SessionState.SetBool(searchDisplaySearchTooltipsKey, config.SearchTooltips);
            SessionState.SetBool(searchDisplaySearchFieldContentKey, config.SearchFieldContent);

            return(drewSearchGUI);
        }
        private static async Task SearchProfileField(UnityEngine.Object profile, SearchConfig config, List <ProfileSearchResult> searchResults)
        {
            await Task.Yield();

            // The result that we will return, if not empty
            ProfileSearchResult result = new ProfileSearchResult();

            result.Profile = profile;
            BaseMixedRealityProfile baseProfile = (profile as BaseMixedRealityProfile);

            result.IsCustomProfile = (baseProfile != null) ? baseProfile.IsCustomProfile : false;
            searchResults.Add(result);

            // Go through the profile's serialized fields
            foreach (SerializedProperty property in GatherProperties(profile))
            {
                if (CheckFieldForProfile(property))
                {
                    await SearchProfileField(property.objectReferenceValue, config, searchResults);
                }
                else
                {
                    CheckFieldForKeywords(property, config, result);
                }
            }

            if (result.Fields.Count > 0)
            {
                result.Fields.Sort(delegate(FieldSearchResult r1, FieldSearchResult r2)
                {
                    if (r1.MatchStrength != r2.MatchStrength)
                    {
                        return(r2.MatchStrength.CompareTo(r1.MatchStrength));
                    }
                    return(r2.Property.name.CompareTo(r1.Property.name));
                });
            }
        }
        private static void DrawSearchResultInterface(UnityEngine.Object target)
        {
            bool optionsFoldout = SessionState.GetBool(searchDisplayOptionsFoldoutKey, false);

            optionsFoldout = EditorGUILayout.Foldout(optionsFoldout, "Search Preferences", true);
            SessionState.SetBool(searchDisplayOptionsFoldoutKey, optionsFoldout);
            if (optionsFoldout)
            {
                config.RequireAllKeywords = EditorGUILayout.Toggle("Require All Keywords", config.RequireAllKeywords);
                config.SearchTooltips     = EditorGUILayout.Toggle("Search Tooltips", config.SearchTooltips);
                config.SearchFieldContent = EditorGUILayout.Toggle("Search Field Content", config.SearchFieldContent);
            }

            if (!config.Equals(prevConfig) && !MixedRealitySearchUtility.Searching)
            {
                MixedRealitySearchUtility.StartProfileSearch(target, config, OnSearchComplete);
                searchResults.Clear();
                prevConfig = config;
            }

            #region display results

            using (new EditorGUILayout.VerticalScope())
            {
                if (searchResults.Count == 0)
                {
                    if (MixedRealitySearchUtility.Searching)
                    {
                        EditorGUILayout.HelpBox("Searching...", MessageType.Info);
                    }
                    else
                    {
                        EditorGUILayout.HelpBox("No search results. Try selecting a subject or entering a keyword.", MessageType.Warning);
                    }
                }

                int numDisplayedSearchResults = 0;
                if (searchResults.Count > 0)
                {
                    EditorGUILayout.Space();
                    EditorGUILayout.LabelField("Results:");
                    foreach (ProfileSearchResult search in searchResults)
                    {
                        if (search.Fields.Count == 0)
                        {   // Don't display results with no fields
                            continue;
                        }

                        using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
                        {
                            using (new EditorGUILayout.HorizontalScope())
                            {
                                EditorGUILayout.LabelField("Fields found in: ", EditorStyles.boldLabel, GUILayout.MaxWidth(105));
                                EditorGUILayout.ObjectField(search.Profile, typeof(UnityEngine.Object), false, GUILayout.ExpandWidth(true));
                                if (GUILayout.Button("View Asset", GUILayout.MaxWidth(75)))
                                {
                                    Selection.activeObject = search.Profile;
                                    EditorGUIUtility.PingObject(search.Profile);
                                }
                            }

                            if (MixedRealityProjectPreferences.LockProfiles && !search.IsCustomProfile)
                            {
                                EditorGUILayout.HelpBox("Clone this profile to edit default properties.", MessageType.Warning);
                            }

                            using (new EditorGUI.DisabledGroupScope(MixedRealityProjectPreferences.LockProfiles && !search.IsCustomProfile))
                            {
                                using (new EditorGUI.IndentLevelScope(1))
                                {
                                    EditorGUILayout.Space();

                                    foreach (FieldSearchResult r in search.Fields)
                                    {
                                        numDisplayedSearchResults++;

                                        GUI.color = Color.white;
                                        EditorGUI.BeginChangeCheck();
                                        EditorGUILayout.PropertyField(r.Property, true);

                                        if (EditorGUI.EndChangeCheck())
                                        {
                                            r.Property.serializedObject.ApplyModifiedProperties();
                                        }

                                        EditorGUILayout.Space();
                                    }
                                }
                            }
                        }

                        EditorGUILayout.Space();
                    }
                }
            }

            #endregion
        }
        /// <summary>
        /// Starts a profile search. 'Searching' must be false or an exception will be thrown.
        /// </summary>
        /// <param name="profile">Profile object to search.</param>
        /// <param name="config">Configuration for the search.</param>
        /// <param name="onSearchComplete">Action to invoke once search is complete - delivers final results.</param>
        public static async void StartProfileSearch(UnityEngine.Object profile, SearchConfig config, Action <bool, UnityEngine.Object, IReadOnlyCollection <ProfileSearchResult> > onSearchComplete)
        {
            if (activeTask != null && !activeTask.IsCompleted)
            {
                throw new Exception("Can't start a new search until the old one has completed.");
            }

            List <ProfileSearchResult> searchResults = new List <ProfileSearchResult>();

            // Validate search configuration
            if (string.IsNullOrEmpty(config.SearchFieldString))
            {   // If the config is empty, bail early
                onSearchComplete?.Invoke(true, profile, searchResults);
                return;
            }

            // Generate keywords if we haven't yet
            if (config.Keywords == null)
            {
                config.Keywords = new HashSet <string>(config.SearchFieldString.Split(new string[] { " ", "," }, StringSplitOptions.RemoveEmptyEntries));
                config.Keywords.RemoveWhere(s => s.Length < minSearchStringLength);
            }

            if (config.Keywords.Count == 0)
            {   // If there are no useful keywords, bail early
                onSearchComplete?.Invoke(true, profile, searchResults);
                return;
            }

            // Launch the search task
            bool cancelled = false;

            try
            {
                activeTask = SearchProfileField(profile, config, searchResults);
                await activeTask;
            }
            catch (Exception e)
            {
                // Profile was probably deleted in the middle of searching.
                Debug.LogException(e);
                cancelled = true;
            }
            finally
            {
                searchResults.Sort(delegate(ProfileSearchResult r1, ProfileSearchResult r2)
                {
                    if (r1.ProfileMatchStrength != r2.ProfileMatchStrength)
                    {
                        return(r2.ProfileMatchStrength.CompareTo(r1.ProfileMatchStrength));
                    }
                    else
                    {
                        return(r2.Profile.name.CompareTo(r1.Profile.name));
                    }
                });

                searchResults.RemoveAll(r => r.Fields.Count <= 0);

                onSearchComplete?.Invoke(cancelled, profile, searchResults);
            }
        }
        private static void CheckFieldForKeywords(SerializedProperty property, SearchConfig config, ProfileSearchResult result)
        {
            int    numMatchedKeywords = 0;
            int    numExactMatches    = 0;
            int    numFieldMatches    = 0;
            int    numTooltipMatches  = 0;
            int    numContentMatches  = 0;
            string propertyName       = property.name.ToLower();
            string toolTip            = property.tooltip.ToLower();

            foreach (string keyword in config.Keywords)
            {
                bool keywordMatch = false;

                if (propertyName.Contains(keyword))
                {
                    keywordMatch = true;
                    numFieldMatches++;
                    if (propertyName == keyword)
                    {
                        numExactMatches++;
                    }
                }

                if (config.SearchTooltips)
                {
                    if (toolTip.Contains(keyword))
                    {
                        keywordMatch = true;
                        numTooltipMatches++;
                    }
                }

                if (config.SearchFieldContent)
                {
                    switch (property.propertyType)
                    {
                    case SerializedPropertyType.ObjectReference:
                        if (property.objectReferenceValue != null && property.objectReferenceValue.name.ToLower().Contains(keyword))
                        {
                            keywordMatch = true;
                            numContentMatches++;
                        }
                        break;

                    case SerializedPropertyType.String:
                        if (!string.IsNullOrEmpty(property.stringValue) && property.stringValue.ToLower().Contains(keyword))
                        {
                            keywordMatch = true;
                            numContentMatches++;
                        }
                        break;
                    }
                }

                if (keywordMatch)
                {
                    numMatchedKeywords++;
                }
            }

            bool requirementsMet = numMatchedKeywords > 0;

            if (config.RequireAllKeywords && config.Keywords.Count > 1)
            {
                requirementsMet &= numMatchedKeywords >= config.Keywords.Count;
            }

            if (requirementsMet)
            {
                int matchStrength = numMatchedKeywords + numExactMatches;
                if (numMatchedKeywords >= config.Keywords.Count)
                {   // If we match all keywords in a multi-keyword search, double the score
                    matchStrength *= 2;
                }

                // Weight the score based on match type
                matchStrength += numFieldMatches * 3;
                matchStrength += numTooltipMatches * 2;
                matchStrength += numContentMatches * 1;

                result.ProfileMatchStrength += matchStrength;
                result.Fields.Add(new FieldSearchResult()
                {
                    Property      = property.Copy(),
                    MatchStrength = numMatchedKeywords,
                });
            }
        }