using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;

////TODO: add ability to add to existing arrays rather than creating per-device arrays

////TODO: the next step here is to write a code generator that generates code for a given layout that when
////      executed, does what InputDeviceBuilder does but without the use of reflection and much more quickly

////REVIEW: it probably makes sense to have an initial phase where we process the initial set of
////        device discoveries from native and keep the layout cache around instead of throwing
////        it away after the creation of every single device; best approach may be to just
////        reuse the same InputDeviceBuilder instance over and over

////TODO: ensure that things are aligned properly for ARM; should that be done on the reading side or in the state layouts?
////       (make sure that alignment works the same on *all* platforms; otherwise editor will not be able to process events from players properly)

namespace UnityEngine.InputSystem.Layouts
{
    /// <summary>
    /// Turns a device layout into an actual <see cref="InputDevice"/> instance.
    /// </summary>
    /// <remarks>
    /// Ultimately produces a device but can also be used to query the control setup described
    /// by a layout.
    ///
    /// Can be used both to create control hierarchies from scratch as well as to re-create or
    /// change existing hierarchies.
    ///
    /// InputDeviceBuilder is the only way to create control hierarchies. InputControls cannot be
    /// <c>new</c>'d directly.
    ///
    /// Also computes a final state layout when setup is finished.
    ///
    /// Note that InputDeviceBuilders generate garbage. They are meant to be used for initialization only. Don't
    /// use them during normal gameplay.
    ///
    /// Running an *existing* device through another control build is a *destructive* operation.
    /// Existing controls may be reused while at the same time the hierarchy and even the device instance
    /// itself may change.
    /// </remarks>
    internal struct InputDeviceBuilder : IDisposable
    {
        public void Setup(InternedString layout, InternedString variants,
            InputDeviceDescription deviceDescription = default)
        {
            m_LayoutCacheRef = InputControlLayout.CacheRef();

            InstantiateLayout(layout, variants, new InternedString(), null);
            FinalizeControlHierarchy();

            m_StateOffsetToControlMap.Sort();

            m_Device.m_Description = deviceDescription;
            m_Device.m_StateOffsetToControlMap = m_StateOffsetToControlMap.ToArray();

            m_Device.CallFinishSetupRecursive();
        }

        // Complete the setup and return the full control hierarchy setup
        // with its device root.
        public InputDevice Finish()
        {
            var device = m_Device;

            // Kill off our state.
            Reset();

            return device;
        }

        public void Dispose()
        {
            m_LayoutCacheRef.Dispose();
        }

        private InputDevice m_Device;

        // Make sure the global layout cache sticks around for at least as long
        // as the device builder so that we don't load layouts over and over.
        private InputControlLayout.CacheRefInstance m_LayoutCacheRef;

        // Table mapping (lower-cased) control paths to control layouts that contain
        // overrides for the control at the given path.
        private Dictionary<string, InputControlLayout.ControlItem> m_ChildControlOverrides;

        private List<uint> m_StateOffsetToControlMap;

        private StringBuilder m_StringBuilder;

        // Reset the setup in a way where it can be reused for another setup.
        // Should retain allocations that can be reused.
        private void Reset()
        {
            m_Device = null;
            m_ChildControlOverrides?.Clear();
            m_StateOffsetToControlMap?.Clear();
            // Leave the cache in place so we can reuse them in another setup path.
        }

        private InputControl InstantiateLayout(InternedString layout, InternedString variants, InternedString name, InputControl parent)
        {
            // Look up layout by name.
            var layoutInstance = FindOrLoadLayout(layout);

            // Create control hierarchy.
            return InstantiateLayout(layoutInstance, variants, name, parent);
        }

        private InputControl InstantiateLayout(InputControlLayout layout, InternedString variants, InternedString name,
            InputControl parent)
        {
            Debug.Assert(layout.type != null, "Layout has no type set on it");

            // No, so create a new control.
            var controlObject = Activator.CreateInstance(layout.type);
            if (!(controlObject is InputControl control))
            {
                throw new InvalidOperationException(
                    $"Type '{layout.type.Name}' referenced by layout '{layout.name}' is not an InputControl");
            }

            // If it's a device, perform some extra work specific to the control
            // hierarchy root.
            if (control is InputDevice controlAsDevice)
            {
                if (parent != null)
                    throw new InvalidOperationException(
                        $"Cannot instantiate device layout '{layout.name}' as child of '{parent.path}'; devices must be added at root");

                m_Device = controlAsDevice;
                m_Device.m_StateBlock.byteOffset = 0;
                m_Device.m_StateBlock.bitOffset = 0;
                m_Device.m_StateBlock.format = layout.stateFormat;

                // If we have an existing device, we'll start the various control arrays
                // from scratch. Note that all the controls still refer to the existing
                // arrays and so we can iterate children, for example, just fine while
                // we are rebuilding the control hierarchy.
                m_Device.m_AliasesForEachControl = null;
                m_Device.m_ChildrenForEachControl = null;
                m_Device.m_UsagesForEachControl = null;
                m_Device.m_UsageToControl = null;

                if (layout.m_UpdateBeforeRender == true)
                    m_Device.m_DeviceFlags |= InputDevice.DeviceFlags.UpdateBeforeRender;
                if (layout.canRunInBackground != null)
                {
                    m_Device.m_DeviceFlags |= InputDevice.DeviceFlags.CanRunInBackgroundHasBeenQueried;
                    if (layout.canRunInBackground == true)
                        m_Device.m_DeviceFlags |= InputDevice.DeviceFlags.CanRunInBackground;
                }
            }
            else if (parent == null)
            {
                // Someone did "new InputDeviceBuilder(...)" with a control layout.
                // We don't support creating control hierarchies without a device at the root.
                throw new InvalidOperationException(
                    $"Toplevel layout used with InputDeviceBuilder must be a device layout; '{layout.name}' is a control layout");
            }

            // Name defaults to name of layout.
            if (name.IsEmpty())
            {
                name = layout.name;

                // If there's a namespace in the layout name, snip it out.
                var indexOfLastColon = name.ToString().LastIndexOf(':');
                if (indexOfLastColon != -1)
                    name = new InternedString(name.ToString().Substring(indexOfLastColon + 1));
            }

            // Make sure name does not contain any slashes.
            if (name.ToString().IndexOf(InputControlPath.Separator) != -1)
                name = new InternedString(name.ToString().CleanSlashes());

            // Variant defaults to variants of layout.
            if (variants.IsEmpty())
            {
                variants = layout.variants;

                if (variants.IsEmpty())
                    variants = InputControlLayout.DefaultVariant;
            }

            control.m_Name = name;
            control.m_DisplayNameFromLayout = layout.m_DisplayName; // No short display names at layout roots.
            control.m_Layout = layout.name;
            control.m_Variants = variants;
            control.m_Parent = parent;
            control.m_Device = m_Device;

            // this has to be done down here instead of in the device block above because the state for the
            // device needs to be set up before setting noisy or it will throw because the device's m_Device
            // hasn't been set yet. Yes, a device's m_Device is itself.
            if (control is InputDevice)
                control.noisy = layout.isNoisy;

            // Create children and configure their settings from our
            // layout values.
            var haveChildrenUsingStateFromOtherControl = false;
            try
            {
                // Pass list of existing control on to function as we may have decided to not
                // actually reuse the existing control (and thus control.m_ChildrenReadOnly will
                // now be blank) but still want crawling down the hierarchy to preserve existing
                // controls where possible.
                AddChildControls(layout, variants, control,
                    ref haveChildrenUsingStateFromOtherControl);
            }
            catch
            {
                ////TODO: remove control from collection and rethrow
                throw;
            }

            // Come up with a layout for our state.
            ComputeStateLayout(control);

            // Finally, if we have child controls that take their state blocks from other
            // controls, assign them their blocks now.
            if (haveChildrenUsingStateFromOtherControl)
            {
                var controls = layout.m_Controls;
                for (var i = 0; i < controls.Length; ++i)
                {
                    ref var item = ref controls[i];
                    if (string.IsNullOrEmpty(item.useStateFrom))
                        continue;
                    ApplyUseStateFrom(control, ref item, layout);
                }
            }

            return control;
        }

        private const uint kSizeForControlUsingStateFromOtherControl = InputStateBlock.InvalidOffset;

        private void AddChildControls(InputControlLayout layout, InternedString variants, InputControl parent,
            ref bool haveChildrenUsingStateFromOtherControls)
        {
            var controlLayouts = layout.m_Controls;
            if (controlLayouts == null)
                return;

            // Find out how many direct children we will add.
            var childCount = 0;
            var haveControlLayoutWithPath = false;
            for (var i = 0; i < controlLayouts.Length; ++i)
            {
                // Skip if variants don't match.
                if (!controlLayouts[i].variants.IsEmpty() &&
                    !StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(controlLayouts[i].variants,
                        variants, InputControlLayout.VariantSeparator[0]))
                    continue;

                ////REVIEW: I'm not sure this is good enough. ATM if you have a control layout with
                ////        name "foo" and one with name "foo/bar", then the latter is taken as an override
                ////        but the former isn't. However, whether it has a slash in the path or not shouldn't
                ////        matter. If a control layout of the same name already exists, it should be
                ////        considered an override, if not, it shouldn't.
                // Not a new child if it's a layout reaching in to the hierarchy to modify
                // an existing child.
                if (controlLayouts[i].isModifyingExistingControl)
                {
                    if (controlLayouts[i].isArray)
                        throw new NotSupportedException(
                            $"Control '{controlLayouts[i].name}' in layout '{layout.name}' is modifying the child of another control but is marked as an array");

                    haveControlLayoutWithPath = true;
                    InsertChildControlOverride(parent, ref controlLayouts[i]);
                    continue;
                }

                if (controlLayouts[i].isArray)
                    childCount += controlLayouts[i].arraySize;
                else
                    ++childCount;
            }

            // Nothing to do if there's no children.
            if (childCount == 0)
            {
                parent.m_ChildCount = default;
                parent.m_ChildStartIndex = default;
                haveChildrenUsingStateFromOtherControls = false;
                return;
            }

            // Add room for us in the device's child array.
            var firstChildIndex = ArrayHelpers.GrowBy(ref m_Device.m_ChildrenForEachControl, childCount);

            // Add controls from all control layouts except the ones that have
            // paths in them.
            var childIndex = firstChildIndex;
            for (var i = 0; i < controlLayouts.Length; ++i)
            {
                var controlLayout = controlLayouts[i];

                // Skip control layouts that don't add controls but rather modify child
                // controls of other controls added by the layout. We do a second pass
                // to apply their settings.
                if (controlLayout.isModifyingExistingControl)
                    continue;

                // If the control is part of a variant, skip it if it isn't in the variants we're
                // looking for.
                if (!controlLayout.variants.IsEmpty() &&
                    !StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(controlLayout.variants,
                        variants, InputControlLayout.VariantSeparator[0]))
                    continue;

                // If it's an array, add a control for each array element.
                if (controlLayout.isArray)
                {
                    for (var n = 0; n < controlLayout.arraySize; ++n)
                    {
                        var name = controlLayout.name + n;
                        var control = AddChildControl(layout, variants, parent, ref haveChildrenUsingStateFromOtherControls,
                            controlLayout, childIndex, nameOverride: name);
                        ++childIndex;

                        // Adjust offset, if the control uses explicit offsets.
                        if (control.m_StateBlock.byteOffset != InputStateBlock.InvalidOffset)
                            control.m_StateBlock.byteOffset += (uint)n * control.m_StateBlock.alignedSizeInBytes;
                    }
                }
                else
                {
                    AddChildControl(layout, variants, parent, ref haveChildrenUsingStateFromOtherControls,
                        controlLayout, childIndex);
                    ++childIndex;
                }
            }

            parent.m_ChildCount = childCount;
            parent.m_ChildStartIndex = firstChildIndex;

            ////REVIEW: there's probably a better way to do this based on m_ChildControlOverrides
            // We apply all overrides through m_ChildControlOverrides. However, there may be a control item
            // that *adds* a child control to another existing control. This will look the same as overriding
            // properties on a child control just that in this case the child control doesn't exist.
            //
            // Go through all the controls and check for ones that need to be added.
            if (haveControlLayoutWithPath)
            {
                for (var i = 0; i < controlLayouts.Length; ++i)
                {
                    var controlLayout = controlLayouts[i];
                    if (!controlLayout.isModifyingExistingControl)
                        continue;

                    // If the control is part of a variants, skip it if it isn't the variants we're
                    // looking for.
                    if (!controlLayout.variants.IsEmpty() &&
                        !StringHelpers.CharacterSeparatedListsHaveAtLeastOneCommonElement(controlLayouts[i].variants,
                            variants, InputControlLayout.VariantSeparator[0]))
                        continue;

                    AddChildControlIfMissing(layout, variants, parent, ref haveChildrenUsingStateFromOtherControls,
                        ref controlLayout);
                }
            }
        }

        private InputControl AddChildControl(InputControlLayout layout, InternedString variants, InputControl parent,
            ref bool haveChildrenUsingStateFromOtherControls,
            InputControlLayout.ControlItem controlItem,
            int childIndex, string nameOverride = null)
        {
            var name = nameOverride != null ? new InternedString(nameOverride) : controlItem.name;

            ////REVIEW: can we check this in InputControlLayout instead?
            if (string.IsNullOrEmpty(controlItem.layout))
                throw new InvalidOperationException($"Layout has not been set on control '{controlItem.name}' in '{layout.name}'");

            // See if there is an override for the control.
            if (m_ChildControlOverrides != null)
            {
                var pathLowerCase = ChildControlOverridePath(parent, name);
                if (m_ChildControlOverrides.TryGetValue(pathLowerCase, out var controlOverride))
                    controlItem = controlOverride.Merge(controlItem);
            }

            // Get name of layout to use for control.
            var layoutName = controlItem.layout;

            // Create control.
            InputControl control;
            try
            {
                control = InstantiateLayout(layoutName, variants, name, parent);
            }
            catch (InputControlLayout.LayoutNotFoundException exception)
            {
                // Throw better exception that gives more info.
                throw new InputControlLayout.LayoutNotFoundException(
                    $"Cannot find layout '{exception.layout}' used in control '{name}' of layout '{layout.name}'",
                    exception);
            }

            // Add to array.
            // NOTE: AddChildControls and InstantiateLayout take care of growing the array and making
            //       room for the immediate children of each control.
            m_Device.m_ChildrenForEachControl[childIndex] = control;

            // Set flags and misc things.
            control.noisy = controlItem.isNoisy;
            control.synthetic = controlItem.isSynthetic;
            control.usesStateFromOtherControl = !string.IsNullOrEmpty(controlItem.useStateFrom);
            control.dontReset = (control.noisy || controlItem.dontReset) && !control.usesStateFromOtherControl; // Imply dontReset for noisy controls.
            if (control.noisy)
                m_Device.noisy = true;
            control.isButton = control is ButtonControl;
            if (control.dontReset)
                m_Device.hasDontResetControls = true;

            // Remember the display names from the layout. We later do a proper pass once we have
            // the full hierarchy to set final names.
            control.m_DisplayNameFromLayout = controlItem.displayName;
            control.m_ShortDisplayNameFromLayout = controlItem.shortDisplayName;

            // Set default value.
            control.m_DefaultState = controlItem.defaultState;
            if (!control.m_DefaultState.isEmpty)
                m_Device.hasControlsWithDefaultState = true;

            // Set min and max value. Don't just overwrite here as the control's constructor may
            // have set a default value.
            if (!controlItem.minValue.isEmpty)
                control.m_MinValue = controlItem.minValue;
            if (!controlItem.maxValue.isEmpty)
                control.m_MaxValue = controlItem.maxValue;

            // Pass state block config on to control.
            if (!control.usesStateFromOtherControl)
            {
                control.m_StateBlock.byteOffset = controlItem.offset;
                control.m_StateBlock.bitOffset = controlItem.bit;
                if (controlItem.sizeInBits != 0)
                    control.m_StateBlock.sizeInBits = controlItem.sizeInBits;
                if (controlItem.format != 0)
                    SetFormat(control, controlItem);
            }
            else
            {
                // Mark controls that don't have state blocks of their own but rather get their
                // blocks from other controls by setting their state size to InvalidOffset.
                control.m_StateBlock.sizeInBits = kSizeForControlUsingStateFromOtherControl;
                haveChildrenUsingStateFromOtherControls = true;
            }

            ////REVIEW: the constant appending to m_UsagesForEachControl and m_AliasesForEachControl may lead to a lot
            ////        of successive re-allocations

            // Add usages.
            var usages = controlItem.usages;
            if (usages.Count > 0)
            {
                var usageCount = usages.Count;
                var usageIndex =
                    ArrayHelpers.AppendToImmutable(ref m_Device.m_UsagesForEachControl, usages.m_Array);
                control.m_UsageStartIndex = usageIndex;
                control.m_UsageCount = usageCount;

                ArrayHelpers.GrowBy(ref m_Device.m_UsageToControl, usageCount);
                for (var n = 0; n < usageCount; ++n)
                    m_Device.m_UsageToControl[usageIndex + n] = control;
            }

            // Add aliases.
            if (controlItem.aliases.Count > 0)
            {
                var aliasCount = controlItem.aliases.Count;
                var aliasIndex =
                    ArrayHelpers.AppendToImmutable(ref m_Device.m_AliasesForEachControl, controlItem.aliases.m_Array);
                control.m_AliasStartIndex = aliasIndex;
                control.m_AliasCount = aliasCount;
            }

            // Set parameters.
            if (controlItem.parameters.Count > 0)
                NamedValue.ApplyAllToObject(control, controlItem.parameters);

            // Add processors.
            if (controlItem.processors.Count > 0)
                AddProcessors(control, ref controlItem, layout.name);

            return control;
        }

        private void InsertChildControlOverride(InputControl parent, ref InputControlLayout.ControlItem controlItem)
        {
            if (m_ChildControlOverrides == null)
                m_ChildControlOverrides = new Dictionary<string, InputControlLayout.ControlItem>();

            // See if there are existing overrides for the control.
            var pathLowerCase = ChildControlOverridePath(parent, controlItem.name);
            if (!m_ChildControlOverrides.TryGetValue(pathLowerCase, out var existingOverrides))
            {
                // So, so just insert our overrides and we're done.
                m_ChildControlOverrides[pathLowerCase] = controlItem;
                return;
            }

            // Yes, there's existing overrides so we have to merge.
            // NOTE: The existing override's properties take precedence here. This is because
            //       the override has been established from higher up in the layout hierarchy.
            existingOverrides = existingOverrides.Merge(controlItem);
            m_ChildControlOverrides[pathLowerCase] = existingOverrides;
        }

        private string ChildControlOverridePath(InputControl parent, InternedString controlName)
        {
            var pathLowerCase = controlName.ToLower();
            for (var current = parent; current != m_Device; current = current.m_Parent)
                pathLowerCase = $"{current.m_Name.ToLower()}/{pathLowerCase}";
            return pathLowerCase;
        }

        private void AddChildControlIfMissing(InputControlLayout layout, InternedString variants, InputControl parent,
            ref bool haveChildrenUsingStateFromOtherControls,
            ref InputControlLayout.ControlItem controlItem)
        {
            ////TODO: support arrays (we may modify an entire array in bulk)

            // Find the child control.
            var child = InputControlPath.TryFindChild(parent, controlItem.name);
            if (child != null)
                return;

            // We're adding a child somewhere in the existing hierarchy. This is a tricky
            // case as we have to potentially shift indices around in the hierarchy to make
            // room for the new control.

            ////TODO: this path does not support recovering existing controls? does it matter?

            child = InsertChildControl(layout, variants, parent,
                ref haveChildrenUsingStateFromOtherControls, ref controlItem);

            // Apply layout change.
            if (!ReferenceEquals(child.parent, parent))
                ComputeStateLayout(child.parent);
        }

        private InputControl InsertChildControl(InputControlLayout layout, InternedString variant, InputControl parent,
            ref bool haveChildrenUsingStateFromOtherControls,
            ref InputControlLayout.ControlItem controlItem)
        {
            var path = controlItem.name.ToString();

            // First we need to find the immediate parent from the given path.
            var indexOfSlash = path.LastIndexOf('/');
            if (indexOfSlash == -1)
                throw new InvalidOperationException("InsertChildControl has to be called with a slash-separated path");
            Debug.Assert(indexOfSlash != 0, "Could not find slash in path");
            var immediateParentPath = path.Substring(0, indexOfSlash);
            var immediateParent = InputControlPath.TryFindChild(parent, immediateParentPath);
            if (immediateParent == null)
                throw new InvalidOperationException(
                    $"Cannot find parent '{immediateParentPath}' of control '{controlItem.name}' in layout '{layout.name}'");

            var controlName = path.Substring(indexOfSlash + 1);
            if (controlName.Length == 0)
                throw new InvalidOperationException(
                    $"Path cannot end in '/' (control '{controlItem.name}' in layout '{layout.name}')");

            // Make room in the device's child array.
            var childStartIndex = immediateParent.m_ChildStartIndex;
            if (childStartIndex == default)
            {
                // First child of parent.
                childStartIndex = m_Device.m_ChildrenForEachControl.LengthSafe();
                immediateParent.m_ChildStartIndex = childStartIndex;
            }
            var childIndex = childStartIndex + immediateParent.m_ChildCount;
            ShiftChildIndicesInHierarchyOneUp(m_Device, childIndex, immediateParent);
            ArrayHelpers.InsertAt(ref m_Device.m_ChildrenForEachControl, childIndex, null);
            ++immediateParent.m_ChildCount;

            // Insert the child.
            // NOTE: This may *add several* controls depending on the layout of the control we are inserting.
            //       The children will be appended to the child array.
            var control = AddChildControl(layout, variant, immediateParent,
                ref haveChildrenUsingStateFromOtherControls, controlItem, childIndex, controlName);

            return control;
        }

        private static void ApplyUseStateFrom(InputControl parent, ref InputControlLayout.ControlItem controlItem, InputControlLayout layout)
        {
            var child = InputControlPath.TryFindChild(parent, controlItem.name);
            Debug.Assert(child != null, "Could not find child control which should be present at this point");

            // Find the referenced control.
            var referencedControl = InputControlPath.TryFindChild(parent, controlItem.useStateFrom);
            if (referencedControl == null)
                throw new InvalidOperationException(
                    $"Cannot find control '{controlItem.useStateFrom}' referenced in 'useStateFrom' of control '{controlItem.name}' in layout '{layout.name}'");

            // Copy its state settings.
            child.m_StateBlock = referencedControl.m_StateBlock;
            child.usesStateFromOtherControl = true;
            child.dontReset = referencedControl.dontReset;

            // At this point, all byteOffsets are relative to parents so we need to
            // walk up the referenced control's parent chain and add offsets until
            // we are at the same level that we are at.
            if (child.parent != referencedControl.parent)
                for (var parentInChain = referencedControl.parent; parentInChain != parent; parentInChain = parentInChain.parent)
                    child.m_StateBlock.byteOffset += parentInChain.m_StateBlock.byteOffset;
        }

        private static void ShiftChildIndicesInHierarchyOneUp(InputDevice device, int startIndex, InputControl exceptControl)
        {
            var controls = device.m_ChildrenForEachControl;
            var count = controls.Length;
            for (var i = 0; i < count; ++i)
            {
                var control = controls[i];
                if (control != null && control != exceptControl && control.m_ChildStartIndex >= startIndex)
                    ++control.m_ChildStartIndex;
            }
        }

        // NOTE: We can only do this once we've initialized the names on the parent control. I.e. it has to be
        //       done in the second pass we do over the control hierarchy.
        private void SetDisplayName(InputControl control, string longDisplayNameFromLayout, string shortDisplayNameFromLayout, bool shortName)
        {
            var displayNameFromLayout = shortName ? shortDisplayNameFromLayout : longDisplayNameFromLayout;

            // Display name may not be set in layout.
            if (string.IsNullOrEmpty(displayNameFromLayout))
            {
                // For short names, we leave it unassigned if there's nothing in the layout
                // except if it's a nested control where the parent has a short name.
                if (shortName)
                {
                    if (control.parent != null && control.parent != control.device)
                    {
                        if (m_StringBuilder == null)
                            m_StringBuilder = new StringBuilder();
                        m_StringBuilder.Length = 0;
                        AddParentDisplayNameRecursive(control.parent, m_StringBuilder, true);
                        if (m_StringBuilder.Length == 0)
                        {
                            control.m_ShortDisplayNameFromLayout = null;
                            return;
                        }

                        if (!string.IsNullOrEmpty(longDisplayNameFromLayout))
                            m_StringBuilder.Append(longDisplayNameFromLayout);
                        else
                            m_StringBuilder.Append(control.name);
                        control.m_ShortDisplayNameFromLayout = m_StringBuilder.ToString();
                        return;
                    }

                    control.m_ShortDisplayNameFromLayout = null;
                    return;
                }

                ////REVIEW: automatically uppercase or prettify this?
                // For long names, we default to the control's name.
                displayNameFromLayout = control.name;
            }

            // If it's a nested control, synthesize a path that includes parents.
            if (control.parent != null && control.parent != control.device)
            {
                if (m_StringBuilder == null)
                    m_StringBuilder = new StringBuilder();
                m_StringBuilder.Length = 0;
                AddParentDisplayNameRecursive(control.parent, m_StringBuilder, shortName);
                m_StringBuilder.Append(displayNameFromLayout);
                displayNameFromLayout = m_StringBuilder.ToString();
            }

            // Assign.
            if (shortName)
                control.m_ShortDisplayNameFromLayout = displayNameFromLayout;
            else
                control.m_DisplayNameFromLayout = displayNameFromLayout;
        }

        private static void AddParentDisplayNameRecursive(InputControl control, StringBuilder stringBuilder,
            bool shortName)
        {
            if (control.parent != null && control.parent != control.device)
                AddParentDisplayNameRecursive(control.parent, stringBuilder, shortName);

            if (shortName)
            {
                var text = control.shortDisplayName;
                if (string.IsNullOrEmpty(text))
                    text = control.displayName;

                stringBuilder.Append(text);
            }
            else
            {
                stringBuilder.Append(control.displayName);
            }
            stringBuilder.Append(' ');
        }

        private static void AddProcessors(InputControl control, ref InputControlLayout.ControlItem controlItem, string layoutName)
        {
            var processorCount = controlItem.processors.Count;
            for (var n = 0; n < processorCount; ++n)
            {
                var name = controlItem.processors[n].name;
                var type = InputProcessor.s_Processors.LookupTypeRegistration(name);
                if (type == null)
                    throw new InvalidOperationException(
                        $"Cannot find processor '{name}' referenced by control '{controlItem.name}' in layout '{layoutName}'");

                var processor = Activator.CreateInstance(type);

                var parameters = controlItem.processors[n].parameters;
                if (parameters.Count > 0)
                    NamedValue.ApplyAllToObject(processor, parameters);

                control.AddProcessor(processor);
            }
        }

        private static void SetFormat(InputControl control, InputControlLayout.ControlItem controlItem)
        {
            control.m_StateBlock.format = controlItem.format;
            if (controlItem.sizeInBits == 0)
            {
                var primitiveFormatSize = InputStateBlock.GetSizeOfPrimitiveFormatInBits(controlItem.format);
                if (primitiveFormatSize != -1)
                    control.m_StateBlock.sizeInBits = (uint)primitiveFormatSize;
            }
        }

        private static InputControlLayout FindOrLoadLayout(string name)
        {
            Debug.Assert(InputControlLayout.s_CacheInstanceRef > 0, "Should have acquired layout cache reference");
            return InputControlLayout.cache.FindOrLoadLayout(name);
        }

        private static void ComputeStateLayout(InputControl control)
        {
            var children = control.children;

            // If the control has a format but no size specified and the format is a
            // primitive format, just set the size automatically.
            if (control.m_StateBlock.sizeInBits == 0 && control.m_StateBlock.format != 0)
            {
                var sizeInBits = InputStateBlock.GetSizeOfPrimitiveFormatInBits(control.m_StateBlock.format);
                if (sizeInBits != -1)
                    control.m_StateBlock.sizeInBits = (uint)sizeInBits;
            }

            // If state size is not set, it means it's computed from the size of the
            // children so make sure we actually have children.
            if (control.m_StateBlock.sizeInBits == 0 && children.Count == 0)
            {
                throw new InvalidOperationException(
                    $"Control '{control.path}' with layout '{control.layout}' has no size set and has no children to compute size from");
            }

            // If there's no children, our job is done.
            if (children.Count == 0)
                return;

            // First deal with children that want fixed offsets. All the other ones
            // will get appended to the end.
            var firstUnfixedByteOffset = 0u;
            foreach (var child in children)
            {
                Debug.Assert(child.m_StateBlock.sizeInBits != 0, "Size of state block not set on child");

                // Skip children using state from other controls.
                if (child.m_StateBlock.sizeInBits == kSizeForControlUsingStateFromOtherControl)
                    continue;

                // Make sure the child has a valid size set on it.
                var childSizeInBits = child.m_StateBlock.sizeInBits;
                if (childSizeInBits == 0 || childSizeInBits == InputStateBlock.InvalidOffset)
                    throw new InvalidOperationException(
                        $"Child '{child.name}' of '{control.name}' has no size set!");

                // Skip children that don't have fixed offsets.
                if (child.m_StateBlock.byteOffset == InputStateBlock.InvalidOffset ||
                    child.m_StateBlock.byteOffset == InputStateBlock.AutomaticOffset)
                    continue;

                // At this point, if the child has no valid bit offset, put it at #0 now.
                if (child.m_StateBlock.bitOffset == InputStateBlock.InvalidOffset)
                    child.m_StateBlock.bitOffset = 0;

                // See if the control bumps our fixed layout size.
                var endOffset =
                    MemoryHelpers.ComputeFollowingByteOffset(child.m_StateBlock.byteOffset, child.m_StateBlock.bitOffset + childSizeInBits);
                if (endOffset > firstUnfixedByteOffset)
                    firstUnfixedByteOffset = endOffset;
            }

            ////TODO: this doesn't support mixed automatic and fixed layouting *within* bitfields;
            ////      I think it's okay not to support that but we should at least detect it

            // Now assign an offset to every control that wants an
            // automatic offset. For bitfields, we need to delay advancing byte
            // offsets until we've seen all bits in the fields.
            // NOTE: Bit addressing controls using automatic offsets *must* be consecutive.
            var runningByteOffset = firstUnfixedByteOffset;
            InputControl firstBitAddressingChild = null;
            var bitfieldSizeInBits = 0u;
            foreach (var child in children)
            {
                // Skip children with fixed offsets.
                if (child.m_StateBlock.byteOffset != InputStateBlock.InvalidOffset &&
                    child.m_StateBlock.byteOffset != InputStateBlock.AutomaticOffset)
                    continue;

                // Skip children using state from other controls.
                if (child.m_StateBlock.sizeInBits == kSizeForControlUsingStateFromOtherControl)
                    continue;

                // See if it's a bit addressing control.
                var isBitAddressingChild = (child.m_StateBlock.sizeInBits % 8) != 0;
                if (isBitAddressingChild)
                {
                    // Remember start of bitfield group.
                    if (firstBitAddressingChild == null)
                        firstBitAddressingChild = child;

                    // Keep a running count of the size of the bitfield.
                    if (child.m_StateBlock.bitOffset == InputStateBlock.InvalidOffset ||
                        child.m_StateBlock.bitOffset == InputStateBlock.AutomaticOffset)
                    {
                        // Put child at current bit offset.
                        child.m_StateBlock.bitOffset = bitfieldSizeInBits;

                        bitfieldSizeInBits += child.m_StateBlock.sizeInBits;
                    }
                    else
                    {
                        // Child already has bit offset. Keep it but make sure we're accounting for it
                        // in the bitfield size.
                        var lastBit = child.m_StateBlock.bitOffset + child.m_StateBlock.sizeInBits;
                        if (lastBit > bitfieldSizeInBits)
                            bitfieldSizeInBits = lastBit;
                    }
                }
                else
                {
                    // Terminate bitfield group (if there was one).
                    if (firstBitAddressingChild != null)
                    {
                        runningByteOffset = MemoryHelpers.ComputeFollowingByteOffset(runningByteOffset, bitfieldSizeInBits);
                        firstBitAddressingChild = null;
                    }

                    if (child.m_StateBlock.bitOffset == InputStateBlock.InvalidOffset)
                        child.m_StateBlock.bitOffset = 0;

                    // Conform to memory addressing constraints of CPU architecture. If we don't do
                    // this, ARMs will end up choking on misaligned memory accesses.
                    runningByteOffset = MemoryHelpers.AlignNatural(runningByteOffset, child.m_StateBlock.alignedSizeInBytes);
                }

                ////FIXME: seems like this should take bitOffset into account
                child.m_StateBlock.byteOffset = runningByteOffset;

                if (!isBitAddressingChild)
                    runningByteOffset =
                        MemoryHelpers.ComputeFollowingByteOffset(runningByteOffset, child.m_StateBlock.sizeInBits);
            }

            // Compute total size.
            // If we ended on a bitfield, account for its size.
            if (firstBitAddressingChild != null)
                runningByteOffset = MemoryHelpers.ComputeFollowingByteOffset(runningByteOffset, bitfieldSizeInBits);
            var totalSizeInBytes = runningByteOffset;

            // Set size. We force all parents to the combined size of their children.
            control.m_StateBlock.sizeInBits = totalSizeInBytes * 8;
        }

        private void FinalizeControlHierarchy()
        {
            if (m_StateOffsetToControlMap == null)
                m_StateOffsetToControlMap = new List<uint>();

            if (m_Device.allControls.Count > (1U << InputDevice.kControlIndexBits))
                throw new NotSupportedException($"Device '{m_Device}' exceeds maximum supported control count of {1U << InputDevice.kControlIndexBits} (has {m_Device.allControls.Count} controls)");

            var rootNode = new InputDevice.ControlBitRangeNode((ushort)(m_Device.m_StateBlock.sizeInBits - 1));
            m_Device.m_ControlTreeNodes = new InputDevice.ControlBitRangeNode[1];
            m_Device.m_ControlTreeNodes[0] = rootNode;

            var controlIndiciesNextFreeIndex = 0;
            // Device is not in m_ChildrenForEachControl so use index -1.
            FinalizeControlHierarchyRecursive(m_Device, -1, m_Device.m_ChildrenForEachControl, false, false, ref controlIndiciesNextFreeIndex);
        }

        private void FinalizeControlHierarchyRecursive(InputControl control, int controlIndex, InputControl[] allControls, bool noisy, bool dontReset, ref int controlIndiciesNextFreeIndex)
        {
            // Make sure we're staying within limits on state offsets and sizes.
            if (control.m_ChildCount == 0)
            {
                if (control.m_StateBlock.effectiveBitOffset >= (1U << InputDevice.kStateOffsetBits))
                    throw new NotSupportedException($"Control '{control}' exceeds maximum supported state bit offset of {(1U << InputDevice.kStateOffsetBits) - 1} (bit offset {control.stateBlock.effectiveBitOffset})");
                if (control.m_StateBlock.sizeInBits >= (1U << InputDevice.kStateSizeBits))
                    throw new NotSupportedException($"Control '{control}' exceeds maximum supported state bit size of {(1U << InputDevice.kStateSizeBits) - 1} (bit offset {control.stateBlock.sizeInBits})");
            }

            // Construct control bit range tree
            if (control != m_Device)
                InsertControlBitRangeNode(ref m_Device.m_ControlTreeNodes[0], control, ref controlIndiciesNextFreeIndex, 0);

            // Add all leaf controls to state offset mapping.
            if (control.m_ChildCount == 0)
                m_StateOffsetToControlMap.Add(
                    InputDevice.EncodeStateOffsetToControlMapEntry((uint)controlIndex, control.m_StateBlock.effectiveBitOffset, control.m_StateBlock.sizeInBits));

            // Set final display names. This may overwrite the ones supplied by the layout so temporarily
            // store the values here.
            var displayNameFromLayout = control.m_DisplayNameFromLayout;
            var shortDisplayNameFromLayout = control.m_ShortDisplayNameFromLayout;
            SetDisplayName(control, displayNameFromLayout, shortDisplayNameFromLayout, false);
            SetDisplayName(control, displayNameFromLayout, shortDisplayNameFromLayout, true);

            if (control != control.device)
            {
                if (noisy)
                    control.noisy = true;
                else
                    noisy = control.noisy;

                if (dontReset)
                    control.dontReset = true;
                else
                    dontReset = control.dontReset;
            }

            // Recurse into children. Also bake our state offset into our children.
            var ourOffset = control.m_StateBlock.byteOffset;
            var childCount = control.m_ChildCount;
            var childStartIndex = control.m_ChildStartIndex;
            for (var i = 0; i < childCount; ++i)
            {
                var childIndex = childStartIndex + i;
                var child = allControls[childIndex];
                child.m_StateBlock.byteOffset += ourOffset;

                FinalizeControlHierarchyRecursive(child, childIndex, allControls, noisy, dontReset, ref controlIndiciesNextFreeIndex);
            }

            control.isSetupFinished = true;
        }

        private void InsertControlBitRangeNode(ref InputDevice.ControlBitRangeNode parent, InputControl control, ref int controlIndiciesNextFreeIndex, ushort startOffset)
        {
            InputDevice.ControlBitRangeNode leftNode;
            InputDevice.ControlBitRangeNode rightNode;

            // we don't recalculate mid-points for nodes that have already been created
            if (parent.leftChildIndex == -1)
            {
                var midPoint = GetBestMidPoint(parent, startOffset);
                leftNode = new InputDevice.ControlBitRangeNode(midPoint);
                rightNode = new InputDevice.ControlBitRangeNode(parent.endBitOffset);
                AddChildren(ref parent, leftNode, rightNode);
            }
            else
            {
                leftNode = m_Device.m_ControlTreeNodes[parent.leftChildIndex];
                rightNode = m_Device.m_ControlTreeNodes[parent.leftChildIndex + 1];
            }


            // if the control starts in the left node and ends in the right, add a pointer to both nodes and return
            if (control.m_StateBlock.effectiveBitOffset < leftNode.endBitOffset &&
                control.m_StateBlock.effectiveBitOffset + control.m_StateBlock.sizeInBits > leftNode.endBitOffset)
            {
                AddControlToNode(control, ref controlIndiciesNextFreeIndex, parent.leftChildIndex);
                AddControlToNode(control, ref controlIndiciesNextFreeIndex, parent.leftChildIndex + 1);
                return;
            }

            // if it exactly fits one of the nodes, add a pointer to just that node and return
            if (control.m_StateBlock.effectiveBitOffset == startOffset &&
                control.m_StateBlock.effectiveBitOffset + control.m_StateBlock.sizeInBits == leftNode.endBitOffset)
            {
                AddControlToNode(control, ref controlIndiciesNextFreeIndex, parent.leftChildIndex);
                return;
            }

            if (control.m_StateBlock.effectiveBitOffset == leftNode.endBitOffset &&
                control.m_StateBlock.effectiveBitOffset + control.m_StateBlock.sizeInBits == rightNode.endBitOffset)
            {
                AddControlToNode(control, ref controlIndiciesNextFreeIndex, parent.leftChildIndex + 1);
                return;
            }

            // otherwise, if the node ends in the left node, recurse left
            if (control.m_StateBlock.effectiveBitOffset < leftNode.endBitOffset)
                InsertControlBitRangeNode(ref m_Device.m_ControlTreeNodes[parent.leftChildIndex], control,
                    ref controlIndiciesNextFreeIndex, startOffset);
            else
                InsertControlBitRangeNode(ref m_Device.m_ControlTreeNodes[parent.leftChildIndex + 1], control,
                    ref controlIndiciesNextFreeIndex, leftNode.endBitOffset);
        }

        private ushort GetBestMidPoint(InputDevice.ControlBitRangeNode parent, ushort startOffset)
        {
            // find the absolute mid-point, rounded up
            var absoluteMidPoint = (ushort)(startOffset + ((parent.endBitOffset - startOffset - 1) / 2 + 1));
            var closestControlEndPointToMidPoint = ushort.MaxValue;
            var closestControlStartPointToMidPoint = ushort.MaxValue;

            // go through all controls and find the start and end offsets that are closest to the absolute mid-point
            foreach (var control in m_Device.m_ChildrenForEachControl)
            {
                var stateBlock = control.m_StateBlock;

                // don't consider controls that end before the start of the parent range, or start after
                // the end of the parent range
                if (stateBlock.effectiveBitOffset + stateBlock.sizeInBits - 1 < startOffset ||
                    stateBlock.effectiveBitOffset >= parent.endBitOffset)
                    continue;

                // don't consider controls that are larger than the parent range
                if (stateBlock.sizeInBits > parent.endBitOffset - startOffset)
                    continue;

                // don't consider controls that start or end on the same boundary as the parent
                if (stateBlock.effectiveBitOffset == startOffset ||
                    stateBlock.effectiveBitOffset + stateBlock.sizeInBits == parent.endBitOffset)
                    continue;

                if (Math.Abs(stateBlock.effectiveBitOffset + stateBlock.sizeInBits - (int)absoluteMidPoint) <
                    Math.Abs(closestControlEndPointToMidPoint - absoluteMidPoint) &&
                    stateBlock.effectiveBitOffset + stateBlock.sizeInBits < parent.endBitOffset)
                {
                    closestControlEndPointToMidPoint = (ushort)(stateBlock.effectiveBitOffset + stateBlock.sizeInBits);
                }

                if (Math.Abs(stateBlock.effectiveBitOffset - (int)absoluteMidPoint) <
                    Math.Abs(closestControlStartPointToMidPoint - absoluteMidPoint) &&
                    stateBlock.effectiveBitOffset >= startOffset)
                {
                    closestControlStartPointToMidPoint = (ushort)stateBlock.effectiveBitOffset;
                }
            }

            var absoluteMidPointCollisions = 0;
            var controlStartMidPointCollisions  = 0;
            var controlEndMidPointCollisions = 0;

            // figure out which of the possible midpoints intersects the fewest controls. The one with the fewest
            // is the best one because it means fewer controls will be added to this node.
            foreach (var control in m_Device.m_ChildrenForEachControl)
            {
                if (closestControlStartPointToMidPoint != ushort.MaxValue &&
                    closestControlStartPointToMidPoint > control.m_StateBlock.effectiveBitOffset &&
                    closestControlStartPointToMidPoint < control.m_StateBlock.effectiveBitOffset + control.m_StateBlock.sizeInBits)
                    controlStartMidPointCollisions++;

                if (closestControlEndPointToMidPoint != ushort.MaxValue &&
                    closestControlEndPointToMidPoint > control.m_StateBlock.effectiveBitOffset &&
                    closestControlEndPointToMidPoint < control.m_StateBlock.effectiveBitOffset + control.m_StateBlock.sizeInBits)
                    controlEndMidPointCollisions++;

                if (absoluteMidPoint > control.m_StateBlock.effectiveBitOffset &&
                    absoluteMidPoint < control.m_StateBlock.effectiveBitOffset + control.m_StateBlock.sizeInBits)
                    absoluteMidPointCollisions++;
            }

            if (closestControlEndPointToMidPoint != ushort.MaxValue &&
                controlEndMidPointCollisions <= controlStartMidPointCollisions &&
                controlEndMidPointCollisions <= absoluteMidPointCollisions)
            {
                Debug.Assert(closestControlEndPointToMidPoint >= startOffset && closestControlEndPointToMidPoint <= startOffset + parent.endBitOffset);
                return closestControlEndPointToMidPoint;
            }

            if (closestControlStartPointToMidPoint != ushort.MaxValue &&
                controlStartMidPointCollisions <= controlEndMidPointCollisions &&
                controlStartMidPointCollisions <= absoluteMidPointCollisions)
            {
                Debug.Assert(closestControlStartPointToMidPoint >= startOffset && closestControlStartPointToMidPoint <= startOffset + parent.endBitOffset);
                return closestControlStartPointToMidPoint;
            }

            Debug.Assert(absoluteMidPoint >= startOffset && absoluteMidPoint <= startOffset + parent.endBitOffset);
            return absoluteMidPoint;
        }

        private void AddControlToNode(InputControl control, ref int controlIndiciesNextFreeIndex, int nodeIndex)
        {
            Debug.Assert(m_Device.m_ControlTreeNodes[nodeIndex].controlCount < 255,
                "Control bit range nodes can address maximum of 255 controls.");

            ref var node = ref m_Device.m_ControlTreeNodes[nodeIndex];
            var leafControlStartIndex = node.controlStartIndex;
            if (node.controlCount == 0)
            {
                node.controlStartIndex = (ushort)controlIndiciesNextFreeIndex;
                leafControlStartIndex = node.controlStartIndex;
            }

            ArrayHelpers.InsertAt(ref m_Device.m_ControlTreeIndices,
                node.controlStartIndex + node.controlCount,
                GetControlIndex(control));
            ++node.controlCount;
            ++controlIndiciesNextFreeIndex;

            // bump up all the start indicies for nodes that have a start index larger than the one we just inserted into
            for (var i = 0; i < m_Device.m_ControlTreeNodes.Length; i++)
            {
                if (m_Device.m_ControlTreeNodes[i].controlCount == 0 ||
                    m_Device.m_ControlTreeNodes[i].controlStartIndex <= leafControlStartIndex)
                    continue;

                ++m_Device.m_ControlTreeNodes[i].controlStartIndex;
            }
        }

        private void AddChildren(ref InputDevice.ControlBitRangeNode parent, InputDevice.ControlBitRangeNode left, InputDevice.ControlBitRangeNode right)
        {
            // if this node has a child start index, its already in the tree
            if (parent.leftChildIndex != -1)
                return;

            var startIndex = m_Device.m_ControlTreeNodes.Length;
            parent.leftChildIndex = (short)startIndex;
            Array.Resize(ref m_Device.m_ControlTreeNodes, startIndex + 2);
            m_Device.m_ControlTreeNodes[startIndex] = left;
            m_Device.m_ControlTreeNodes[startIndex + 1] = right;
        }

        private ushort GetControlIndex(InputControl control)
        {
            for (var i = 0; i < m_Device.m_ChildrenForEachControl.Length; i++)
            {
                if (control == m_Device.m_ChildrenForEachControl[i])
                    return (ushort)i;
            }

            throw new InvalidOperationException($"InputDeviceBuilder error. Couldn't find control {control}.");
        }

        private static InputDeviceBuilder s_Instance;
        private static int s_InstanceRef;

        internal static ref InputDeviceBuilder instance
        {
            get
            {
                Debug.Assert(s_InstanceRef > 0, "Must hold an instance reference");
                return ref s_Instance;
            }
        }

        internal static RefInstance Ref()
        {
            Debug.Assert(s_Instance.m_Device == null,
                "InputDeviceBuilder is already in use! Cannot use the builder recursively");
            ++s_InstanceRef;
            return new RefInstance();
        }

        // Helper that allows setting up an InputDeviceBuilder such that it will either be created
        // locally and temporarily or, if one already exists globally, reused.
        internal struct RefInstance : IDisposable
        {
            public void Dispose()
            {
                --s_InstanceRef;
                if (s_InstanceRef <= 0)
                {
                    s_Instance.Dispose();
                    s_Instance = default;
                    s_InstanceRef = 0;
                }
                else
                    // Make sure we reset when there is an exception.
                    s_Instance.Reset();
            }
        }
    }
}