using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

namespace Barotrauma.Items.Components
{
    partial class Fabricator : Powered, IServerSerializable, IClientSerializable
    {
        private enum SortBy
        {
            Category,
            Alphabetical,
            SkillRequirement,
            Price
        }

        private GUIListBox itemList;

        private GUIFrame selectedItemFrame;
        private GUIFrame selectedItemReqsFrame;

        private GUILayoutGroup outputTopArea, paddedOutputArea;

        private GUITextBlock amountTextMax;
        private GUIScrollBar amountInput;

        public GUIButton ActivateButton
        {
            get { return activateButton; }
        }
        private GUIButton activateButton;

        private GUITextBox itemFilterBox;
        private GUITickBox availableOnlyTickBox;
        private GUIDropDown sortByDropdown;

        private GUIComponent outputSlot;
        private GUIComponent inputInventoryHolder, outputInventoryHolder;

        private readonly List<GUIButton> itemCategoryButtons = new List<GUIButton>();
        private MapEntityCategory? selectedItemCategory;

        private GUITextBlock requiresRecipeText;
        private GUITextBlock nothingToShowText;

        public FabricationRecipe SelectedItem
        {
            get { return selectedItem; }
        }
        private FabricationRecipe selectedItem;

        /// <summary>
        /// Which character's skills the current view is displayed based on
        /// </summary>
        private Character displayingForCharacter;

        public Identifier SelectedItemIdentifier => SelectedItem?.TargetItem.Identifier ?? Identifier.Empty;

        private GUIComponent inSufficientPowerWarning;

        private FabricationRecipe pendingFabricatedItem;

        private class ToolTip
        {
            public Rectangle TargetElement;
            public LocalizedString Tooltip;
        }
        private ToolTip tooltip;

        private GUITextBlock requiredTimeBlock;

        [Serialize("FabricatorCreate", IsPropertySaveable.Yes)]
        public string CreateButtonText { get; set; }

        [Serialize("vendingmachine.outofstock", IsPropertySaveable.Yes)]
        public string FabricationLimitReachedText { get; set; }

        [Serialize(true, IsPropertySaveable.No)]
        public bool ShowSortByDropdown { get; set; }

        [Serialize(true, IsPropertySaveable.No)]
        public bool ShowAvailableOnlyTickBox { get; set; }

        [Serialize(true, IsPropertySaveable.No)]
        public bool ShowCategoryButtons { get; set; }

        public override bool RecreateGUIOnResolutionChange => true;

        protected override void OnResolutionChanged()
        {
            if (GuiFrame != null)
            {
                InitInventoryUIs();
            }
        }

        protected override void CreateGUI()
        {
            var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), GuiFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter);

            // === LABEL === //
            new GUITextBlock(new RectTransform(new Vector2(1f, 0.05f), paddedFrame.RectTransform), item.Prefab.Name, font: GUIStyle.SubHeadingFont)
            {
                TextAlignment = Alignment.Center,
                AutoScaleVertical = true
            };

            var innerArea = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.95f), paddedFrame.RectTransform, Anchor.Center), isHorizontal: true)
            {
                RelativeSpacing = 0.01f,
                Stretch = true,
                CanBeFocused = true
            };

            List<MapEntityCategory> itemCategories = Enum.GetValues<MapEntityCategory>().ToList();
            itemCategories.Remove(MapEntityCategory.None);
            itemCategories.RemoveAll(c => fabricationRecipes.None(f => f.Value?.TargetItem is ItemPrefab ti && ti.Category.HasFlag(c)));
            itemCategoryButtons.Clear();

            //only create category buttons if there's more than one category in addition to "All"
            if (ShowCategoryButtons && itemCategories.Count > 2)
            {
                // ===  Item category buttons ===
                var categoryButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.05f, 1.0f), innerArea.RectTransform))
                {
                    RelativeSpacing = 0.01f
                };

                int buttonSize = Math.Min(categoryButtonContainer.Rect.Width, categoryButtonContainer.Rect.Height / itemCategories.Count);

                var categoryButton = new GUIButton(new RectTransform(new Point(buttonSize), categoryButtonContainer.RectTransform), style: "CategoryButton.All")
                {
                    ToolTip = TextManager.Get("MapEntityCategory.All"),
                    OnClicked = OnClickedCategoryButton
                };
                itemCategoryButtons.Add(categoryButton);
                foreach (MapEntityCategory category in itemCategories)
                {
                    categoryButton = new GUIButton(new RectTransform(new Point(buttonSize), categoryButtonContainer.RectTransform),
                        style: "CategoryButton." + category)
                    {
                        ToolTip = TextManager.Get("MapEntityCategory." + category),
                        UserData = category,
                        OnClicked = OnClickedCategoryButton
                    };
                    itemCategoryButtons.Add(categoryButton);
                }
                bool OnClickedCategoryButton(GUIButton button, object userData)
                {
                    MapEntityCategory? newCategory = !button.Selected ? (MapEntityCategory?)userData : null;
                    if (newCategory.HasValue) { itemFilterBox.Text = ""; }
                    selectedItemCategory = newCategory;
                    FilterEntities(newCategory, itemFilterBox.Text);
                    return true;
                }
                foreach (var btn in itemCategoryButtons)
                {
                    btn.RectTransform.SizeChanged += () =>
                    {
                        if (btn.Frame.sprites == null || !btn.Frame.sprites.TryGetValue(GUIComponent.ComponentState.None, out var spriteList)) { return; }
                        var sprite = spriteList?.First();
                        if (sprite == null) { return; }
                        btn.RectTransform.NonScaledSize = new Point(btn.Rect.Width, (int)(btn.Rect.Width * ((float)sprite.Sprite.SourceRect.Height / sprite.Sprite.SourceRect.Width)));
                    };
                }
            }            

            var mainFrame = new GUILayoutGroup(new RectTransform(Vector2.One, innerArea.RectTransform), childAnchor: Anchor.TopCenter)
            {
                RelativeSpacing = 0.02f,
                Stretch = true,
                CanBeFocused = true
            };
            
            // === TOP AREA ===
            var topFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.8f), mainFrame.RectTransform), style: "InnerFrameDark");

                // === ITEM LIST ===
                var itemListFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), topFrame.RectTransform), childAnchor: Anchor.Center);
                    var paddedItemFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), itemListFrame.RectTransform), isHorizontal: false)
                    {
                        Stretch = true
                    };
                        var filterArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), paddedItemFrame.RectTransform), isHorizontal: true)
                        {
                            Stretch = true,
                            RelativeSpacing = 0.03f,
                            UserData = "filterarea"
                        };
                            new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), filterArea.RectTransform), TextManager.Get("serverlog.filter"),
                                font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft)
                            {
                                Padding = Vector4.Zero,
                                AutoScaleVertical = true
                            };
                            itemFilterBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), createClearButton: true)
                            {
                                OverflowClip = true
                            };
                            itemFilterBox.OnTextChanged += (textBox, text) =>
                            {
                                FilterEntities(selectedItemCategory, text); 
                                return true;
                            };
                            filterArea.RectTransform.MinSize = new Point(0, itemFilterBox.Rect.Height);
                            filterArea.RectTransform.MaxSize = new Point(int.MaxValue, itemFilterBox.Rect.Height);

                        var sortByArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), paddedItemFrame.RectTransform), isHorizontal: true)
                        {
                            Stretch = true,
                            RelativeSpacing = 0.03f,
                            Visible = ShowSortByDropdown,
                            IgnoreLayoutGroups = !ShowSortByDropdown
                        };
                            new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), sortByArea.RectTransform), TextManager.Get("campaignstore.sortby"),
                                font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft)
                            {
                                Padding = Vector4.Zero,
                                AutoScaleVertical = true
                            };
                            sortByDropdown = new GUIDropDown(new RectTransform(new Vector2(0.8f, 1.0f), sortByArea.RectTransform));
                            foreach (SortBy sortBy in Enum.GetValues<SortBy>())
                            {
                                sortByDropdown.AddItem(TextManager.Get("fabricator.sortby." + sortBy), userData: sortBy);
                            }
                            sortByDropdown.Select(index: 0);
                            sortByDropdown.AfterSelected += (GUIComponent selected, object userdata) =>
                            {
                                FilterEntities(selectedItemCategory, itemFilterBox.Text);
                                SortItems(character: Character.Controlled);
                                return true;
                            };
                            sortByArea.RectTransform.MinSize = new Point(0, sortByDropdown.Rect.Height);
                            sortByArea.RectTransform.MaxSize = new Point(int.MaxValue, sortByDropdown.Rect.Height);

                        var availableOnlyTickBoxArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), paddedItemFrame.RectTransform), isHorizontal: true)
                        {
                            Stretch = true,
                            Visible = ShowAvailableOnlyTickBox,
                            IgnoreLayoutGroups = !ShowAvailableOnlyTickBox
                        };
                            new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), availableOnlyTickBoxArea.RectTransform), TextManager.Get("fabricator.onlyshowavailable"),
                                font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft)
                            {
                                Padding = Vector4.Zero,
                                AutoScaleVertical = true
                            };
                            availableOnlyTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f), availableOnlyTickBoxArea.RectTransform, scaleBasis: ScaleBasis.BothHeight), label: string.Empty)
                            {
                                ToolTip = TextManager.Get("fabricator.onlyshowavailable.tooltip")
                            };
                            availableOnlyTickBox.OnSelected += (tickbox) =>
                            {                                
                                FilterEntities(selectedItemCategory, itemFilterBox.Text);
                                return true;
                            };
                            availableOnlyTickBox.RectTransform.MinSize = new Point(availableOnlyTickBox.Rect.Height);
                            availableOnlyTickBox.RectTransform.IsFixedSize = true;
                            availableOnlyTickBoxArea.RectTransform.MinSize = new Point(0, availableOnlyTickBox.Rect.Height);
                            availableOnlyTickBoxArea.RectTransform.MaxSize = new Point(int.MaxValue, availableOnlyTickBox.Rect.Height);

                        itemList = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), paddedItemFrame.RectTransform), style: null)
                        {
                            PlaySoundOnSelect = true,
                            OnSelected = (component, userdata) =>
                            {
                                if (userdata is FabricationRecipe fabricationRecipe) 
                                { 
                                    selectedItem = fabricationRecipe;
                                    SelectItem(Character.Controlled, selectedItem); 
                                    return true;
                                }
                                else
                                {
                                    return false;
                                }
                            }
                        };

            // === SEPARATOR === //
            new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), topFrame.RectTransform, Anchor.Center), style: "VerticalLine");

                // === OUTPUT AREA === //
                var outputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1f), topFrame.RectTransform, Anchor.TopRight), childAnchor: Anchor.Center);
                    paddedOutputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), outputArea.RectTransform)) { Stretch = true };
                        outputTopArea = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), paddedOutputArea.RectTransform, Anchor.Center), isHorizontal: true);
                            // === OUTPUT SLOT === //
                            outputSlot = new GUIFrame(new RectTransform(new Vector2(0.4f, 0.4f), outputTopArea.RectTransform, scaleBasis: ScaleBasis.BothWidth), style: null);
                                outputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(1f, 1.0f), outputSlot.RectTransform, Anchor.BottomCenter), style: null);
                                    new GUICustomComponent(new RectTransform(Vector2.One, outputInventoryHolder.RectTransform), DrawOutputOverLay) { CanBeFocused = false };
                            // === DESCRIPTION === //
                            selectedItemFrame = new GUIFrame(new RectTransform(new Vector2(0.6f, 1f), outputTopArea.RectTransform), style: null);
                        // === REQUIREMENTS === //
                        selectedItemReqsFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), paddedOutputArea.RectTransform), style: null);

            // === BOTTOM AREA === //
            var bottomFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.2f), mainFrame.RectTransform), style: null);

            if (inputContainer.Capacity > 0)
            {
                // === SEPARATOR === //
                var separatorArea = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.15f), bottomFrame.RectTransform, Anchor.TopCenter), childAnchor: Anchor.CenterLeft, isHorizontal: true)
                {
                    Stretch = true,
                    RelativeSpacing = 0.03f
                };
                var inputLabel = new GUITextBlock(new RectTransform(Vector2.One, separatorArea.RectTransform), TextManager.Get("fabricator.input", "uilabel.input"), font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero };
                inputLabel.RectTransform.Resize(new Point((int)inputLabel.Font.MeasureString(inputLabel.Text).X, inputLabel.RectTransform.Rect.Height));
                new GUIFrame(new RectTransform(Vector2.One, separatorArea.RectTransform), style: "HorizontalLine");

                // === INPUT AREA === //
                var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 1f), bottomFrame.RectTransform, Anchor.BottomCenter), isHorizontal: true, childAnchor: Anchor.BottomLeft);

                // === INPUT SLOTS === //
                inputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(0.7f, 1f), inputArea.RectTransform), style: null);
                new GUICustomComponent(new RectTransform(Vector2.One, inputInventoryHolder.RectTransform), DrawInputOverLay) { CanBeFocused = false };

                // === ACTIVATE BUTTON === //
                var buttonFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 0.9f), inputArea.RectTransform))
                {
                    Stretch = true,
                    RelativeSpacing = 0.05f
                };

                var amountInputHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), buttonFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft)
                {
                    Stretch = true
                };

                new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), amountInputHolder.RectTransform), "1", textAlignment: Alignment.Center);

                amountInput = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1.0f), amountInputHolder.RectTransform), barSize: 0.1f, style: "GUISlider")
                {
                    OnMoved = (GUIScrollBar scrollBar, float barScroll) =>
                    {
                        scrollBar.Step = 1.0f / Math.Max(scrollBar.Range.Y - 1, 1);
                        AmountToFabricate = (int)MathF.Round(scrollBar.BarScrollValue);
                        RefreshActivateButtonText();
                        if (GameMain.Client != null)
                        {
                            pendingFabricatedItem = null;
                            item.CreateClientEvent(this);
                        }
                        return true;
                    }
                };

                amountTextMax = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), amountInputHolder.RectTransform), "1", textAlignment: Alignment.Center);

                activateButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.6f), buttonFrame.RectTransform),
                    TextManager.Get(CreateButtonText), style: "DeviceButton")
                {
                    OnClicked = StartButtonClicked,
                    UserData = selectedItem,
                    Enabled = false
                }; 

                //spacing
                new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), buttonFrame.RectTransform), style: null);
            }
            else
            {
                bottomFrame.RectTransform.RelativeSize = new Vector2(1.0f, 0.1f);
                activateButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), bottomFrame.RectTransform, Anchor.CenterRight),
                    TextManager.Get(CreateButtonText), style: "DeviceButtonFixedSize")
                {
                    OnClicked = StartButtonClicked,
                    UserData = selectedItem,
                    Enabled = false
                };
            }
            // === POWER WARNING === //
            inSufficientPowerWarning = new GUITextBlock(new RectTransform(Vector2.One, activateButton.RectTransform),
                TextManager.Get("FabricatorNoPower"), textColor: GUIStyle.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow", wrap: true)
            {
                HoverColor = Color.Black,
                IgnoreLayoutGroups = true,
                Visible = false,
                CanBeFocused = false
            };
            CreateRecipes();

            foreach (MapEntityCategory category in itemCategories)
            {
                new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform),
                    TextManager.Get("MapEntityCategory." + category), textColor: GUIStyle.TextColorBright)
                {
                    CanBeFocused = false,
                    UserData = category,
                    Visible = false
                };
            }

            requiresRecipeText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), itemList.Content.RectTransform),
                TextManager.Get("fabricatorrequiresrecipe"), textColor: Color.Red, font: GUIStyle.SubHeadingFont)
            {
                AutoScaleHorizontal = true,
                CanBeFocused = false
            };

            nothingToShowText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.8f), itemList.Content.RectTransform), TextManager.Get("noitemsheader"),
                textAlignment: Alignment.Center, textColor: GUIStyle.TextColorDim)
            {
                CanBeFocused = false,
                Visible = false
            };

            SortItems(character: Character.Controlled);
        }

        private void RefreshActivateButtonText()
        {
            if (amountInput == null)
            {
                activateButton.Text = TextManager.Get(IsActive ? "FabricatorCancel" : CreateButtonText);
            }
            else
            {
                activateButton.Text =
                    IsActive ?
                    $"{TextManager.Get("FabricatorCancel")} ({amountRemaining})" :
                    $"{TextManager.Get(CreateButtonText)} ({AmountToFabricate})";
            }
        }

        partial void CreateRecipes()
        {
            itemList.Content.RectTransform.ClearChildren();

            foreach (FabricationRecipe fi in fabricationRecipes.Values)
            {
                RichString recipeTooltip =
                    fi.RequiresRecipe ?
                    RichString.Rich(fi.TargetItem.Description + "\n\n" + $"‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{TextManager.Get("fabricatorrequiresrecipe")}‖color:end‖") :
                    RichString.Rich(fi.TargetItem.Description);

                var frame = new GUIFrame(new RectTransform(new Point(itemList.Content.Rect.Width, (int)(40 * GUI.yScale)), itemList.Content.RectTransform), style: null)
                {
                    UserData = fi,
                    HoverColor = Color.Gold * 0.2f,
                    SelectedColor = Color.Gold * 0.5f,
                    ToolTip = recipeTooltip
                };
                
                var container = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform),
                    childAnchor: Anchor.CenterLeft, isHorizontal: true) { RelativeSpacing = 0.02f };

                var itemIcon = fi.TargetItem.InventoryIcon ?? fi.TargetItem.Sprite;
                if (itemIcon != null)
                {
                    new GUIImage(new RectTransform(new Point(frame.Rect.Height,frame.Rect.Height), container.RectTransform),
                        itemIcon, scaleToFit: true)
                    {
                        Color = itemIcon == fi.TargetItem.Sprite ? fi.TargetItem.SpriteColor : fi.TargetItem.InventoryIconColor,
                        ToolTip = recipeTooltip
                    };
                }

                new GUITextBlock(new RectTransform(new Vector2(0.85f, 1f), container.RectTransform),
                    RichString.Rich(GetRecipeNameAndAmount(fi)), font: GUIStyle.SmallFont)
                {
                    Padding = Vector4.Zero,
                    AutoScaleVertical = true,
                    ToolTip = recipeTooltip
                };

                new GUITextBlock(new RectTransform(new Vector2(0.85f, 1f), frame.RectTransform, Anchor.BottomRight), 
                    TextManager.Get(FabricationLimitReachedText), font: GUIStyle.SmallFont, textAlignment: Alignment.BottomRight)
                {
                    UserData = nameof(FabricationLimitReachedText),
                    Visible = false
                };
            }
        }

        private void InitInventoryUIs()
        {
            if (inputInventoryHolder != null)
            {
                inputContainer.AllowUIOverlap = true;
                inputContainer.Inventory.DrawWhenEquipped = true;
                inputContainer.Inventory.RectTransform = inputInventoryHolder.RectTransform;
            }
            outputContainer.AllowUIOverlap = true;
            outputContainer.Inventory.DrawWhenEquipped = true;
            outputContainer.Inventory.RectTransform = outputInventoryHolder.RectTransform;
        }

        private static RichString GetRecipeNameAndAmount(FabricationRecipe fabricationRecipe)
        {
            if (fabricationRecipe == null) { return ""; }
            if (fabricationRecipe.Amount > 1)
            {
                return TextManager.GetWithVariables("fabricationrecipenamewithamount",
                    ("[name]", RichString.Rich(fabricationRecipe.DisplayName)), ("[amount]", fabricationRecipe.Amount.ToString()));
            }
            else
            {
                return RichString.Rich(fabricationRecipe.DisplayName);
            }
        }

        partial void OnItemLoadedProjSpecific()
        {
            CreateGUI();
            InitInventoryUIs();
        }

        partial void SelectProjSpecific(Character character)
        {
            if (character != Character.Controlled) { return; }

            var nonItems = itemList.Content.Children.Where(c => c.UserData is not FabricationRecipe).ToList();
            nonItems.ForEach(i => i.Visible = false);

            SortItems(character);
            FilterEntities(selectedItemCategory, itemFilterBox?.Text ?? string.Empty);
            HideEmptyItemListCategories();
        }

        private void SortItems(Character character)
        {
            SortBy sortBy = (SortBy)sortByDropdown.SelectedData;

            itemList.Content.RectTransform.SortChildren((c1, c2) =>
            {
                var item1 = c1.GUIComponent.UserData as FabricationRecipe;
                var item2 = c2.GUIComponent.UserData as FabricationRecipe;

                if (item1 == null && item2 == null)
                {
                    return 0;
                }
                else if (item1 == null)
                {
                    return -1;
                }
                else if (item2 == null)
                {
                    return 1;
                }

                bool missingRecipe1 = MissingRequiredRecipe(item1, character);
                bool missingRecipe2 = MissingRequiredRecipe(item2, character);
                if (missingRecipe1 != missingRecipe2)
                {
                    return missingRecipe1.CompareTo(missingRecipe2);
                }

                switch (sortBy)
                {
                    case SortBy.Alphabetical:
                        return string.Compare(item1.DisplayName.Value, item2.DisplayName.Value);
                    case SortBy.Category:
                        var category1 = EnumExtensions.GetIndividualFlags(item1.TargetItem.Category).FirstOrDefault();
                        var category2 = EnumExtensions.GetIndividualFlags(item2.TargetItem.Category).FirstOrDefault();
                        if (category1 == category2)
                        {
                            return string.Compare(item1.DisplayName.Value, item2.DisplayName.Value);
                        }
                        return category1.CompareTo(category2);                        
                    case SortBy.SkillRequirement:
                        float skillRequirement1 = item1.RequiredSkills.Sum(skill => skill.Level);
                        float skillRequirement2 = item2.RequiredSkills.Sum(skill => skill.Level);
                        if (MathUtils.NearlyEqual(skillRequirement1, skillRequirement2))
                        {
                            return string.Compare(item1.DisplayName.Value, item2.DisplayName.Value);
                        }
                        return skillRequirement1.CompareTo(skillRequirement2);
                    case SortBy.Price:
                        float itemValue1 = item1.TargetItem.DefaultPrice?.Price ?? 0;
                        float itemValue2 = item2.TargetItem.DefaultPrice?.Price ?? 0;
                        if (MathUtils.NearlyEqual(itemValue1, itemValue2))
                        {
                            return string.Compare(item1.DisplayName.Value, item2.DisplayName.Value);
                        }
                        return itemValue2.CompareTo(itemValue1);
                    default:
                        throw new NotImplementedException($"Sorting by {sortBy} has not been implemented.");
                }
            });

            if (sortBy == SortBy.Category)
            {
                foreach (var categoryText in itemList.Content.Children.Where(c => c.UserData?.GetType() == typeof(MapEntityCategory)).ToList())
                {
                    categoryText.RectTransform.SetAsLastChild();
                    var category = (MapEntityCategory)categoryText.UserData;
                    var firstChildWithMatchingCategory = itemList.Content.Children.FirstOrDefault(c => c.UserData is FabricationRecipe recipe && EnumExtensions.GetIndividualFlags(recipe.TargetItem.Category).FirstOrDefault() == category);
                    if (firstChildWithMatchingCategory != null)
                    { 
                        categoryText.RectTransform.RepositionChildInHierarchy(itemList.Content.GetChildIndex(firstChildWithMatchingCategory));
                        categoryText.Visible = true;
                    }
                    else
                    {
                        categoryText.Visible = false;
                    }
                }
            }

            requiresRecipeText.RectTransform.SetAsLastChild();
            var firstMissingRecipe = itemList.Content.Children.FirstOrDefault(c => c.UserData is FabricationRecipe recipe && MissingRequiredRecipe(recipe, character));
            if (firstMissingRecipe != null)
            {
                requiresRecipeText.RectTransform.RepositionChildInHierarchy(itemList.Content.GetChildIndex(firstMissingRecipe));
                requiresRecipeText.Visible = true;
            }
            else
            {
                requiresRecipeText.Visible = false;
            }

            HideEmptyItemListCategories();
        }

        private readonly Dictionary<FabricationRecipe.RequiredItem, int> missingIngredientCounts = new Dictionary<FabricationRecipe.RequiredItem, int>();
        private float ingredientHighlightTimer;

        private void DrawInputOverLay(SpriteBatch spriteBatch, GUICustomComponent overlayComponent)
        {
            overlayComponent.RectTransform.SetAsLastChild();

            missingIngredientCounts.Clear();

            FabricationRecipe targetItem = fabricatedItem ?? selectedItem;
            if (targetItem != null)
            {
                foreach (FabricationRecipe.RequiredItem requiredItem in targetItem.RequiredItems)
                {
                    if (missingIngredientCounts.ContainsKey(requiredItem))
                    {
                        missingIngredientCounts[requiredItem] += requiredItem.Amount;
                    }
                    else
                    {
                        missingIngredientCounts[requiredItem] = requiredItem.Amount;
                    }
                }
                foreach (Item item in inputContainer.Inventory.AllItems)
                {
                    var missingIngredient = missingIngredientCounts.Keys.FirstOrDefault(mi => mi.MatchesItem(item));
                    if (missingIngredient == null) { continue; }

                    if (missingIngredientCounts[missingIngredient] == 1)
                    {
                        missingIngredientCounts.Remove(missingIngredient);
                    }
                    else
                    {
                        missingIngredientCounts[missingIngredient]--;
                    }
                }

                if (ingredientHighlightTimer <= 0.0f)
                {
                    //highlight inventory slots that contain suitable ingredients in linked inventories
                    foreach (var inventory in linkedInventories)
                    {
                        if (inventory.visualSlots == null) { continue; }
                        for (int i = 0; i < inventory.Capacity; i++)
                        {
                            if (inventory.visualSlots[i].HighlightTimer > 0.0f) { continue; }
                            var availableItem = inventory.GetItemAt(i);
                            if (availableItem == null) { continue; }

                            if (missingIngredientCounts.Keys.Any(it => it.MatchesItem(availableItem)))
                            {
                                inventory.visualSlots[i].ShowBorderHighlight(GUIStyle.Green, 0.5f, 0.5f, 0.2f);
                                continue;
                            }
                            if (availableItem.OwnInventory != null)
                            {
                                for (int j = 0; j < availableItem.OwnInventory.Capacity; j++)
                                {
                                    var availableContainedItem = availableItem.OwnInventory.GetItemAt(i);
                                    if (availableContainedItem == null) { continue; }
                                    if (missingIngredientCounts.Keys.Any(it => it.MatchesItem(availableContainedItem)))
                                    {
                                        inventory.visualSlots[i].ShowBorderHighlight(GUIStyle.Green, 0.5f, 0.5f, 0.2f);
                                        break;
                                    }
                                }
                            }
                        }
                    }
                    ingredientHighlightTimer = 1.0f;
                }

                int slotIndex = 0;
                foreach (var kvp in missingIngredientCounts)
                {
                    if (inputContainer.Inventory?.visualSlots == null) { break; }

                    var requiredItem = kvp.Key;
                    int missingCount = kvp.Value;

                    while (slotIndex < inputContainer.Capacity && inputContainer.Inventory.GetItemAt(slotIndex) != null)
                    {
                        slotIndex++;
                    }

                    if (slotIndex >= inputContainer.Capacity) { break; }

                    if (slotIndex < inputContainer.Capacity && 
                        inputContainer.Inventory.visualSlots[slotIndex].HighlightTimer <= 0.0f &&
                        availableIngredients.Any(i => i.Value.Any() && requiredItem.MatchesItem(i.Value.First())))
                    {
                        inputContainer.Inventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Green, 0.5f, 0.5f, 0.2f);
                    }

                    Rectangle slotRect = inputContainer.Inventory.visualSlots[slotIndex].Rect;

                    var requiredItemPrefab = requiredItem.FirstMatchingPrefab;

                    float iconAlpha = 0.0f;
                    ItemPrefab requiredItemToDisplay = requiredItem.DefaultItem.IsEmpty ? null : requiredItem.ItemPrefabs.FirstOrDefault(p => p.Identifier == requiredItem.DefaultItem);
                    if (requiredItemToDisplay == null && requiredItem.ItemPrefabs.Multiple())
                    {
                        float iconCycleSpeed = 0.75f;
                        float iconCycleT = (float)Timing.TotalTime * iconCycleSpeed;
                        int iconIndex = (int)(iconCycleT % requiredItem.ItemPrefabs.Count());

                        requiredItemToDisplay = requiredItem.ItemPrefabs.Skip(iconIndex).FirstOrDefault();
                        iconAlpha = Math.Min(Math.Abs(MathF.Sin(iconCycleT * MathHelper.Pi)) * 2.0f, 1.0f);
                    }
                    else
                    {
                        requiredItemToDisplay ??= requiredItem.ItemPrefabs.FirstOrDefault();
                        iconAlpha = 1.0f;
                    }
                    if (iconAlpha > 0.0f)
                    {
                        var itemIcon = requiredItemToDisplay.InventoryIcon ?? requiredItemToDisplay.Sprite;
                        itemIcon.Draw(
                            spriteBatch,
                            slotRect.Center.ToVector2(),
                            color: requiredItemToDisplay.InventoryIconColor * 0.3f * iconAlpha,
                            scale: Math.Min(slotRect.Width * 0.9f / itemIcon.size.X, slotRect.Height * 0.9f / itemIcon.size.Y));
                    }

                    if (missingCount > 1)
                    {
                        Vector2 stackCountPos = new Vector2(slotRect.Right, slotRect.Bottom);
                        string stackCountText = "x" + missingCount;
                        stackCountPos -= GUIStyle.SmallFont.MeasureString(stackCountText) + new Vector2(4, 2);
                        GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black);
                        GUIStyle.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White);
                    }

                    if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f)
                    {
                        DrawConditionBar(spriteBatch, requiredItem.MinCondition);
                    }
                    else if (requiredItem.MaxCondition < 1.0f)
                    {
                        DrawConditionBar(spriteBatch, requiredItem.MaxCondition);
                    }

                    void DrawConditionBar(SpriteBatch sb, float condition)
                    {
                        int spacing = GUI.IntScale(4);
                        int height = GUI.IntScale(10);
                        GUI.DrawRectangle(spriteBatch, new Rectangle(slotRect.X + spacing, slotRect.Bottom - spacing - height, slotRect.Width - spacing * 2, height), Color.Black * 0.8f, true);
                        GUI.DrawRectangle(spriteBatch,
                            new Rectangle(slotRect.X + spacing, slotRect.Bottom - spacing - height, (int)((slotRect.Width - spacing * 2) * condition), height),
                            GUIStyle.Green * 0.8f, true);
                    }

                    if (slotRect.Contains(PlayerInput.MousePosition))
                    {
                        LocalizedString toolTipText = requiredItem.OverrideHeader;
                        if (requiredItem.OverrideHeader.IsNullOrEmpty())
                        {
                            var suitableIngredients = requiredItem.ItemPrefabs.Where(ip => !ip.HideInMenus).OrderBy(ip => ip.DefaultPrice?.Price ?? 0).Select(ip => ip.Name).Distinct();
                            toolTipText = GetSuitableIngredientText(suitableIngredients);
                        }
                        if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f)
                        {
                            toolTipText += " " + (int)Math.Round(requiredItem.MinCondition * 100) + "%";
                        }
                        else if (requiredItem.MaxCondition < 1.0f)
                        {
                            if (requiredItem.MaxCondition <= 0.0f)
                            {
                                toolTipText += " " + (int)Math.Round(requiredItem.MaxCondition * 100) + "%";
                            }
                            else
                            {
                                toolTipText += " 0-" + (int)Math.Round(requiredItem.MaxCondition * 100) + "%";
                            }
                        }
                        else if (requiredItem.MaxCondition <= 0.0f)
                        {
                            toolTipText = TextManager.GetWithVariable("displayname.emptyitem", "[itemname]", toolTipText);
                        }

                        toolTipText = $"‖color:{Color.White.ToStringHex()}‖{toolTipText}‖color:end‖";
                        if (!requiredItem.OverrideDescription.IsNullOrEmpty())
                        {
                            toolTipText += '\n' + requiredItem.OverrideDescription;
                        }
                        else if (!requiredItemPrefab.Description.IsNullOrEmpty())
                        {
                            toolTipText += '\n' + requiredItemPrefab.Description;
                        }
                        tooltip = new ToolTip { TargetElement = slotRect, Tooltip = toolTipText };
                    }

                    slotIndex++;
                }
            }
        }

        private LocalizedString GetSuitableIngredientText(IEnumerable<LocalizedString> itemNameList)
        {
            int count = itemNameList.Count();
            if (count == 0)
            {
                return string.Empty;
            }
            else if (count == 1)
            {
                return itemNameList.First();
            }
            else if (count == 2)
            {
                //[item1] or [item2]
                return TextManager.GetWithVariables(
                    "DialogRequiredTreatmentOptionsLast",
                    ("[treatment1]", itemNameList.ElementAt(0)),
                    ("[treatment2]", itemNameList.ElementAt(1)));
            }
            else
            {
                // [item1], [item2], [item3] ... or [lastitem]
                LocalizedString itemListStr = TextManager.GetWithVariables(
                    "DialogRequiredTreatmentOptionsFirst",
                    ("[treatment1]", itemNameList.ElementAt(0)),
                    ("[treatment2]", itemNameList.ElementAt(1)));

                int i;
                bool isTruncated = false;
                for (i = 2; i < count - 1; i++)
                {
                    if (itemListStr.Length > 50)
                    {
                        isTruncated = true;
                        break;
                    }
                    itemListStr = TextManager.GetWithVariables(
                      "DialogRequiredTreatmentOptionsFirst",
                      ("[treatment1]", itemListStr),
                      ("[treatment2]", itemNameList.ElementAt(i)));
                }
                itemListStr = TextManager.GetWithVariables(
                    "DialogRequiredTreatmentOptionsLast",
                    ("[treatment1]", itemListStr),
                    ("[treatment2]", itemNameList.ElementAt(i)));

                if (isTruncated)
                {
                    itemListStr += TextManager.Get("ellipsis");
                }
                return itemListStr;
            }
        }

        private void DrawOutputOverLay(SpriteBatch spriteBatch, GUICustomComponent overlayComponent)
        {
            overlayComponent.RectTransform.SetAsLastChild();

            FabricationRecipe targetItem = fabricatedItem ?? selectedItem;
            if (targetItem != null && outputContainer.Inventory?.visualSlots != null)
            {
                Rectangle slotRect = outputContainer.Inventory.visualSlots[0].Rect;
                if (fabricatedItem != null)
                {
                    float clampedProgressState = Math.Clamp(progressState, 0f, 1f);
                    GUI.DrawRectangle(spriteBatch,
                        new Rectangle(
                            slotRect.X, slotRect.Y + (int)(slotRect.Height * (1.0f - clampedProgressState)),
                            slotRect.Width, (int)(slotRect.Height * clampedProgressState)),
                        GUIStyle.Green * 0.5f, isFilled: true);
                }

                if (outputContainer.Inventory.IsEmpty())
                {
                    var itemIcon = targetItem.TargetItem.InventoryIcon ?? targetItem.TargetItem.Sprite;
                    Color iconColor = itemIcon == targetItem.TargetItem.Sprite ? targetItem.TargetItem.SpriteColor : targetItem.TargetItem.InventoryIconColor;
                    itemIcon.Draw(
                        spriteBatch,
                        slotRect.Center.ToVector2(),
                        color: Color.Lerp(iconColor, Color.TransparentBlack, 0.5f),
                        scale: Math.Min(slotRect.Width / itemIcon.size.X, slotRect.Height / itemIcon.size.Y) * 0.9f);
                }
            }
            
            if (tooltip != null)
            {
                GUIComponent.DrawToolTip(spriteBatch, RichString.Rich(tooltip.Tooltip), tooltip.TargetElement);
                tooltip = null;
            }
        }

        private bool FilterEntities(MapEntityCategory? category, string filter)
        {
            bool onlyShowAvailable = availableOnlyTickBox is { Selected: true };

            bool anyVisible = false;
            foreach (GUIComponent child in itemList.Content.Children)
            {
                FabricationRecipe recipe = child.UserData as FabricationRecipe;
                if (recipe?.DisplayName == null) { continue; }

                if (recipe.HideForNonTraitors)
                {
                    if (Character.Controlled is not { IsTraitor: true })
                    {
                        child.Visible = false;
                        continue;
                    }
                }

                if (recipe.RequiresRecipe && recipe.HideIfNoRecipe)
                {
                    if (Character.Controlled != null)
                    {
                        if (!AnyOneHasRecipeForItem(Character.Controlled, recipe.TargetItem))
                        {
                            child.Visible = false;
                            continue;
                        }
                    }
                }

                child.Visible =
                    (string.IsNullOrWhiteSpace(filter) || recipe.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase)) &&
                    (!category.HasValue || recipe.TargetItem.Category.HasFlag(category.Value)) &&
                    (!onlyShowAvailable || CanBeFabricated(recipe, availableIngredients, Character.Controlled));
                if (child.Visible)
                {
                    anyVisible = true;
                }
            }

            foreach (GUIButton btn in itemCategoryButtons)
            {
                btn.Selected = (MapEntityCategory?)btn.UserData == selectedItemCategory;
            }
            HideEmptyItemListCategories();
            nothingToShowText.Visible = !anyVisible;
            itemList.UserData = "itemlist";

            return true;
        }

        private void HideEmptyItemListCategories()
        {
            bool visibleElementsChanged = false;
            //go through the elements backwards, and disable the labels if there's no items below them
            bool recipeVisible = false;
            foreach (GUIComponent child in itemList.Content.Children.Reverse())
            {
                if (child.UserData is not FabricationRecipe recipe)
                {
                    if (child.Enabled)
                    {
                        if (child.Visible != recipeVisible)
                        {
                            child.Visible = recipeVisible;
                            visibleElementsChanged = true;
                        }
                    }
                    recipeVisible = false;
                }
                else
                {
                    recipeVisible |= child.Visible;
                }
            }

            SortBy sortBy = (SortBy)sortByDropdown.SelectedData;
            if (sortBy != SortBy.Category)
            {
                itemList.Content.Children.Where(c => c.UserData?.GetType() == typeof(MapEntityCategory)).ForEach(c => c.Visible = false);
            }

            if (visibleElementsChanged)
            {
                itemList.UpdateScrollBarSize();
                itemList.BarScroll = 0.0f;
            }
        }

        public bool ClearFilter()
        {
            FilterEntities(selectedItemCategory, "");
            itemList.UpdateScrollBarSize();
            itemList.BarScroll = 0.0f;
            itemFilterBox.Text = "";
            return true;
        }

        private readonly record struct SelectedRecipe(Character User, FabricationRecipe SelectedItem, Option<float> OverrideRequiredTime);
        private Option<SelectedRecipe> LastSelectedRecipe = Option.None;

        private bool SelectItem(Character user, FabricationRecipe selectedItem, float? overrideRequiredTime = null)
        {
            this.selectedItem = selectedItem;
            displayingForCharacter = user;
            var selectedRecipe = new SelectedRecipe(user, selectedItem, overrideRequiredTime is null ? Option.None : Option.Some(overrideRequiredTime.Value));
            LastSelectedRecipe = Option.Some(selectedRecipe);
            CreateSelectedItemUI(selectedRecipe);
            return true;
        }

        private void CreateSelectedItemUI(SelectedRecipe recipe)
        {
            var (user, selectedRecipe, overrideRequiredTime) = recipe;
            int max = Math.Max(selectedRecipe.TargetItem.GetMaxStackSize(outputContainer.Inventory) / selectedRecipe.Amount, 1);

            if (amountInput != null)
            {
                float prevBarScroll = amountInput.BarScroll;
                amountInput.Range = new Vector2(1, max);
                amountInput.BarScroll = prevBarScroll;

                amountTextMax.Text = max.ToString();
                amountInput.Enabled = amountTextMax.Enabled = max > 1;
                AmountToFabricate = Math.Min((int)amountInput.BarScrollValue, max);
            }
            RefreshActivateButtonText();

            selectedItemFrame.ClearChildren();
            selectedItemReqsFrame.ClearChildren();

            var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), selectedItemFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f, CanBeFocused = true };
            var paddedReqFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), selectedItemReqsFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f };

            LocalizedString itemName = GetRecipeNameAndAmount(selectedRecipe);
            LocalizedString name = itemName;

            QualityResult result = GetFabricatedItemQuality(selectedRecipe, user);

            float minimumQuality = selectedRecipe.Quality ?? result.Quality;

            LocalizedString qualityTooltip = string.Empty;
            if (result.HasRandomQualityRollChance)
            {
                float plusOnePercentage = result.TotalPlusOnePercentage;
                float plusTwoPercentage = result.TotalPlusTwoPercentage;

                string plusOnePercentageText = plusOnePercentage.ToString("F1", CultureInfo.InvariantCulture);
                string plusTwoPercentageText = plusTwoPercentage.ToString("F1", CultureInfo.InvariantCulture);

                int plusOneQuality = Math.Clamp(result.Quality + 1, min: 0, max: 3);
                int plusTwoQuality = Math.Clamp(result.Quality + 2, min: 0, max: 3);

                LocalizedString plusOneQualityText = TextManager.Get($"quality{plusOneQuality}");
                LocalizedString plusTwoQualityText = TextManager.Get($"quality{plusTwoQuality}");

                string localizationTag = plusTwoPercentage > 0f && plusOnePercentage > 0 && plusOneQuality != plusTwoQuality ? "meetsbonusrequirementtwice" : "meetsbonusrequirement";

                var variables = new (string Key, LocalizedString Value)[]
                {
                    ("[chance]", plusOnePercentageText), ("[quality]", plusOneQualityText),
                    ("[chance2]", plusTwoPercentageText), ("[quality2]", plusTwoQualityText)
                };

                if (MathUtils.NearlyEqual(plusOnePercentage, 0))
                {
                    variables = new[] { ("[chance]", plusTwoPercentageText), ("[quality]", plusTwoQualityText) };
                }

                if (plusOneQuality == plusTwoQuality)
                {
                    LocalizedString rawPercentage = result.PlusOnePercentage.ToString("F1", CultureInfo.InvariantCulture);
                    variables = new[] { ("[chance]", rawPercentage), ("[quality]", plusOneQualityText) };
                }

                if (plusOnePercentage >= 100.0f) { minimumQuality = plusOneQuality; }
                if (plusTwoPercentage >= 100.0f) { minimumQuality = plusTwoQuality; }

                qualityTooltip = TextManager.GetWithVariables(localizationTag, variables);
            }

            if (minimumQuality > 0 || result.HasRandomQualityRollChance)
            {
                name = TextManager.GetWithVariable("itemname.quality" + (int)minimumQuality, "[itemname]", itemName + '\n')
                    .Fallback(TextManager.GetWithVariable("itemname.quality3", "[itemname]", itemName + '\n'));
            }

            var nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform),
               RichString.Rich(name), textAlignment: Alignment.TopLeft, textColor: Color.Aqua, font: GUIStyle.SubHeadingFont)
            {
                AutoScaleHorizontal = true
            };

            if (result.HasRandomQualityRollChance)
            {
                var iconLayout = new GUIFrame(new RectTransform(new Vector2(0.4f, 1f), selectedItemFrame.RectTransform, anchor: Anchor.TopRight), style: null);
                var icon = GameSession.CreateNotificationIcon(iconLayout, offset: true);
                icon.ToolTip = RichString.Rich(qualityTooltip);
                icon.Visible = icon.CanBeFocused = true;
            }

            outputTopArea.RectTransform.MaxSize = new Point(int.MaxValue, outputInventoryHolder.Rect.Height);
            paddedOutputArea.Recalculate();

            nameBlock.Padding = new Vector4(0, nameBlock.Padding.Y, GUI.IntScale(5), nameBlock.Padding.W);
            if (nameBlock.TextScale < 0.7f)
            {
                nameBlock.AutoScaleHorizontal = false;
                nameBlock.TextScale = 0.7f;
                nameBlock.Wrap = true;
                nameBlock.SetTextPos();
                nameBlock.RectTransform.MinSize = new Point(0, (int)(nameBlock.TextSize.Y * nameBlock.TextScale));
            }

            bool largeUI = GuiFrame.Rect.Height > GUI.IntScale(500);
            if (largeUI)
            {
                paddedFrame.ChildAnchor = Anchor.CenterLeft;
            }

            if (!selectedRecipe.TargetItem.Description.IsNullOrEmpty())
            {
                RichString richDescription = RichString.Rich(selectedRecipe.TargetItem.Description);

                var descriptionParent = largeUI ? paddedReqFrame : paddedFrame;
                var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), descriptionParent.RectTransform),
                    richDescription,
                    font: GUIStyle.SmallFont, wrap: true);
                if (!largeUI)
                {
                    description.Padding = new Vector4(0, description.Padding.Y, description.Padding.Z, description.Padding.W);
                }

                while (description.Rect.Height + nameBlock.Rect.Height > descriptionParent.Rect.Height / 2)
                {
                    var lines = description.WrappedText.Split('\n');
                    if (lines.Count <= 1) { break; }
                    string newString = string.Join('\n', lines.Take(lines.Count - 1));
                    description.Text = newString.Substring(0, newString.Length - 4) + "...";
                    description.CalculateHeightFromText();
                    description.ToolTip = richDescription;
                }
                description.Text.RetrieveValue();
            }

            IEnumerable<Skill> inadequateSkills = Enumerable.Empty<Skill>();
            if (user != null)
            {
                inadequateSkills = selectedRecipe.RequiredSkills.Where(skill => user.GetSkillLevel(skill.Identifier) < Math.Round(skill.Level * SkillRequirementMultiplier));
            }

            if (selectedRecipe.RequiredSkills.Any())
            {
                LocalizedString text = "";
                new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), 
                    TextManager.Get("FabricatorRequiredSkills"), textColor: inadequateSkills.Any() ? GUIStyle.Red : GUIStyle.Green, font: GUIStyle.SubHeadingFont)
                {
                    AutoScaleHorizontal = true,
                    ToolTip = TextManager.Get("fabricatorrequiredskills.tooltip")
                };
                foreach (Skill skill in selectedRecipe.RequiredSkills)
                {
                    text += TextManager.Get("SkillName." + skill.Identifier) + " " + TextManager.Get("Lvl").ToLower() + " " + Math.Round(skill.Level * SkillRequirementMultiplier);
                    if (skill != selectedRecipe.RequiredSkills.Last()) { text += "\n"; }
                }
                new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), text, font: GUIStyle.SmallFont);
            }

            float degreeOfSuccess = user == null ? 0.0f : FabricationDegreeOfSuccess(user, selectedRecipe.RequiredSkills);
            if (degreeOfSuccess > 0.5f) { degreeOfSuccess = 1.0f; }

            float requiredTime = overrideRequiredTime.TryUnwrap(out var time) 
                ? time
                : (user == null ? selectedRecipe.RequiredTime : GetRequiredTime(selectedRecipe, user));

            if ((int)requiredTime > 0)
            {
                new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), 
                    TextManager.Get("FabricatorRequiredTime") , textColor: ToolBox.GradientLerp(degreeOfSuccess, GUIStyle.Red, Color.Yellow, GUIStyle.Green), font: GUIStyle.SubHeadingFont)
                {
                    AutoScaleHorizontal = true,
                };
                requiredTimeBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), ToolBox.SecondsToReadableTime(requiredTime), 
                    font: GUIStyle.SmallFont);
            }

            if (selectedRecipe.RequiredMoney > 0)
            {
                new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform),
                    TextManager.Get("subeditor.price"), textColor: ToolBox.GradientLerp(degreeOfSuccess, GUIStyle.Red, Color.Yellow, GUIStyle.Green), font: GUIStyle.SubHeadingFont)
                {
                    AutoScaleHorizontal = true,
                };
                new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform), TextManager.FormatCurrency(SelectedItem.RequiredMoney),
                    font: GUIStyle.SmallFont);
            }

            if (selectedRecipe.RequiresRecipe && !AnyOneHasRecipeForItem(Character.Controlled, selectedRecipe.TargetItem))
            {
                new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedReqFrame.RectTransform),
                    TextManager.Get("fabricatorrequiresrecipe"), textColor: GUIStyle.Red, font: GUIStyle.SubHeadingFont)
                {
                    AutoScaleHorizontal = true,
                };
            }
        }

        public void HighlightRecipe(string identifier, Color color)
        {
            foreach (GUIComponent child in itemList.Content.Children)
            {
                FabricationRecipe recipe = child.UserData as FabricationRecipe;
                if (recipe?.DisplayName == null) { continue; }
                if (recipe.TargetItem.Identifier == identifier)
                {
                    if (child.FlashTimer > 0.0f) return;
                    child.Flash(color, 1.5f, false);

                    for (int i = 0; i < child.CountChildren; i++)
                    {
                        var grandChild = child.GetChild(i);
                        if (grandChild is GUITextBlock) continue;
                        grandChild.Flash(color, 1.5f, false);
                    }

                    return;
                }
            }
        }
        
        private bool StartButtonClicked(GUIButton button, object obj)
        {
            if (selectedItem == null) { return false; }
            if (fabricatedItem == null && 
                !outputContainer.Inventory.CanProbablyBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health))
            {
                outputSlot.Flash(GUIStyle.Red);
                return false;
            }

            amountRemaining = AmountToFabricate;

            if (GameMain.Client != null)
            {
                pendingFabricatedItem = fabricatedItem != null ? null : selectedItem;
                item.CreateClientEvent(this);
            }
            else
            {
                if (fabricatedItem == null)
                {
                    StartFabricating(selectedItem, Character.Controlled);
                }
                else
                {
                    CancelFabricating(Character.Controlled);
                }
            }

            return true;
        }

        public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam)
        {
            activateButton.Enabled = false;
            inSufficientPowerWarning.Visible = IsActive && !hasPower;

            ingredientHighlightTimer -= deltaTime;

            if (!IsActive)
            {
                if (outputContainer != null && outputContainer.Inventory.AllItems.Any())
                {
                    if (outputContainer.Inventory.visualSlots is { } visualSlots && visualSlots.Any() &&
                        visualSlots[0].HighlightTimer <= 0.0f)
                    {
                        visualSlots[0].ShowBorderHighlight(GUIStyle.Green, 0.5f, 0.5f);
                    }
                }

                if (selectedItem != null && displayingForCharacter != character)
                {
                    //reselect to recreate the info based on the new user's skills
                    SelectItem(character, selectedItem);
                }

                //only check ingredients if the fabricator isn't active (if it is, this is done in Update)
                if (refreshIngredientsTimer <= 0.0f)
                {
                    RefreshAvailableIngredients();
                    refreshIngredientsTimer = RefreshIngredientsInterval;
                }
                refreshIngredientsTimer -= deltaTime;
            }

            if (character != null)
            {
                foreach (GUIComponent child in itemList.Content.Children)
                {
                    if (child.UserData is not FabricationRecipe recipe) { continue; }

                    if (recipe != selectedItem &&
                        (child.Rect.Y > itemList.Rect.Bottom || child.Rect.Bottom < itemList.Rect.Y))
                    {
                        continue;
                    }

                    bool canBeFabricated = CanBeFabricated(recipe, availableIngredients, character);
                    if (recipe == selectedItem)
                    {
                        activateButton.Enabled = canBeFabricated;
                    }

                    bool sufficientSkills = FabricationDegreeOfSuccess(character, recipe.RequiredSkills) >= 0.5f;

                    Color baseColor = MissingRequiredRecipe(recipe, character) ? 
                        GUIStyle.Red :
                        (sufficientSkills ? GUIStyle.TextColorNormal : GUIStyle.Orange);

                    var childContainer = child.GetChild<GUILayoutGroup>();
                    childContainer.GetChild<GUITextBlock>().TextColor = baseColor * (canBeFabricated ? 1.0f : 0.5f);

                    GUIImage icon = childContainer.GetChild<GUIImage>();
                    Color iconColor = icon.Sprite == recipe.TargetItem.Sprite ? recipe.TargetItem.SpriteColor : recipe.TargetItem.InventoryIconColor;
                    childContainer.GetChild<GUIImage>().Color = iconColor * (canBeFabricated ? 1.0f : 0.5f);

                    var limitReachedText = child.FindChild(nameof(FabricationLimitReachedText));
                    limitReachedText.Visible = !canBeFabricated && fabricationLimits.TryGetValue(recipe.RecipeHash, out int amount) && amount <= 0;
                }
            }
        }

        public override void OnPlayerSkillsChanged()
            => RefreshSelectedItem();

        public void RefreshSelectedItem()
        {
            if (!LastSelectedRecipe.TryUnwrap(out var lastSelected)) { return; }
            CreateSelectedItemUI(lastSelected);
        }

        partial void UpdateRequiredTimeProjSpecific()
        {
            if (requiredTimeBlock == null) { return; }
            requiredTimeBlock.Text = ToolBox.SecondsToReadableTime(timeUntilReady > 0.0f ? timeUntilReady : requiredTime);
        }

        public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null)
        {
            uint recipeHash = pendingFabricatedItem?.RecipeHash ?? 0;
            msg.WriteUInt32(recipeHash);
            msg.WriteRangedInteger(AmountToFabricate, 1, MaxAmountToFabricate);
        }

        public void ClientEventRead(IReadMessage msg, float sendingTime)
        {
            FabricatorState newState = (FabricatorState)msg.ReadByte();
            int amountToFabricate = msg.ReadRangedInteger(0, MaxAmountToFabricate);
            int amountRemaining = msg.ReadRangedInteger(0, MaxAmountToFabricate);
            float newTimeUntilReady = msg.ReadSingle();
            uint recipeHash = msg.ReadUInt32();
            UInt16 userID = msg.ReadUInt16();
            Character user = Entity.FindEntityByID(userID) as Character;

            ushort reachedLimitCount = msg.ReadUInt16();
            for (int i = 0; i < reachedLimitCount; i++)
            {
                fabricationLimits[msg.ReadUInt32()] = 0;
            }
            State = newState;
            //don't touch the amount unless another character changed it or the fabricator is running
            //otherwise we may end up reverting the changes the client just did to the amount
            if ((user != null && user != Character.Controlled) || State != FabricatorState.Stopped)
            {
                this.amountToFabricate = amountToFabricate;
            }
            this.amountRemaining = amountRemaining;
            if (newState == FabricatorState.Stopped || recipeHash == 0)
            {
                CancelFabricating();
            }
            else if (newState == FabricatorState.Active || newState == FabricatorState.Paused)
            {
                //if already fabricating the selected item, return
                if (fabricatedItem != null && fabricatedItem.RecipeHash == recipeHash) { return; }
                if (recipeHash == 0) { return; }

                SelectItem(user, fabricationRecipes[recipeHash]);
                StartFabricating(fabricationRecipes[recipeHash], user);
            }
            timeUntilReady = newTimeUntilReady;
        }
    }
}
