#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;

////TODO: have tooltips on each entry in the picker

////TODO: find better way to present controls when filtering to specific devices

namespace UnityEngine.InputSystem.Editor
{
    internal class InputControlPickerDropdown : AdvancedDropdown, IDisposable
    {
        public InputControlPickerDropdown(
            InputControlPickerState state,
            Action<string> onPickCallback,
            InputControlPicker.Mode mode = InputControlPicker.Mode.PickControl)
            : base(state.advancedDropdownState)
        {
            m_Gui = new InputControlPickerGUI(this);

            minimumSize = new Vector2(275, 300);
            maximumSize = new Vector2(0, 300);

            m_OnPickCallback = onPickCallback;
            m_Mode = mode;
        }

        public void SetControlPathsToMatch(string[] controlPathsToMatch)
        {
            m_ControlPathsToMatch = controlPathsToMatch;
            Reload();
        }

        public void SetExpectedControlLayout(string expectedControlLayout)
        {
            m_ExpectedControlLayout = expectedControlLayout;

            if (string.Equals(expectedControlLayout, "InputDevice", StringComparison.InvariantCultureIgnoreCase))
                m_ExpectedControlType = typeof(InputDevice);
            else
                m_ExpectedControlType = !string.IsNullOrEmpty(expectedControlLayout)
                    ? InputSystem.s_Manager.m_Layouts.GetControlTypeForLayout(new InternedString(expectedControlLayout))
                    : null;

            // If the layout is for a device, automatically switch to device
            // picking mode.
            if (m_ExpectedControlType != null && typeof(InputDevice).IsAssignableFrom(m_ExpectedControlType))
                m_Mode = InputControlPicker.Mode.PickDevice;

            Reload();
        }

        protected override void OnDestroy()
        {
            m_RebindingOperation?.Dispose();
            m_RebindingOperation = null;
        }

        public void Dispose()
        {
            m_RebindingOperation?.Dispose();
        }

        protected override AdvancedDropdownItem BuildRoot()
        {
            var root = new AdvancedDropdownItem(string.Empty);

            // Usages.
            if (m_Mode != InputControlPicker.Mode.PickDevice)
            {
                var usages = BuildTreeForUsages();
                if (usages.children.Any())
                {
                    root.AddChild(usages);
                    root.AddSeparator();
                }
            }

            // Devices.
            AddItemsForDevices(root);

            return root;
        }

        protected override AdvancedDropdownItem BuildCustomSearch(string searchString,
            IEnumerable<AdvancedDropdownItem> elements)
        {
            if (!isListening)
                return null;

            var root = new AdvancedDropdownItem(!string.IsNullOrEmpty(m_ExpectedControlLayout)
                ? $"Listening for {m_ExpectedControlLayout}..."
                : "Listening for input...");

            if (searchString == "\u0017")
                return root;

            var paths = searchString.Substring(1).Split('\u0017');
            foreach (var element in elements)
            {
                if (element is ControlDropdownItem controlItem && paths.Any(x => controlItem.controlPathWithDevice == x))
                    root.AddChild(element);
            }

            return root;
        }

        protected override void ItemSelected(AdvancedDropdownItem item)
        {
            var path = ((InputControlDropdownItem)item).controlPathWithDevice;
            m_OnPickCallback(path);
        }

        private AdvancedDropdownItem BuildTreeForUsages()
        {
            var usageRoot = new AdvancedDropdownItem("Usages");
            foreach (var usageAndLayouts in EditorInputControlLayoutCache.allUsages)
            {
                if (usageAndLayouts.Item2.Any(LayoutMatchesExpectedControlLayoutFilter))
                {
                    var child = new UsageDropdownItem(usageAndLayouts.Item1);
                    usageRoot.AddChild(child);
                }
            }
            return usageRoot;
        }

        private void AddItemsForDevices(AdvancedDropdownItem parent)
        {
            // Add devices that are marked as generic types of devices directly to the parent.
            // E.g. adds "Gamepad" and then underneath all the more specific types of gamepads.
            foreach (var deviceLayout in EditorInputControlLayoutCache.allLayouts
                     .Where(x => x.isDeviceLayout && !x.isOverride && x.isGenericTypeOfDevice && !x.hideInUI).OrderBy(a => a.displayName))
                AddDeviceTreeItemRecursive(deviceLayout, parent);

            // We have devices that are based directly on InputDevice but are not marked as generic types
            // of devices (e.g. Vive Lighthouses). We do not want them to clutter the list at the root so we
            // all of them in a group called "Other" at the end of the list.
            var otherGroup = new AdvancedDropdownItem("Other");
            foreach (var deviceLayout in EditorInputControlLayoutCache.allLayouts
                     .Where(x => x.isDeviceLayout && !x.isOverride && !x.isGenericTypeOfDevice && x.type.BaseType == typeof(InputDevice) &&
                         !x.hideInUI && !x.baseLayouts.Any()).OrderBy(a => a.displayName))
                AddDeviceTreeItemRecursive(deviceLayout, otherGroup);

            if (otherGroup.children.Any())
                parent.AddChild(otherGroup);
        }

        private void AddDeviceTreeItemRecursive(InputControlLayout layout, AdvancedDropdownItem parent, bool searchable = true)
        {
            // Find all layouts directly based on this one (ignoring overrides).
            var childLayouts = EditorInputControlLayoutCache.allLayouts
                .Where(x => x.isDeviceLayout && !x.isOverride && !x.hideInUI && x.baseLayouts.Contains(layout.name)).OrderBy(x => x.displayName);

            // See if the entire tree should be excluded.
            var shouldIncludeDeviceLayout = ShouldIncludeDeviceLayout(layout);
            var shouldIncludeAtLeastOneChildLayout = childLayouts.Any(ShouldIncludeDeviceLayout);

            if (!shouldIncludeDeviceLayout && !shouldIncludeAtLeastOneChildLayout)
                return;

            // Add toplevel item for device.
            var deviceItem = new DeviceDropdownItem(layout, searchable: searchable);
            parent.AddChild(deviceItem);

            // Add common usage variants.
            if (layout.commonUsages.Count > 0)
            {
                foreach (var usage in layout.commonUsages)
                {
                    var usageItem = new DeviceDropdownItem(layout, usage);
                    if (m_Mode == InputControlPicker.Mode.PickControl)
                        AddControlTreeItemsRecursive(layout, usageItem, layout.name, usage, searchable);
                    deviceItem.AddChild(usageItem);
                }
                deviceItem.AddSeparator();
            }

            // Add controls.
            if (m_Mode != InputControlPicker.Mode.PickDevice)
            {
                // The keyboard is special in that we want to allow binding by display name (i.e. character
                // generated by a key) instead of only by physical key location. Also, we want to give an indication
                // of which specific key an entry refers to by taking the current keyboard layout into account.
                //
                // So what we do is add an extra level to the keyboard where key's can be bound by character
                // according to the current layout. And in the top level of the keyboard we display keys with
                // both physical and logical names.
                if (layout.type == typeof(Keyboard) && Keyboard.current != null)
                {
                    var byLocationGroup = new AdvancedDropdownItem("By Location of Key (Using US Layout)");
                    var byCharacterGroup = new AdvancedDropdownItem("By Character Mapped to Key");

                    deviceItem.AddChild(byLocationGroup);
                    deviceItem.AddChild(byCharacterGroup);

                    var keyboard = Keyboard.current;

                    AddCharacterKeyBindingsTo(byCharacterGroup, keyboard);
                    AddPhysicalKeyBindingsTo(byLocationGroup, keyboard, searchable);

                    // AnyKey won't appear in either group. Add it explicitly.
                    AddControlItem(deviceItem, null,
                        layout.FindControl(new InternedString("anyKey")).Value, layout.name, null, searchable);
                }
                else
                {
                    AddControlTreeItemsRecursive(layout, deviceItem, layout.name, null, searchable);
                }
            }

            // Add child items.
            var isFirstChild = true;
            foreach (var childLayout in childLayouts)
            {
                if (!ShouldIncludeDeviceLayout(childLayout))
                    continue;

                if (isFirstChild)
                    deviceItem.AddSeparator("More Specific " + deviceItem.name.GetPlural());
                isFirstChild = false;

                AddDeviceTreeItemRecursive(childLayout, deviceItem, searchable && !childLayout.isGenericTypeOfDevice);
            }

            // When picking devices, it must be possible to select a device that itself has more specific types
            // of devices underneath it. However in the dropdown, such a device will be a foldout and not itself
            // be selectable. We solve this problem by adding an entry for the device underneath the device
            // itself (e.g. "Gamepad >> Gamepad").
            if (m_Mode == InputControlPicker.Mode.PickDevice && deviceItem.m_Children.Count > 0)
            {
                var item = new DeviceDropdownItem(layout);
                deviceItem.m_Children.Insert(0, item);
            }
        }

        private void AddControlTreeItemsRecursive(InputControlLayout layout, DeviceDropdownItem parent,
            string device, string usage, bool searchable, ControlDropdownItem parentControl = null)
        {
            foreach (var control in layout.controls.OrderBy(a => a.name))
            {
                if (control.isModifyingExistingControl)
                    continue;

                // Skip variants except the default variant and variants dictated by the layout itself.
                if (!control.variants.IsEmpty() && control.variants != InputControlLayout.DefaultVariant
                    && (layout.variants.IsEmpty() || !InputControlLayout.VariantsMatch(layout.variants, control.variants)))
                {
                    continue;
                }

                AddControlItem(parent, parentControl, control, device, usage, searchable);
            }

            // Add optional controls for devices.
            var optionalControls = EditorInputControlLayoutCache.GetOptionalControlsForLayout(layout.name);
            if (optionalControls.Any() && layout.isDeviceLayout)
            {
                var optionalGroup = new AdvancedDropdownItem("Optional Controls");
                foreach (var optionalControl in optionalControls)
                {
                    ////FIXME: this should list children, too
                    ////FIXME: this should handle arrays, too
                    if (LayoutMatchesExpectedControlLayoutFilter(optionalControl.layout))
                    {
                        var child = new OptionalControlDropdownItem(optionalControl, device, usage);
                        child.icon = EditorInputControlLayoutCache.GetIconForLayout(optionalControl.layout);
                        optionalGroup.AddChild(child);
                    }
                }

                if (optionalGroup.children.Any())
                {
                    var deviceName = EditorInputControlLayoutCache.TryGetLayout(device).m_DisplayName ??
                        ObjectNames.NicifyVariableName(device);
                    parent.AddSeparator("Controls Present on More Specific " + deviceName.GetPlural());
                    parent.AddChild(optionalGroup);
                }
            }
        }

        private void AddControlItem(DeviceDropdownItem parent, ControlDropdownItem parentControl,
            InputControlLayout.ControlItem control, string device, string usage, bool searchable)
        {
            // If it's an array, generate a control entry for each array element.
            for (var i = 0; i < (control.isArray ? control.arraySize : 1); ++i)
            {
                var name = control.isArray ? control.name + i : control.name;
                var displayName = !string.IsNullOrEmpty(control.displayName)
                    ? (control.isArray ? $"{control.displayName} #{i}" : control.displayName)
                    : name;

                var child = new ControlDropdownItem(parentControl, name, displayName,
                    device, usage, searchable);
                child.icon = EditorInputControlLayoutCache.GetIconForLayout(control.layout);
                var controlLayout = EditorInputControlLayoutCache.TryGetLayout(control.layout);

                if (LayoutMatchesExpectedControlLayoutFilter(control.layout))
                    parent.AddChild(child);
                else if (controlLayout.controls.Any(x => LayoutMatchesExpectedControlLayoutFilter(x.layout)))
                {
                    child.enabled = false;
                    parent.AddChild(child);
                }
                // Add children.
                if (controlLayout != null)
                    AddControlTreeItemsRecursive(controlLayout, parent, device, usage,
                        searchable, child);
            }
        }

        private static void AddPhysicalKeyBindingsTo(AdvancedDropdownItem parent, Keyboard keyboard, bool searchable)
        {
            foreach (var key in keyboard.children.OfType<KeyControl>())
            {
                // If the key has a display name that differs from the key name, show it in the UI.
                var displayName = key.m_DisplayNameFromLayout;
                var keyDisplayName = key.displayName;
                if (keyDisplayName.All(x => x.IsPrintable()) && string.Compare(keyDisplayName, displayName,
                    StringComparison.InvariantCultureIgnoreCase) != 0)
                    displayName = $"{displayName} (Current Layout: {key.displayName})";

                // For left/right modifier keys, prepend artificial combined version.
                ButtonControl combinedVersion = null;
                if (key == keyboard.leftShiftKey)
                    combinedVersion = keyboard.shiftKey;
                else if (key == keyboard.leftAltKey)
                    combinedVersion = keyboard.altKey;
                else if (key == keyboard.leftCtrlKey)
                    combinedVersion = keyboard.ctrlKey;
                if (combinedVersion != null)
                    parent.AddChild(new ControlDropdownItem(null, combinedVersion.name, combinedVersion.displayName, keyboard.layout,
                        "", searchable));

                var item = new ControlDropdownItem(null, key.name, displayName,
                    keyboard.layout, "", searchable);

                parent.AddChild(item);
            }
        }

        private static void AddCharacterKeyBindingsTo(AdvancedDropdownItem parent, Keyboard keyboard)
        {
            foreach (var key in keyboard.children.OfType<KeyControl>())
            {
                if (!key.keyCode.IsTextInputKey())
                    continue;

                // We can only bind to characters that can be printed.
                var displayName = key.displayName;
                if (!displayName.All(x => x.IsPrintable()))
                    continue;

                if (displayName.Contains(')'))
                    displayName = string.Join("", displayName.Select(x => "\\" + x));

                ////TODO: should be searchable; when searching, needs different display name
                var item = new ControlDropdownItem(null, $"#({displayName})", "", keyboard.layout, "", false);
                item.name = key.displayName;

                parent.AddChild(item);
            }
        }

        private bool LayoutMatchesExpectedControlLayoutFilter(string layout)
        {
            if (m_ExpectedControlType == null)
                return true;

            var layoutType = InputSystem.s_Manager.m_Layouts.GetControlTypeForLayout(new InternedString(layout));
            return m_ExpectedControlType.IsAssignableFrom(layoutType);
        }

        private bool ShouldIncludeDeviceLayout(InputControlLayout layout)
        {
            if (layout.hideInUI)
                return false;

            // By default, if a device has no (usable) controls, we don't want it listed in the control picker
            // except if we're picking devices.
            if (!layout.controls.Any(x => LayoutMatchesExpectedControlLayoutFilter(x.layout)) && layout.controls.Any(x => true) &&
                m_Mode != InputControlPicker.Mode.PickDevice)
                return false;

            // If we have a device filter, see if we should ignore the device.
            if (m_ControlPathsToMatch != null && m_ControlPathsToMatch.Length > 0)
            {
                var matchesAnyInDeviceFilter = false;
                foreach (var entry in m_ControlPathsToMatch)
                {
                    // Include the layout if it's in the inheritance hierarchy of the layout we expect (either below
                    // or above it or, well, just right on it).
                    var expectedLayout = InputControlPath.TryGetDeviceLayout(entry);
                    if (!string.IsNullOrEmpty(expectedLayout) &&
                        (expectedLayout == layout.name ||
                         InputControlLayout.s_Layouts.IsBasedOn(layout.name, new InternedString(expectedLayout)) ||
                         InputControlLayout.s_Layouts.IsBasedOn(new InternedString(expectedLayout), layout.name)))
                    {
                        matchesAnyInDeviceFilter = true;
                        break;
                    }
                }

                if (!matchesAnyInDeviceFilter)
                    return false;
            }

            return true;
        }

        private void StartListening()
        {
            if (m_RebindingOperation == null)
                m_RebindingOperation = new InputActionRebindingExtensions.RebindingOperation();

            ////TODO: for keyboard, generate both possible paths (physical and by display name)

            m_RebindingOperation.Reset();
            m_RebindingOperation
                .WithExpectedControlType(m_ExpectedControlLayout)
                // Require minimum actuation of 0.15f. This is after deadzoning has been applied.
                .WithMagnitudeHavingToBeGreaterThan(0.15f)
                ////REVIEW: should we exclude only the system's active pointing device?
                // With the mouse operating the UI, its cursor control is too fickle a thing to
                // bind to. Ignore mouse position and delta and clicks.
                // NOTE: We go for all types of pointers here, not just mice.
                .WithControlsExcluding("<Pointer>/position")
                .WithControlsExcluding("<Pointer>/delta")
                .WithControlsExcluding("<Pointer>/press")
                .WithControlsExcluding("<Pointer>/clickCount")
                .WithControlsExcluding("<Pointer>/{PrimaryAction}")
                .WithControlsExcluding("<Mouse>/scroll")
                .OnPotentialMatch(
                    operation =>
                    {
                        // We never really complete the pick but keep listening for as long as the "Interactive"
                        // button is toggled on.

                        Repaint();
                    })
                .OnCancel(
                    operation =>
                    {
                        Repaint();
                    })
                .OnApplyBinding(
                    (operation, newPath) =>
                    {
                        // This is never invoked (because we don't complete the pick) but we need it nevertheless
                        // as RebindingOperation requires the callback if we don't supply an action to apply the binding to.
                    });

            // If we have control paths to match, pass them on.
            if (m_ControlPathsToMatch.LengthSafe() > 0)
                m_ControlPathsToMatch.Select(x => m_RebindingOperation.WithControlsHavingToMatchPath(x));

            m_RebindingOperation.Start();
        }

        private void StopListening()
        {
            m_RebindingOperation?.Cancel();
        }

        // This differs from RebindingOperation.GeneratePathForControl in that it cycles through all
        // layouts in the inheritance chain and generates a path for each one that contains the given control.
        private static IEnumerable<string> GeneratePossiblePathsForControl(InputControl control)
        {
            var builder = new StringBuilder();
            var deviceLayoutName = control.device.m_Layout;
            do
            {
                // Skip layout if it is supposed to be hidden in the UI.
                var layout = EditorInputControlLayoutCache.TryGetLayout(deviceLayoutName);
                if (layout.hideInUI)
                    continue;

                builder.Length = 0;
                yield return control.BuildPath(deviceLayoutName, builder);
            }
            while (InputControlLayout.s_Layouts.baseLayoutTable.TryGetValue(deviceLayoutName, out deviceLayoutName));
        }

        private readonly Action<string> m_OnPickCallback;
        private InputControlPicker.Mode m_Mode;
        private string[] m_ControlPathsToMatch;
        private string m_ExpectedControlLayout;
        private Type m_ExpectedControlType;
        private InputActionRebindingExtensions.RebindingOperation m_RebindingOperation;

        private bool isListening => m_RebindingOperation != null && m_RebindingOperation.started;

        private class InputControlPickerGUI : AdvancedDropdownGUI
        {
            private readonly InputControlPickerDropdown m_Owner;

            public InputControlPickerGUI(InputControlPickerDropdown owner)
            {
                m_Owner = owner;
            }

            internal override void BeginDraw(EditorWindow window)
            {
                if (Event.current.isKey && Event.current.keyCode == KeyCode.Escape)
                {
                    window.Close();
                    return;
                }

                if (m_Owner.isListening)
                {
                    // Eat key events to suppress the editor from passing them to the OS
                    // (causing beeps or menu commands being triggered).
                    if (Event.current.isKey)
                        Event.current.Use();
                }
            }

            internal override string DrawSearchFieldControl(string searchString)
            {
                using (new EditorGUILayout.HorizontalScope())
                {
                    var isListening = false;

                    // When picking controls, have a "Listen" button that allows listening for input.
                    if (m_Owner.m_Mode == InputControlPicker.Mode.PickControl)
                    {
                        using (new EditorGUILayout.VerticalScope(GUILayout.MaxWidth(50)))
                        {
                            GUILayout.Space(4);
                            var isListeningOld = m_Owner.isListening;
                            var isListeningNew = GUILayout.Toggle(isListeningOld, "Listen",
                                EditorStyles.miniButton, GUILayout.MaxWidth(50));

                            if (isListeningOld != isListeningNew)
                            {
                                if (isListeningNew)
                                {
                                    m_Owner.StartListening();
                                }
                                else
                                {
                                    m_Owner.StopListening();
                                    searchString = string.Empty;
                                }
                            }

                            isListening = isListeningNew;
                        }
                    }

                    ////FIXME: the search box doesn't clear out when listening; no idea why the new string isn't taking effect
                    EditorGUI.BeginDisabledGroup(isListening);
                    var newSearchString = base.DrawSearchFieldControl(isListening ? string.Empty : searchString);
                    EditorGUI.EndDisabledGroup();

                    if (isListening)
                    {
                        var rebind = m_Owner.m_RebindingOperation;
                        return "\u0017" + string.Join("\u0017",
                            rebind.candidates.SelectMany(x => GeneratePossiblePathsForControl(x).Reverse()));
                    }

                    return newSearchString;
                }
            }

            internal override void DrawItem(AdvancedDropdownItem item, string name, Texture2D icon, bool enabled,
                bool drawArrow, bool selected, bool hasSearch, bool richText = false)
            {
                if (hasSearch && item is InputControlDropdownItem viewItem)
                    name = viewItem.searchableName;

                base.DrawItem(item, name, icon, enabled, drawArrow, selected, hasSearch);
            }

            internal override void DrawFooter(AdvancedDropdownItem selectedItem)
            {
                //dun work because there is no selection
                if (selectedItem is ControlDropdownItem controlItem)
                {
                    var content = new GUIContent(controlItem.controlPath);
                    var rect = GUILayoutUtility.GetRect(content, headerStyle, GUILayout.ExpandWidth(true));
                    EditorGUI.TextField(rect, controlItem.controlPath, headerStyle);
                }
            }
        }

        private static class Styles
        {
            public static readonly GUIStyle waitingForInputLabel = new GUIStyle("WhiteBoldLabel").WithFontSize(22);
        }
    }
}
#endif // UNITY_EDITOR
