using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.Steam;

namespace Barotrauma
{
    sealed class ServerListScreen : Screen
    {
        private enum MsgUserData
        {
            RefreshingServerList,
            NoServers,
            NoMatchingServers
        }
        
        //how often the client is allowed to refresh servers
        private static readonly TimeSpan AllowedRefreshInterval = TimeSpan.FromSeconds(3);
        
        private DateTime lastRefreshTime = DateTime.Now;

        // Cache for spam-filtered servers to avoid re-checking on every filter change
        private readonly HashSet<string> spamServerCache = new HashSet<string>();
        private readonly Dictionary<string, string> serverInfoStringCache = new Dictionary<string, string>();

        private GUIFrame menu;

        private GUIListBox serverList;
        private PanelAnimator panelAnimator;
        private GUIFrame serverPreviewContainer;
        private GUIListBox serverPreview;

        private GUIButton joinButton;
        private Option<ServerInfo> selectedServer;

        private GUIButton scanServersButton;

        private enum TernaryOption
        {
            Any,
            Enabled,
            Disabled
        }

        public enum TabEnum
        {
            All,
            Favorites,
            Recent
        }

        public readonly struct Tab
        {
            public readonly string Storage;
            public readonly GUIButton Button;
            
            private readonly List<ServerInfo> servers;
            public IReadOnlyList<ServerInfo> Servers => servers;

            public Tab(TabEnum tabEnum, ServerListScreen serverListScreen, GUILayoutGroup tabber, string storage)
            {
                Storage = storage;
                servers = new List<ServerInfo>();
                Button = new GUIButton(new RectTransform(new Vector2(0.33f, 1.0f), tabber.RectTransform),
                    TextManager.Get($"ServerListTab.{tabEnum}"), style: "GUITabButton")
                {
                    OnClicked = (_,__) =>
                    {
                        serverListScreen.selectedTab = tabEnum;
                        return false;
                    }
                };

                Reload();
            }
            
            public void Reload()
            {
                if (Storage.IsNullOrEmpty()) { return; }
                servers.Clear();
                XDocument doc = XMLExtensions.TryLoadXml(Storage, out _);
                if (doc?.Root is null) { return; }
                servers.AddRange(doc.Root.Elements().Select(ServerInfo.FromXElement).NotNone().Distinct());
            }

            public bool Contains(ServerInfo info) => servers.Contains(info);
            public bool Remove(ServerInfo info) => servers.Remove(info);
            public void AddOrUpdate(ServerInfo info)
            {
                servers.Remove(info); servers.Add(info);
            }

            public void Clear() => servers.Clear();

            public void Save()
            {
                XDocument doc = new XDocument();
                XElement rootElement = new XElement("servers");
                doc.Add(rootElement);

                foreach (ServerInfo info in servers)
                {
                    rootElement.Add(info.ToXElement());
                }

                doc.SaveSafe(Storage);
            }
        }

        private readonly Dictionary<TabEnum, Tab> tabs = new Dictionary<TabEnum, Tab>();

        private TabEnum _selectedTabBackingField;
        private TabEnum selectedTab
        {
            get => _selectedTabBackingField;
            set
            {
                _selectedTabBackingField = value;
                tabs.ForEach(kvp => kvp.Value.Button.Selected = (value == kvp.Key));
                if (Screen.Selected == this) { RefreshServers(); }
            }
        }

        private ServerProvider serverProvider = null;

        public GUITextBox ClientNameBox { get; private set; }

        enum ColumnLabel
        {
            ServerListCompatible,
            ServerListHasPassword,
            ServerListName,
            ServerListRoundStarted,
            ServerListPlayers,
            ServerListPing
        }
        private struct Column
        {
            public float RelativeWidth;
            public ColumnLabel Label;

            public static implicit operator Column((float W, ColumnLabel L) pair) =>
                new Column { RelativeWidth = pair.W, Label = pair.L };

            public static Column[] Normalize(params Column[] columns)
            {
                var totalWidth = columns.Select(c => c.RelativeWidth).Aggregate((a, b) => a + b);
                for (int i = 0; i < columns.Length; i++)
                {
                    columns[i].RelativeWidth /= totalWidth;
                }
                return columns;
            }
        }

        private static readonly ImmutableDictionary<ColumnLabel, Column> columns =
            Column.Normalize(
                (0.1f, ColumnLabel.ServerListCompatible),
                (0.1f, ColumnLabel.ServerListHasPassword),
                (0.7f, ColumnLabel.ServerListName),
                (0.12f, ColumnLabel.ServerListRoundStarted),
                (0.08f, ColumnLabel.ServerListPlayers),
                (0.08f, ColumnLabel.ServerListPing)
            ).Select(c => (c.Label, c)).ToImmutableDictionary();
        
        private GUILayoutGroup labelHolder;
        private readonly List<GUITextBlock> labelTexts = new List<GUITextBlock>();

        //filters
        private GUITextBox searchBox;
        private GUITickBox filterSameVersion;
        private GUITickBox filterPassword;
        private GUITickBox filterFull;
        private GUITickBox filterEmpty;
        private GUIDropDown languageDropdown;
        private Dictionary<Identifier, GUIDropDown> ternaryFilters;
        private Dictionary<Identifier, GUITickBox> filterTickBoxes;
        private Dictionary<Identifier, GUITickBox> playStyleTickBoxes;
        private Dictionary<Identifier, GUITickBox> gameModeTickBoxes;
        private GUITickBox filterOffensive;

        //GUIDropDown sends the OnSelected event before SelectedData is set, so we have to cache it manually.
        private TernaryOption filterFriendlyFireValue = TernaryOption.Any;
        private TernaryOption filterKarmaValue = TernaryOption.Any;
        private TernaryOption filterTraitorValue = TernaryOption.Any;
        private TernaryOption filterVoipValue = TernaryOption.Any;
        private TernaryOption filterModdedValue = TernaryOption.Any;

        private ColumnLabel sortedBy;
        private bool sortedAscending = true;

        private const float sidebarWidth = 0.2f;
        public ServerListScreen() : base()
        {
            selectedServer = Option<ServerInfo>.None();
            GameMain.Instance.ResolutionChanged += CreateUI;
            CreateUI();
        }

        private static Task<string> GetDefaultUserName()
        {
            return new CompositeFriendProvider(new SteamFriendProvider(), new EpicFriendProvider()).GetSelfUserName();
        }

        private void AddTernaryFilter(RectTransform parent, float elementHeight, Identifier tag, Action<TernaryOption> valueSetter)
        {
            var filterLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementHeight), parent), isHorizontal: true)
            {
                Stretch = true
            };

            var box = new GUIFrame(new RectTransform(Vector2.One, filterLayoutGroup.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight)
            {
                IsFixedSize = true,
            }, null)
            {
                HoverColor = Color.Gray,
                SelectedColor = Color.DarkGray,
                CanBeFocused = false
            };
            if (box.RectTransform.MinSize.Y > 0)
            {
                box.RectTransform.MinSize = new Point(box.RectTransform.MinSize.Y);
                box.RectTransform.Resize(box.RectTransform.MinSize);
            }
            Vector2 textBlockScale = new Vector2((float)(filterLayoutGroup.Rect.Width - filterLayoutGroup.Rect.Height) / (float)Math.Max(filterLayoutGroup.Rect.Width, 1.0), 1.0f);

            var filterLabel = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f) * textBlockScale, filterLayoutGroup.RectTransform, Anchor.CenterLeft), TextManager.Get("servertag." + tag + ".label"), textAlignment: Alignment.CenterLeft)
            {
                UserData = TextManager.Get($"servertag.{tag}.label")
            };
            GUIStyle.Apply(filterLabel, "GUITextBlock", null);

            var dropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f) * textBlockScale, filterLayoutGroup.RectTransform, Anchor.CenterLeft), elementCount: 3);
            dropDown.AddItem(TextManager.Get("any"), TernaryOption.Any);
            dropDown.AddItem(TextManager.Get($"servertag.{tag}.true"), TernaryOption.Enabled, TextManager.Get(
                $"servertagdescription.{tag}.true"));
            dropDown.AddItem(TextManager.Get($"servertag.{tag}.false"), TernaryOption.Disabled, TextManager.Get(
                $"servertagdescription.{tag}.false"));
            dropDown.SelectItem(TernaryOption.Any);
            dropDown.OnSelected = (_, data) => {
                valueSetter((TernaryOption)data);
                FilterServers();
                StoreServerFilters();
                return true;
            };

            ternaryFilters.Add(tag, dropDown);
        }

        private void CreateUI()
        {
            menu = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.85f), GUI.Canvas, Anchor.Center) { MinSize = new Point(GameMain.GraphicsHeight, 0) });

            var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.98f), menu.RectTransform, Anchor.Center))
            {
                RelativeSpacing = 0.02f,
                Stretch = true
            };

            //-------------------------------------------------------------------------------------
            //Top row
            //-------------------------------------------------------------------------------------

            var topRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), paddedFrame.RectTransform)) { Stretch = true };

            var titleContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.995f, 0.33f), topRow.RectTransform), isHorizontal: true) { Stretch = true };
            
            var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), titleContainer.RectTransform), TextManager.Get("JoinServer"), font: GUIStyle.LargeFont)
            {
                Padding = Vector4.Zero,
                ForceUpperCase = ForceUpperCase.Yes,
                AutoScaleHorizontal = true
            };

            var friendsButton = new GUIButton(
                new RectTransform(Vector2.One * 0.9f, titleContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight),
                style: "FriendsButton")
            {
                OnClicked = (_, _) =>
                {
                    if (SocialOverlay.Instance is { } socialOverlay) { socialOverlay.IsOpen = true; }
                    return false;
                },
                ToolTip = TextManager.GetWithVariable("SocialOverlayShortcutHint", "[shortcut]", SocialOverlay.ShortcutBindText)
            };
            new GUIFrame(new RectTransform(Vector2.One, friendsButton.RectTransform, Anchor.Center),
                style: "FriendsButtonIcon")
            {
                CanBeFocused = false
            };

            var infoHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.33f), topRow.RectTransform), isHorizontal: true, Anchor.BottomLeft) { RelativeSpacing = 0.01f,  Stretch = false };

            var clientNameHolder = new GUILayoutGroup(new RectTransform(new Vector2(sidebarWidth, 1.0f), infoHolder.RectTransform)) { RelativeSpacing = 0.05f };

            new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), clientNameHolder.RectTransform), TextManager.Get("YourName"), font: GUIStyle.SubHeadingFont);
            ClientNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), clientNameHolder.RectTransform), "")
            {
                Text = MultiplayerPreferences.Instance.PlayerName,
                MaxTextLength = Client.MaxNameLength,
                OverflowClip = true
            };

            var tabButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - sidebarWidth - infoHolder.RelativeSpacing, 0.5f), infoHolder.RectTransform), isHorizontal: true);

            tabs[TabEnum.All] = new Tab(TabEnum.All, this, tabButtonHolder, "");
            tabs[TabEnum.Favorites] = new Tab(TabEnum.Favorites, this, tabButtonHolder, "Data/favoriteservers.xml");
            tabs[TabEnum.Recent] = new Tab(TabEnum.Recent, this, tabButtonHolder, "Data/recentservers.xml");

            //-------------------------------------------------------------------------------------
            // Bottom row
            //-------------------------------------------------------------------------------------

            var bottomRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f - topRow.RectTransform.RelativeSize.Y),
                paddedFrame.RectTransform, Anchor.CenterRight))
            {
                Stretch = true
            };

            var serverListHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), bottomRow.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft)
            {
                OutlineColor = Color.Black
            };

            GUILayoutGroup serverListContainer = null;
            GUIFrame filtersHolder = null;

            // filters -------------------------------------------

            filtersHolder = new GUIFrame(new RectTransform(new Vector2(sidebarWidth, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null)
            {
                Color = new Color(12, 14, 15, 255) * 0.5f,
                OutlineColor = Color.Black
            };

            float elementHeight = 0.05f;
            var filterTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), filtersHolder.RectTransform), TextManager.Get("FilterServers"), font: GUIStyle.SubHeadingFont)
            {
                Padding = Vector4.Zero,
                AutoScaleHorizontal = true,
                CanBeFocused = false
            };

            var searchHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementHeight), filtersHolder.RectTransform) { RelativeOffset = new Vector2(0.0f, elementHeight) }, isHorizontal: true) { Stretch = true };

            var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), searchHolder.RectTransform), TextManager.Get("Search") + "...");
            searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), searchHolder.RectTransform), "");
            searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; };
            searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; };
            searchBox.OnTextChanged += (txtBox, txt) => { FilterServers(); return true; };

            var filters = new GUIListBox(new RectTransform(new Vector2(0.98f, 1.0f - elementHeight * 2), filtersHolder.RectTransform, Anchor.BottomLeft))
            {
                ScrollBarVisible = true,
                Spacing = (int)(5 * GUI.Scale)
            };

            ternaryFilters = new Dictionary<Identifier, GUIDropDown>();
            filterTickBoxes = new Dictionary<Identifier, GUITickBox>();

            RectTransform createFilterRectT()
                => new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform);
            
            GUITickBox addTickBox(Identifier key, LocalizedString text = null, bool defaultState = false, bool addTooltip = false)
            {
                text ??= TextManager.Get(key);
                var tickBox = new GUITickBox(createFilterRectT(), text)
                {
                    UserData = text,
                    Selected = defaultState,
                    ToolTip = addTooltip ? text : null,
                    OnSelected = (tickBox) =>
                    {
                        FilterServers();
                        StoreServerFilters();
                        return true;
                    }
                };
                filterTickBoxes.Add(key, tickBox);
                return tickBox;
            }

            filterSameVersion = addTickBox("FilterSameVersion".ToIdentifier(), defaultState: true);
            filterPassword = addTickBox("FilterPassword".ToIdentifier());
            filterFull = addTickBox("FilterFullServers".ToIdentifier());
            filterEmpty = addTickBox("FilterEmptyServers".ToIdentifier());
            filterOffensive = addTickBox("FilterOffensiveServers".ToIdentifier());

            // Language filter
            if (ServerLanguageOptions.Options.Any())
            {
                var languageKey = "Language".ToIdentifier();
                var allLanguagesKey = "AllLanguages".ToIdentifier();

                new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get(languageKey), font: GUIStyle.SubHeadingFont)
                {
                    CanBeFocused = false
                };

                languageDropdown = new GUIDropDown(createFilterRectT(), selectMultiple: true);

                languageDropdown.AddItem(TextManager.Get(allLanguagesKey), allLanguagesKey);
                var allTickbox = languageDropdown.ListBox.Content.FindChild(allLanguagesKey)?.GetChild<GUITickBox>();

                // Spacer between "All" and the individual languages
                new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), languageDropdown.ListBox.Content.RectTransform)
                {
                    MinSize = new Point(0, GUI.IntScaleCeiling(2))
                }, style: null)
                {
                    Color = Color.DarkGray,
                    CanBeFocused = false
                };
                
                var selectedLanguages
                    = ServerListFilters.Instance.GetAttributeLanguageIdentifierArray(
                        languageKey,
                        Array.Empty<LanguageIdentifier>());
                foreach (var (label, identifier, _) in ServerLanguageOptions.Options)
                {
                    languageDropdown.AddItem(label, identifier);
                }

                if (!selectedLanguages.Any())
                {
                    selectedLanguages = ServerLanguageOptions.Options.Select(o => o.Identifier).ToArray();
                }

                foreach (var lang in selectedLanguages)
                {
                    languageDropdown.SelectItem(lang);
                }

                if (ServerLanguageOptions.Options.All(o => selectedLanguages.Any(l => o.Identifier == l)))
                {
                    languageDropdown.SelectItem(allLanguagesKey);
                    languageDropdown.Text = TextManager.Get(allLanguagesKey);
                }

                var langTickboxes = languageDropdown.ListBox.Content.Children
                    .Where(c => c.UserData is LanguageIdentifier)
                    .Select(c => c.GetChild<GUITickBox>())
                    .ToArray();

                bool inSelectedCall = false;
                languageDropdown.OnSelected = (_, userData) =>
                {
                    if (inSelectedCall) { return true; }
                    try
                    {
                        inSelectedCall = true;

                        if (Equals(allLanguagesKey, userData))
                        {
                            foreach (var tb in langTickboxes)
                            {
                                tb.Selected = allTickbox.Selected;
                            }
                        }

                        bool noneSelected = langTickboxes.All(tb => !tb.Selected);
                        bool allSelected = langTickboxes.All(tb => tb.Selected);
                        if (allSelected != allTickbox.Selected)
                        {
                            allTickbox.Selected = allSelected;
                        }

                        return true;
                    }
                    finally
                    {
                        inSelectedCall = false;
                    }
                };
                languageDropdown.AfterSelected = (_, userData) =>
                {
                    bool noneSelected = langTickboxes.All(tb => !tb.Selected);
                    bool allSelected = langTickboxes.All(tb => tb.Selected);
                    if (allSelected)
                    {
                        languageDropdown.Text = TextManager.Get(allLanguagesKey);
                    }
                    else if (noneSelected)
                    {
                        languageDropdown.Text = TextManager.Get("None");
                    }

                    var languages = languageDropdown.SelectedDataMultiple.OfType<LanguageIdentifier>();

                    ServerListFilters.Instance.SetAttribute(languageKey, string.Join(", ", languages));
                    GameSettings.SaveCurrentConfig();
                    FilterServers();
                    return true;
                };
            }
            
            // Filter Tags
            new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("servertags"), font: GUIStyle.SubHeadingFont)
            {
                CanBeFocused = false
            };

            AddTernaryFilter(filters.Content.RectTransform, elementHeight, "karma".ToIdentifier(), (value) => { filterKarmaValue = value; });
            AddTernaryFilter(filters.Content.RectTransform, elementHeight, "traitors".ToIdentifier(), (value) => { filterTraitorValue = value; });
            AddTernaryFilter(filters.Content.RectTransform, elementHeight, "friendlyfire".ToIdentifier(), (value) => { filterFriendlyFireValue = value; });
            AddTernaryFilter(filters.Content.RectTransform, elementHeight, "voip".ToIdentifier(), (value) => { filterVoipValue = value; });
            AddTernaryFilter(filters.Content.RectTransform, elementHeight, "modded".ToIdentifier(), (value) => { filterModdedValue = value; });

            // Play Style Selection
            new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUIStyle.SubHeadingFont)
            {
                CanBeFocused = false
            };

            playStyleTickBoxes = new Dictionary<Identifier, GUITickBox>();
            foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle)))
            {
                var selectionTick = addTickBox($"servertag.{playStyle}".ToIdentifier(), defaultState: true, addTooltip: true);
                selectionTick.UserData = playStyle;
                playStyleTickBoxes.Add($"servertag.{playStyle}".ToIdentifier(), selectionTick);
            }

            // Game mode Selection
            new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("gamemode"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false };

            gameModeTickBoxes = new Dictionary<Identifier, GUITickBox>();
            foreach (GameModePreset mode in GameModePreset.List)
            {
                if (mode.IsSinglePlayer) { continue; }

                var selectionTick = addTickBox(mode.Identifier, mode.Name, defaultState: true, addTooltip: true);
                selectionTick.UserData = mode.Identifier;
                gameModeTickBoxes.Add(mode.Identifier, selectionTick);
            }

            filters.Content.RectTransform.SizeChanged += () =>
            {
                filters.Content.RectTransform.RecalculateChildren(true, true);
                filterTickBoxes.ForEach(t => t.Value.Text = t.Value.UserData is LocalizedString lStr ? lStr : t.Value.UserData.ToString());
                gameModeTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip);
                playStyleTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip);
                GUITextBlock.AutoScaleAndNormalize(
                    filterTickBoxes.Values.Select(tb => tb.TextBlock)
                    .Concat(ternaryFilters.Values.Select(dd => dd.Parent.GetChild<GUITextBlock>())),
                    defaultScale: 1.0f);
                if (filterTickBoxes.Values.First().TextBlock.TextScale < 0.8f)
                {
                    filterTickBoxes.ForEach(t => t.Value.TextBlock.TextScale = 1.0f);
                    filterTickBoxes.ForEach(t => t.Value.TextBlock.Text = ToolBox.LimitString(t.Value.TextBlock.Text, t.Value.TextBlock.Font, (int)(filters.Content.Rect.Width * 0.8f)));
                }
            };

            // server list ---------------------------------------------------------------------

            serverListContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), serverListHolder.RectTransform)) { Stretch = true };

            labelHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), serverListContainer.RectTransform) { MinSize = new Point(0, 15) },
                isHorizontal: true, childAnchor: Anchor.BottomLeft)
            {
                Stretch = false
            };
            
            foreach (var column in columns.Values)
            {
                var label = TextManager.Get(column.Label.ToString());
                var btn = new GUIButton(new RectTransform(new Vector2(column.RelativeWidth, 1.0f), labelHolder.RectTransform),
                    text: label, textAlignment: Alignment.Center, style: "GUIButtonSmall")
                {
                    ToolTip = label,
                    ForceUpperCase = ForceUpperCase.Yes,
                    UserData = column.Label,
                    OnClicked = SortList
                };
                btn.Color *= 0.5f;
                labelTexts.Add(btn.TextBlock);
                
                GUIImage arrowImg(object userData, SpriteEffects sprEffects)
                    => new GUIImage(new RectTransform(new Vector2(0.5f, 0.3f), btn.RectTransform, Anchor.BottomCenter, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonVerticalArrow", scaleToFit: true)
                    {
                        CanBeFocused = false,
                        UserData = userData,
                        SpriteEffects = sprEffects,
                        Visible = false
                    };

                arrowImg("arrowup", SpriteEffects.None);
                arrowImg("arrowdown", SpriteEffects.FlipVertically);
            }

            serverList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), serverListContainer.RectTransform, Anchor.Center))
            {
                PlaySoundOnSelect = true,
                ScrollBarVisible = true,
                OnSelected = (btn, obj) =>
                {
                    if (GUI.MouseOn is GUIButton) { return false; }
                    if (obj is not ServerInfo serverInfo) { return false; }

                    joinButton.Enabled = true;
                    selectedServer = Option<ServerInfo>.Some(serverInfo);
                    if (!serverPreviewContainer.Visible)
                    {
                        serverPreviewContainer.RectTransform.RelativeSize = new Vector2(sidebarWidth, 1.0f);
                        serverPreviewContainer.Visible = true;
                        serverPreviewContainer.IgnoreLayoutGroups = false;
                    }
                    serverInfo.CreatePreviewWindow(serverPreview.Content);
                    serverPreview.ForceLayoutRecalculation();
                    panelAnimator.RightEnabled = true;
                    panelAnimator.RightVisible = true;
                    btn.Children.ForEach(c => c.SpriteEffects = serverPreviewContainer.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally);
                    return true;
                }
            };

            //server preview panel --------------------------------------------------
            serverPreviewContainer = new GUIFrame(new RectTransform(new Vector2(sidebarWidth, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null)
            {
                Color = new Color(12, 14, 15, 255) * 0.5f,
                OutlineColor = Color.Black,
                IgnoreLayoutGroups = true
            };
            serverPreview = new GUIListBox(new RectTransform(Vector2.One, serverPreviewContainer.RectTransform, Anchor.Center))
            {
                Padding = Vector4.One * 10 * GUI.Scale,
                HoverCursor = CursorState.Default,
                OnSelected = (component, o) => false
            };

            panelAnimator = new PanelAnimator(new RectTransform(Vector2.One, serverListHolder.RectTransform),
                filtersHolder,
                serverListContainer,
                serverPreviewContainer);
            panelAnimator.RightEnabled = false;
            
            // Spacing
            new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), bottomRow.RectTransform), style: null);

            var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.075f), bottomRow.RectTransform, Anchor.Center), isHorizontal: true)
            {
                RelativeSpacing = 0.02f,
                Stretch = true
            };

            GUIButton button = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform),
                TextManager.Get("Back"))
            {
                OnClicked = GameMain.MainMenuScreen.ReturnToMainMenu
            };

            scanServersButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform),
                TextManager.Get("ServerListRefresh"))
            {
				OnClicked = (btn, userdata) => { RefreshServers(); return true; }
			};

            var directJoinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform),
                TextManager.Get("serverlistdirectjoin"))
            {
                OnClicked = (btn, userdata) => 
                {
                    if (string.IsNullOrWhiteSpace(ClientNameBox.Text))
                    {
                        ClientNameBox.Flash();
                        ClientNameBox.Select();
                        SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
                        return false;
                    }
                    ShowDirectJoinPrompt(); 
                    return true; 
                }
            };

            joinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform),
                TextManager.Get("ServerListJoin"))
            {
                OnClicked = (btn, userdata) =>
                {
                    if (selectedServer.TryUnwrap(out var serverInfo))
                    {
                        JoinServer(serverInfo.Endpoints, serverInfo.ServerName);
                    }
                    return true;
                },
                Enabled = false
            };

            buttonContainer.RectTransform.MinSize = new Point(0, (int)(buttonContainer.RectTransform.Children.Max(c => c.MinSize.Y) * 1.2f));

            //--------------------------------------------------------

            bottomRow.Recalculate();
            serverListHolder.Recalculate();
            serverListContainer.Recalculate();
            labelHolder.RectTransform.MaxSize = new Point(serverList.Content.Rect.Width, int.MaxValue);
            labelHolder.RectTransform.AbsoluteOffset = new Point((int)serverList.Padding.X, 0);
            labelHolder.Recalculate();

            serverList.Content.RectTransform.SizeChanged += () =>
            {
                labelHolder.RectTransform.MaxSize = new Point(serverList.Content.Rect.Width, int.MaxValue);
                labelHolder.RectTransform.AbsoluteOffset = new Point((int)serverList.Padding.X, 0);
                labelHolder.Recalculate();
                foreach (GUITextBlock labelText in labelTexts)
                {
                    labelText.Text = ToolBox.LimitString(labelText.ToolTip, labelText.Font, labelText.Rect.Width);
                }
            };

            button.SelectedColor = button.Color;

            selectedTab = TabEnum.All;
        }

        public void UpdateOrAddServerInfo(ServerInfo serverInfo)
        {
            GUIComponent existingElement = serverList.Content.FindChild(d => 
                d.UserData is ServerInfo existingServerInfo &&
                existingServerInfo.Endpoints.Any(serverInfo.Endpoints.Contains));
            if (existingElement == null)
            {
                AddToServerList(serverInfo);
            }
            else
            {
                existingElement.UserData = serverInfo;
            }
        }

        public void AddToRecentServers(ServerInfo info)
        {
            if (info.Endpoints.First().Address.IsLocalHost) { return; }
            tabs[TabEnum.Recent].AddOrUpdate(info);
            tabs[TabEnum.Recent].Save();
        }

        public bool IsFavorite(ServerInfo info)
            => tabs[TabEnum.Favorites].Contains(info);

        public void AddToFavoriteServers(ServerInfo info)
        {
            tabs[TabEnum.Favorites].AddOrUpdate(info);
            tabs[TabEnum.Favorites].Save();
        }

        public void RemoveFromFavoriteServers(ServerInfo info)
        {
            tabs[TabEnum.Favorites].Remove(info);
            tabs[TabEnum.Favorites].Save();
        }

        private bool SortList(GUIButton button, object obj)
        {
            if (obj is not ColumnLabel sortBy) { return false; }
            SortList(sortBy, toggle: true);
            return true;
        }

        private void SortList(ColumnLabel sortBy, bool toggle)
        {
            if (labelHolder.GetChildByUserData(sortBy) is not GUIButton button) { return; }

            sortedBy = sortBy;

            var arrowUp = button.GetChildByUserData("arrowup");
            var arrowDown = button.GetChildByUserData("arrowdown");
            
            //disable arrow buttons in other labels
            foreach (var child in button.Parent.Children)
            {
                if (child != button)
                {
                    child.GetChildByUserData("arrowup").Visible = false;
                    child.GetChildByUserData("arrowdown").Visible = false;
                }
            }

            sortedAscending = arrowUp.Visible;
            if (toggle)
            {
                sortedAscending = !sortedAscending;
            }

            arrowUp.Visible = sortedAscending;
            arrowDown.Visible = !sortedAscending;
            serverList.Content.RectTransform.SortChildren((c1, c2) => 
            {
                if (c1.GUIComponent.UserData is not ServerInfo s1) { return 0; }
                if (c2.GUIComponent.UserData is not ServerInfo s2) { return 0; }
                return CompareServer(sortBy, s1, s2, sortedAscending);
            });
        }

        public void HideServerPreview()
        {
            serverPreviewContainer.Visible = false;
            panelAnimator.RightEnabled = false;
            panelAnimator.RightVisible = false;
        }

        private void InsertServer(ServerInfo serverInfo, GUIComponent component)
        {
            var children = serverList.Content.RectTransform.Children.Reverse().ToList();

            foreach (var child in children)
            {
                if (child.GUIComponent.UserData is not ServerInfo serverInfo2 || serverInfo.Equals(serverInfo2)) { continue; }
                if (CompareServer(sortedBy, serverInfo, serverInfo2, sortedAscending) >= 0)
                {
                    var index = serverList.Content.RectTransform.GetChildIndex(child);
                    component.RectTransform.RepositionChildInHierarchy(Math.Min(index + 1, serverList.Content.CountChildren - 1));
                    return;
                }
            }
            component.RectTransform.SetAsFirstChild();
        }

        private static int CompareServer(ColumnLabel sortBy, ServerInfo s1, ServerInfo s2, bool ascending)
        {
            //always put servers with unknown ping at the bottom (unless we're specifically sorting by ping)
            //servers without a ping are often unreachable/spam
            bool s1HasPing = s1.Ping.IsSome();
            bool s2HasPing = s2.Ping.IsSome();
            if (s1HasPing != s2HasPing)
            {
                return s1HasPing ? -1 : 1;
            }            

            int comparison = ascending ? 1 : -1;
            switch (sortBy)
            {
                case ColumnLabel.ServerListCompatible:
                    bool s1Compatible = NetworkMember.IsCompatible(GameMain.Version, s1.GameVersion);
                    bool s2Compatible = NetworkMember.IsCompatible(GameMain.Version, s2.GameVersion);

                    if (s1Compatible == s2Compatible) { return 0; }
                    return (s1Compatible ? -1 : 1) * comparison;
                case ColumnLabel.ServerListHasPassword:
                    if (s1.HasPassword == s2.HasPassword) { return 0; }
                    return (s1.HasPassword ? 1 : -1) * comparison;
                case ColumnLabel.ServerListName:
                    // I think we actually want culture-specific sorting here?
                    return string.Compare(s1.ServerName, s2.ServerName, StringComparison.CurrentCulture) * comparison;
                case ColumnLabel.ServerListRoundStarted:
                    if (s1.GameStarted == s2.GameStarted) { return 0; }
                    return (s1.GameStarted ? 1 : -1) * comparison;
                case ColumnLabel.ServerListPlayers:
                    return s2.PlayerCount.CompareTo(s1.PlayerCount) * comparison;
                case ColumnLabel.ServerListPing:
                    return (s1.Ping.TryUnwrap(out var s1Ping), s2.Ping.TryUnwrap(out var s2Ping)) switch
                    {
                        (false, false) => 0,
                        (true, true) => s2Ping.CompareTo(s1Ping),
                        (false, true) => 1,
                        (true, false) => -1
                    } * comparison;
                default:
                    return 0;
            }
        }

        public override void Select()
        {
            base.Select();
          
            if (string.IsNullOrEmpty(ClientNameBox.Text))
            {
                TaskPool.Add("GetDefaultUserName",
                    GetDefaultUserName(),
                    t =>
                    {
                        if (!t.TryGetResult(out string name)) { return; }
                        if (ClientNameBox.Text.IsNullOrEmpty())
                        {
                            ClientNameBox.Text = name;
                            string nameWithoutInvisibleSymbols = string.Empty;
                            foreach (char c in ClientNameBox.Text)
                            {
                                Vector2 size = ClientNameBox.Font.MeasureChar(c);
                                if (size.X > 0 && size.Y > 0)
                                {
                                    nameWithoutInvisibleSymbols += c;
                                }
                            }
                            if (nameWithoutInvisibleSymbols != ClientNameBox.Text)
                            {
                                MultiplayerPreferences.Instance.PlayerName = ClientNameBox.Text = nameWithoutInvisibleSymbols;
                                new GUIMessageBox(TextManager.Get("Warning"), TextManager.GetWithVariable("NameContainsInvisibleSymbols", "[name]", nameWithoutInvisibleSymbols));
                            }
                        }
                    });
            }

            ClientNameBox.OnTextChanged += (textbox, text) =>
            {
                MultiplayerPreferences.Instance.PlayerName = text;
                return true;
            };
            if (EosInterface.IdQueries.IsLoggedIntoEosConnect)
            {
                if (SteamManager.IsInitialized)
                {
                    serverProvider = new CompositeServerProvider(
                        new EosServerProvider(),
                        new SteamDedicatedServerProvider(),
                        new SteamP2PServerProvider());
                }
                else
                {
                    serverProvider = new EosServerProvider();
                }
            }
            else if (SteamManager.IsInitialized)
            {
                serverProvider = new CompositeServerProvider(
                    new SteamDedicatedServerProvider(),
                    new SteamP2PServerProvider());
            }
            else
            {
                serverProvider = null;
            }

            Steamworks.SteamMatchmaking.ResetActions();

            selectedTab = TabEnum.All;
            GameMain.ServerListScreen.LoadServerFilters();
            if (GameSettings.CurrentConfig.ShowOffensiveServerPrompt)
            {
                var filterOffensivePrompt = new GUIMessageBox(string.Empty, TextManager.Get("FilterOffensiveServersPrompt"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") });
                filterOffensivePrompt.Buttons[0].OnClicked = (btn, userData) =>
                {
                    filterOffensive.Selected = true;
                    filterOffensivePrompt.Close();
                    return true;
                };
                filterOffensivePrompt.Buttons[1].OnClicked = filterOffensivePrompt.Close;

                var config = GameSettings.CurrentConfig;
                config.ShowOffensiveServerPrompt = false;
                GameSettings.SetCurrentConfig(config);
            }

            if (GameMain.Client != null)
            {
                GameMain.Client.Quit();
                GameMain.Client = null;
            }

            RefreshServers();
        }

        public override void Deselect()
        {
            base.Deselect();
            serverProvider?.Cancel();
            GameSettings.SaveCurrentConfig();
        }

        public override void Update(double deltaTime)
        {
            base.Update(deltaTime);

            panelAnimator?.Update();

            scanServersButton.Enabled = (DateTime.Now - lastRefreshTime) >= AllowedRefreshInterval;
        }

        public void FilterServers()
        {
            RemoveMsgFromServerList(MsgUserData.NoMatchingServers);
            foreach (GUIComponent child in serverList.Content.Children)
            {
                if (child.UserData is not ServerInfo serverInfo) { continue; }
                child.Visible = ShouldShowServer(serverInfo);
            }

            if (serverList.Content.Children.All(c => !c.Visible))
            {
                PutMsgInServerList(MsgUserData.NoMatchingServers);
            }
            serverList.UpdateScrollBarSize();
        }

        private bool AllLanguagesVisible
        {
            get
            {
                if (languageDropdown is null) { return true; }

                // CountChildren-1 because there's a separator element in there that can't be selected
                int tickBoxCount = languageDropdown.ListBox.Content.CountChildren - 1;
                int selectedCount = languageDropdown.SelectedIndexMultiple.Count();
                
                return selectedCount >= tickBoxCount;
            }
        }
        
        private bool ShouldShowServer(ServerInfo serverInfo)
        {
            if (serverInfo == null) { return false; }

#if !DEBUG
            //never show newer versions
            //(ignore revision number, it doesn't affect compatibility)
            if (ToolBox.VersionNewerIgnoreRevision(GameMain.Version, serverInfo.GameVersion))
            {
                return false;
            }
#endif

            if (!string.IsNullOrEmpty(searchBox.Text) && !serverInfo.ServerName.Contains(searchBox.Text, StringComparison.OrdinalIgnoreCase)) { return false; }

            if (filterSameVersion.Selected)
            {
                if (!NetworkMember.IsCompatible(serverInfo.GameVersion, GameMain.Version)) { return false; }
            }
            if (filterPassword.Selected)
            {
                if (serverInfo.HasPassword) { return false; }
            }
            if (filterFull.Selected)
            {
                if (serverInfo.PlayerCount >= serverInfo.MaxPlayers) { return false; }
            }
            if (filterEmpty.Selected)
            {
                if (serverInfo.PlayerCount <= 0) { return false; }
            }
            if (filterOffensive.Selected)
            {
                if (ForbiddenWordFilter.IsForbidden(serverInfo.ServerName)) { return false; }
            }

            if (filterKarmaValue != TernaryOption.Any)
            {
                if (serverInfo.KarmaEnabled != (filterKarmaValue == TernaryOption.Enabled)) { return false; }
            }
            if (filterFriendlyFireValue != TernaryOption.Any)
            {
                if (serverInfo.FriendlyFireEnabled != (filterFriendlyFireValue == TernaryOption.Enabled)) { return false; }
            }
            if (filterTraitorValue != TernaryOption.Any)
            {
                if ((serverInfo.TraitorProbability > 0.0f) != (filterTraitorValue == TernaryOption.Enabled)) 
                { 
                    return false; 
                }
            }
            if (filterVoipValue != TernaryOption.Any)
            {
                if (serverInfo.VoipEnabled != (filterVoipValue == TernaryOption.Enabled)) { return false; }
            }
            if (filterModdedValue != TernaryOption.Any)
            {
                if (serverInfo.IsModded != (filterModdedValue == TernaryOption.Enabled)) { return false; }
            }

            foreach (GUITickBox tickBox in playStyleTickBoxes.Values)
            {
                var playStyle = (PlayStyle)tickBox.UserData;
                if (!tickBox.Selected && serverInfo.PlayStyle == playStyle)
                {
                    return false;
                }
            }

            if (!AllLanguagesVisible)
            {
                if (!languageDropdown.SelectedDataMultiple.OfType<LanguageIdentifier>().Contains(serverInfo.Language))
                {
                    return false;
                }
            }

            foreach (GUITickBox tickBox in gameModeTickBoxes.Values)
            {
                var gameMode = (Identifier)tickBox.UserData;
                if (!tickBox.Selected && !serverInfo.GameMode.IsEmpty && serverInfo.GameMode == gameMode)
                {
                    return false;
                }
            }

            return true;
        }

        private void ShowDirectJoinPrompt()
        {
            var msgBox = new GUIMessageBox(TextManager.Get("ServerListDirectJoin"), "",
                new LocalizedString[] { TextManager.Get("ServerListJoin"), TextManager.Get("AddToFavorites"), TextManager.Get("Cancel") },
                relativeSize: new Vector2(0.25f, 0.2f), minSize: new Point(400, 150));
            msgBox.Content.ChildAnchor = Anchor.TopCenter;

            var content = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.5f), msgBox.Content.RectTransform), childAnchor: Anchor.TopCenter)
            {
                IgnoreLayoutGroups = false,
                Stretch = true,
                RelativeSpacing = 0.05f
            };

            new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform),
                SteamManager.IsInitialized ? TextManager.Get("ServerEndpoint") : TextManager.Get("ServerIP"), textAlignment: Alignment.Center);
            var endpointBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform));

            content.RectTransform.NonScaledSize = new Point(content.Rect.Width, (int)(content.RectTransform.Children.Sum(c => c.Rect.Height)));
            content.RectTransform.IsFixedSize = true;
            msgBox.InnerFrame.RectTransform.MinSize = new Point(0, (int)((content.RectTransform.NonScaledSize.Y + msgBox.Content.RectTransform.Children.Sum(c => c.NonScaledSize.Y + msgBox.Content.AbsoluteSpacing)) * 1.1f));

            var okButton = msgBox.Buttons[0];
            okButton.Enabled = false;
            okButton.OnClicked = (btn, userdata) =>
            {
                if (Endpoint.Parse(endpointBox.Text).TryUnwrap(out var endpoint))
                {
                    if (endpoint is SteamP2PEndpoint && !SteamManager.IsInitialized)
                    {
                        new GUIMessageBox(TextManager.Get("error"), TextManager.Get("CannotJoinSteamServer.SteamNotInitialized"));
                    }
                    else if (endpoint is EosP2PEndpoint && !EosInterface.Core.IsInitialized)
                    {
                        new GUIMessageBox(TextManager.Get("error"), TextManager.Get("EosStatus.NotInitialized"));
                    }
                    else
                    {
                        JoinServer(endpoint.ToEnumerable().ToImmutableArray(), "");
                    }
                }
                else if (LidgrenEndpoint.ParseFromWithHostNameCheck(endpointBox.Text, tryParseHostName: true).TryUnwrap(out var lidgrenEndpoint))
                {
                    JoinServer(((Endpoint)lidgrenEndpoint).ToEnumerable().ToImmutableArray(), "");
                }
                else
                {
                    new GUIMessageBox(TextManager.Get("error"), TextManager.GetWithVariable("invalidipaddress", "[serverip]:[port]", endpointBox.Text));
                    endpointBox.Flash();
                }
                msgBox.Close();
                return false;
            };

            var favoriteButton = msgBox.Buttons[1];
            favoriteButton.Enabled = false;
            favoriteButton.OnClicked = (button, userdata) =>
            {
                if (!Endpoint.Parse(endpointBox.Text).TryUnwrap(out var endpoint)) { return false; }

                var serverInfo = new ServerInfo(endpoint)
                {
                    ServerName = "Server",
                    GameVersion = GameMain.Version
                };

                var serverFrame = serverList.Content.FindChild(d =>
                    d.UserData is ServerInfo info
                    && info.Equals(serverInfo));

                if (serverFrame != null)
                {
                    serverInfo = (ServerInfo)serverFrame.UserData;
                }
                else
                {
                    AddToServerList(serverInfo);
                }

                AddToFavoriteServers(serverInfo);

                selectedTab = TabEnum.Favorites;
                FilterServers();

                msgBox.Close();
                return false;
            };

            var cancelButton = msgBox.Buttons[2];
            cancelButton.OnClicked = msgBox.Close;

            endpointBox.OnTextChanged += (textBox, text) =>
            {
                okButton.Enabled = favoriteButton.Enabled = !string.IsNullOrEmpty(text);
                return true;
            };

            // Connect on enter press, gotta go fast
            endpointBox.OnEnterPressed += (textBox, text) =>
            {
                if (okButton.Enabled)
                {
                    if (okButton.PlaySoundOnSelect)
                    {
                        SoundPlayer.PlayUISound(okButton.ClickSound);
                    }
                    okButton.OnClicked.Invoke(okButton, okButton.UserData);
                }
                return true;
            };
            
            // Focus on and select all the text, for easier input/deletion (after the dialog is shown, apparently it takes a moment)
            CoroutineManager.Invoke(() =>
            {
                endpointBox.Select(ignoreSelectSound: true);
            }, 0.1f);
        }

        private void RemoveMsgFromServerList()
        {
            serverList.Content.Children
                .Where(c => c.UserData is MsgUserData)
                .ForEachMod(serverList.Content.RemoveChild);
        }

        private void RemoveMsgFromServerList(MsgUserData userData)
        {
            serverList.Content.RemoveChild(serverList.Content.FindChild(userData));
        }
        
        private void PutMsgInServerList(MsgUserData userData)
        {
            RemoveMsgFromServerList();
            new GUITextBlock(new RectTransform(Vector2.One, serverList.Content.RectTransform),
                TextManager.Get(userData.ToString()), textAlignment: Alignment.Center)
            {
                CanBeFocused = false,
                UserData = userData
            };
        }
        
        private void RefreshServers()
        {
            lastRefreshTime = DateTime.Now;
            serverProvider?.Cancel();
            currentServerDataRecvCallbackObj = null;

            PingUtils.QueryPingData();

            // Clear spam server cache to allow re-checking servers (user might have changed filters)
            spamServerCache.Clear();
            // Also clear server info string cache when manually refreshing, just so we don't end up with broken data in any situation
            serverInfoStringCache.Clear();

            tabs[TabEnum.All].Clear();
            serverList.ClearChildren();
            serverPreview.Content.ClearChildren();
            panelAnimator.RightEnabled = false;
            joinButton.Enabled = false;
            selectedServer = Option.None;

            if (selectedTab == TabEnum.All)
            {
                PutMsgInServerList(MsgUserData.RefreshingServerList);
            }
            else
            {
                var servers = tabs[selectedTab].Servers.ToArray();
                foreach (var server in servers)
                {
                    server.Ping = Option<int>.None();
                    AddToServerList(server, skipPing: true);
                }

                if (!servers.Any())
                {
                    PutMsgInServerList(MsgUserData.NoServers);
                    return;
                }
            }

            var (onServerDataReceived, onQueryCompleted) = MakeServerQueryCallbacks();
            serverProvider?.RetrieveServers(onServerDataReceived, onQueryCompleted);
        }

        private GUIComponent FindFrameMatchingServerInfo(ServerInfo serverInfo)
        {
            bool matches(GUIComponent c)
                => c.UserData is ServerInfo info
                   && info.Equals(serverInfo);

#if DEBUG
            if (serverList.Content.Children.Count(matches) > 1)
            {
                DebugConsole.ThrowError($"There are several entries in the server list for endpoints {string.Join(", ", serverInfo.Endpoints)}");
            }
#endif

            return serverList.Content.FindChild(matches);
        }

        private object currentServerDataRecvCallbackObj = null;
        private (Action<ServerInfo, ServerProvider> OnServerDataReceived, Action OnQueryCompleted) MakeServerQueryCallbacks()
        {
            var uniqueObject = new object();
            currentServerDataRecvCallbackObj = uniqueObject;

            bool shouldRunCallback()
            {
                // If currentServerDataRecvCallbackObj != uniqueObject, then one of the following happened:
                // - The query this call is associated to was meant to be over
                // - Another query was started before the one associated to this call was finished
                // In either case, do not add the received info to the server list.
                return ReferenceEquals(currentServerDataRecvCallbackObj, uniqueObject);
            }
            
            return (
                (serverInfo, serverProvider) =>
                {
                    if (!shouldRunCallback()) { return; }

                    if (serverProvider is not EosServerProvider
                        && EosInterface.IdQueries.IsLoggedIntoEosConnect)
                    {
                        if (serverInfo.EosCrossplay)
                        {
                            // EosServerProvider should get us this server,
                            // don't add it again
                            return;
                        }
                    }

                    if (selectedTab == TabEnum.All)
                    {
                        AddToServerList(serverInfo);
                    }
                    else
                    {
                        if (FindFrameMatchingServerInfo(serverInfo) == null) { return; }
                        UpdateServerInfoUI(serverInfo);
                        PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI);
                    }
                },
                () =>
                {
                    if (shouldRunCallback()) { ServerQueryFinished(); }
                }
            );
        }

        private void AddToServerList(ServerInfo serverInfo, bool skipPing = false)
        {
            const int MaxAllowedPlayers = 1000;
            const int MaxAllowedSimilarServers = 10;
            const float MinSimilarityPercentage = 0.8f;

            if (string.IsNullOrWhiteSpace(serverInfo.ServerName)) { return; }

            if (serverInfo.ServerName.Length > NetConfig.ServerNameMaxLength) { return; }
            /*no newline symbols in server names!*/
            if (serverInfo.ServerName.Contains('\n') || serverInfo.ServerName.Contains('\r')) { return; }
            if (serverInfo.ServerMessage.Length > NetConfig.ServerMessageMaxLength) { return; }
            //+1 because it seems the count can sometimes be slightly off (something to do with steam lobbies not immediately refreshing?)
            if (serverInfo.PlayerCount > serverInfo.MaxPlayers + 1) { return; }
            if (serverInfo.PlayerCount < 0) { return; }
            if (serverInfo.MaxPlayers <= 0) { return; }
            if (!serverInfo.SelectedSub.IsNullOrEmpty())
            {
                if (serverInfo.SelectedSub.Length > SubmarineInfo.MaxNameLength) { return; }
            }
            //no way a legit server can have this many players
            if (serverInfo.MaxPlayers > MaxAllowedPlayers) { return; }

            // Check spam filter with caching to avoid re-checking on every filter change
            string serverCacheKey = serverInfo.Endpoints.First().StringRepresentation;
            if (spamServerCache.Contains(serverCacheKey)) { return; }
            if (SpamServerFilters.IsFiltered(serverInfo))
            {
                spamServerCache.Add(serverCacheKey);
                return;
            }

            static string getServerInfoStr(ServerInfo serverInfo)
            {
                string str = serverInfo.ServerName + serverInfo.ServerMessage + serverInfo.MaxPlayers;
                if (str.Length > 200) { return str.Substring(0, 200); }
                return str;
            }

            string getCachedServerInfoStr(ServerInfo serverInfo)
            {
                string cacheKey = serverInfo.Endpoints.First().StringRepresentation;
                if (!serverInfoStringCache.TryGetValue(cacheKey, out string cachedStr))
                {
                    cachedStr = getServerInfoStr(serverInfo);
                    serverInfoStringCache[cacheKey] = cachedStr;
                }
                return cachedStr;
            }

            int similarServerCount = 0;
            string serverInfoStr = getServerInfoStr(serverInfo);
            foreach (var serverElement in serverList.Content.Children)
            {
                if (!serverElement.Visible) { continue; }
                if (serverElement.UserData is not ServerInfo otherServer || otherServer == serverInfo) { continue; }
                if (ToolBox.LevenshteinDistance(serverInfoStr, getCachedServerInfoStr(otherServer)) < serverInfoStr.Length * (1.0f - MinSimilarityPercentage))
                {
                    similarServerCount++;
                    if (similarServerCount > MaxAllowedSimilarServers)
                    {
                        DebugConsole.Log($"Server {serverInfo.ServerName} seems to be almost identical to {otherServer.ServerName}. Hiding as a potential spam server.");
                        break;
                    }
                }
            }
            if (similarServerCount > MaxAllowedSimilarServers) { return; }

            RemoveMsgFromServerList(MsgUserData.RefreshingServerList);
            RemoveMsgFromServerList(MsgUserData.NoServers);
            var serverFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), serverList.Content.RectTransform) { MinSize = new Point(0, 35) },
                style: "ListBoxElement")
            {
                UserData = serverInfo,
            };

            serverFrame.OnSecondaryClicked += (_, data) =>
            {
                if (data is not ServerInfo info) { return false; }
                CreateContextMenu(info);
                return true;
            };

            new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), serverFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft)
            {
                Stretch = false
            };
            UpdateServerInfoUI(serverInfo);
            if (!skipPing) { PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI); }
        }

        private static readonly Vector2 confirmPopupSize = new Vector2(0.2f, 0.2625f);
        private static readonly Point confirmPopupMinSize = new Point(300, 300);

        private void CreateContextMenu(ServerInfo info)
        {
            var favoriteOption = new ContextMenuOption(IsFavorite(info) ? "removefromfavorites" : "addtofavorites", isEnabled: true, () =>
            {
                if (IsFavorite(info))
                {
                    RemoveFromFavoriteServers(info);
                }
                else
                {
                    AddToFavoriteServers(info);
                }
                FilterServers();
            });
            var reportOption = new ContextMenuOption("reportserver", isEnabled: true, () => { CreateReportPrompt(info); });
            var filterOption = new ContextMenuOption("filterserver", isEnabled: true, () =>
            {
                CreateFilterServerPrompt(info);
            })
            {
                Tooltip = TextManager.Get("filterservertooltip")
            };

            GUIContextMenu.CreateContextMenu(favoriteOption, filterOption, reportOption);
        }

        public static void CreateFilterServerPrompt(ServerInfo info)
        {
            GUI.AskForConfirmation(
                header: TextManager.Get("filterserver"),
                body: TextManager.GetWithVariables("filterserverconfirm", ("[server]", info.ServerName), ("[filepath]", SpamServerFilter.SavePath)),
                onConfirm: () =>
                {
                    SpamServerFilters.AddServerToLocalSpamList(info);

                    if (GameMain.ServerListScreen is not { } serverListScreen) { return; }

                    if (serverListScreen.selectedServer.TryUnwrap(out var selectedServer) && selectedServer.Equals(info))
                    {
                        serverListScreen.HideServerPreview();
                    }
                    serverListScreen.FilterServers();
                }, relativeSize: confirmPopupSize, minSize: confirmPopupMinSize);
        }

        private enum ReportReason
        {
            Spam,
            Advertising,
            Inappropriate
        }

        public static void CreateReportPrompt(ServerInfo info)
        {
            if (!GameAnalyticsManager.SendUserStatistics)
            {
                GUI.NotifyPrompt(TextManager.Get("reportserver"), TextManager.Get("reportserverdisabled"));
                return;
            }

            var msgBox = new GUIMessageBox(
                headerText: TextManager.Get("reportserver"),
                text: string.Empty,
                relativeSize: new Vector2(0.2f, 0.4f),
                minSize: new Point(380, 430),
                buttons: Array.Empty<LocalizedString>());

            var layout = new GUILayoutGroup(new RectTransform(Vector2.One, msgBox.Content.RectTransform, Anchor.Center));

            new GUITextBlock(new RectTransform(new Vector2(1f, 0.3f), layout.RectTransform), TextManager.GetWithVariable("reportserverexplanation", "[server]", info.ServerName), wrap: true)
            {
                ToolTip = TextManager.Get("reportserverprompttooltip")
            };

            var listBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.3f), layout.RectTransform));

            var enums = Enum.GetValues<ReportReason>();
            foreach (ReportReason reason in enums)
            {
                new GUITickBox(new RectTransform(new Vector2(1f, 1f / enums.Length), listBox.Content.RectTransform), TextManager.Get($"reportreason.{reason}"))
                {
                    UserData = reason
                };
            }

            // padding
            new GUIFrame(new RectTransform(new Vector2(1f, 0.05f), layout.RectTransform), style: null);

            var buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), layout.RectTransform))
            {
                Stretch = true
            };

            var reportAndHideButton = new GUIButton(new RectTransform(new Vector2(1f, 0.333f), buttonLayout.RectTransform), TextManager.Get("reportoption.reportandhide"))
            {
                Enabled = false,
                OnClicked = (_, _) =>
                {
                    CreateFilterServerPrompt(info);
                    msgBox.Close();
                    return true;
                }
            };
            var reportButton = new GUIButton(new RectTransform(new Vector2(1f, 0.333f), buttonLayout.RectTransform), TextManager.Get("reportoption.report"))
            {
                Enabled = false,
                OnClicked = (_, _) =>
                {
                    ReportServer(info, GetUserSelectedReasons());
                    msgBox.Close();
                    return true;
                }
            };

            new GUIButton(new RectTransform(new Vector2(1f, 0.333f), buttonLayout.RectTransform), TextManager.Get("cancel"))
            {
                OnClicked = (_, _) =>
                {
                    msgBox.Close();
                    return true;
                }
            };

            foreach (var child in listBox.Content.GetAllChildren<GUITickBox>())
            {
                child.OnSelected += _ =>
                {
                    reportAndHideButton.Enabled = reportButton.Enabled = GetUserSelectedReasons().Any();
                    return true;
                };
            }

            IEnumerable<ReportReason> GetUserSelectedReasons()
                => listBox.Content.Children
                          .Where(static c => c.UserData is ReportReason && c.Selected)
                          .Select(static c => (ReportReason)c.UserData).ToArray();
        }

        private static void ReportServer(ServerInfo info, IEnumerable<ReportReason> reasons)
        {
            if (!reasons.Any()) { return; }
            GameAnalyticsManager.AddErrorEvent(GameAnalyticsManager.ErrorSeverity.Info, $"[Spam] Reported server: Name: \"{info.ServerName}\", Message: \"{info.ServerMessage}\", Endpoint: \"{info.Endpoints.First().StringRepresentation}\". Reason: \"{string.Join(", ", reasons)}\".");
        }

        private void UpdateServerInfoUI(ServerInfo serverInfo)
        {
            var serverFrame = FindFrameMatchingServerInfo(serverInfo);
            if (serverFrame == null) { return; }

            serverFrame.UserData = serverInfo;

            var serverContent = serverFrame.Children.First() as GUILayoutGroup;
            serverContent.ClearChildren();

            Dictionary<ColumnLabel, GUIFrame> sections = new Dictionary<ColumnLabel, GUIFrame>();
            foreach (ColumnLabel label in Enum.GetValues(typeof(ColumnLabel)))
            {
                sections[label] =
                    new GUIFrame(
                        new RectTransform(new Vector2(columns[label].RelativeWidth, 1.0f), serverContent.RectTransform),
                        style: null);
            }

            void disableElementFocus()
            {
                sections.Values.ForEach(c =>
                {
                    c.CanBeFocused = false;
                    c.Children.First().CanBeFocused = false;
                });
            }

            RectTransform columnRT(ColumnLabel label, float scale = 0.95f)
                => new RectTransform(Vector2.One * scale, sections[label].RectTransform, Anchor.Center);

            void sectionTooltip(ColumnLabel label, RichString toolTip)
            {
                var section = sections[label];
                section.CanBeFocused = true;
                section.ToolTip = toolTip;
            }

            var compatibleBox = new GUITickBox(columnRT(ColumnLabel.ServerListCompatible), label: "")
            {
                CanBeFocused = false,
                Selected =
                    NetworkMember.IsCompatible(GameMain.Version, serverInfo.GameVersion),
                UserData = "compatible"
            };

            var passwordBox = new GUITickBox(columnRT(ColumnLabel.ServerListHasPassword, scale: 0.6f), label: "", style: "GUIServerListPasswordTickBox")
            {
				Selected = serverInfo.HasPassword,
                UserData = "password",
                CanBeFocused = false
            };
            sectionTooltip(ColumnLabel.ServerListHasPassword,
                TextManager.Get((serverInfo.HasPassword) ? "ServerListHasPassword" : "FilterPassword"));

            var serverName = new GUITextBlock(columnRT(ColumnLabel.ServerListName),
#if DEBUG
                $"[{serverInfo.Endpoints.First().GetType().Name}] " +
#endif
                serverInfo.ServerName,
                style: "GUIServerListTextBox") { CanBeFocused = false };

            if (serverInfo.IsModded)
            {
                serverName.TextColor = GUIStyle.ModdedServerColor;
            }

            new GUITickBox(columnRT(ColumnLabel.ServerListRoundStarted), label: "")
            {
				Selected = serverInfo.GameStarted,
                CanBeFocused = false
			};
            sectionTooltip(ColumnLabel.ServerListRoundStarted,
                TextManager.Get(serverInfo.GameStarted ? "ServerListRoundStarted" : "ServerListRoundNotStarted"));

            var serverPlayers = new GUITextBlock(columnRT(ColumnLabel.ServerListPlayers),
                $"{serverInfo.PlayerCount}/{serverInfo.MaxPlayers}", style: "GUIServerListTextBox", textAlignment: Alignment.Right)
            {
                ToolTip = TextManager.Get("ServerListPlayers")
            };

            var serverPingText = new GUITextBlock(columnRT(ColumnLabel.ServerListPing), "?", 
                style: "GUIServerListTextBox", textColor: Color.White * 0.5f, textAlignment: Alignment.Right)
            {
                ToolTip = TextManager.Get("ServerListPing")
            };

            if (serverInfo.Ping.TryUnwrap(out var ping))
            {
                serverPingText.Text = ping.ToString();
                serverPingText.TextColor = GetPingTextColor(ping);
            }
            else if ((serverInfo.Endpoints.Length == 1 && serverInfo.Endpoints.First() is EosP2PEndpoint)
                     || (!SteamManager.IsInitialized && serverInfo.Endpoints.Any(e => e is P2PEndpoint)))
            {
                serverPingText.Text = "-";
                serverPingText.ToolTip = TextManager.Get("EosPingUnavailable");
                serverPingText.TextAlignment = Alignment.Center;
            }
            else
            {
                serverPingText.Text = "?";
                serverPingText.TextColor = Color.DarkRed;
            }

            LocalizedString toolTip = "";
            if (!serverInfo.Checked)
            {
                toolTip = TextManager.Get("ServerOffline");
                serverName.TextColor *= 0.8f;
                serverPlayers.TextColor *= 0.8f;
            }
            else if (!serverInfo.ContentPackages.Any())
            {
                compatibleBox.Selected = false;
                new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), compatibleBox.Box.RectTransform, Anchor.Center),
                    " ? ", GUIStyle.Orange * 0.85f, textAlignment: Alignment.Center)
                {
                    ToolTip = TextManager.Get("ServerListUnknownContentPackage")
                };
            }
            else if (!compatibleBox.Selected)
            {
                if (serverInfo.GameVersion != GameMain.Version)
                {
                    toolTip = TextManager.GetWithVariable("ServerListIncompatibleVersion", "[version]", serverInfo.GameVersion.ToString());
                }

                int maxIncompatibleToList = 10;
                List<LocalizedString> incompatibleModNames = new List<LocalizedString>();
                foreach (var contentPackage in serverInfo.ContentPackages)
                {
                    bool listAsIncompatible = !ContentPackageManager.EnabledPackages.All.Any(cp => cp.Hash.StringRepresentation == contentPackage.Hash);
                    if (listAsIncompatible)
                    {
                        incompatibleModNames.Add(TextManager.GetWithVariables("ModNameAndHashFormat", 
                            ("[name]", contentPackage.Name), 
                            ("[hash]", Md5Hash.GetShortHash(contentPackage.Hash))));
                    }
                }
                if (incompatibleModNames.Any())
                {
                    toolTip += '\n' + TextManager.Get("ModDownloadHeader") + "\n" + string.Join(", ", incompatibleModNames.Take(maxIncompatibleToList));
                    if (incompatibleModNames.Count > maxIncompatibleToList)
                    {
                        toolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (incompatibleModNames.Count - maxIncompatibleToList).ToString());
                    }
                }

                serverName.TextColor *= 0.5f;
                serverPlayers.TextColor *= 0.5f;
            }
            else
            {
                foreach (var contentPackage in serverInfo.ContentPackages)
                {
                    if (ContentPackageManager.EnabledPackages.All.None(cp => cp.Hash.StringRepresentation == contentPackage.Hash))
                    {
                        if (toolTip != "") { toolTip += "\n"; }
                        toolTip += TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", contentPackage.Name);
                        break;
                    }
                }
            }
            disableElementFocus();

            string separator = toolTip.IsNullOrWhiteSpace() ? "" : "\n\n";
            serverFrame.ToolTip = RichString.Rich(toolTip + separator + $"‖color:gui.blue‖{TextManager.GetWithVariable("serverlisttooltip", "[button]", PlayerInput.SecondaryMouseLabel)}‖end‖");

            foreach (var section in sections.Values)
            {
                var child = section.Children.First();
                child.RectTransform.ScaleBasis
                    = child is GUITextBlock ? ScaleBasis.Normal : ScaleBasis.BothHeight;
            }

            // The next twenty-something lines are an optimization.
            // The issue is that the serverlist has a ton of text elements,
            // and resizing all of them is extremely expensive. However, since
            // you don't see most of them most of the time, it makes sense to
            // just resize them lazily based on when you actually can see them.
            // That would entail a UI refactor of some kind, and I don't want to
            // do that just yet, so here's a hack instead!
            bool isDirty = true;
            void markAsDirty() => isDirty = true;
            serverContent.GetAllChildren().ForEach(c =>
            {
                c.RectTransform.ResetSizeChanged();
                c.RectTransform.SizeChanged += markAsDirty;
            });
            new GUICustomComponent(new RectTransform(Vector2.Zero, serverContent.RectTransform), onUpdate: (_, __) =>
            {
                if (serverFrame.MouseRect.Height <= 0 || !isDirty) { return; }
                serverContent.GetAllChildren().ForEach(c =>
                {
                    switch (c)
                    {
                        case GUITextBlock textBlock:
                            textBlock.SetTextPos();
                            break;
                        case GUITickBox tickBox:
                            tickBox.ResizeBox();
                            break;
                    }
                });
                serverName.Text = ToolBox.LimitString(serverInfo.ServerName, serverName.Font, serverName.Rect.Width);
                isDirty = false;
            });
            // Hacky optimization ends here
            
            serverContent.Recalculate();

            if (tabs[TabEnum.Favorites].Contains(serverInfo))
            {
                AddToFavoriteServers(serverInfo);
            }

            InsertServer(serverInfo, serverFrame);
            FilterServers();
        }

        private void ServerQueryFinished()
        {
            currentServerDataRecvCallbackObj = null;
            if (!serverList.Content.Children.Any(c => c.UserData is ServerInfo))
            {
                PutMsgInServerList(MsgUserData.NoServers);
            }
            else if (serverList.Content.Children.All(c => !c.Visible))
            {
                PutMsgInServerList(MsgUserData.NoMatchingServers);
            }
        }

        public void JoinServer(ImmutableArray<Endpoint> endpoints, string serverName)
        {
            if (string.IsNullOrWhiteSpace(ClientNameBox.Text))
            {
                ClientNameBox.Flash();
                ClientNameBox.Select();
                SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
                return;
            }

            MultiplayerPreferences.Instance.PlayerName = ClientNameBox.Text;
            GameSettings.SaveCurrentConfig();

            if (MultiplayerPreferences.Instance.PlayerName.IsNullOrEmpty())
            {
                TaskPool.Add("GetDefaultUserName",
                    GetDefaultUserName(),
                    t =>
                    {
                        if (!t.TryGetResult(out string name)) { return; }
                        startClient(name);
                    });
            }
            else
            {
                startClient(MultiplayerPreferences.Instance.PlayerName);
            }

            void startClient(string name)
            {
#if !DEBUG
                try
                {
#endif
                    GameMain.Client = new GameClient(name, endpoints, serverName, Option.None);
#if !DEBUG
                }
                catch (Exception e)
                {
                    DebugConsole.ThrowError("Failed to start the client", e);
                }
#endif
            }
        }

        private static Color GetPingTextColor(int ping)
        {
            if (ping < 0) { return Color.DarkRed; }
            return ToolBox.GradientLerp(ping / 200.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red);
        }

        public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch)
        {
            graphics.Clear(Color.CornflowerBlue);

            spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable);
            GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch);            
            GUI.Draw(Cam, spriteBatch);
            spriteBatch.End();
        }

        public override void AddToGUIUpdateList()
        {
            menu.AddToGUIUpdateList();
        }

        public void StoreServerFilters()
        {
            if (loadingServerFilters) { return; }
            foreach (KeyValuePair<Identifier, GUITickBox> filterBox in filterTickBoxes)
            {
                ServerListFilters.Instance.SetAttribute(filterBox.Key, filterBox.Value.Selected.ToString());
            }
            foreach (KeyValuePair<Identifier, GUIDropDown> ternaryFilter in ternaryFilters)
            {
                ServerListFilters.Instance.SetAttribute(ternaryFilter.Key, ternaryFilter.Value.SelectedData.ToString());
            }
            GameSettings.SaveCurrentConfig();
        }

        private bool loadingServerFilters;
        public void LoadServerFilters()
        {
            loadingServerFilters = true;
            XDocument currentConfigDoc = XMLExtensions.TryLoadXml(GameSettings.PlayerConfigPath);
            ServerListFilters.Init(currentConfigDoc.Root.GetChildElement("serverfilters"));
            foreach (KeyValuePair<Identifier, GUITickBox> filterBox in filterTickBoxes)
            {
                filterBox.Value.Selected =
                    ServerListFilters.Instance.GetAttributeBool(filterBox.Key, filterBox.Value.Selected);
            }
            foreach (KeyValuePair<Identifier, GUIDropDown> ternaryFilter in ternaryFilters)
            {
                TernaryOption ternaryOption =
                    ServerListFilters.Instance.GetAttributeEnum(
                        ternaryFilter.Key,
                        (TernaryOption)ternaryFilter.Value.SelectedData);

                var child = ternaryFilter.Value.ListBox.Content.GetChildByUserData(ternaryOption);
                ternaryFilter.Value.Select(ternaryFilter.Value.ListBox.Content.GetChildIndex(child));
            }
            loadingServerFilters = false;
        }
        
    }
}
