#if UNITY_EDITOR
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEditor;
using UnityEditor.ShortcutManagement;
using UnityEngine.UIElements;
using UnityEditor.UIElements;

namespace UnityEngine.InputSystem.Editor
{
    internal class InputActionsEditorSettingsProvider : SettingsProvider
    {
        private static InputActionsEditorSettingsProvider s_Provider;

        public static string SettingsPath => InputSettingsPath.kSettingsRootPath;

        [SerializeField] InputActionsEditorState m_State;
        VisualElement m_RootVisualElement;
        private bool m_HasEditFocus;
        private bool m_IsActivated;
        private static bool m_IMGUIDropdownVisible;
        StateContainer m_StateContainer;
        private static InputActionsEditorSettingsProvider m_ActiveSettingsProvider;

        private InputActionsEditorView m_View;

        private InputActionsEditorSessionAnalytic m_ActionEditorAnalytics;

        public InputActionsEditorSettingsProvider(string path, SettingsScope scopes, IEnumerable<string> keywords = null)
            : base(path, scopes, keywords)
        {}

        public override void OnActivate(string searchContext, VisualElement rootElement)
        {
            // There is an editor bug UUM-55238 that may cause OnActivate and OnDeactivate to be called in unexpected order.
            // This flag avoids making assumptions and executing logic twice.
            if (m_IsActivated)
                return;

            // Monitor play mode state changes
            EditorApplication.playModeStateChanged += ModeChanged;

            // Setup root element with focus monitoring
            m_RootVisualElement = rootElement;
            m_RootVisualElement.focusable = true;
            m_RootVisualElement.RegisterCallback<FocusOutEvent>(OnFocusOut);
            m_RootVisualElement.RegisterCallback<FocusInEvent>(OnFocusIn);

            // Always begin a session when activated (note that OnActivate isn't called when navigating back
            // to editor from another setting category)
            m_ActionEditorAnalytics = new InputActionsEditorSessionAnalytic(
                InputActionsEditorSessionAnalytic.Data.Kind.EmbeddedInProjectSettings);
            m_ActionEditorAnalytics.Begin();

            CreateUI();

            // Monitor any changes to InputSystem.actions for as long as this editor is active
            InputSystem.onActionsChange += BuildUI;

            // Set the asset assigned with the editor which indirectly builds the UI based on setting
            BuildUI();

            // Note that focused element will be set if we are navigating back to an existing instance when switching
            // setting in the left project settings panel since this doesn't recreate the editor.
            if (m_RootVisualElement?.focusController?.focusedElement != null)
                OnFocusIn();

            m_IsActivated = true;
        }

        public override void OnDeactivate()
        {
            // There is an editor bug UUM-55238 that may cause OnActivate and OnDeactivate to be called in unexpected order.
            // This flag avoids making assumptions and executing logic twice.
            if (!m_IsActivated)
                return;

            // Stop monitoring play mode state changes
            EditorApplication.playModeStateChanged -= ModeChanged;

            if (m_RootVisualElement != null)
            {
                m_RootVisualElement.UnregisterCallback<FocusInEvent>(OnFocusIn);
                m_RootVisualElement.UnregisterCallback<FocusOutEvent>(OnFocusOut);
            }

            // Make sure any remaining changes are actually saved
            SaveAssetOnFocusLost();

            // Note that OnDeactivate will also trigger when opening the Project Settings (existing instance).
            // Hence we guard against duplicate OnDeactivate() calls.
            if (m_HasEditFocus)
            {
                OnFocusOut();
                m_HasEditFocus = false;
            }

            InputSystem.onActionsChange -= BuildUI;

            m_IsActivated = false;

            // Always end a session when deactivated.
            m_ActionEditorAnalytics?.End();

            m_View?.DestroyView();
        }

        private void OnFocusIn(FocusInEvent @event = null)
        {
            if (!m_HasEditFocus)
            {
                m_HasEditFocus = true;
                m_ActionEditorAnalytics.RegisterEditorFocusIn();
                m_ActiveSettingsProvider = this;
                SetIMGUIDropdownVisible(false, false);
            }
        }

        void SaveAssetOnFocusLost()
        {
            var asset = GetAsset();
            if (asset != null)
                ValidateAndSaveAsset(asset);
        }

        public static void SetIMGUIDropdownVisible(bool visible, bool optionWasSelected)
        {
            if (m_ActiveSettingsProvider == null)
                return;

            // If we selected an item from the dropdown, we *should* still be focused on this settings window - but
            // since the IMGUI dropdown is technically a separate window, we have to refocus manually.
            //
            // If we didn't select a dropdown option, there's not a simple way to know where the focus has gone,
            // so assume we lost focus and save if appropriate. ISXB-801
            if (!visible && m_IMGUIDropdownVisible)
            {
                if (optionWasSelected)
                    m_ActiveSettingsProvider.m_RootVisualElement.Focus();
                else
                    m_ActiveSettingsProvider.SaveAssetOnFocusLost();
            }
            else if (visible && !m_IMGUIDropdownVisible)
            {
                m_ActiveSettingsProvider.m_HasEditFocus = false;
            }

            m_IMGUIDropdownVisible = visible;
        }

        private async void DelayFocusLost(bool relatedTargetWasNull)
        {
            await Task.Delay(120);

            // We delay this call to ensure that the IMGUI flag has a chance to change first.
            if (relatedTargetWasNull && m_HasEditFocus && !m_IMGUIDropdownVisible)
            {
                m_HasEditFocus = false;
                SaveAssetOnFocusLost();
            }
        }

        private void OnFocusOut(FocusOutEvent @event = null)
        {
            // This can be used to detect focus lost events of container elements, but will not detect window focus.
            // Note that `event.relatedTarget` contains the element that gains focus, which is null if we select
            // elements outside of project settings Editor Window. Also note that @event is null when we call this
            // from OnDeactivate().
            var element = (VisualElement)@event?.relatedTarget;

            m_ActionEditorAnalytics.RegisterEditorFocusOut();

            DelayFocusLost(element == null);
        }

        private void ValidateAndSaveAsset(InputActionAsset asset)
        {
            // This code should be cleaned up once we migrate the InputControl stuff from ImGUI completely.
            // Since at that point it stops being a separate window that steals focus.
            // (See case ISXB-1713)
            if (!InputEditorUserSettings.autoSaveInputActionAssets || m_View.IsControlSchemeViewActive())
            {
                return;
            }

            ProjectWideActionsAsset.Verify(asset);     // Ignore verification result for save
            EditorHelpers.SaveAsset(AssetDatabase.GetAssetPath(asset), asset.ToJson());
        }

        private void CreateUI()
        {
            var projectSettingsAsset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
                InputActionsEditorConstants.PackagePath +
                InputActionsEditorConstants.ResourcesPath +
                InputActionsEditorConstants.ProjectSettingsUxml);

            projectSettingsAsset.CloneTree(m_RootVisualElement);

            m_RootVisualElement.styleSheets.Add(InputActionsEditorWindowUtils.theme);
        }

        private void BuildUI()
        {
            // Construct from InputSystem.actions asset
            var asset = InputSystem.actions;
            var hasAsset = asset != null;
            m_State = (asset != null) ? new InputActionsEditorState(m_ActionEditorAnalytics, new SerializedObject(asset)) : default;

            // Dynamically show a section indicating that an asset is missing if not currently having an associated asset
            var missingAssetSection = m_RootVisualElement.Q<VisualElement>("missing-asset-section");
            if (missingAssetSection != null)
            {
                missingAssetSection.style.visibility = hasAsset ? Visibility.Hidden : Visibility.Visible;
                missingAssetSection.style.display = hasAsset ? DisplayStyle.None : DisplayStyle.Flex;
            }

            // Allow the user to select an asset out of the assets available in the project via picker.
            // Note that we show "None" (null) even if InputSystem.actions is currently a broken/missing reference.
            var objectField = m_RootVisualElement.Q<ObjectField>("current-asset");
            if (objectField != null)
            {
                objectField.value = (asset == null) ? null : asset;
                objectField.RegisterCallback<ChangeEvent<Object>>((evt) =>
                {
                    if (evt.newValue != asset)
                        InputSystem.actions = evt.newValue as InputActionAsset;
                });

                // Prevent reassignment in in editor which would result in exception during play-mode
                objectField.SetEnabled(!EditorApplication.isPlayingOrWillChangePlaymode);
            }

            // Configure a button to allow the user to create and assign a new project-wide asset based on default template
            var createAssetButton = m_RootVisualElement.Q<Button>("create-asset");
            createAssetButton?.RegisterCallback<ClickEvent>(evt =>
            {
                var assetPath = ProjectWideActionsAsset.defaultAssetPath;
                Dialog.Result result = Dialog.Result.Discard;
                if (AssetDatabase.LoadAssetAtPath<Object>(assetPath) != null)
                    result = Dialog.InputActionAsset.ShowCreateAndOverwriteExistingAsset(assetPath);
                if (result == Dialog.Result.Discard)
                    InputSystem.actions = ProjectWideActionsAsset.CreateDefaultAssetAtPath(assetPath);
            });

            // Remove input action editor if already present
            {
                VisualElement element = m_RootVisualElement.Q("action-editor");
                if (element != null)
                    m_RootVisualElement.Remove(element);
            }

            // If the editor is associated with an asset we show input action editor
            if (hasAsset)
            {
                m_StateContainer = new StateContainer(m_State, AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(asset)));
                m_View = new InputActionsEditorView(m_RootVisualElement, m_StateContainer, true, null);
                m_StateContainer.Initialize(m_RootVisualElement.Q("action-editor"));
            }
        }

        private InputActionAsset GetAsset()
        {
            return m_State.serializedObject?.targetObject as InputActionAsset;
        }

        private void SetObjectFieldEnabled(bool enabled)
        {
            // Update object picker enabled state based off editor play mode
            if (m_RootVisualElement != null)
                UQueryExtensions.Q<ObjectField>(m_RootVisualElement, "current-asset")?.SetEnabled(enabled);
        }

        private void ModeChanged(PlayModeStateChange change)
        {
            switch (change)
            {
                case PlayModeStateChange.EnteredEditMode:
                    SetObjectFieldEnabled(true);
                    break;
                case PlayModeStateChange.ExitingEditMode:
                    // Ensure any changes are saved to the asset; FocusLost isn't always triggered when entering PlayMode.
                    SaveAssetOnFocusLost();
                    SetObjectFieldEnabled(false);
                    break;
                case PlayModeStateChange.EnteredPlayMode:
                case PlayModeStateChange.ExitingPlayMode:
                default:
                    break;
            }
        }

        [SettingsProvider]
        public static SettingsProvider CreateGlobalInputActionsEditorProvider()
        {
            if (s_Provider == null)
                s_Provider = new InputActionsEditorSettingsProvider(SettingsPath, SettingsScope.Project);

            return s_Provider;
        }

        #region Shortcuts
        [Shortcut("Input Action Editor/Project Settings/Add Action Map", null, KeyCode.M, ShortcutModifiers.Alt)]
        private static void AddActionMapShortcut(ShortcutArguments arguments)
        {
            if (m_ActiveSettingsProvider is { m_HasEditFocus : true })
                m_ActiveSettingsProvider.m_StateContainer.Dispatch(Commands.AddActionMap());
        }

        [Shortcut("Input Action Editor/Project Settings/Add Action", null, KeyCode.A, ShortcutModifiers.Alt)]
        private static void AddActionShortcut(ShortcutArguments arguments)
        {
            if (m_ActiveSettingsProvider is { m_HasEditFocus : true })
                m_ActiveSettingsProvider.m_StateContainer.Dispatch(Commands.AddAction());
        }

        [Shortcut("Input Action Editor/Project Settings/Add Binding", null, KeyCode.B, ShortcutModifiers.Alt)]
        private static void AddBindingShortcut(ShortcutArguments arguments)
        {
            if (m_ActiveSettingsProvider is { m_HasEditFocus : true })
                m_ActiveSettingsProvider.m_StateContainer.Dispatch(Commands.AddBinding());
        }

        #endregion
    }
}

#endif