using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace UnityEditor.SettingsManagement
{
    static class SettingsGUIStyles
    {
        const string k_SettingsGearIcon = "Packages/" + UserSettings.packageName + "/Content/Options.png";

        static bool s_Initialized;
        public static GUIStyle s_SettingsGizmo;
        public static GUIStyle s_SettingsArea;
        public static GUIStyle s_IndentedSettingBlock;

        static void Init()
        {
            if (s_Initialized)
                return;

            s_Initialized = true;

            s_SettingsGizmo = new GUIStyle()
            {
                normal = new GUIStyleState()
                {
                    background = AssetDatabase.LoadAssetAtPath<Texture2D>(k_SettingsGearIcon)
                },
                fixedWidth = 14,
                fixedHeight = 14,
                padding = new RectOffset(0, 0, 0, 0),
                margin = new RectOffset(4, 4, 4, 4),
                imagePosition = ImagePosition.ImageOnly
            };

            s_SettingsArea = new GUIStyle()
            {
                margin = new RectOffset(6, 6, 0, 0)
            };

            s_IndentedSettingBlock = new GUIStyle()
            {
                padding = new RectOffset(16, 0, 0, 0)
            };
        }

        public static GUIStyle settingsGizmo
        {
            get
            {
                Init();
                return s_SettingsGizmo;
            }
        }

        public static GUIStyle settingsArea
        {
            get
            {
                Init();
                return s_SettingsArea;
            }
        }

        public static GUIStyle indentedSettingBlock
        {
            get
            {
                Init();
                return s_IndentedSettingBlock;
            }
        }
    }

    /// <summary>
    /// Extension methods for GUILayout that also implement settings-specific functionality.
    /// </summary>
    public static class SettingsGUILayout
    {
        /// <inheritdoc />
        /// <summary>
        /// Create an indented GUI section.
        /// </summary>
        public class IndentedGroup : IDisposable
        {
            bool m_IsDisposed;

            /// <summary>
            /// Create an indented GUI section.
            /// </summary>
            public IndentedGroup()
            {
                EditorGUIUtility.labelWidth -= SettingsGUIStyles.indentedSettingBlock.padding.left - 4;
                GUILayout.BeginVertical(SettingsGUIStyles.indentedSettingBlock);
            }

            /// <summary>
            /// Create an indented GUI section with a header.
            /// </summary>
            public IndentedGroup(string label)
            {
                GUILayout.Label(label);
                EditorGUIUtility.labelWidth -= SettingsGUIStyles.indentedSettingBlock.padding.left - 4;
                GUILayout.BeginVertical(SettingsGUIStyles.indentedSettingBlock);
            }

            /// <inheritdoc />
            /// <summary>
            /// Revert the GUI indent back to it's original value.
            /// </summary>
            public void Dispose()
            {
                if (m_IsDisposed)
                    return;
                m_IsDisposed = true;
                GUILayout.EndVertical();
                EditorGUIUtility.labelWidth += SettingsGUIStyles.indentedSettingBlock.padding.left - 4;
            }
        }

        internal static HashSet<string> s_Keywords = null;

        internal static bool MatchSearchGroups(string searchContext, string label)
        {
            if (s_Keywords != null)
            {
                foreach (var keyword in label.Split(' '))
                    s_Keywords.Add(keyword);
            }

            if (searchContext == null)
                return true;

            var ctx = searchContext.Trim();

            if (string.IsNullOrEmpty(ctx))
                return true;

            var split = searchContext.Split(' ');

            return split.Any(x => !string.IsNullOrEmpty(x) && label.IndexOf(x, StringComparison.InvariantCultureIgnoreCase) > -1);
        }

        internal static bool DebugModeFilter(IUserSetting pref)
        {
            if (!EditorPrefs.GetBool("DeveloperMode", false))
                return true;

            if (pref.scope == SettingsScope.Project && UserSettingsProvider.showProjectSettings)
                return true;

            if (pref.scope == SettingsScope.User && UserSettingsProvider.showUserSettings)
                return true;

            return false;
        }

        /// <summary>
        /// A slider that implements search filtering.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="min">The value at the left end of the slider.</param>
        /// <param name="max">The value at the right end of the slider.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static float SearchableSlider(GUIContent label, float value, float min, float max, string searchContext)
        {
            if (!MatchSearchGroups(searchContext, label.text))
                return value;
            return EditorGUILayout.Slider(label, value, min, max);
        }

        /// <summary>
        /// A slider that implements search filtering.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="min">The value at the left end of the slider.</param>
        /// <param name="max">The value at the right end of the slider.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static float SearchableSlider(string label, float value, float min, float max, string searchContext)
        {
            if (!MatchSearchGroups(searchContext, label))
                return value;
            return EditorGUILayout.Slider(label, value, min, max);
        }

        /// <summary>
        /// A float field that implements search filtering.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static float SearchableFloatField(GUIContent label, float value, string searchContext)
        {
            if (!MatchSearchGroups(searchContext, label.text))
                return value;
            return EditorGUILayout.FloatField(label, value);
        }

        /// <summary>
        /// A float field that implements search filtering.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static float SearchableFloatField(string label, float value, string searchContext)
        {
            if (!MatchSearchGroups(searchContext, label))
                return value;
            return EditorGUILayout.FloatField(label, value);
        }

        /// <summary>
        /// An int field that implements search filtering.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static int SearchableIntField(GUIContent label, int value, string searchContext)
        {
            if (!MatchSearchGroups(searchContext, label.text))
                return value;
            return EditorGUILayout.IntField(label, value);
        }

        /// <summary>
        /// An int field that implements search filtering.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static int SearchableIntField(string label, int value, string searchContext)
        {
            if (!MatchSearchGroups(searchContext, label))
                return value;
            return EditorGUILayout.IntField(label, value);
        }

        /// <summary>
        /// An toggle field that implements search filtering.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static bool SearchableToggle(GUIContent label, bool value, string searchContext)
        {
            if (!MatchSearchGroups(searchContext, label.text))
                return value;
            return EditorGUILayout.Toggle(label, value);
        }

        /// <summary>
        /// An toggle field that implements search filtering.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static bool SearchableToggle(string label, bool value, string searchContext)
        {
            if (!MatchSearchGroups(searchContext, label))
                return value;
            return EditorGUILayout.Toggle(label, value);
        }

        /// <summary>
        /// An text field that implements search filtering.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static string SearchableTextField(GUIContent label, string value, string searchContext)
        {
            if (!MatchSearchGroups(searchContext, label.text))
                return value;
            return EditorGUILayout.TextField(label, value);
        }

        /// <summary>
        /// An text field that implements search filtering.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static string SearchableTextField(string label, string value, string searchContext)
        {
            if (!MatchSearchGroups(searchContext, label))
                return value;
            return EditorGUILayout.TextField(label, value);
        }

        /// <summary>
        /// An color field that implements search filtering.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static Color SearchableColorField(GUIContent label, Color value, string searchContext)
        {
            if (!MatchSearchGroups(searchContext, label.text))
                return value;
            return EditorGUILayout.ColorField(label, value);
        }

        /// <summary>
        /// An color field that implements search filtering.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static Color SearchableColorField(string label, Color value, string searchContext)
        {
            if (!MatchSearchGroups(searchContext, label))
                return value;
            return EditorGUILayout.ColorField(label, value);
        }

        /// <summary>
        /// A slider that implements search filtering and context menu reset.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="min">The value at the left end of the slider.</param>
        /// <param name="max">The value at the right end of the slider.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static float SettingsSlider(GUIContent label, UserSetting<float> value, float min, float max, string searchContext)
        {
            if (!DebugModeFilter(value) || !MatchSearchGroups(searchContext, label.text))
                return value;
            var res = EditorGUILayout.Slider(label, value, min, max);
            DoResetContextMenuForLastRect(value);
            return res;
        }

        /// <summary>
        /// A slider that implements search filtering and context menu reset.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="min">The value at the left end of the slider.</param>
        /// <param name="max">The value at the right end of the slider.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static float SettingsSlider(string label, UserSetting<float> value, float min, float max, string searchContext)
        {
            if (!DebugModeFilter(value) || !MatchSearchGroups(searchContext, label))
                return value;
            var res = EditorGUILayout.Slider(label, value, min, max);
            DoResetContextMenuForLastRect(value);
            return res;
        }

        /// <summary>
        /// A slider that implements search filtering and context menu reset.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="min">The value at the left end of the slider.</param>
        /// <param name="max">The value at the right end of the slider.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static int SettingsSlider(GUIContent label, UserSetting<int> value, int min, int max, string searchContext)
        {
            if (!DebugModeFilter(value) || !MatchSearchGroups(searchContext, label.text))
                return value;
            var res = EditorGUILayout.IntSlider(label, value, min, max);
            DoResetContextMenuForLastRect(value);
            return res;
        }

        /// <summary>
        /// A slider that implements search filtering and context menu reset.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="min">The value at the left end of the slider.</param>
        /// <param name="max">The value at the right end of the slider.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static int SettingsSlider(string label, UserSetting<int> value, int min, int max, string searchContext)
        {
            if (!DebugModeFilter(value) || !MatchSearchGroups(searchContext, label))
                return value;
            var res = EditorGUILayout.IntSlider(label, value, min, max);
            DoResetContextMenuForLastRect(value);
            return res;
        }

        /// <summary>
        /// A float field that implements search filtering and context menu reset.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static float SettingsFloatField(GUIContent label, UserSetting<float> value, string searchContext)
        {
            if (!DebugModeFilter(value) || !MatchSearchGroups(searchContext, label.text))
                return value;
            var res = EditorGUILayout.FloatField(label, value);
            DoResetContextMenuForLastRect(value);
            return res;
        }

        /// <summary>
        /// A float field that implements search filtering and context menu reset.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static float SettingsFloatField(string label, UserSetting<float> value, string searchContext)
        {
            if (!DebugModeFilter(value) || !MatchSearchGroups(searchContext, label))
                return value;
            var res = EditorGUILayout.FloatField(label, value);
            DoResetContextMenuForLastRect(value);
            return res;
        }

        /// <summary>
        /// An integer field that implements search filtering and context menu reset.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static int SettingsIntField(GUIContent label, UserSetting<int> value, string searchContext)
        {
            if (!DebugModeFilter(value) || !MatchSearchGroups(searchContext, label.text))
                return value;
            var res = EditorGUILayout.IntField(label, value);
            DoResetContextMenuForLastRect(value);
            return res;
        }

        /// <summary>
        /// An integer field that implements search filtering and context menu reset.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static int SettingsIntField(string label, UserSetting<int> value, string searchContext)
        {
            if (!DebugModeFilter(value) || !MatchSearchGroups(searchContext, label))
                return value;
            var res = EditorGUILayout.IntField(label, value);
            DoResetContextMenuForLastRect(value);
            return res;
        }

        /// <summary>
        /// A boolean toggle field that implements search filtering and context menu reset.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static bool SettingsToggle(GUIContent label, UserSetting<bool> value, string searchContext)
        {
            if (!DebugModeFilter(value) || !MatchSearchGroups(searchContext, label.text))
                return value;
            var res = EditorGUILayout.Toggle(label, value);
            DoResetContextMenuForLastRect(value);
            return res;
        }

        /// <summary>
        /// A boolean toggle field that implements search filtering and context menu reset.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static bool SettingsToggle(string label, UserSetting<bool> value, string searchContext)
        {
            if (!DebugModeFilter(value) || !MatchSearchGroups(searchContext, label))
                return value;
            var res = EditorGUILayout.Toggle(label, value);
            DoResetContextMenuForLastRect(value);
            return res;
        }

        /// <summary>
        /// A text field that implements search filtering and context menu reset.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static string SettingsTextField(GUIContent label, UserSetting<string> value, string searchContext)
        {
            if (!DebugModeFilter(value) || !MatchSearchGroups(searchContext, label.text))
                return value;
            var res = EditorGUILayout.TextField(label, value);
            DoResetContextMenuForLastRect(value);
            return res;
        }

        /// <summary>
        /// A text field that implements search filtering and context menu reset.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static string SettingsTextField(string label, UserSetting<string> value, string searchContext)
        {
            if (!DebugModeFilter(value) || !MatchSearchGroups(searchContext, label))
                return value;
            var res = EditorGUILayout.TextField(label, value);
            DoResetContextMenuForLastRect(value);
            return res;
        }

        /// <summary>
        /// A color field that implements search filtering and context menu reset.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static Color SettingsColorField(GUIContent label, UserSetting<Color> value, string searchContext)
        {
            if (!DebugModeFilter(value) || !MatchSearchGroups(searchContext, label.text))
                return value;
            var res = EditorGUILayout.ColorField(label, value);
            DoResetContextMenuForLastRect(value);
            return res;
        }

        /// <summary>
        /// A color field that implements search filtering and context menu reset.
        /// </summary>
        /// <param name="label">Label in front of the value field.</param>
        /// <param name="value">The value to edit.</param>
        /// <param name="searchContext">A string representing the current search query. Empty or null strings are to be treated as matching any value.</param>
        /// <returns>The value that has been set by the user.</returns>
        public static Color SettingsColorField(string label, UserSetting<Color> value, string searchContext)
        {
            if (!DebugModeFilter(value) || !MatchSearchGroups(searchContext, label))
                return value;
            var res = EditorGUILayout.ColorField(label, value);
            DoResetContextMenuForLastRect(value);
            return res;
        }

        /// <summary>
        /// Using the last automatically layoutted rect, implement a context click menu for a user setting.
        /// </summary>
        /// <param name="setting">The target setting for the reset context menu.</param>
        public static void DoResetContextMenuForLastRect(IUserSetting setting)
        {
            DoResetContextMenu(GUILayoutUtility.GetLastRect(), setting);
        }

        static void DoResetContextMenu(Rect rect, IUserSetting pref)
        {
            var evt = Event.current;

            if (evt.type == EventType.ContextClick && rect.Contains(evt.mousePosition))
            {
                var menu = new GenericMenu();
                menu.AddItem(new GUIContent("Reset [" + pref.scope + "] " + pref.key), false, () =>
                    {
                        pref.Reset(true);
                    });
                menu.ShowAsContext();
            }
        }
    }
}
