#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine.InputSystem.LowLevel;
using UnityEditor;
using UnityEditorInternal;
using UnityEditor.IMGUI.Controls;
using UnityEditor.Networking.PlayerConnection;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Users;
using UnityEngine.InputSystem.Utilities;

////FIXME: Generate proper IDs for the individual tree view items; the current sequential numbering scheme just causes lots of
////       weird expansion/collapsing to happen.

////TODO: add way to load and replay event traces

////TODO: refresh metrics on demand

////TODO: when an action is triggered and when a device changes state, make them bold in the list for a brief moment

////TODO: show input users and their actions and devices

////TODO: append " (Disabled) to disabled devices and grey them out

////TODO: split 'Local' and 'Remote' at root rather than inside subnodes

////TODO: refresh when unrecognized device pops up

namespace UnityEngine.InputSystem.Editor
{
    // Allows looking at input activity in the editor.
    internal class InputDebuggerWindow : EditorWindow, ISerializationCallbackReceiver
    {
        private static int s_Disabled;
        private static InputDebuggerWindow s_Instance;

        [MenuItem("Window/Analysis/Input Debugger", false, 2100)]
        public static void CreateOrShow()
        {
            if (s_Instance == null)
            {
                s_Instance = GetWindow<InputDebuggerWindow>();
                s_Instance.Show();
                s_Instance.titleContent = new GUIContent("Input Debug");
            }
            else
            {
                s_Instance.Show();
                s_Instance.Focus();
            }
        }

        public static void Enable()
        {
            if (s_Disabled == 0)
                return;

            --s_Disabled;
            if (s_Disabled == 0 && s_Instance != null)
                s_Instance.InstallHooks();

            ////REVIEW: technically, we'd have to do a refresh here but that'd mean that in the current setup
            ////        we'd do a refresh after every single test; find a better solution
        }

        public static void Disable()
        {
            ++s_Disabled;
            if (s_Disabled == 1 && s_Instance != null)
                s_Instance.UninstallHooks();
        }

        private void OnDeviceChange(InputDevice device, InputDeviceChange change)
        {
            // Update tree if devices are added or removed.
            if (change == InputDeviceChange.Added || change == InputDeviceChange.Removed)
                Refresh();
        }

        private void OnLayoutChange(string name, InputControlLayoutChange change)
        {
            // Update tree if layout setup has changed.
            Refresh();
        }

        private void OnActionChange(object actionOrMap, InputActionChange change)
        {
            switch (change)
            {
                // When an action is triggered, we only need a repaint.
                case InputActionChange.ActionStarted:
                case InputActionChange.ActionPerformed:
                case InputActionChange.ActionCanceled:
                    Repaint();
                    break;

                case InputActionChange.ActionEnabled:
                case InputActionChange.ActionDisabled:
                case InputActionChange.ActionMapDisabled:
                case InputActionChange.ActionMapEnabled:
                case InputActionChange.BoundControlsChanged:
                    Refresh();
                    break;
            }
        }

        private void OnSettingsChange()
        {
            Refresh();
        }

        private string OnFindLayout(ref InputDeviceDescription description, string matchedLayout,
            InputDeviceExecuteCommandDelegate executeCommandDelegate)
        {
            // If there's no matched layout, there's a chance this device will go in
            // the unsupported list. There's no direct notification for that so we
            // preemptively trigger a refresh.
            if (string.IsNullOrEmpty(matchedLayout))
                Refresh();

            return null;
        }

        private void Refresh()
        {
            m_NeedReload = true;
            Repaint();
        }

        public void OnDestroy()
        {
            UninstallHooks();
        }

        private void InstallHooks()
        {
            InputSystem.onDeviceChange += OnDeviceChange;
            InputSystem.onLayoutChange += OnLayoutChange;
            InputSystem.onFindLayoutForDevice += OnFindLayout;
            InputSystem.onActionChange += OnActionChange;
            InputSystem.onSettingsChange += OnSettingsChange;
        }

        private void UninstallHooks()
        {
            InputSystem.onDeviceChange -= OnDeviceChange;
            InputSystem.onLayoutChange -= OnLayoutChange;
            InputSystem.onFindLayoutForDevice -= OnFindLayout;
            InputSystem.onActionChange -= OnActionChange;
            InputSystem.onSettingsChange -= OnSettingsChange;
        }

        private void Initialize()
        {
            InstallHooks();

            var newTreeViewState = m_TreeViewState == null;
            if (newTreeViewState)
                m_TreeViewState = new TreeViewState();

            m_TreeView = new InputSystemTreeView(m_TreeViewState);

            // Set default expansion states.
            if (newTreeViewState)
                m_TreeView.SetExpanded(m_TreeView.devicesItem.id, true);

            m_Initialized = true;
        }

        public void OnGUI()
        {
            if (s_Disabled > 0)
            {
                EditorGUILayout.LabelField("Disabled");
                return;
            }

            // If the new backends aren't enabled, show a warning in the debugger.
            if (!EditorPlayerSettingHelpers.newSystemBackendsEnabled)
            {
                EditorGUILayout.HelpBox(
                    "Platform backends for the new input system are not enabled. " +
                    "No devices and input from hardware will come through in the new input system APIs.\n\n" +
                    "To enable the backends, set 'Active Input Handling' in the player settings to either 'Input System (Preview)' " +
                    "or 'Both' and restart the editor.", MessageType.Warning);
            }

            // This also brings us back online after a domain reload.
            if (!m_Initialized)
            {
                Initialize();
            }
            else if (m_NeedReload)
            {
                m_TreeView.Reload();
                m_NeedReload = false;
            }

            DrawToolbarGUI();

            var rect = EditorGUILayout.GetControlRect(GUILayout.ExpandHeight(true));
            m_TreeView.OnGUI(rect);
        }

        private static void ToggleLockInputToGameView()
        {
            InputEditorUserSettings.lockInputToGameView = !InputEditorUserSettings.lockInputToGameView;
        }

        private static void ToggleAddDevicesNotSupportedByProject()
        {
            InputEditorUserSettings.addDevicesNotSupportedByProject =
                !InputEditorUserSettings.addDevicesNotSupportedByProject;
        }

        private void ToggleDiagnosticMode()
        {
            if (InputSystem.s_Manager.m_Diagnostics != null)
            {
                InputSystem.s_Manager.m_Diagnostics = null;
            }
            else
            {
                if (m_Diagnostics == null)
                    m_Diagnostics = new InputDiagnostics();
                InputSystem.s_Manager.m_Diagnostics = m_Diagnostics;
            }
        }

        private static void ToggleTouchSimulation()
        {
            InputEditorUserSettings.simulateTouch = !InputEditorUserSettings.simulateTouch;
        }

        private static void EnableRemoteDevices(bool enable = true)
        {
            foreach (var player in EditorConnection.instance.ConnectedPlayers)
            {
                EditorConnection.instance.Send(enable ? RemoteInputPlayerConnection.kStartSendingMsg : RemoteInputPlayerConnection.kStopSendingMsg, new byte[0], player.playerId);
                if (!enable)
                    InputSystem.remoting.RemoveRemoteDevices(player.playerId);
            }
        }

        private static void DrawConnectionGUI()
        {
            if (GUILayout.Button("Remote Devices…", EditorStyles.toolbarDropDown))
            {
                var menu = new GenericMenu();
                var haveRemotes = InputSystem.devices.Any(x => x.remote);
                if (EditorConnection.instance.ConnectedPlayers.Count > 0)
                    menu.AddItem(new GUIContent("Show remote devices"), haveRemotes, () =>
                    {
                        EnableRemoteDevices(!haveRemotes);
                    });
                else
                    menu.AddDisabledItem(new GUIContent("Show remote input devices"));

                menu.AddSeparator("");

                var availableProfilers = ProfilerDriver.GetAvailableProfilers();
                foreach (var profiler in availableProfilers)
                {
                    var enabled = ProfilerDriver.IsIdentifierConnectable(profiler);
                    var profilerName = ProfilerDriver.GetConnectionIdentifier(profiler);
                    var isConnected = ProfilerDriver.connectedProfiler == profiler;
                    if (enabled)
                        menu.AddItem(new GUIContent(profilerName), isConnected, () => {
                            ProfilerDriver.connectedProfiler = profiler;
                            EnableRemoteDevices();
                        });
                    else
                        menu.AddDisabledItem(new GUIContent(profilerName));
                }

                foreach (var device in UnityEditor.Hardware.DevDeviceList.GetDevices())
                {
                    var supportsPlayerConnection = (device.features & UnityEditor.Hardware.DevDeviceFeatures.PlayerConnection) != 0;
                    if (!device.isConnected || !supportsPlayerConnection)
                        continue;

                    var url = "device://" + device.id;
                    var isConnected = ProfilerDriver.connectedProfiler == 0xFEEE && ProfilerDriver.directConnectionUrl == url;
                    menu.AddItem(new GUIContent(device.name), isConnected, () => {
                        ProfilerDriver.DirectURLConnect(url);
                        EnableRemoteDevices();
                    });
                }

                menu.ShowAsContext();
            }
        }

        private void DrawToolbarGUI()
        {
            EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);

            if (GUILayout.Button(Contents.optionsContent, EditorStyles.toolbarDropDown))
            {
                var menu = new GenericMenu();

                menu.AddItem(Contents.addDevicesNotSupportedByProjectContent, InputEditorUserSettings.addDevicesNotSupportedByProject,
                    ToggleAddDevicesNotSupportedByProject);
                menu.AddItem(Contents.diagnosticsModeContent, InputSystem.s_Manager.m_Diagnostics != null,
                    ToggleDiagnosticMode);
                menu.AddItem(Contents.lockInputToGameViewContent, InputEditorUserSettings.lockInputToGameView,
                    ToggleLockInputToGameView);
                menu.AddItem(Contents.touchSimulationContent, InputEditorUserSettings.simulateTouch, ToggleTouchSimulation);

                // Add the inverse of "Copy Device Description" which adds a device with the description from
                // the clipboard to the system. This is most useful for debugging and makes it very easy to
                // have a first pass at device descriptions supplied by users.
                try
                {
                    var copyBuffer = EditorGUIUtility.systemCopyBuffer;
                    if (!string.IsNullOrEmpty(copyBuffer) &&
                        copyBuffer.StartsWith("{") && !InputDeviceDescription.FromJson(copyBuffer).empty)
                    {
                        menu.AddItem(Contents.pasteDeviceDescriptionAsDevice, false, () =>
                        {
                            var description = InputDeviceDescription.FromJson(copyBuffer);
                            InputSystem.AddDevice(description);
                        });
                    }
                }
                catch (ArgumentException)
                {
                    // Catch and ignore exception if buffer doesn't actually contain an InputDeviceDescription
                    // in (proper) JSON format.
                }

                menu.ShowAsContext();
            }

            DrawConnectionGUI();

            GUILayout.FlexibleSpace();
            EditorGUILayout.EndHorizontal();
        }

        [SerializeField] private TreeViewState m_TreeViewState;

        [NonSerialized] private InputDiagnostics m_Diagnostics;
        [NonSerialized] private InputSystemTreeView m_TreeView;
        [NonSerialized] private bool m_Initialized;
        [NonSerialized] private bool m_NeedReload;

        internal static void ReviveAfterDomainReload()
        {
            if (s_Instance != null)
            {
                // Trigger initial repaint. Will call Initialize() to install hooks and
                // refresh tree.
                s_Instance.Repaint();
            }
        }

        private static class Contents
        {
            public static readonly GUIContent optionsContent = new GUIContent("Options");
            public static readonly GUIContent lockInputToGameViewContent = new GUIContent("Lock Input to Game View");
            public static readonly GUIContent touchSimulationContent = new GUIContent("Simulate Touch Input From Mouse or Pen");
            public static readonly GUIContent pasteDeviceDescriptionAsDevice = new GUIContent("Paste Device Description as Device");
            public static readonly GUIContent addDevicesNotSupportedByProjectContent = new GUIContent("Add Devices Not Listed in 'Supported Devices'");
            public static readonly GUIContent diagnosticsModeContent = new GUIContent("Enable Event Diagnostics");
            public static readonly GUIContent openDebugView = new GUIContent("Open Device Debug View");
            public static readonly GUIContent copyDeviceDescription = new GUIContent("Copy Device Description");
            public static readonly GUIContent copyLayoutAsJSON = new GUIContent("Copy Layout as JSON");
            public static readonly GUIContent createDeviceFromLayout = new GUIContent("Create Device from Layout");
            public static readonly GUIContent removeDevice = new GUIContent("Remove Device");
            public static readonly GUIContent enableDevice = new GUIContent("Enable Device");
            public static readonly GUIContent disableDevice = new GUIContent("Disable Device");
        }

        void ISerializationCallbackReceiver.OnBeforeSerialize()
        {
        }

        void ISerializationCallbackReceiver.OnAfterDeserialize()
        {
            s_Instance = this;
        }

        private class InputSystemTreeView : TreeView
        {
            public TreeViewItem actionsItem { get; private set; }
            public TreeViewItem devicesItem { get; private set; }
            public TreeViewItem layoutsItem { get; private set; }
            public TreeViewItem settingsItem { get; private set; }
            public TreeViewItem metricsItem { get; private set; }
            public TreeViewItem usersItem { get; private set; }

            public InputSystemTreeView(TreeViewState state)
                : base(state)
            {
                Reload();
            }

            protected override void ContextClickedItem(int id)
            {
                var item = FindItem(id, rootItem);
                if (item == null)
                    return;

                if (item is DeviceItem deviceItem)
                {
                    var menu = new GenericMenu();
                    menu.AddItem(Contents.openDebugView, false, () => InputDeviceDebuggerWindow.CreateOrShowExisting(deviceItem.device));
                    menu.AddItem(Contents.copyDeviceDescription, false,
                        () => EditorGUIUtility.systemCopyBuffer = deviceItem.device.description.ToJson());
                    menu.AddItem(Contents.removeDevice, false, () => InputSystem.RemoveDevice(deviceItem.device));
                    if (deviceItem.device.enabled)
                        menu.AddItem(Contents.disableDevice, false, () => InputSystem.DisableDevice(deviceItem.device));
                    else
                        menu.AddItem(Contents.enableDevice, false, () => InputSystem.EnableDevice(deviceItem.device));
                    menu.ShowAsContext();
                }

                if (item is UnsupportedDeviceItem unsupportedDeviceItem)
                {
                    var menu = new GenericMenu();
                    menu.AddItem(Contents.copyDeviceDescription, false,
                        () => EditorGUIUtility.systemCopyBuffer = unsupportedDeviceItem.description.ToJson());
                    menu.ShowAsContext();
                }

                if (item is LayoutItem layoutItem)
                {
                    var layout = EditorInputControlLayoutCache.TryGetLayout(layoutItem.layoutName);
                    if (layout != null)
                    {
                        var menu = new GenericMenu();
                        menu.AddItem(Contents.copyLayoutAsJSON, false,
                            () => EditorGUIUtility.systemCopyBuffer = layout.ToJson());
                        if (layout.isDeviceLayout)
                            menu.AddItem(Contents.createDeviceFromLayout, false,
                                () => InputSystem.AddDevice(layout.name));
                        menu.ShowAsContext();
                    }
                }
            }

            protected override void DoubleClickedItem(int id)
            {
                var item = FindItem(id, rootItem);

                if (item is DeviceItem deviceItem)
                    InputDeviceDebuggerWindow.CreateOrShowExisting(deviceItem.device);
            }

            protected override TreeViewItem BuildRoot()
            {
                var id = 0;

                var root = new TreeViewItem
                {
                    id = id++,
                    depth = -1
                };

                ////TODO: this will need to be improved for multi-user scenarios
                // Actions.
                m_EnabledActions.Clear();
                InputSystem.ListEnabledActions(m_EnabledActions);
                if (m_EnabledActions.Count > 0)
                {
                    actionsItem = AddChild(root, "", ref id);
                    AddEnabledActions(actionsItem, ref id);

                    if (!actionsItem.hasChildren)
                    {
                        // We are culling actions that are assigned to users so we may end up with an empty
                        // list even if we have enabled actions. If we do, remove the "Actions" item from the tree.
                        root.children.Remove(actionsItem);
                    }
                    else
                    {
                        // Update title to include action count.
                        actionsItem.displayName = $"Actions ({actionsItem.children.Count})";
                    }
                }

                // Users.
                var userCount = InputUser.all.Count;
                if (userCount > 0)
                {
                    usersItem = AddChild(root, $"Users ({userCount})", ref id);
                    foreach (var user in InputUser.all)
                        AddUser(usersItem, user, ref id);
                }

                // Devices.
                var devices = InputSystem.devices;
                devicesItem = AddChild(root, $"Devices ({devices.Count})", ref id);
                var haveRemotes = devices.Any(x => x.remote);
                TreeViewItem localDevicesNode = null;
                if (haveRemotes)
                {
                    // Split local and remote devices into groups.

                    localDevicesNode = AddChild(devicesItem, "Local", ref id);
                    AddDevices(localDevicesNode, devices, ref id);

                    var remoteDevicesNode = AddChild(devicesItem, "Remote", ref id);
                    foreach (var player in EditorConnection.instance.ConnectedPlayers)
                    {
                        var playerNode = AddChild(remoteDevicesNode, player.name, ref id);
                        AddDevices(playerNode, devices, ref id, player.playerId);
                    }
                }
                else
                {
                    // We don't have remote devices so don't add an extra group for local devices.
                    // Put them all directly underneath the "Devices" node.
                    AddDevices(devicesItem, devices, ref id);
                }

                ////TDO: unsupported and disconnected devices should also be shown for remotes

                if (m_UnsupportedDevices == null)
                    m_UnsupportedDevices = new List<InputDeviceDescription>();
                m_UnsupportedDevices.Clear();
                InputSystem.GetUnsupportedDevices(m_UnsupportedDevices);
                if (m_UnsupportedDevices.Count > 0)
                {
                    var parent = haveRemotes ? localDevicesNode : devicesItem;
                    var unsupportedDevicesNode = AddChild(parent, $"Unsupported ({m_UnsupportedDevices.Count})", ref id);
                    foreach (var device in m_UnsupportedDevices)
                    {
                        var item = new UnsupportedDeviceItem
                        {
                            id = id++,
                            depth = unsupportedDevicesNode.depth + 1,
                            displayName = device.ToString(),
                            description = device
                        };
                        unsupportedDevicesNode.AddChild(item);
                    }
                    unsupportedDevicesNode.children.Sort((a, b) =>
                        string.Compare(a.displayName, b.displayName, StringComparison.InvariantCulture));
                }

                var disconnectedDevices = InputSystem.disconnectedDevices;
                if (disconnectedDevices.Count > 0)
                {
                    var parent = haveRemotes ? localDevicesNode : devicesItem;
                    var disconnectedDevicesNode = AddChild(parent, $"Disconnected ({disconnectedDevices.Count})", ref id);
                    foreach (var device in disconnectedDevices)
                        AddChild(disconnectedDevicesNode, device.ToString(), ref id);
                    disconnectedDevicesNode.children.Sort((a, b) =>
                        string.Compare(a.displayName, b.displayName, StringComparison.InvariantCulture));
                }

                // Layouts.
                layoutsItem = AddChild(root, "Layouts", ref id);
                AddControlLayouts(layoutsItem, ref id);

                ////FIXME: this shows local configuration only
                // Settings.
                var settings = InputSystem.settings;
                var settingsAssetPath = AssetDatabase.GetAssetPath(settings);
                var settingsLabel = "Settings";
                if (!string.IsNullOrEmpty(settingsAssetPath))
                    settingsLabel = $"Settings ({Path.GetFileName(settingsAssetPath)})";
                settingsItem = AddChild(root, settingsLabel, ref id);
                AddValueItem(settingsItem, "Update Mode", settings.updateMode, ref id);
                AddValueItem(settingsItem, "Compensate For Screen Orientation", settings.compensateForScreenOrientation, ref id);
                AddValueItem(settingsItem, "Filter Noise On .current", settings.filterNoiseOnCurrent, ref id);
                AddValueItem(settingsItem, "Default Button Press Point", settings.defaultButtonPressPoint, ref id);
                AddValueItem(settingsItem, "Default Deadzone Min", settings.defaultDeadzoneMin, ref id);
                AddValueItem(settingsItem, "Default Deadzone Max", settings.defaultDeadzoneMax, ref id);
                AddValueItem(settingsItem, "Default Tap Time", settings.defaultTapTime, ref id);
                AddValueItem(settingsItem, "Default Slow Tap Time", settings.defaultSlowTapTime, ref id);
                AddValueItem(settingsItem, "Default Hold Time", settings.defaultHoldTime, ref id);
                AddValueItem(settingsItem, "Lock Input To Game View", InputEditorUserSettings.lockInputToGameView, ref id);
                if (settings.supportedDevices.Count > 0)
                {
                    var supportedDevices = AddChild(settingsItem, "Supported Devices", ref id);
                    foreach (var item in settings.supportedDevices)
                    {
                        var icon = EditorInputControlLayoutCache.GetIconForLayout(item);
                        AddChild(supportedDevices, item, ref id, icon);
                    }
                }
                settingsItem.children.Sort((a, b) => string.Compare(a.displayName, b.displayName));

                // Metrics.
                var metrics = InputSystem.metrics;
                metricsItem = AddChild(root, "Metrics", ref id);
                AddChild(metricsItem,
                    "Current State Size in Bytes: " + StringHelpers.NicifyMemorySize(metrics.currentStateSizeInBytes),
                    ref id);
                AddValueItem(metricsItem, "Current Control Count", metrics.currentControlCount, ref id);
                AddValueItem(metricsItem, "Current Layout Count", metrics.currentLayoutCount, ref id);

                return root;
            }

            private void AddUser(TreeViewItem parent, InputUser user, ref int id)
            {
                ////REVIEW: can we get better identification? allow associating GameObject with user?
                var userItem = AddChild(parent, "User #" + user.index, ref id);

                // Control scheme.
                var controlScheme = user.controlScheme;
                if (controlScheme != null)
                    AddChild(userItem, "Control Scheme: " + controlScheme, ref id);

                // Paired devices.
                var pairedDevices = user.pairedDevices;
                if (pairedDevices.Count > 0)
                {
                    var devicesItem = AddChild(userItem, "Paired Devices", ref id);
                    foreach (var device in user.pairedDevices)
                    {
                        var item = new DeviceItem
                        {
                            id = id++,
                            depth = devicesItem.depth + 1,
                            displayName = device.ToString(),
                            device = device,
                            icon = EditorInputControlLayoutCache.GetIconForLayout(device.layout),
                        };
                        devicesItem.AddChild(item);
                    }
                }

                // Actions.
                var actions = user.actions;
                if (actions != null)
                {
                    var actionsItem = AddChild(userItem, "Actions", ref id);
                    foreach (var action in actions)
                        AddActionItem(actionsItem, action, ref id);

                    parent.children?.Sort((a, b) => string.Compare(a.displayName, b.displayName, StringComparison.CurrentCultureIgnoreCase));
                }
            }

            private void AddDevices(TreeViewItem parent, IEnumerable<InputDevice> devices, ref int id, int participantId = InputDevice.kLocalParticipantId)
            {
                foreach (var device in devices)
                {
                    if (device.m_ParticipantId != participantId)
                        continue;

                    var item = new DeviceItem
                    {
                        id = id++,
                        depth = parent.depth + 1,
                        displayName = device.name,
                        device = device,
                        icon = EditorInputControlLayoutCache.GetIconForLayout(device.layout),
                    };
                    parent.AddChild(item);
                }

                parent.children?.Sort((a, b) => string.Compare(a.displayName, b.displayName));
            }

            private void AddControlLayouts(TreeViewItem parent, ref int id)
            {
                // Split root into three different groups:
                // 1) Control layouts
                // 2) Device layouts that don't match specific products
                // 3) Device layouts that match specific products

                var controls = AddChild(parent, "Controls", ref id);
                var devices = AddChild(parent, "Abstract Devices", ref id);
                var products = AddChild(parent, "Specific Devices", ref id);

                foreach (var layout in EditorInputControlLayoutCache.allControlLayouts)
                    AddControlLayoutItem(layout, controls, ref id);
                foreach (var layout in EditorInputControlLayoutCache.allDeviceLayouts)
                    AddControlLayoutItem(layout, devices, ref id);
                foreach (var layout in EditorInputControlLayoutCache.allProductLayouts)
                {
                    var rootBaseLayoutName = InputControlLayout.s_Layouts.GetRootLayoutName(layout.name).ToString();
                    var groupName = string.IsNullOrEmpty(rootBaseLayoutName) ? "Other" : rootBaseLayoutName + "s";

                    var group = products.children?.FirstOrDefault(x => x.displayName == groupName);
                    if (group == null)
                    {
                        group = AddChild(products, groupName, ref id);
                        if (!string.IsNullOrEmpty(rootBaseLayoutName))
                            group.icon = EditorInputControlLayoutCache.GetIconForLayout(rootBaseLayoutName);
                    }

                    AddControlLayoutItem(layout, group, ref id);
                }

                controls.children?.Sort((a, b) => string.Compare(a.displayName, b.displayName));
                devices.children?.Sort((a, b) => string.Compare(a.displayName, b.displayName));

                if (products.children != null)
                {
                    products.children.Sort((a, b) => string.Compare(a.displayName, b.displayName));
                    foreach (var productGroup in products.children)
                        productGroup.children.Sort((a, b) => string.Compare(a.displayName, b.displayName));
                }
            }

            private TreeViewItem AddControlLayoutItem(InputControlLayout layout, TreeViewItem parent, ref int id)
            {
                var item = new LayoutItem
                {
                    parent = parent,
                    depth = parent.depth + 1,
                    id = id++,
                    displayName = layout.displayName ?? layout.name,
                    layoutName = layout.name,
                };
                item.icon = EditorInputControlLayoutCache.GetIconForLayout(layout.name);
                parent.AddChild(item);

                // Header.
                AddChild(item, "Type: " + layout.type.Name, ref id);
                if (!string.IsNullOrEmpty(layout.m_DisplayName))
                    AddChild(item, "Display Name: " + layout.m_DisplayName, ref id);
                var baseLayouts = StringHelpers.Join(layout.baseLayouts, ", ");
                if (!string.IsNullOrEmpty(baseLayouts))
                    AddChild(item, "Extends: " + baseLayouts, ref id);
                if (layout.stateFormat != 0)
                    AddChild(item, "Format: " + layout.stateFormat, ref id);
                if (layout.m_UpdateBeforeRender != null)
                {
                    var value = layout.m_UpdateBeforeRender.Value ? "Update" : "Disabled";
                    AddChild(item, "Before Render: " + value, ref id);
                }
                if (layout.commonUsages.Count > 0)
                {
                    AddChild(item,
                        "Common Usages: " +
                        string.Join(", ", layout.commonUsages.Select(x => x.ToString()).ToArray()),
                        ref id);
                }
                if (layout.appliedOverrides.Count() > 0)
                {
                    AddChild(item,
                        "Applied Overrides: " +
                        string.Join(", ", layout.appliedOverrides),
                        ref id);
                }

                ////TODO: find a more elegant solution than multiple "Matching Devices" parents when having multiple
                ////      matchers
                // Device matchers.
                foreach (var matcher in EditorInputControlLayoutCache.GetDeviceMatchers(layout.name))
                {
                    var node = AddChild(item, "Matching Devices", ref id);
                    foreach (var pattern in matcher.patterns)
                        AddChild(node, $"{pattern.Key} => \"{pattern.Value}\"", ref id);
                }

                // Controls.
                if (layout.controls.Count > 0)
                {
                    var controls = AddChild(item, "Controls", ref id);
                    foreach (var control in layout.controls)
                        AddControlItem(control, controls, ref id);

                    controls.children.Sort((a, b) => string.Compare(a.displayName, b.displayName));
                }

                return item;
            }

            private void AddControlItem(InputControlLayout.ControlItem control, TreeViewItem parent, ref int id)
            {
                var item = AddChild(parent, control.variants.IsEmpty() ? control.name : string.Format("{0} ({1})",
                    control.name, control.variants), ref id);

                if (!control.layout.IsEmpty())
                    item.icon = EditorInputControlLayoutCache.GetIconForLayout(control.layout);

                ////TODO: fully merge TreeViewItems from isModifyingExistingControl control layouts into the control they modify

                ////TODO: allow clicking this field to jump to the layout
                if (!control.layout.IsEmpty())
                    AddChild(item, $"Layout: {control.layout}", ref id);
                if (!control.variants.IsEmpty())
                    AddChild(item, $"Variant: {control.variants}", ref id);
                if (!string.IsNullOrEmpty(control.displayName))
                    AddChild(item, $"Display Name: {control.displayName}", ref id);
                if (!string.IsNullOrEmpty(control.shortDisplayName))
                    AddChild(item, $"Short Display Name: {control.shortDisplayName}", ref id);
                if (control.format != 0)
                    AddChild(item, $"Format: {control.format}", ref id);
                if (control.offset != InputStateBlock.InvalidOffset)
                    AddChild(item, $"Offset: {control.offset}", ref id);
                if (control.bit != InputStateBlock.InvalidOffset)
                    AddChild(item, $"Bit: {control.bit}", ref id);
                if (control.sizeInBits != 0)
                    AddChild(item, $"Size In Bits: {control.sizeInBits}", ref id);
                if (control.isArray)
                    AddChild(item, $"Array Size: {control.arraySize}", ref id);
                if (!string.IsNullOrEmpty(control.useStateFrom))
                    AddChild(item, $"Use State From: {control.useStateFrom}", ref id);
                if (!control.defaultState.isEmpty)
                    AddChild(item, $"Default State: {control.defaultState.ToString()}", ref id);
                if (!control.minValue.isEmpty)
                    AddChild(item, $"Min Value: {control.minValue.ToString()}", ref id);
                if (!control.maxValue.isEmpty)
                    AddChild(item, $"Max Value: {control.maxValue.ToString()}", ref id);

                if (control.usages.Count > 0)
                    AddChild(item, "Usages: " + string.Join(", ", control.usages.Select(x => x.ToString()).ToArray()), ref id);
                if (control.aliases.Count > 0)
                    AddChild(item, "Aliases: " + string.Join(", ", control.aliases.Select(x => x.ToString()).ToArray()), ref id);

                if (control.isNoisy || control.isSynthetic)
                {
                    var flags = "Flags: ";
                    if (control.isNoisy)
                        flags += "Noisy";
                    if (control.isSynthetic)
                    {
                        if (control.isNoisy)
                            flags += ", Synthetic";
                        else
                            flags += "Synthetic";
                    }
                    AddChild(item, flags, ref id);
                }

                if (control.parameters.Count > 0)
                {
                    var parameters = AddChild(item, "Parameters", ref id);
                    foreach (var parameter in control.parameters)
                        AddChild(parameters, parameter.ToString(), ref id);
                }

                if (control.processors.Count > 0)
                {
                    var processors = AddChild(item, "Processors", ref id);
                    foreach (var processor in control.processors)
                    {
                        var processorItem = AddChild(processors, processor.name, ref id);
                        foreach (var parameter in processor.parameters)
                            AddChild(processorItem, parameter.ToString(), ref id);
                    }
                }
            }

            private void AddValueItem<TValue>(TreeViewItem parent, string name, TValue value, ref int id)
            {
                var item = new ConfigurationItem
                {
                    id = id++,
                    depth = parent.depth + 1,
                    displayName = $"{name}: {value.ToString()}",
                    name = name
                };
                parent.AddChild(item);
            }

            private void AddEnabledActions(TreeViewItem parent, ref int id)
            {
                foreach (var action in m_EnabledActions)
                {
                    // If we have users, find out if the action is owned by a user. If so, don't display
                    // it separately.
                    var isOwnedByUser = false;
                    foreach (var user in InputUser.all)
                    {
                        var userActions = user.actions;
                        if (userActions != null && userActions.Contains(action))
                        {
                            isOwnedByUser = true;
                            break;
                        }
                    }

                    if (!isOwnedByUser)
                        AddActionItem(parent, action, ref id);
                }

                parent.children?.Sort((a, b) => string.Compare(a.displayName, b.displayName, StringComparison.CurrentCultureIgnoreCase));
            }

            private unsafe void AddActionItem(TreeViewItem parent, InputAction action, ref int id)
            {
                // Add item for action.
                var name = action.actionMap != null ? $"{action.actionMap.name}/{action.name}" : action.name;
                if (!action.enabled)
                    name += " (Disabled)";
                var item = AddChild(parent, name, ref id);

                // Grab state.
                var actionMap = action.GetOrCreateActionMap();
                actionMap.ResolveBindingsIfNecessary();
                var state = actionMap.m_State;

                // Add list of resolved controls.
                var actionIndex = action.m_ActionIndexInState;
                var totalBindingCount = state.totalBindingCount;
                for (var i = 0; i < totalBindingCount; ++i)
                {
                    ref var bindingState = ref state.bindingStates[i];
                    if (bindingState.actionIndex != actionIndex)
                        continue;
                    if (bindingState.isComposite)
                        continue;

                    var binding = state.GetBinding(i);
                    var controlCount = bindingState.controlCount;
                    var controlStartIndex = bindingState.controlStartIndex;
                    for (var n = 0; n < controlCount; ++n)
                    {
                        var control = state.controls[controlStartIndex + n];
                        var interactions =
                            StringHelpers.Join(new[] {binding.effectiveInteractions, action.interactions}, ",");

                        var text = control.path;
                        if (!string.IsNullOrEmpty(interactions))
                        {
                            var namesAndParameters = NameAndParameters.ParseMultiple(interactions);
                            text += " [";
                            text += string.Join(",", namesAndParameters.Select(x => x.name));
                            text += "]";
                        }

                        AddChild(item, text, ref id);
                    }
                }
            }

            private TreeViewItem AddChild(TreeViewItem parent, string displayName, ref int id, Texture2D icon = null)
            {
                var item = new TreeViewItem
                {
                    id = id++,
                    depth = parent.depth + 1,
                    displayName = displayName,
                    icon = icon,
                };
                parent.AddChild(item);
                return item;
            }

            private List<InputDeviceDescription> m_UnsupportedDevices;
            private List<InputAction> m_EnabledActions = new List<InputAction>();

            private class DeviceItem : TreeViewItem
            {
                public InputDevice device;
            }

            private class UnsupportedDeviceItem : TreeViewItem
            {
                public InputDeviceDescription description;
            }

            private class ConfigurationItem : TreeViewItem
            {
                public string name;
            }

            private class LayoutItem : TreeViewItem
            {
                public InternedString layoutName;
            }
        }
    }
}
#endif // UNITY_EDITOR
