private SoulsDropdown GetDropdown(string[] items, string prompt, int width, bool enabled = true)
        {
            SoulsDropdown box = new SoulsDropdown(this)
            {
                Enabled = enabled,
                Width   = width,
                Prompt  = prompt
            };

            if (items != null)
            {
                box.Items.AddRange(items);
            }

            // See https://stackoverflow.com/a/1883072/7281613.
            box.MouseWheel += (sender, args) =>
            {
                if (!box.DroppedDown)
                {
                    ((HandledMouseEventArgs)args).Handled = true;
                }
            };

            return(box);
        }
        private void LinkItemLines(Control[] line1, Control[] line2)
        {
            // Infusions are called "modifications" in most of the codebase. Swapping to "infusions" happened pretty
            // late in the project, so it wasn't worth renaming everything internally.
            const string ModString       = "Infusions";
            const string ReinforceString = "Reinforcement";

            // There were originally two strings here ("Uninfusable" and "Unreinforceable"). They were later replaced
            // with "Locked" in order to reclaim space (since LiveSplit's window width is fixed).
            const string LockedString = "Locked";
            const string NaString     = "N/A";

            SoulsDropdown itemTypes      = (SoulsDropdown)line1[0];
            SoulsDropdown itemList       = (SoulsDropdown)line1[1];
            SoulsDropdown mods           = (SoulsDropdown)line2[0];
            SoulsDropdown reinforcements = (SoulsDropdown)line2[1];

            TextBox itemCount = (TextBox)line2[2];

            itemTypes.SelectedIndexChanged += (sender, args) =>
            {
                const int FlameIndex  = 8;
                const int ShieldIndex = 27;
                const int WeaponIndex = 34;

                itemList.Enabled  = true;
                itemCount.Enabled = true;
                itemCount.Text    = "1";

                var rawList = itemMap[itemTypes.Text];
                var items   = itemList.Items;

                // Refreshing the prompt (rather than just clearing items) also resets the background color to red.
                itemList.RefreshPrompt("Items", true);

                int typeIndex = itemTypes.SelectedIndex;

                bool isArmorType  = typeIndex >= 1 && typeIndex <= 4;
                bool isShield     = typeIndex == ShieldIndex;
                bool isWeaponType = typeIndex >= WeaponIndex;
                bool isFlame      = typeIndex == FlameIndex;

                modsApplicable = isWeaponType || isShield;

                if (isArmorType || isShield || isWeaponType || isFlame)
                {
                    upgrades = new int[rawList.Length];

                    for (int i = 0; i < rawList.Length; i++)
                    {
                        string value = rawList[i];

                        if (value.Length == 0 || value[0] == '-')
                        {
                            items.Add(value);

                            continue;
                        }

                        string[] tokens = value.Split('|');
                        string   name   = tokens[0];

                        // Armor and weapons store upgrade data differently. Since armor can't be modified, the maximum
                        // reinforcement value is stored. For weapons, the modification category is stored, which in
                        // turn informs reinforcement.
                        int upgradeValue;

                        if (!modsApplicable)
                        {
                            string reinforcementString = tokens[1];

                            upgradeValue = reinforcementString[0] == '+'
                                                                ? int.Parse(reinforcementString.Substring(1))
                                                                : 0;
                        }
                        // Weapons and shields follow the same logic for toggling mods and reinforcement.
                        else
                        {
                            upgradeValue = (int)Enum.Parse(typeof(ModificationTypes), tokens[1]);
                        }

                        items.Add(name);
                        upgrades[i] = upgradeValue;
                    }

                    mods.RefreshPrompt(modsApplicable ? ModString : NaString);
                    reinforcements.RefreshPrompt(ReinforceString);
                }
                else
                {
                    items.AddRange(rawList);
                    mods.RefreshPrompt(NaString);
                    reinforcements.RefreshPrompt(NaString);
                    upgrades = null;
                }
            };

            itemList.SelectedIndexChanged += (sender, args) =>
            {
                const int ConsumableIndex = 16;
                const int EstusIndex      = 6;

                int index = itemList.SelectedIndex;

                // The estus flask can be upgraded, so some special logic is needed to account for that.
                bool isConsumable = itemTypes.SelectedIndex == ConsumableIndex;
                bool isEstus      = isConsumable && index == EstusIndex;

                // The upgrade array being null means that the current item type is not equipment.
                if (upgrades == null && !isEstus)
                {
                    bool previouslyEstus = isConsumable && itemList.PreviousIndex == EstusIndex;

                    if (previouslyEstus)
                    {
                        reinforcements.RefreshPrompt("N/A");
                    }

                    return;
                }

                // The maximum estus upgrade is +7.
                int data = isEstus ? 7 : upgrades[index];

                // Armor and flames can be reinforced, but never modified.
                if (!modsApplicable)
                {
                    if (data > 0)
                    {
                        reinforcements.RefreshPrompt(ReinforceString, true);
                        reinforcements.Items.AddRange(GetReinforcementList(data));
                    }
                    else
                    {
                        reinforcements.RefreshPrompt(LockedString);
                    }
                }
                else
                {
                    ModificationTypes modType = (ModificationTypes)data;

                    switch (modType)
                    {
                    case ModificationTypes.None:
                        mods.RefreshPrompt(LockedString);
                        reinforcements.RefreshPrompt(LockedString);

                        break;

                    // "Special" in this context means that the weapon is unique. It can be reinforced up to +5,
                    // but can't be modified.
                    case ModificationTypes.Special:
                        mods.RefreshPrompt(LockedString);
                        reinforcements.RefreshPrompt(ReinforceString, true);
                        reinforcements.Items.AddRange(GetReinforcementList(5));

                        break;

                    // For standard weapons, the maximum reinforcement is based on mod type.
                    default:
                        // Crossbows and shields use a restricted set of mods. Max reinforcement values are the
                        // same for each type.
                        var availableMods = modType == ModificationTypes.Standard
                                                                ? modReinforcementMap.Keys.ToArray()
                                                                : new []
                        {
                            "Basic",
                            "Crystal",
                            "Lightning",
                            "Magic",
                            "Divine",
                            "Fire"
                        };

                        mods.RefreshPrompt(ModString, true);
                        mods.Items.AddRange(availableMods);
                        reinforcements.RefreshPrompt(ReinforceString);

                        break;
                    }
                }
            };

            mods.SelectedIndexChanged += (sender, args) =>
            {
                // This function can only be called for standard (i.e. modifiable) weapons.
                int max = modReinforcementMap[mods.Text];

                reinforcements.RefreshPrompt(ReinforceString, true);
                reinforcements.Items.AddRange(GetReinforcementList(max));
            };
        }