#if UNITY_2018_3_OR_NEWER
#define SETTINGS_PROVIDER_ENABLED
#endif

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using UnityEngine;
#if SETTINGS_PROVIDER_ENABLED
#if UNITY_2019_1_OR_NEWER
using UnityEngine.UIElements;
#else
using UnityEngine.Experimental.UIElements;
#endif
#endif

namespace UnityEditor.SettingsManagement
{
    /// <summary>
    /// A <see cref="UnityEditor.SettingsProvider"/> implementation that creates an interface from settings reflected
    /// from a collection of assemblies.
    /// </summary>
#if SETTINGS_PROVIDER_ENABLED
    public sealed class UserSettingsProvider : SettingsProvider
#else
    public sealed class UserSettingsProvider
#endif
    {
        /// <summary>
        /// Category string constant to check whether Unity is running in developer (internal) mode.
        /// </summary>
        public const string developerModeCategory = "Developer Mode";

        const string k_SettingsName = "UserSettingsProviderSettings";

#if SETTINGS_PROVIDER_ENABLED
        const int k_LabelWidth = 240;

        static int labelWidth
        {
            get
            {
                if (s_DefaultLabelWidth != null)
                    return (int)((float)s_DefaultLabelWidth.GetValue(null, null));

                return k_LabelWidth;
            }
        }

        static int defaultLayoutMaxWidth
        {
            get
            {
                if (s_DefaultLayoutMaxWidth != null)
                    return (int)((float)s_DefaultLayoutMaxWidth.GetValue(null, null));

                return 0;
            }
        }
#else
        const int k_LabelWidth = 180;

        int labelWidth
        {
            get { return k_LabelWidth; }
        }

        int defaultLayoutMaxWidth
        {
            get { return 0; }
        }
#endif

        List<string> m_Categories;
        Dictionary<string, List<PrefEntry>> m_Settings;
        Dictionary<string, List<MethodInfo>> m_SettingBlocks;
#if !SETTINGS_PROVIDER_ENABLED
        HashSet<string> keywords = new HashSet<string>();
#endif
        static readonly string[] s_SearchContext = new string[1];
        EventType m_SettingsBlockKeywordsInitialized;
        Assembly[] m_Assemblies;
        static Settings s_Settings;
        Settings m_SettingsInstance;

#if SETTINGS_PROVIDER_ENABLED
        static PropertyInfo s_DefaultLabelWidth;
        static PropertyInfo s_DefaultLayoutMaxWidth;
#endif

        static Settings userSettingsProviderSettings
        {
            get
            {
                if (s_Settings == null)
                    s_Settings = new Settings(new[] { new UserSettingsRepository() });

                return s_Settings;
            }
        }

        internal static UserSetting<bool> showHiddenSettings = new UserSetting<bool>(userSettingsProviderSettings, "settings.showHidden", false, SettingsScope.User);
        internal static UserSetting<bool> showUnregisteredSettings = new UserSetting<bool>(userSettingsProviderSettings, "settings.showUnregistered", false, SettingsScope.User);
        internal static UserSetting<bool> listByKey = new UserSetting<bool>(userSettingsProviderSettings, "settings.listByKey", false, SettingsScope.User);
        internal static UserSetting<bool> showUserSettings = new UserSetting<bool>(userSettingsProviderSettings, "settings.showUserSettings", true, SettingsScope.User);
        internal static UserSetting<bool> showProjectSettings = new UserSetting<bool>(userSettingsProviderSettings, "settings.showProjectSettings", true, SettingsScope.User);

#if SETTINGS_PROVIDER_ENABLED
        /// <summary>
        /// Initializes and returns a new `UserSettingsProvider` instance.
        /// </summary>
        /// <param name="path">The settings menu path.</param>
        /// <param name="settings">The Settings instance that this provider is inspecting.</param>
        /// <param name="assemblies">A collection of assemblies to scan for <see cref="UserSettingAttribute"/> and <see cref="UserSettingBlockAttribute"/> attributes.</param>
        /// <param name="scopes">Which scopes this provider is valid for.</param>
        /// <exception cref="ArgumentNullException">Thrown if settings or assemblies is null.</exception>
        public UserSettingsProvider(string path, Settings settings, Assembly[] assemblies, SettingsScope scopes = SettingsScope.User)
            : base(path, scopes)
#else
        /// <summary>
        /// Initializes and returns a new `UserSettingsProvider` instance.
        /// </summary>
        /// <param name="settings">The <see cref="Settings"/> instance that this provider is inspecting.</param>
        /// <param name="assemblies">A collection of assemblies to scan for <see cref="UserSettingAttribute"/> and <see cref="UserSettingBlockAttribute"/> attributes.</param>
        public UserSettingsProvider(Settings settings, Assembly[] assemblies)
#endif
        {
            if (settings == null)
                throw new ArgumentNullException("settings");

            if (assemblies == null)
                throw new ArgumentNullException("assemblies");

            m_SettingsInstance = settings;
            m_Assemblies = assemblies;

#if !SETTINGS_PROVIDER_ENABLED
            SearchForUserSettingAttributes();
#endif
        }

#if SETTINGS_PROVIDER_ENABLED
        /// <summary>
        /// Invoked by the <see cref="UnityEditor.SettingsProvider"/> when activated in the Editor.
        /// </summary>
        /// <param name="searchContext">
        /// Search context in the search box on the
        /// [Settings](https://docs.unity3d.com/Manual/comp-ManagerGroup.html) window.
        /// </param>
        /// <param name="rootElement">
        /// Root of the UIElements tree. If you add to this root, the SettingsProvider uses
        /// [UIElements](https://docs.unity3d.com/ScriptReference/UnityEngine.UIElementsModule.html)
        /// instead of calling <see cref="OnGUI"/> to build the UI.
        /// See <see cref="UnityEditor.SettingsProvider.OnActivate"/> for details.
        /// </param>
        public override void OnActivate(string searchContext, VisualElement rootElement)
        {
            base.OnActivate(searchContext, rootElement);

            SearchForUserSettingAttributes();

            var window = GetType().GetProperty("settingsWindow", BindingFlags.Instance | BindingFlags.NonPublic);

            if (window != null)
            {
                s_DefaultLabelWidth = window.PropertyType.GetProperty("s_DefaultLabelWidth", BindingFlags.Public | BindingFlags.Static);
                s_DefaultLayoutMaxWidth = window.PropertyType.GetProperty("s_DefaultLayoutMaxWidth", BindingFlags.Public | BindingFlags.Static);
            }
        }
#endif

        struct PrefEntry
        {
            GUIContent m_Content;
            IUserSetting m_Pref;

            public GUIContent content
            {
                get { return m_Content; }
            }

            public IUserSetting pref
            {
                get { return m_Pref; }
            }

            public PrefEntry(GUIContent content, IUserSetting pref)
            {
                m_Content = content;
                m_Pref = pref;
            }
        }

        void SearchForUserSettingAttributes()
        {
            var isDeveloperMode = EditorPrefs.GetBool("DeveloperMode", false);
            var keywordsHash = new HashSet<string>();

            if (m_Settings != null)
                m_Settings.Clear();
            else
                m_Settings = new Dictionary<string, List<PrefEntry>>();

            if (m_SettingBlocks != null)
                m_SettingBlocks.Clear();
            else
                m_SettingBlocks = new Dictionary<string, List<MethodInfo>>();

            var types = m_Assemblies.SelectMany(x => x.GetTypes());

            // collect instance fields/methods too, but only so we can throw a warning that they're invalid.
            var fields = types.SelectMany(x =>
                    x.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)
                    .Where(prop => Attribute.IsDefined(prop, typeof(UserSettingAttribute))));

            var methods = types.SelectMany(x => x.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
                    .Where(y => Attribute.IsDefined(y, typeof(UserSettingBlockAttribute))));

            foreach (var field in fields)
            {
                if (!field.IsStatic)
                {
                    Debug.LogWarning("Cannot create setting entries for instance fields. Skipping \"" + field.Name + "\".");
                    continue;
                }

                var attrib = (UserSettingAttribute)Attribute.GetCustomAttribute(field, typeof(UserSettingAttribute));

                if (!attrib.visibleInSettingsProvider)
                    continue;

                var pref = (IUserSetting)field.GetValue(null);

                if (pref == null)
                {
                    Debug.LogWarning("[UserSettingAttribute] is only valid for types implementing the IUserSetting interface. Skipping \"" + field.Name + "\"");
                    continue;
                }

                var category = string.IsNullOrEmpty(attrib.category) ? "Uncategorized" : attrib.category;
                var content = listByKey ? new GUIContent(pref.key) : attrib.title;

                if (developerModeCategory.Equals(category) && !isDeveloperMode)
                    continue;

                List<PrefEntry> settings;

                if (m_Settings.TryGetValue(category, out settings))
                    settings.Add(new PrefEntry(content, pref));
                else
                    m_Settings.Add(category, new List<PrefEntry>() { new PrefEntry(content, pref) });
            }

            foreach (var method in methods)
            {
                var attrib = (UserSettingBlockAttribute)Attribute.GetCustomAttribute(method, typeof(UserSettingBlockAttribute));
                var category = string.IsNullOrEmpty(attrib.category) ? "Uncategorized" : attrib.category;

                if (developerModeCategory.Equals(category) && !isDeveloperMode)
                    continue;

                List<MethodInfo> blocks;

                var parameters = method.GetParameters();

                if (!method.IsStatic || parameters.Length < 1 || parameters[0].ParameterType != typeof(string))
                {
                    Debug.LogWarning("[UserSettingBlockAttribute] is only valid for static functions with a single string parameter. Ex, `static void MySettings(string searchContext)`. Skipping \"" + method.Name + "\"");
                    continue;
                }

                if (m_SettingBlocks.TryGetValue(category, out blocks))
                    blocks.Add(method);
                else
                    m_SettingBlocks.Add(category, new List<MethodInfo>() { method });
            }

            if (showHiddenSettings)
            {
                var unlisted = new List<PrefEntry>();
                m_Settings.Add("Unlisted", unlisted);
                foreach (var pref in UserSettings.FindUserSettings(m_Assemblies, SettingVisibility.Unlisted | SettingVisibility.Hidden))
                    unlisted.Add(new PrefEntry(new GUIContent(pref.key), pref));
            }

            if (showUnregisteredSettings)
            {
                var unregistered = new List<PrefEntry>();
                m_Settings.Add("Unregistered", unregistered);
                foreach (var pref in UserSettings.FindUserSettings(m_Assemblies, SettingVisibility.Unregistered))
                    unregistered.Add(new PrefEntry(new GUIContent(pref.key), pref));
            }

            foreach (var cat in m_Settings)
            {
                foreach (var entry in cat.Value)
                {
                    var content = entry.content;

                    if (content != null && !string.IsNullOrEmpty(content.text))
                    {
                        foreach (var word in content.text.Split(' '))
                            keywordsHash.Add(word);
                    }
                }
            }

            keywords = keywordsHash;
            m_Categories = m_Settings.Keys.Union(m_SettingBlocks.Keys).ToList();
            m_Categories.Sort();
        }

#if SETTINGS_PROVIDER_ENABLED
        /// <summary>
        /// Invoked by the SettingsProvider container when drawing the UI header.
        /// </summary>
        public override void OnTitleBarGUI()
        {
            if (GUILayout.Button(GUIContent.none, SettingsGUIStyles.settingsGizmo))
                DoContextMenu();
        }
#endif

        void InitSettingsBlockKeywords()
        {
            // Have to let the blocks run twice - one for Layout, one for Repaint.
            if (m_SettingsBlockKeywordsInitialized == EventType.Repaint)
                return;

            m_SettingsBlockKeywordsInitialized = Event.current.type;

            // Allows SettingsGUILayout.SettingsField to populate keywords
            SettingsGUILayout.s_Keywords = new HashSet<string>(keywords);

            // Set a dummy value so that GUI blocks with conditional foldouts will behave as though searching.
            s_SearchContext[0] = "Search";

            foreach (var category in m_SettingBlocks)
            {
                foreach (var block in category.Value)
                    block.Invoke(null, s_SearchContext);
            }

            keywords = SettingsGUILayout.s_Keywords;
            SettingsGUILayout.s_Keywords = null;
            s_SearchContext[0] = "";
        }

        void DoContextMenu()
        {
            var menu = new GenericMenu();

            menu.AddItem(new GUIContent("Reset All"), false, () =>
                {
                    if (!UnityEditor.EditorUtility.DisplayDialog("Reset All Settings", "Reset all settings? This is not undo-able.", "Reset", "Cancel"))
                        return;

                    // Do not reset SettingVisibility.Unregistered
                    foreach (var pref in UserSettings.FindUserSettings(m_Assemblies, SettingVisibility.Visible | SettingVisibility.Hidden | SettingVisibility.Unlisted))
                        pref.Reset();

                    m_SettingsInstance.Save();
                });

            if (EditorPrefs.GetBool("DeveloperMode", false))
            {
                menu.AddSeparator("");

                menu.AddItem(new GUIContent("Developer/List Settings By Key"), listByKey, () =>
                    {
                        listByKey.SetValue(!listByKey, true);
                        SearchForUserSettingAttributes();
                    });

                menu.AddSeparator("Developer/");

                menu.AddItem(new GUIContent("Developer/Show User Settings"), showUserSettings, () =>
                    {
                        showUserSettings.SetValue(!showUserSettings, true);
                        SearchForUserSettingAttributes();
                    });

                menu.AddItem(new GUIContent("Developer/Show Project Settings"), showProjectSettings, () =>
                    {
                        showProjectSettings.SetValue(!showProjectSettings, true);
                        SearchForUserSettingAttributes();
                    });

                menu.AddSeparator("Developer/");

                menu.AddItem(new GUIContent("Developer/Show Unlisted Settings"), showHiddenSettings, () =>
                    {
                        showHiddenSettings.SetValue(!showHiddenSettings, true);
                        SearchForUserSettingAttributes();
                    });

                menu.AddItem(new GUIContent("Developer/Show Unregistered Settings"), showUnregisteredSettings, () =>
                    {
                        showUnregisteredSettings.SetValue(!showUnregisteredSettings, true);
                        SearchForUserSettingAttributes();
                    });

                menu.AddSeparator("Developer/");

                menu.AddItem(new GUIContent("Developer/Open Project Settings File"), false, () =>
                    {
                        var project = m_SettingsInstance.GetRepository(SettingsScope.Project);

                        if (project != null)
                        {
                            var path = Path.GetFullPath(project.path);
                            System.Diagnostics.Process.Start(path);
                        }
                    });

                menu.AddItem(new GUIContent("Developer/Print All Settings"), false, () =>
                    {
                        Debug.Log(UserSettings.GetSettingsString(m_Assemblies));
                    });

#if UNITY_2019_1_OR_NEWER
                menu.AddSeparator("Developer/");
#if UNITY_2019_3_OR_NEWER
                menu.AddItem(new GUIContent("Developer/Recompile Scripts"), false, EditorUtility.RequestScriptReload);
#else
                menu.AddItem(new GUIContent("Developer/Recompile Scripts"), false, UnityEditorInternal.InternalEditorUtility.RequestScriptReload);
#endif
#endif
            }

            menu.ShowAsContext();
        }

#if SETTINGS_PROVIDER_ENABLED
        /// <summary>
        /// Called when the Settings window opens in the Editor.
        /// </summary>
        /// <param name="searchContext">
        /// Search context in the search box on the
        /// [Settings](https://docs.unity3d.com/Manual/comp-ManagerGroup.html) window.
        /// </param>
        /// <seealso cref="UnityEditor.SettingsProvider.OnGUI"/>
        public override void OnGUI(string searchContext)
#else
        /// <summary>
        /// Called when the Settings window opens in the Editor.
        /// </summary>
        /// <param name="searchContext">
        /// Search context in the search box on the
        /// [Settings](https://docs.unity3d.com/Manual/comp-ManagerGroup.html) window.
        /// </param>
        /// <seealso cref="UnityEditor.SettingsProvider.OnGUI"/>
        public void OnGUI(string searchContext)
#endif
        {
#if !SETTINGS_PROVIDER_ENABLED
            var evt = Event.current;
            if (evt.type == EventType.ContextClick)
                DoContextMenu();
#endif
            InitSettingsBlockKeywords();

            EditorGUIUtility.labelWidth = labelWidth;

            EditorGUI.BeginChangeCheck();

            var maxWidth = defaultLayoutMaxWidth;

            if (maxWidth != 0)
                GUILayout.BeginVertical(SettingsGUIStyles.settingsArea, GUILayout.MaxWidth(maxWidth));
            else
                GUILayout.BeginVertical(SettingsGUIStyles.settingsArea);

            var hasSearchContext = !string.IsNullOrEmpty(searchContext);
            s_SearchContext[0] = searchContext;
            if (hasSearchContext)
            {
                // todo - Improve search comparison
                var searchKeywords = searchContext.Split(' ');

                foreach (var settingField in m_Settings)
                {
                    foreach (var setting in settingField.Value)
                    {
                        if (searchKeywords.Any(x => !string.IsNullOrEmpty(x) && setting.content.text.IndexOf(x, StringComparison.InvariantCultureIgnoreCase) > -1))
                            DoPreferenceField(setting.content, setting.pref);
                    }
                }

                foreach (var settingsBlock in m_SettingBlocks)
                {
                    foreach (var block in settingsBlock.Value)
                    {
                        block.Invoke(null, s_SearchContext);
                    }
                }
            }
            else
            {
                foreach (var key in m_Categories)
                {
                    GUILayout.Label(key, EditorStyles.boldLabel);

                    List<PrefEntry> settings;

                    if (m_Settings.TryGetValue(key, out settings))
                        foreach (var setting in settings)
                            DoPreferenceField(setting.content, setting.pref);

                    List<MethodInfo> blocks;

                    if (m_SettingBlocks.TryGetValue(key, out blocks))
                        foreach (var block in blocks)
                            block.Invoke(null, s_SearchContext);

                    GUILayout.Space(8);
                }
            }

            EditorGUIUtility.labelWidth = 0;

            GUILayout.EndVertical();

            if (EditorGUI.EndChangeCheck())
            {
                m_SettingsInstance.Save();
            }
        }

        void DoPreferenceField(GUIContent title, IUserSetting pref)
        {
            if (EditorPrefs.GetBool("DeveloperMode", false))
            {
                if (pref.scope == SettingsScope.Project && !showProjectSettings)
                    return;
                if (pref.scope == SettingsScope.User && !showUserSettings)
                    return;
            }

            if (pref is UserSetting<float>)
            {
                var cast = (UserSetting<float>)pref;
                cast.value = EditorGUILayout.FloatField(title, cast.value);
            }
            else if (pref is UserSetting<int>)
            {
                var cast = (UserSetting<int>)pref;
                cast.value = EditorGUILayout.IntField(title, cast.value);
            }
            else if (pref is UserSetting<bool>)
            {
                var cast = (UserSetting<bool>)pref;
                cast.value = EditorGUILayout.Toggle(title, cast.value);
            }
            else if (pref is UserSetting<string>)
            {
                var cast = (UserSetting<string>)pref;
                cast.value = EditorGUILayout.TextField(title, cast.value);
            }
            else if (pref is UserSetting<Color>)
            {
                var cast = (UserSetting<Color>)pref;
                cast.value = EditorGUILayout.ColorField(title, cast.value);
            }
#if UNITY_2018_3_OR_NEWER
            else if (pref is UserSetting<Gradient>)
            {
                var cast = (UserSetting<Gradient>)pref;
                cast.value = EditorGUILayout.GradientField(title, cast.value);
            }
#endif
            else if (pref is UserSetting<Vector2>)
            {
                var cast = (UserSetting<Vector2>)pref;
                cast.value = EditorGUILayout.Vector2Field(title, cast.value);
            }
            else if (pref is UserSetting<Vector3>)
            {
                var cast = (UserSetting<Vector3>)pref;
                cast.value = EditorGUILayout.Vector3Field(title, cast.value);
            }
            else if (pref is UserSetting<Vector4>)
            {
                var cast = (UserSetting<Vector4>)pref;
                cast.value = EditorGUILayout.Vector4Field(title, cast.value);
            }
            else if (typeof(Enum).IsAssignableFrom(pref.type))
            {
                Enum val = (Enum)pref.GetValue();
                EditorGUI.BeginChangeCheck();
                if (Attribute.IsDefined(pref.type, typeof(FlagsAttribute)))
                    val = EditorGUILayout.EnumFlagsField(title, val);
                else
                    val = EditorGUILayout.EnumPopup(title, val);
                if (EditorGUI.EndChangeCheck())
                    pref.SetValue(val);
            }
            else if (typeof(UnityEngine.Object).IsAssignableFrom(pref.type))
            {
                var obj = (UnityEngine.Object)pref.GetValue();
                EditorGUI.BeginChangeCheck();
                obj = EditorGUILayout.ObjectField(title, obj, pref.type, false);
                if (EditorGUI.EndChangeCheck())
                    pref.SetValue(obj);
            }
            else
            {
                GUILayout.BeginHorizontal();
                GUILayout.Label(title, GUILayout.Width(EditorGUIUtility.labelWidth - EditorStyles.label.margin.right * 2));
                var obj = pref.GetValue();
                GUILayout.Label(obj == null ? "null" : pref.GetValue().ToString());
                GUILayout.EndHorizontal();
            }

            SettingsGUILayout.DoResetContextMenuForLastRect(pref);
        }
    }
}
