using System;
using System.Collections.Generic;
using System.Diagnostics;
using Barotrauma.IO;
using System.Linq;
using Barotrauma.CharacterEditor;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Barotrauma.Sounds;
using EventInput;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System.Collections.Immutable;

namespace Barotrauma
{
    public enum GUISoundType
    {
        UIMessage,
        ChatMessage,
        RadioMessage,
        DeadMessage,
        Select,
        PickItem,
        PickItemFail,
        DropItem,
        PopupMenu,
        Decrease,
        Increase,
        UISwitch,
        TickBox,
        ConfirmTransaction,
        Cart,
    }

    public enum CursorState
    {
        Default = 0, // Cursor
        Hand = 1, // Hand with a finger
        Move = 2, // arrows pointing to all directions
        IBeam = 3, // Text
        Dragging = 4,// Closed hand
        Waiting = 5, // Hourglass
        WaitingBackground = 6, // Cursor + Hourglass
    }

    static class GUI
    {
        // Controls where a line is drawn for given coords.
        public enum OutlinePosition
        {
            Default = 0, // Thickness is inside of top left and outside of bottom right coord
            Inside = 1, // Thickness is subtracted from the inside
            Centered = 2, // Thickness is centered on given coords
            Outside = 3, // Tickness is added to the outside
        }
        public static GUICanvas Canvas => GUICanvas.Instance;
        public static CursorState MouseCursor = CursorState.Default;

        public static readonly SamplerState SamplerState = new SamplerState()
        {
            Filter = TextureFilter.Linear,
            AddressU = TextureAddressMode.Wrap,
            AddressV = TextureAddressMode.Wrap,
            AddressW = TextureAddressMode.Wrap,
            BorderColor = Color.White,
            MaxAnisotropy = 4,
            MaxMipLevel = 0,
            MipMapLevelOfDetailBias = -0.8f,
            ComparisonFunction = CompareFunction.Never,
            FilterMode = TextureFilterMode.Default,
        };

        public static readonly SamplerState SamplerStateClamp = new SamplerState()
        {
            Filter = TextureFilter.Linear,
            AddressU = TextureAddressMode.Clamp,
            AddressV = TextureAddressMode.Clamp,
            AddressW = TextureAddressMode.Clamp,
            BorderColor = Color.White,
            MaxAnisotropy = 4,
            MaxMipLevel = 0,
            MipMapLevelOfDetailBias = -0.8f,
            ComparisonFunction = CompareFunction.Never,
            FilterMode = TextureFilterMode.Default,
        };

        public static readonly string[] VectorComponentLabels = { "X", "Y", "Z", "W" };
        public static readonly string[] RectComponentLabels = { "X", "Y", "W", "H" };
        public static readonly string[] ColorComponentLabels = { "R", "G", "B", "A" };

        private static readonly object mutex = new object();

        public static readonly Vector2 ReferenceResolution = new Vector2(1920f, 1080f);
        public static float Scale => (UIWidth / ReferenceResolution.X + GameMain.GraphicsHeight / ReferenceResolution.Y) / 2.0f * GameSettings.CurrentConfig.Graphics.HUDScale;
        public static float xScale => UIWidth / ReferenceResolution.X * GameSettings.CurrentConfig.Graphics.HUDScale;
        public static float yScale => GameMain.GraphicsHeight / ReferenceResolution.Y * GameSettings.CurrentConfig.Graphics.HUDScale;
        public static int IntScale(float f) => (int)(f * Scale);
        public static int IntScaleFloor(float f) => (int)Math.Floor(f * Scale);
        public static int IntScaleCeiling(float f) => (int)Math.Ceiling(f * Scale);
        public static float AdjustForTextScale(float f) => f * GameSettings.CurrentConfig.Graphics.TextScale;
        public static float HorizontalAspectRatio => GameMain.GraphicsWidth / (float)GameMain.GraphicsHeight;
        public static float VerticalAspectRatio => GameMain.GraphicsHeight / (float)GameMain.GraphicsWidth;
        public static float RelativeHorizontalAspectRatio => HorizontalAspectRatio / (ReferenceResolution.X / ReferenceResolution.Y);
        public static float RelativeVerticalAspectRatio => VerticalAspectRatio / (ReferenceResolution.Y / ReferenceResolution.X);
        /// <summary>
        /// A horizontal scaling factor for low aspect ratios (small width relative to height)
        /// </summary>
        public static float AspectRatioAdjustment => HorizontalAspectRatio < 1.4f ? (1.0f - (1.4f - HorizontalAspectRatio)) : 1.0f;

        public static bool IsUltrawide => HorizontalAspectRatio > 2.0f;

        public static int UIWidth
        {
            get
            {
                if (IsUltrawide)
                {
                    return (int)(GameMain.GraphicsHeight * ReferenceResolution.X / ReferenceResolution.Y);
                }
                else
                {
                    return GameMain.GraphicsWidth;
                }
            }
        }

        public static float SlicedSpriteScale
        {
            get
            {
                if (Math.Abs(1.0f - Scale) < 0.1f)
                {
                    //don't scale if very close to the "reference resolution"
                    return 1.0f;
                }
                return Scale;
            }
        }

        private static Texture2D solidWhiteTexture;
        public static Texture2D WhiteTexture => solidWhiteTexture;
        private static GUICursor MouseCursorSprites => GUIStyle.CursorSprite;

        private static bool debugDrawSounds, debugDrawEvents;

        private static DebugDrawMetaData debugDrawMetaData;

        public struct DebugDrawMetaData
        {
            public bool Enabled;
            public bool FactionMetadata, UpgradeLevels, UpgradePrices;
            public int Offset;
        }

        public static GraphicsDevice GraphicsDevice => GameMain.Instance.GraphicsDevice;

        private static readonly List<GUIMessage> messages = new List<GUIMessage>();

        public static GUIFrame PauseMenu { get; private set; }
        public static GUIFrame SettingsMenuContainer { get; private set; }
        public static Sprite Arrow => GUIStyle.Arrow.Value.Sprite;

        public static bool HideCursor;

        public static KeyboardDispatcher KeyboardDispatcher { get; set; }

        /// <summary>
        /// Has the selected Screen changed since the last time the GUI was drawn.
        /// </summary>
        public static bool ScreenChanged;

        private static bool settingsMenuOpen;
        public static bool SettingsMenuOpen
        {
            get { return settingsMenuOpen; }
            set
            {
                if (value == SettingsMenuOpen) { return; }

                if (value)
                {
                    SettingsMenuContainer = new GUIFrame(new RectTransform(Vector2.One, Canvas, Anchor.Center), style: null);
                    new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, SettingsMenuContainer.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker");

                    var settingsMenuInner = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.8f), SettingsMenuContainer.RectTransform, Anchor.Center, scaleBasis: ScaleBasis.Smallest) { MinSize = new Point(640, 480) });
                    SettingsMenu.Create(settingsMenuInner.RectTransform);
                }
                else
                {
                    SettingsMenu.Instance?.Close();
                }
                settingsMenuOpen = value;
            }
        }

        public static bool PauseMenuOpen { get; private set; }

        public static bool InputBlockingMenuOpen
        {
            get
            {
                return PauseMenuOpen ||
                    SettingsMenuOpen ||
                    DebugConsole.IsOpen ||
                    GameSession.IsTabMenuOpen ||
                    GameMain.GameSession?.GameMode is { Paused: true } ||
                    CharacterHUD.IsCampaignInterfaceOpen ||
                    GameMain.GameSession?.Campaign is { SlideshowPlayer: { Finished: false, Visible: true } };
            }
        }

        public static bool PreventPauseMenuToggle = false;

        public static Color ScreenOverlayColor
        {
            get;
            set;
        }

        public static bool DisableHUD, DisableUpperHUD, DisableItemHighlights, DisableCharacterNames;

        private static bool isSavingIndicatorEnabled;
        private static Color savingIndicatorColor = Color.Transparent;
        private static bool IsSavingIndicatorVisible => savingIndicatorColor.A > 0;
        private static float savingIndicatorSpriteIndex;
        private static float savingIndicatorColorLerpAmount;
        private static SavingIndicatorState savingIndicatorState = SavingIndicatorState.None;
        private static float? timeUntilSavingIndicatorDisabled;

        private static string loadedSpritesText;
        private static DateTime loadedSpritesUpdateTime;

        private enum SavingIndicatorState
        {
            None,
            FadingIn,
            FadingOut
        }

        public static void Init()
        {
            // create 1x1 texture for line drawing
            CrossThread.RequestExecutionOnMainThread(() =>
            {
                solidWhiteTexture = new Texture2D(GraphicsDevice, 1, 1);
                solidWhiteTexture.SetData(new Color[] { Color.White });// fill the texture with white
            });
        }

        /// <summary>
        /// By default, all the gui elements are drawn automatically in the same order they appear on the update list.
        /// </summary>
        public static void Draw(Camera cam, SpriteBatch spriteBatch)
        {
            lock (mutex)
            {
                usedIndicatorAngles.Clear();

                if (ScreenChanged)
                {
                    updateList.Clear();
                    updateListSet.Clear();
                    Screen.Selected?.AddToGUIUpdateList();
                    ScreenChanged = false;
                }

                foreach (GUIComponent c in updateList)
                {
                    c.DrawAuto(spriteBatch);
                }

                // always draw IME preview on top of everything else
                foreach (GUIComponent c in updateList)
                {
                    if (c is not GUITextBox box) { continue; }
                    box.DrawIMEPreview(spriteBatch);
                }

                if (ScreenOverlayColor.A > 0.0f)
                {
                    DrawRectangle(
                        spriteBatch,
                        new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight),
                        ScreenOverlayColor, true);
                }

#if UNSTABLE
                string line1 = "Barotrauma Unstable v" + GameMain.Version;
                string line2 = "(" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")";

                Rectangle watermarkRect = new Rectangle(-50, GameMain.GraphicsHeight - 80, 50 + (int)(Math.Max(GUIStyle.LargeFont.MeasureString(line1).X, GUIStyle.Font.MeasureString(line2).X) * 1.2f), 100);
                float alpha = 1.0f;

                int yOffset = 0;

                if (Screen.Selected == GameMain.GameScreen)
                {
                    yOffset = (int)(-HUDLayoutSettings.ChatBoxArea.Height * 1.2f);
                    watermarkRect.Y += yOffset;
                }

                if (Screen.Selected == GameMain.GameScreen || Screen.Selected == GameMain.SubEditorScreen)
                {
                    alpha = 0.2f;
                }

                GUIStyle.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0].Draw(
                    spriteBatch, watermarkRect, Color.Black * 0.8f * alpha);
                GUIStyle.LargeFont.DrawString(spriteBatch, line1,
                new Vector2(10, GameMain.GraphicsHeight - 30 - GUIStyle.LargeFont.MeasureString(line1).Y + yOffset), Color.White * 0.6f * alpha);
                GUIStyle.Font.DrawString(spriteBatch, line2,
                new Vector2(10, GameMain.GraphicsHeight - 30 + yOffset), Color.White * 0.6f * alpha);

                if (Screen.Selected != GameMain.GameScreen)
                {
                    var buttonRect =
                        new Rectangle(20 + (int)Math.Max(GUIStyle.LargeFont.MeasureString(line1).X, GUIStyle.Font.MeasureString(line2).X), GameMain.GraphicsHeight - (int)(45 * Scale) + yOffset, (int)(150 * Scale), (int)(40 * Scale));
                    if (DrawButton(spriteBatch, buttonRect, "Report Bug", GUIStyle.GetComponentStyle("GUIBugButton").Color * 0.8f))
                    {
                        GameMain.Instance.ShowBugReporter();
                    }
                }
#endif

                if (DisableHUD)
                {
                    DrawSavingIndicator(spriteBatch);
                    return;
                }

                float startY = 10.0f;
                float yStep = AdjustForTextScale(18) * yScale;
                if (GameMain.ShowFPS || GameMain.DebugDraw || GameMain.ShowPerf)
                {
                    float y = startY;
                    DrawString(spriteBatch, new Vector2(10, y),
                        "FPS: " + Math.Round(GameMain.PerformanceCounter.AverageFramesPerSecond),
                        Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                    if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration > 1.0)
                    {
                        y += yStep;
                        DrawString(spriteBatch, new Vector2(10, y),
                            $"Physics: {GameMain.CurrentUpdateRate}",
                            (GameMain.CurrentUpdateRate < Timing.FixedUpdateRate) ? Color.Red : Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                    }
                    if (GameMain.DebugDraw || GameMain.ShowPerf)
                    {
                        y += yStep;
                        DrawString(spriteBatch, new Vector2(10, y),
                            "Active lights: " + Lights.LightManager.ActiveLightCount,
                            Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                        y += yStep;
                        DrawString(spriteBatch, new Vector2(10, y),
                            "Physics: " + GameMain.World.UpdateTime.TotalMilliseconds + " ms",
                            Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                        y += yStep;
                        try
                        {
                            DrawString(spriteBatch, new Vector2(10, y),
                                $"Bodies: {GameMain.World.BodyList.Count} ({GameMain.World.BodyList.Count(b => b != null && b.Awake && b.Enabled)} awake, {GameMain.World.BodyList.Count(b => b != null && b.Awake && b.BodyType == BodyType.Dynamic && b.Enabled)} dynamic)",
                                Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                        }
                        catch (InvalidOperationException)
                        {
                            DebugConsole.AddWarning("Exception while rendering debug info. Physics bodies may have been created or removed while rendering.");
                        }
                        y += yStep;
                        DrawString(spriteBatch, new Vector2(10, y),
                            "Particle count: " + GameMain.ParticleManager.ParticleCount + "/" + GameMain.ParticleManager.MaxParticles,
                            Color.Lerp(GUIStyle.Green, GUIStyle.Red, (GameMain.ParticleManager.ParticleCount / (float)GameMain.ParticleManager.MaxParticles)), Color.Black * 0.5f, 0, GUIStyle.SmallFont);

                    }
                }

                if (GameMain.ShowPerf)
                {
                    float x = 400;
                    float y = startY;
                    DrawString(spriteBatch, new Vector2(x, y),
                        "Draw - Avg: " + GameMain.PerformanceCounter.DrawTimeGraph.Average().ToString("0.00") + " ms" +
                        " Max: " + GameMain.PerformanceCounter.DrawTimeGraph.LargestValue().ToString("0.00") + " ms",
                        GUIStyle.Green, Color.Black * 0.8f, font: GUIStyle.SmallFont);
                        y += yStep;
                    GameMain.PerformanceCounter.DrawTimeGraph.Draw(spriteBatch, new Rectangle((int)x, (int)y, 170, 50), color: GUIStyle.Green);
                    y += yStep * 4;

                    DrawString(spriteBatch, new Vector2(x, y),
                        "Update - Avg: " + GameMain.PerformanceCounter.UpdateTimeGraph.Average().ToString("0.00") + " ms" +
                        " Max: " + GameMain.PerformanceCounter.UpdateTimeGraph.LargestValue().ToString("0.00") + " ms",
                        Color.LightBlue, Color.Black * 0.8f, font: GUIStyle.SmallFont);
                    y += yStep;
                    GameMain.PerformanceCounter.UpdateTimeGraph.Draw(spriteBatch, new Rectangle((int)x, (int)y, 170, 50), color: Color.LightBlue);
                    y += yStep * 4;
                    foreach (string key in GameMain.PerformanceCounter.GetSavedIdentifiers.OrderBy(i => i))
                    {
                        float elapsedMillisecs = GameMain.PerformanceCounter.GetAverageElapsedMillisecs(key);

                        int categoryDepth = key.Count(c => c == ':');
                        //color the more fine-grained counters red more easily (ok for the whole Update to take a longer time than specific part of the update)
                        float runningSlowThreshold = 10.0f / categoryDepth;
                        DrawString(spriteBatch, new Vector2(x + categoryDepth * 15, y),
                            key.Split(':').Last() + ": " + elapsedMillisecs.ToString("0.00"),
                            ToolBox.GradientLerp(elapsedMillisecs / runningSlowThreshold, Color.LightGreen, GUIStyle.Yellow, GUIStyle.Orange, GUIStyle.Red, Color.Magenta), Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                        y += yStep;
                    }
                    if (Powered.Grids != null)
                    {
                        DrawString(spriteBatch, new Vector2(x, y), "Grids: " + Powered.Grids.Count, Color.LightGreen, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                        y += yStep;
                    }
                    if (Settings.EnableDiagnostics)
                    {
                        x += yStep * 2;
                        DrawString(spriteBatch, new Vector2(x, y), "ContinuousPhysicsTime: " + GameMain.World.ContinuousPhysicsTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ContinuousPhysicsTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                        DrawString(spriteBatch, new Vector2(x, y + yStep), "ControllersUpdateTime: " + GameMain.World.ControllersUpdateTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ControllersUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                        DrawString(spriteBatch, new Vector2(x, y + yStep * 2), "AddRemoveTime: " + GameMain.World.AddRemoveTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.AddRemoveTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                        DrawString(spriteBatch, new Vector2(x, y + yStep * 3), "NewContactsTime: " + GameMain.World.NewContactsTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.NewContactsTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                        DrawString(spriteBatch, new Vector2(x, y + yStep * 4), "ContactsUpdateTime: " + GameMain.World.ContactsUpdateTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ContactsUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                        DrawString(spriteBatch, new Vector2(x, y + yStep * 5), "SolveUpdateTime: " + GameMain.World.SolveUpdateTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.SolveUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                    }
                }

                if (GameMain.DebugDraw && !Submarine.Unloading && !(Screen.Selected is RoundSummaryScreen))
                {
                    float y = startY + yStep * 6;

                    if (Screen.Selected.Cam != null)
                    {
                        y += yStep;
                        DrawString(spriteBatch, new Vector2(10, y),
                            "Camera pos: " + Screen.Selected.Cam.Position.ToPoint() + ", zoom: " + Screen.Selected.Cam.Zoom,
                            Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                    }

                    if (Submarine.MainSub != null)
                    {
                        y += yStep;
                        DrawString(spriteBatch, new Vector2(10, y),
                            "Sub pos: " + Submarine.MainSub.WorldPosition.ToPoint(),
                            Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                    }

                    if (loadedSpritesText == null || DateTime.Now > loadedSpritesUpdateTime)
                    {
                        loadedSpritesText = "Loaded sprites: " + Sprite.LoadedSprites.Count() + "\n(" + Sprite.LoadedSprites.Select(s => s.FilePath).Distinct().Count() + " unique textures)";
                        loadedSpritesUpdateTime = DateTime.Now + new TimeSpan(0, 0, seconds: 5);
                    }
                    y += yStep * 2;
                    DrawString(spriteBatch, new Vector2(10, y), loadedSpritesText, Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);

                    if (debugDrawSounds)
                    {
                        float soundTextY = 0;
                        DrawString(spriteBatch, new Vector2(500, soundTextY),
                            "Sounds (Ctrl+S to hide): ", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                        soundTextY += yStep;

                        DrawString(spriteBatch, new Vector2(500, soundTextY),
                            "Current playback amplitude: " + GameMain.SoundManager.PlaybackAmplitude.ToString(), Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);

                        soundTextY += yStep;

                        DrawString(spriteBatch, new Vector2(500, soundTextY),
                            "Compressed dynamic range gain: " + GameMain.SoundManager.CompressionDynamicRangeGain.ToString(), Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);

                        soundTextY += yStep;

                        DrawString(spriteBatch, new Vector2(500, soundTextY),
                            "Loaded sounds: " + GameMain.SoundManager.LoadedSoundCount + " (" + GameMain.SoundManager.UniqueLoadedSoundCount + " unique)", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                        soundTextY += yStep;

                        for (int i = 0; i < SoundManager.SOURCE_COUNT; i++)
                        {
                            Color clr = Color.White;
                            string soundStr = i + ": ";
                            SoundChannel playingSoundChannel = GameMain.SoundManager.GetSoundChannelFromIndex(SoundManager.SourcePoolIndex.Default, i);
                            if (playingSoundChannel == null)
                            {
                                soundStr += "none";
                                clr *= 0.5f;
                            }
                            else
                            {
                                soundStr += Path.GetFileNameWithoutExtension(playingSoundChannel.Sound.Filename);

#if DEBUG
                                if (PlayerInput.GetKeyboardState.IsKeyDown(Keys.G))
                                {
                                    if (PlayerInput.MousePosition.Y >= soundTextY && PlayerInput.MousePosition.Y <= soundTextY + 12)
                                    {
                                        GameMain.SoundManager.DebugSource(i);
                                    }
                                }
#endif

                                if (playingSoundChannel.Looping)
                                {
                                    soundStr += " (looping)";
                                    clr = Color.Yellow;
                                }
                                if (playingSoundChannel.IsStream)
                                {
                                    soundStr += " (streaming)";
                                    clr = Color.Lime;
                                }
                                if (!playingSoundChannel.IsPlaying)
                                {
                                    soundStr += " (stopped)";
                                    clr *= 0.5f;
                                }
                                else if (playingSoundChannel.Muffled)
                                {
                                    soundStr += " (muffled)";
                                    clr = Color.Lerp(clr, Color.LightGray, 0.5f);
                                }
                            }

                            DrawString(spriteBatch, new Vector2(500, soundTextY), soundStr, clr, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                            soundTextY += yStep;
                        }
                    }
                    else
                    {
                        DrawString(spriteBatch, new Vector2(500, 0),
                            "Ctrl+S to show sound debug info", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                    }


                    y += 185 * yScale;
                    if (debugDrawEvents)
                    {
                        DrawString(spriteBatch, new Vector2(10, y),
                            "Ctrl+E to hide EventManager debug info", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                        GameMain.GameSession?.EventManager?.DebugDrawHUD(spriteBatch, y + 15 * yScale);
                    }
                    else
                    {
                        DrawString(spriteBatch, new Vector2(10, y),
                            "Ctrl+E to show EventManager debug info", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                    }

                    if (GameMain.GameSession?.GameMode is CampaignMode campaignMode)
                    {
                        // TODO: TEST THIS
                        if (debugDrawMetaData.Enabled)
                        {
                            string text = "Ctrl+M to hide campaign metadata debug info\n\n" +
                                          $"Ctrl+1 to {(debugDrawMetaData.FactionMetadata ? "hide" : "show")} faction reputations, \n" +
                                          $"Ctrl+2 to {(debugDrawMetaData.UpgradeLevels ? "hide" : "show")} upgrade levels, \n" +
                                          $"Ctrl+3 to {(debugDrawMetaData.UpgradePrices ? "hide" : "show")} upgrade prices";
                            Vector2 textSize = GUIStyle.SmallFont.MeasureString(text);
                            Vector2 pos = new Vector2(GameMain.GraphicsWidth - (textSize.X + 10), 300);
                            DrawString(spriteBatch, pos, text, Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                            pos.Y += textSize.Y + 8;
                            campaignMode.CampaignMetadata?.DebugDraw(spriteBatch, pos, campaignMode, debugDrawMetaData);
                        }
                        else
                        {
                            const string text = "Ctrl+M to show campaign metadata debug info";
                            DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (GUIStyle.SmallFont.MeasureString(text).X + 10), 300),
                                text, Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
                        }
                    }

                    IEnumerable<string> strings;
                    if (MouseOn != null)
                    {
                        RectTransform mouseOnRect = MouseOn.RectTransform;
                        bool isAbsoluteOffsetInUse = mouseOnRect.AbsoluteOffset != Point.Zero || mouseOnRect.RelativeOffset == Vector2.Zero;

                        strings = new string[]
                        {
                            $"Selected UI Element: {MouseOn.GetType().Name} ({ MouseOn.Style?.Element.Name.LocalName ?? "no style" }, {MouseOn.Rect}",
                            $"Relative Offset: {mouseOnRect.RelativeOffset} | Absolute Offset: {(isAbsoluteOffsetInUse ? mouseOnRect.AbsoluteOffset : mouseOnRect.ParentRect.MultiplySize(mouseOnRect.RelativeOffset))}{(isAbsoluteOffsetInUse ? "" : " (Calculated from RelativeOffset)")}",
                            $"Anchor: {mouseOnRect.Anchor} | Pivot: {mouseOnRect.Pivot}"
                        };
                    }
                    else
                    {
                        strings = new string[]
                        {
                            $"GUI.Scale: {Scale}",
                            $"GUI.xScale: {xScale}",
                            $"GUI.yScale: {yScale}",
                            $"RelativeHorizontalAspectRatio: {RelativeHorizontalAspectRatio}",
                            $"RelativeVerticalAspectRatio: {RelativeVerticalAspectRatio}",
                        };
                    }

                    strings = strings.Concat(new string[] { $"Cam.Zoom: {Screen.Selected.Cam?.Zoom ?? 0f}" });

                    int padding = IntScale(10);
                    int yPos = padding;

                    foreach (string str in strings)
                    {
                        Vector2 stringSize = GUIStyle.SmallFont.MeasureString(str);

                        DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)stringSize.X - padding, yPos), str, Color.LightGreen, Color.Black, 0, GUIStyle.SmallFont);
                        yPos += (int)stringSize.Y + padding / 2;
                    }
                }

                GameMain.GameSession?.EventManager?.DrawPinnedEvent(spriteBatch);

                if (HUDLayoutSettings.DebugDraw) { HUDLayoutSettings.Draw(spriteBatch); }

                GameMain.Client?.Draw(spriteBatch);

                if (Character.Controlled?.Inventory != null)
                {
                    if (Character.Controlled.Stun < 0.1f && !Character.Controlled.IsDead)
                    {
                        Inventory.DrawFront(spriteBatch);
                    }
                }

                DrawMessages(spriteBatch, cam);

                if (MouseOn != null && !MouseOn.ToolTip.IsNullOrWhiteSpace())
                {
                    MouseOn.DrawToolTip(spriteBatch);
                }

                if (SubEditorScreen.IsSubEditor())
                {
                    // Draw our "infinite stack" on the cursor
                    switch (SubEditorScreen.DraggedItemPrefab)
                    {
                        case ItemPrefab itemPrefab:
                            {
                                var sprite = itemPrefab.InventoryIcon ?? itemPrefab.Sprite;
                                sprite?.Draw(spriteBatch, PlayerInput.MousePosition, scale: Math.Min(64 / sprite.size.X, 64 / sprite.size.Y) * Scale);
                                break;
                            }
                        case ItemAssemblyPrefab iPrefab:
                            {
                                var (x, y) = PlayerInput.MousePosition;
                                foreach (var pair in iPrefab.DisplayEntities)
                                {
                                    Rectangle dRect = pair.Item2;
                                    dRect = new Rectangle(x: (int)(dRect.X * iPrefab.Scale + x),
                                                          y: (int)(dRect.Y * iPrefab.Scale - y),
                                                          width: (int)(dRect.Width * iPrefab.Scale),
                                                          height: (int)(dRect.Height * iPrefab.Scale));
                                    MapEntityPrefab prefab = MapEntityPrefab.Find("", pair.Item1);
                                    prefab.DrawPlacing(spriteBatch, dRect, prefab.Scale * iPrefab.Scale);
                                }
                                break;
                            }
                    }
                }

                DrawSavingIndicator(spriteBatch);
                DrawCursor(spriteBatch);
                HideCursor = false;
            }
        }

        public static void DrawMessageBoxesOnly(SpriteBatch spriteBatch)
        {
            bool anyDrawn = false;
            foreach (var component in updateList)
            {
                component.DrawAuto(spriteBatch);
                anyDrawn = true;                
            }
            if (anyDrawn)
            {
                DrawCursor(spriteBatch);
            }
        }

        private static void DrawCursor(SpriteBatch spriteBatch)
        {
            if (GameMain.WindowActive && !HideCursor && MouseCursorSprites.Prefabs.Any())
            {
                spriteBatch.End();
                spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerStateClamp, rasterizerState: GameMain.ScissorTestEnable);

                if (GameMain.GameSession?.CrewManager is { DraggedOrderPrefab: { SymbolSprite: { } orderSprite, Color: var color }, DragOrder: true })
                {
                    float spriteSize = Math.Max(orderSprite.size.X, orderSprite.size.Y);
                    orderSprite.Draw(spriteBatch, PlayerInput.LatestMousePosition, color, orderSprite.size / 2f, scale: 32f / spriteSize * Scale);
                }

                var sprite = MouseCursorSprites[MouseCursor] ?? MouseCursorSprites[CursorState.Default];
                sprite.Draw(spriteBatch, PlayerInput.LatestMousePosition, Color.White, sprite.Origin, 0f, Scale / 1.5f);

                spriteBatch.End();
                spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerState, rasterizerState: GameMain.ScissorTestEnable);
            }
        }

        public static void DrawBackgroundSprite(SpriteBatch spriteBatch, Sprite backgroundSprite, Color color, Rectangle? drawArea = null, SpriteEffects spriteEffects = SpriteEffects.None)
        {
            Rectangle area = drawArea ?? new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight);

            float scale = Math.Max(
                (float)area.Width / backgroundSprite.SourceRect.Width,
                (float)area.Height / backgroundSprite.SourceRect.Height) * 1.1f;
            float paddingX = backgroundSprite.SourceRect.Width * scale - area.Width;
            float paddingY = backgroundSprite.SourceRect.Height * scale - area.Height;

            double noiseT = Timing.TotalTime * 0.02f;
            Vector2 pos = new Vector2((float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0) - 0.5f, (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0.5f) - 0.5f);
            pos = new Vector2(pos.X * paddingX, pos.Y * paddingY);

            spriteBatch.Draw(backgroundSprite.Texture,
                area.Center.ToVector2() + pos,
                null, color, 0.0f, backgroundSprite.size / 2,
                scale, spriteEffects, 0.0f);
        }

        #region Update list
        private static readonly List<GUIComponent> updateList = new List<GUIComponent>();
        //essentially a copy of the update list, used as an optimization to quickly check if the component is present in the update list
        private static readonly HashSet<GUIComponent> updateListSet = new HashSet<GUIComponent>();
        private static readonly Queue<GUIComponent> removals = new Queue<GUIComponent>();
        private static readonly Queue<GUIComponent> additions = new Queue<GUIComponent>();
        // A helpers list for all elements that have a draw order less than 0.
        private static readonly List<GUIComponent> first = new List<GUIComponent>();
        // A helper list for all elements that have a draw order greater than 0.
        private static readonly List<GUIComponent> last = new List<GUIComponent>();

        /// <summary>
        /// Adds the component on the addition queue.
        /// Note: does not automatically add children, because we might want to enforce a custom order for them.
        /// </summary>
        public static void AddToUpdateList(GUIComponent component)
        {
            lock (mutex)
            {
                if (component == null)
                {
                    DebugConsole.ThrowError("Trying to add a null component on the GUI update list!");
                    return;
                }
                if (!component.Visible) { return; }
                if (component.UpdateOrder < 0)
                {
                    first.Add(component);
                }
                else if (component.UpdateOrder > 0)
                {
                    last.Add(component);
                }
                else
                {
                    additions.Enqueue(component);
                }
            }
        }

        /// <summary>
        /// Adds the component on the removal queue.
        /// Removal list is evaluated last, and thus any item on both lists are not added to update list.
        /// </summary>
        public static void RemoveFromUpdateList(GUIComponent component, bool alsoChildren = true)
        {
            lock (mutex)
            {
                if (updateListSet.Contains(component))
                {
                    removals.Enqueue(component);
                }
                if (alsoChildren)
                {
                    if (component.RectTransform != null)
                    {
                        component.RectTransform.Children.ForEach(c => RemoveFromUpdateList(c.GUIComponent));
                    }
                    else
                    {
                        component.Children.ForEach(c => RemoveFromUpdateList(c));
                    }
                }
            }
        }

        public static void ClearUpdateList()
        {
            lock (mutex)
            {
                if (KeyboardDispatcher.Subscriber is GUIComponent && !updateList.Contains(KeyboardDispatcher.Subscriber as GUIComponent))
                {
                    KeyboardDispatcher.Subscriber = null;
                }
                updateList.Clear();
                updateListSet.Clear();
            }
        }

        private static void RefreshUpdateList()
        {
            lock (mutex)
            {
                foreach (var component in updateList)
                {
                    if (!component.Visible)
                    {
                        RemoveFromUpdateList(component);
                    }
                }
                ProcessHelperList(first);
                ProcessAdditions();
                ProcessHelperList(last);
                ProcessRemovals();
            }
        }

        private static void ProcessAdditions()
        {
            lock (mutex)
            {
                while (additions.Count > 0)
                {
                    var component = additions.Dequeue();
                    if (!updateListSet.Contains(component))
                    {
                        updateList.Add(component);
                        updateListSet.Add(component);
                    }
                }
            }
        }

        private static void ProcessRemovals()
        {
            lock (mutex)
            {
                while (removals.Count > 0)
                {
                    var component = removals.Dequeue();
                    updateList.Remove(component);
                    updateListSet.Remove(component);
                    if (component as IKeyboardSubscriber == KeyboardDispatcher.Subscriber)
                    {
                        KeyboardDispatcher.Subscriber = null;
                    }
                }
            }
        }

        private static void ProcessHelperList(List<GUIComponent> list)
        {
            lock (mutex)
            {
                if (list.Count == 0) { return; }
                foreach (var item in list)
                {
                    int index = 0;
                    if (updateList.Count > 0)
                    {
                        index = updateList.Count;
                        while (index > 0 && updateList[index-1].UpdateOrder > item.UpdateOrder)
                        {
                            index--;
                        }
                    }
                    if (!updateListSet.Contains(item))
                    {
                        updateList.Insert(index, item);
                        updateListSet.Add(item);
                    }
                }
                list.Clear();
            }
        }

        private static void HandlePersistingElements(float deltaTime)
        {
            lock (mutex)
            {
                GUIMessageBox.AddActiveToGUIUpdateList();
                GUIContextMenu.AddActiveToGUIUpdateList();

                if (PauseMenuOpen)
                {
                    PauseMenu.AddToGUIUpdateList();
                }
                if (SettingsMenuOpen)
                {
                    SettingsMenuContainer.AddToGUIUpdateList();
                }

                //the "are you sure you want to quit" prompts are drawn on top of everything else
                if (GUIMessageBox.VisibleBox?.UserData as string == "verificationprompt" || GUIMessageBox.VisibleBox?.UserData as string == "bugreporter")
                {
                    GUIMessageBox.VisibleBox.AddToGUIUpdateList();
                }
            }
        }

        public static IEnumerable<GUIComponent> GetAdditions()
        {
            return additions;
        }
        #endregion

        public static GUIComponent MouseOn { get; private set; }

        public static bool IsMouseOn(GUIComponent target)
        {
            lock (mutex)
            {
                if (target == null) { return false; }
                //if (MouseOn == null) { return true; }
                return target == MouseOn || target.IsParentOf(MouseOn);
            }
        }

        public static void ForceMouseOn(GUIComponent c)
        {
            lock (mutex)
            {
                MouseOn = c;
            }
        }

        /// <summary>
        /// Updated automatically before updating the elements on the update list.
        /// </summary>
        public static GUIComponent UpdateMouseOn()
        {
            lock (mutex)
            {
                GUIComponent prevMouseOn = MouseOn;
                MouseOn = null;
                int inventoryIndex = -1;

                Inventory.RefreshMouseOnInventory();
                if (Inventory.IsMouseOnInventory)
                {
                    inventoryIndex = updateList.IndexOf(CharacterHUD.HUDFrame);
                }

                if ((!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) ||
                    (prevMouseOn == null && !PlayerInput.SecondaryMouseButtonHeld() && !Inventory.DraggingItems.Any()))
                {
                    for (var i = updateList.Count - 1; i > inventoryIndex; i--)
                    {
                        var c = updateList[i];
                        if (!c.CanBeFocused) { continue; }
                        if (c.MouseRect.Contains(PlayerInput.MousePosition))
                        {
                            if ((!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) || c == prevMouseOn || prevMouseOn == null)
                            {
                                MouseOn = c;
                            }
                            break;
                        }
                    }
                }
                else
                {
                    MouseOn = prevMouseOn;
                }

                MouseCursor = UpdateMouseCursorState(MouseOn);
                return MouseOn;
            }
        }

        private static CursorState UpdateMouseCursorState(GUIComponent c)
        {
            lock (mutex)
            {
                // Waiting and drag cursor override everything else
                if (MouseCursor == CursorState.Waiting) { return CursorState.Waiting; }
                if (GUIScrollBar.DraggingBar != null) { return GUIScrollBar.DraggingBar.Bar.HoverCursor; }

                if (SubEditorScreen.IsSubEditor() && SubEditorScreen.DraggedItemPrefab != null) { return CursorState.Hand; }

                // Wire cursors
                if (Character.Controlled != null)
                {
                    if (Character.Controlled.SelectedItem?.GetComponent<ConnectionPanel>() != null)
                    {
                        if (Connection.DraggingConnected != null)
                        {
                            return CursorState.Dragging;
                        }
                        else if (ConnectionPanel.HighlightedWire != null)
                        {
                            return CursorState.Hand;
                        }
                    }
                    if (Wire.DraggingWire != null) { return CursorState.Dragging; }
                }

                if (c == null || c is GUICustomComponent)
                {
                    switch (Screen.Selected)
                    {
                        // Character editor limbs
                        case CharacterEditorScreen editor:
                            return editor.GetMouseCursorState();
                        // Portrait area during gameplay
                        case GameScreen _ when !(Character.Controlled?.ShouldLockHud() ?? true):
                            if (CharacterHUD.MouseOnCharacterPortrait() || CharacterHealth.IsMouseOnHealthBar())
                            {
                                return CursorState.Hand;
                            }
                            break;
                        // Sub editor drag and highlight
                        case SubEditorScreen editor:
                        {
                            foreach (var mapEntity in MapEntity.mapEntityList)
                            {
                                if (MapEntity.StartMovingPos != Vector2.Zero)
                                {
                                    return CursorState.Dragging;
                                }
                                if (mapEntity.IsHighlighted)
                                {
                                    return CursorState.Hand;
                                }
                            }
                            break;
                        }
                    }
                }

                if (c != null && c.Visible)
                {
                    if (c.AlwaysOverrideCursor) { return c.HoverCursor; }

                    // When a button opens a submenu, it increases to the size of the entire screen.
                    // And this is of course picked up as clickable area.
                    // There has to be a better way of checking this but for now this works.
                    var monitorRect = new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight);

                    var parent = FindInteractParent(c);

                    if (c.Enabled)
                    {
                        var dragHandle = c as GUIDragHandle ?? parent as GUIDragHandle;
                        if (dragHandle != null)
                        {
                            return dragHandle.Dragging ? CursorState.Dragging : CursorState.Hand;
                        }
                        // Some parent elements take priority
                        // but not when the child is a GUIButton or GUITickBox
                        if (!(parent is GUIButton) && !(parent is GUIListBox) ||
                                  (c is GUIButton) || (c is GUITickBox))
                        {
                            if (!c.Rect.Equals(monitorRect)) { return c.HoverCursor; }
                        }
                    }


                    // Children in list boxes can be interacted with despite not having
                    // a GUIButton inside of them so instead of hard coding we check if
                    // the children can be interacted with by checking their hover state
                    if (parent is GUIListBox listBox && c.Parent == listBox.Content)
                    {
                        if (listBox.DraggedElement != null) { return CursorState.Dragging; }
                        if (listBox.CurrentDragMode != GUIListBox.DragMode.NoDragging) { return CursorState.Move; }

                        if (listBox.HoverCursor != CursorState.Default)
                        {
                            var hoverParent = c;
                            while (true)
                            {
                                if (hoverParent == parent || hoverParent == null) { break; }
                                if (hoverParent.State == GUIComponent.ComponentState.Hover) { return CursorState.Hand; }
                                hoverParent = hoverParent.Parent;
                            }
                        }
                    }

                    if (parent != null && parent.CanBeFocused)
                    {
                        if (!parent.Rect.Equals(monitorRect)) { return parent.HoverCursor; }
                    }
                }

                if (Inventory.IsMouseOnInventory) { return Inventory.GetInventoryMouseCursor(); }

                var character = Character.Controlled;
                // ReSharper disable once InvertIf
                if (character != null)
                {
                    // Health menus
                    if (character.CharacterHealth.MouseOnElement) { return CursorState.Hand; }

                    if (character.SelectedCharacter != null)
                    {
                        if (character.SelectedCharacter.CharacterHealth.MouseOnElement)
                        {
                            return CursorState.Hand;
                        }
                    }

                    // Character is hovering over an item placed in the world
                    if (character.FocusedItem != null) { return CursorState.Hand; }
                }

                return CursorState.Default;

                static GUIComponent FindInteractParent(GUIComponent component)
                {
                    while (true)
                    {
                        var parent = component.Parent;
                        if (parent == null) { return null; }

                        if (ContainsMouse(parent))
                        {
                            if (parent.Enabled)
                            {
                                switch (parent)
                                {
                                    case GUIButton button:
                                        return button;
                                    case GUITextBox box:
                                        return box;
                                    case GUIListBox list:
                                        return list;
                                    case GUIScrollBar bar:
                                        return bar;
                                    case GUIDragHandle dragHandle:
                                        return dragHandle;
                                }
                            }
                            component = parent;
                        }
                        else
                        {
                            return null;
                        }
                    }
                }

                static bool ContainsMouse(GUIComponent component)
                {
                    // If component has a mouse rectangle then use that, if not use it's physical rect
                    return !component.MouseRect.Equals(Rectangle.Empty) ?
                        component.MouseRect.Contains(PlayerInput.MousePosition) :
                        component.Rect.Contains(PlayerInput.MousePosition);
                }
            }
        }

        /// <summary>
        /// Set the cursor to an hourglass.
        /// Will automatically revert after 10 seconds or when <see cref="ClearCursorWait"/> is called.
        /// </summary>
        public static void SetCursorWaiting(int waitSeconds = 10, Func<bool> endCondition = null)
        {
            CoroutineManager.StartCoroutine(WaitCursorCoroutine(), "WaitCursorTimeout");

            IEnumerable<CoroutineStatus> WaitCursorCoroutine()
            {
                MouseCursor = CursorState.Waiting;
                var timeOut = DateTime.Now + new TimeSpan(0, 0, waitSeconds);
                while (DateTime.Now < timeOut)
                {
                    if (endCondition != null)
                    {
                        try
                        {
                            if (endCondition.Invoke()) { break; }
                        }
                        catch { break; }
                    }
                    yield return CoroutineStatus.Running;
                }
                if (MouseCursor == CursorState.Waiting) { MouseCursor = CursorState.Default; }
                yield return CoroutineStatus.Success;
            }
        }

        public static void ClearCursorWait()
        {
            lock (mutex)
            {
                CoroutineManager.StopCoroutines("WaitCursorTimeout");
                MouseCursor = CursorState.Default;
            }
        }

        public static bool HasSizeChanged(Point referenceResolution, float referenceUIScale, float referenceHUDScale)
        {
            return GameMain.GraphicsWidth != referenceResolution.X || GameMain.GraphicsHeight != referenceResolution.Y ||
                   referenceUIScale != Inventory.UIScale || referenceHUDScale != Scale;
        }

        public static void Update(float deltaTime)
        {
            lock (mutex)
            {
                if (PlayerInput.KeyDown(Keys.LeftControl) && PlayerInput.KeyHit(Keys.S))
                {
                    debugDrawSounds = !debugDrawSounds;
                }
                if (PlayerInput.KeyDown(Keys.LeftControl) && PlayerInput.KeyHit(Keys.E))
                {
                    debugDrawEvents = !debugDrawEvents;
                }
                if (PlayerInput.IsCtrlDown() && PlayerInput.KeyHit(Keys.M))
                {
                    debugDrawMetaData.Enabled = !debugDrawMetaData.Enabled;
                }

                if (debugDrawMetaData.Enabled)
                {
                    if (PlayerInput.KeyHit(Keys.Up))
                    {
                        debugDrawMetaData.Offset--;
                    }
                    if (PlayerInput.KeyHit(Keys.Down))
                    {
                        debugDrawMetaData.Offset++;
                    }
                    if (PlayerInput.IsCtrlDown())
                    {
                        if (PlayerInput.KeyHit(Keys.D1))
                        {
                            debugDrawMetaData.FactionMetadata = !debugDrawMetaData.FactionMetadata;
                            debugDrawMetaData.Offset = 0;
                        }
                        if (PlayerInput.KeyHit(Keys.D2))
                        {
                            debugDrawMetaData.UpgradeLevels = !debugDrawMetaData.UpgradeLevels;
                            debugDrawMetaData.Offset = 0;
                        }
                        if (PlayerInput.KeyHit(Keys.D3))
                        {
                            debugDrawMetaData.UpgradePrices = !debugDrawMetaData.UpgradePrices;
                            debugDrawMetaData.Offset = 0;
                        }
                    }
                }

                HandlePersistingElements(deltaTime);
                RefreshUpdateList();
                UpdateMouseOn();
                Debug.Assert(updateList.Count == updateListSet.Count);
                foreach (var c in updateList)
                {
                    c.UpdateAuto(deltaTime);
                }
                UpdateMessages(deltaTime);
                UpdateSavingIndicator(deltaTime);
            }
        }

        public static void UpdateGUIMessageBoxesOnly(float deltaTime)
        {
            GUIMessageBox.AddActiveToGUIUpdateList();
            RefreshUpdateList();
            UpdateMouseOn(); 
            foreach (var c in updateList)
            {
                c.UpdateAuto(deltaTime);
            }
        }

        private static void UpdateMessages(float deltaTime)
        {
            lock (mutex)
            {
                foreach (GUIMessage msg in messages)
                {
                    if (msg.WorldSpace) { continue; }
                    msg.Timer -= deltaTime;

                    if (msg.Size.X > HUDLayoutSettings.MessageAreaTop.Width)
                    {
                        msg.Pos = Vector2.Lerp(Vector2.Zero, new Vector2(-HUDLayoutSettings.MessageAreaTop.Width - msg.Size.X, 0), 1.0f - msg.Timer / msg.LifeTime);
                    }
                    else
                    {
                        //enough space to show the full message, position it at the center of the msg area
                        if (msg.Timer > 1.0f)
                        {
                            msg.Pos = Vector2.Lerp(msg.Pos, new Vector2(-HUDLayoutSettings.MessageAreaTop.Width / 2 - msg.Size.X / 2, 0), Math.Min(deltaTime * 10.0f, 1.0f));
                        }
                        else
                        {
                            msg.Pos = Vector2.Lerp(msg.Pos, new Vector2(-HUDLayoutSettings.MessageAreaTop.Width - msg.Size.X, 0), deltaTime * 10.0f);
                        }
                    }
                    //only the first message (the currently visible one) is updated at a time
                    break;
                }

                foreach (GUIMessage msg in messages)
                {
                    if (!msg.WorldSpace) { continue; }
                    msg.Timer -= deltaTime;
                    msg.Pos += msg.Velocity * deltaTime;
                }

                messages.RemoveAll(m => m.Timer <= 0.0f);
            }
        }

        private static void UpdateSavingIndicator(float deltaTime)
        {
            if (GUIStyle.SavingIndicator == null) { return; }
            lock (mutex)
            {
                if (timeUntilSavingIndicatorDisabled.HasValue)
                {
                    timeUntilSavingIndicatorDisabled -= deltaTime;
                    if (timeUntilSavingIndicatorDisabled <= 0.0f)
                    {
                        isSavingIndicatorEnabled = false;
                        timeUntilSavingIndicatorDisabled = null;
                    }
                }
                if (isSavingIndicatorEnabled)
                {
                    if (savingIndicatorColor == Color.Transparent)
                    {
                        savingIndicatorState = SavingIndicatorState.FadingIn;
                        savingIndicatorColorLerpAmount = 0.0f;
                    }
                    else if (savingIndicatorColor == Color.White)
                    {
                        savingIndicatorState = SavingIndicatorState.None;
                    }
                }
                else
                {
                    if (savingIndicatorColor == Color.White)
                    {
                        savingIndicatorState = SavingIndicatorState.FadingOut;
                        savingIndicatorColorLerpAmount = 0.0f;
                    }
                    else if (savingIndicatorColor == Color.Transparent)
                    {
                        savingIndicatorState = SavingIndicatorState.None;
                    }
                }
                if (savingIndicatorState != SavingIndicatorState.None)
                {
                    bool isFadingIn = savingIndicatorState == SavingIndicatorState.FadingIn;
                    Color lerpStartColor = isFadingIn ? Color.Transparent : Color.White;
                    Color lerpTargetColor = isFadingIn ? Color.White : Color.Transparent;
                    savingIndicatorColorLerpAmount += (isFadingIn ? 2.0f : 0.5f) * deltaTime;
                    savingIndicatorColor = Color.Lerp(lerpStartColor, lerpTargetColor, savingIndicatorColorLerpAmount);
                }
                if (IsSavingIndicatorVisible)
                {
                    savingIndicatorSpriteIndex = (savingIndicatorSpriteIndex + 15.0f * deltaTime) % (GUIStyle.SavingIndicator.FrameCount + 1);
                }
            }
        }

#region Element drawing

        private static readonly List<float> usedIndicatorAngles = new List<float>();

        /// <param name="createOffset">Should the indicator move based on the camera position?</param>
        /// <param name="overrideAlpha">Override the distance-based alpha value with the specified alpha value</param>
        public static void DrawIndicator(SpriteBatch spriteBatch, in Vector2 worldPosition, Camera cam, in Range<float> visibleRange, Sprite sprite, in Color color,
            bool createOffset = true, float scaleMultiplier = 1.0f, float? overrideAlpha = null, LocalizedString label = null)
        {
            Vector2 diff = worldPosition - cam.WorldViewCenter;
            float dist = diff.Length();

            float symbolScale = Math.Min(64.0f / sprite.size.X, 1.0f) * scaleMultiplier * Scale;

            if (overrideAlpha.HasValue || visibleRange.Contains(dist))
            {
                float alpha = overrideAlpha ?? MathUtils.Min((dist - visibleRange.Start) / 100.0f, 1.0f - ((dist - visibleRange.End + 100f) / 100.0f), 1.0f);
                Vector2 targetScreenPos = cam.WorldToScreen(worldPosition);

                if (!createOffset)
                {
                    sprite.Draw(spriteBatch, targetScreenPos, color * alpha, rotate: 0.0f, scale: symbolScale);
                    return;
                }

                float screenDist = Vector2.Distance(cam.WorldToScreen(cam.WorldViewCenter), targetScreenPos);
                float angle = MathUtils.VectorToAngle(diff);
                float originalAngle = angle;

                const float minAngleDiff = 0.05f;
                bool overlapFound = true;
                int iterations = 0;
                while (overlapFound && iterations < 10)
                {
                    overlapFound = false;
                    foreach (float usedIndicatorAngle in usedIndicatorAngles)
                    {
                        float shortestAngle = MathUtils.GetShortestAngle(angle, usedIndicatorAngle);
                        if (MathUtils.NearlyEqual(shortestAngle, 0.0f)) { shortestAngle = 0.01f; }
                        if (Math.Abs(shortestAngle) < minAngleDiff)
                        {
                            angle -= Math.Sign(shortestAngle) * (minAngleDiff - Math.Abs(shortestAngle));
                            overlapFound = true;
                            break;
                        }
                    }
                    iterations++;
                }

                usedIndicatorAngles.Add(angle);

                Vector2 iconDiff = new Vector2(
                    (float)Math.Cos(angle) * Math.Min(GameMain.GraphicsWidth * 0.4f, screenDist + 10),
                    (float)-Math.Sin(angle) * Math.Min(GameMain.GraphicsHeight * 0.4f, screenDist + 10));

                angle = MathHelper.Lerp(originalAngle, angle, MathHelper.Clamp(((screenDist + 10f) - iconDiff.Length()) / 10f, 0f, 1f));

                iconDiff = new Vector2(
                    (float)Math.Cos(angle) * Math.Min(GameMain.GraphicsWidth * 0.4f, screenDist),
                    (float)-Math.Sin(angle) * Math.Min(GameMain.GraphicsHeight * 0.4f, screenDist));

                Vector2 iconPos = cam.WorldToScreen(cam.WorldViewCenter) + iconDiff;
                sprite.Draw(spriteBatch, iconPos, color * alpha, rotate: 0.0f, scale: symbolScale);

                if (label != null)
                {
                    float cursorDist = Vector2.Distance(PlayerInput.MousePosition, iconPos);
                    if (cursorDist < sprite.size.X * symbolScale)
                    {
                        Vector2 textSize = GUIStyle.Font.MeasureString(label);
                        Vector2 textPos = iconPos + new Vector2(sprite.size.X * symbolScale * 0.7f * Math.Sign(-iconDiff.X), -textSize.Y / 2);
                        if (iconDiff.X > 0) { textPos.X -= textSize.X; }
                        DrawString(spriteBatch, textPos + Vector2.One, label, Color.Black);
                        DrawString(spriteBatch, textPos, label,   color);
                    }
                }

                if (screenDist - 10 > iconDiff.Length())
                {
                    Vector2 normalizedDiff = Vector2.Normalize(targetScreenPos - iconPos);
                    Vector2 arrowOffset = normalizedDiff * sprite.size.X * symbolScale * 0.7f;
                    Arrow.Draw(spriteBatch, iconPos + arrowOffset, color * alpha, MathUtils.VectorToAngle(arrowOffset) + MathHelper.PiOver2, scale: 0.5f);
                }
            }
        }

        public static void DrawIndicator(SpriteBatch spriteBatch, Vector2 worldPosition, Camera cam, float hideDist, Sprite sprite, Color color,
            bool createOffset = true, float scaleMultiplier = 1.0f, float? overrideAlpha = null)
        {
            DrawIndicator(spriteBatch, worldPosition, cam, new Range<float>(hideDist, float.PositiveInfinity), sprite, color, createOffset, scaleMultiplier, overrideAlpha);
        }

        public static void DrawLine(SpriteBatch sb, Vector2 start, Vector2 end, Color clr, float depth = 0.0f, float width = 1)
        {
            DrawLine(sb, solidWhiteTexture, start, end, clr, depth, (int)width);
        }

        public static void DrawLine(SpriteBatch sb, Sprite sprite, Vector2 start, Vector2 end, Color clr, float depth = 0.0f, int width = 1)
        {
            Vector2 edge = end - start;
            // calculate angle to rotate line
            float angle = (float)Math.Atan2(edge.Y, edge.X);

            sb.Draw(sprite.Texture,
                new Rectangle(// rectangle defines shape of line and position of start of line
                    (int)start.X,
                    (int)start.Y,
                    (int)edge.Length(), //sb will strech the texture to fill this rectangle
                    width), //width of line, change this to make thicker line
                sprite.SourceRect,
                clr, //colour of line
                angle,     //angle of line (calulated above)
                new Vector2(0, sprite.SourceRect.Height / 2), // point in line about which to rotate
                SpriteEffects.None,
                depth);
        }

        public static void DrawLine(SpriteBatch sb, Texture2D texture, Vector2 start, Vector2 end, Color clr, float depth = 0.0f, int width = 1)
        {
            Vector2 edge = end - start;
            // calculate angle to rotate line
            float angle = (float)Math.Atan2(edge.Y, edge.X);

            sb.Draw(texture,
                new Rectangle(// rectangle defines shape of line and position of start of line
                    (int)start.X,
                    (int)start.Y,
                    (int)edge.Length(), //sb will strech the texture to fill this rectangle
                    width), //width of line, change this to make thicker line
                null,
                clr, //colour of line
                angle,     //angle of line (calulated above)
                new Vector2(0, texture.Height / 2.0f), // point in line about which to rotate
                SpriteEffects.None,
                depth);
        }

        public static void DrawString(SpriteBatch sb, Vector2 pos, LocalizedString text, Color color, Color? backgroundColor = null, int backgroundPadding = 0, GUIFont font = null, ForceUpperCase forceUpperCase = ForceUpperCase.Inherit)
        {
            DrawString(sb, pos, text.Value, color, backgroundColor, backgroundPadding, font, forceUpperCase);
        }

        public static void DrawString(SpriteBatch sb, Vector2 pos, string text, Color color, Color? backgroundColor = null, int backgroundPadding = 0, GUIFont font = null, ForceUpperCase forceUpperCase = ForceUpperCase.Inherit)
        {
            if (color.A == 0) { return; }
            if (font == null) { font = GUIStyle.Font; }
            if (backgroundColor != null && backgroundColor.Value.A > 0)
            {
                Vector2 textSize = font.MeasureString(text);
                DrawRectangle(sb, pos - Vector2.One * backgroundPadding, textSize + Vector2.One * 2.0f * backgroundPadding, (Color)backgroundColor, true);
            }

            font.DrawString(sb, text, pos, color, forceUpperCase: forceUpperCase);
        }

        public static void DrawStringWithColors(SpriteBatch sb, Vector2 pos, string text, Color color, in ImmutableArray<RichTextData>? richTextData, Color? backgroundColor = null, int backgroundPadding = 0, GUIFont font = null, float depth = 0.0f)
        {
            if (font == null) font = GUIStyle.Font;
            if (backgroundColor != null)
            {
                Vector2 textSize = font.MeasureString(text);
                DrawRectangle(sb, pos - Vector2.One * backgroundPadding, textSize + Vector2.One * 2.0f * backgroundPadding, (Color)backgroundColor, true, depth, 5);
            }

            font.DrawStringWithColors(sb, text, pos, color, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, depth, richTextData);
        }

        private const int DonutSegments = 30;
        private static readonly ImmutableArray<Vector2> canonicalCircle
            = Enumerable.Range(0, DonutSegments)
                .Select(i => i * (2.0f * MathF.PI / DonutSegments))
                .Select(angle => new Vector2(MathF.Cos(angle), MathF.Sin(angle)))
                .ToImmutableArray();
        private static readonly VertexPositionColorTexture[] donutVerts = new VertexPositionColorTexture[DonutSegments * 4];

        public static void DrawDonutSection(
            SpriteBatch sb, Vector2 center, Range<float> radii, float sectionRad, Color clr, float depth = 0.0f)
        {
            float getRadius(int vertexIndex)
                => (vertexIndex % 4) switch
                {
                    0 => radii.End,
                    1 => radii.End,
                    2 => radii.Start,
                    3 => radii.Start,
                    _ => throw new InvalidOperationException()
                };
            static int getDirectionIndex(int vertexIndex)
                => (vertexIndex % 4) switch
                {
                    0 => (vertexIndex / 4) + 0,
                    1 => (vertexIndex / 4) + 1,
                    2 => (vertexIndex / 4) + 0,
                    3 => (vertexIndex / 4) + 1,
                    _ => throw new InvalidOperationException()
                };

            float sectionProportion = sectionRad / (MathF.PI * 2.0f);
            int maxDirectionIndex = Math.Min(DonutSegments, (int)MathF.Ceiling(sectionProportion * DonutSegments));

            Vector2 getDirection(int vertexIndex)
            {
                int directionIndex = getDirectionIndex(vertexIndex);
                Vector2 dir = canonicalCircle[directionIndex % DonutSegments];
                if (maxDirectionIndex > 0 && directionIndex >= maxDirectionIndex)
                {
                    float maxSectionProportion = (float)maxDirectionIndex / DonutSegments;
                    dir = Vector2.Lerp(
                        canonicalCircle[maxDirectionIndex - 1],
                        canonicalCircle[maxDirectionIndex % DonutSegments],
                        1.0f - (maxSectionProportion - sectionProportion) * DonutSegments);
                }

                return new Vector2(dir.Y, -dir.X);
            }

            for (int vertexIndex = 0; vertexIndex < maxDirectionIndex * 4; vertexIndex++)
            {
                donutVerts[vertexIndex].Color = clr;
                donutVerts[vertexIndex].Position = new Vector3(center + getDirection(vertexIndex) * getRadius(vertexIndex), 0.0f);
            }
            sb.Draw(solidWhiteTexture, donutVerts, depth, count: maxDirectionIndex);
        }
        
        public static void DrawRectangle(SpriteBatch sb, Vector2 start, Vector2 size, Color clr, bool isFilled = false, float depth = 0.0f, float thickness = 1)
        {
            if (size.X < 0)
            {
                start.X += size.X;
                size.X = -size.X;
            }
            if (size.Y < 0)
            {
                start.Y += size.Y;
                size.Y = -size.Y;
            }
            DrawRectangle(sb, new Rectangle((int)start.X, (int)start.Y, (int)size.X, (int)size.Y), clr, isFilled, depth, thickness);
        }

        public static void DrawRectangle(SpriteBatch sb, Rectangle rect, Color clr, bool isFilled = false, float depth = 0.0f, float thickness = 1)
        {
            if (isFilled)
            {
                sb.Draw(solidWhiteTexture, rect, null, clr, 0.0f, Vector2.Zero, SpriteEffects.None, depth);
            }
            else
            {
                Rectangle srcRect = new Rectangle(0, 0, 1, 1);
                sb.Draw(solidWhiteTexture, new Vector2(rect.X, rect.Y), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(thickness, rect.Height), SpriteEffects.None, depth);
                sb.Draw(solidWhiteTexture, new Vector2(rect.X + thickness, rect.Y), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(rect.Width - thickness, thickness), SpriteEffects.None, depth);
                sb.Draw(solidWhiteTexture, new Vector2(rect.X + thickness, rect.Bottom - thickness), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(rect.Width - thickness, thickness), SpriteEffects.None, depth);
                sb.Draw(solidWhiteTexture, new Vector2(rect.Right - thickness, rect.Y + thickness), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(thickness, rect.Height - thickness * 2f), SpriteEffects.None, depth);
            }
        }

        public static void DrawRectangle(SpriteBatch sb, Vector2 position, Vector2 size, Vector2 origin, float rotation, Color clr, float depth = 0.0f, float thickness = 1, OutlinePosition outlinePos = OutlinePosition.Centered)
        {
            Vector2 topLeft = new Vector2(-origin.X, -origin.Y);
            Vector2 topRight = new Vector2(-origin.X + size.X, -origin.Y);
            Vector2 bottomLeft = new Vector2(-origin.X, -origin.Y + size.Y);
            Vector2 actualSize = size;

            switch(outlinePos)
            {
                case OutlinePosition.Default:
                    actualSize += new Vector2(thickness);
                    break;
                case OutlinePosition.Centered:
                    topLeft -= new Vector2(thickness * 0.5f);
                    topRight -= new Vector2(thickness * 0.5f);
                    bottomLeft -= new Vector2(thickness * 0.5f);
                    actualSize += new Vector2(thickness);
                    break;
                case OutlinePosition.Inside:
                    topRight -= new Vector2(thickness, 0.0f);
                    bottomLeft -= new Vector2(0.0f, thickness);
                    break;
                case OutlinePosition.Outside:
                    topLeft -= new Vector2(thickness);
                    topRight -= new Vector2(0.0f, thickness);
                    bottomLeft -= new Vector2(thickness, 0.0f);
                    actualSize += new Vector2(thickness * 2.0f);
                    break;
            }

            Matrix rotate = Matrix.CreateRotationZ(rotation);
            topLeft = Vector2.Transform(topLeft, rotate) + position;
            topRight = Vector2.Transform(topRight, rotate) + position;
            bottomLeft = Vector2.Transform(bottomLeft, rotate) + position;

            Rectangle srcRect = new Rectangle(0, 0, 1, 1);
            sb.Draw(solidWhiteTexture, topLeft, srcRect, clr, rotation, Vector2.Zero, new Vector2(thickness, actualSize.Y), SpriteEffects.None, depth);
            sb.Draw(solidWhiteTexture, topLeft, srcRect, clr, rotation, Vector2.Zero, new Vector2(actualSize.X, thickness), SpriteEffects.None, depth);
            sb.Draw(solidWhiteTexture, topRight, srcRect, clr, rotation, Vector2.Zero, new Vector2(thickness, actualSize.Y), SpriteEffects.None, depth);
            sb.Draw(solidWhiteTexture, bottomLeft, srcRect, clr, rotation, Vector2.Zero, new Vector2(actualSize.X, thickness), SpriteEffects.None, depth);
        }

        public static void DrawFilledRectangle(SpriteBatch sb, Vector2 position, Vector2 size, Vector2 pivot, float rotation, Color clr, float depth = 0.0f)
        {
            Rectangle srcRect = new Rectangle(0, 0, 1, 1);
            sb.Draw(solidWhiteTexture, position, srcRect, clr, rotation, (pivot/size), size, SpriteEffects.None, depth);
        }

        public static void DrawFilledRectangle(SpriteBatch sb, RectangleF rect, Color clr, float depth = 0.0f)
        {
            DrawFilledRectangle(sb, rect.Location, rect.Size, clr, depth);
        }

        public static void DrawFilledRectangle(SpriteBatch sb, Vector2 start, Vector2 size, Color clr, float depth = 0.0f)
        {
            if (size.X < 0)
            {
                start.X += size.X;
                size.X = -size.X;
            }
            if (size.Y < 0)
            {
                start.Y += size.Y;
                size.Y = -size.Y;
            }

            sb.Draw(solidWhiteTexture, start, null, clr, 0f, Vector2.Zero, size, SpriteEffects.None, depth);
        }

        public static void DrawRectangle(SpriteBatch sb, Vector2 center, float width, float height, float rotation, Color clr, float depth = 0.0f, float thickness = 1)
        {
            Matrix rotate = Matrix.CreateRotationZ(rotation);

            width *= 0.5f;
            height *= 0.5f;
            Vector2 topLeft = center + Vector2.Transform(new Vector2(-width, -height), rotate);
            Vector2 topRight = center + Vector2.Transform(new Vector2(width, -height), rotate);
            Vector2 bottomLeft = center + Vector2.Transform(new Vector2(-width, height), rotate);
            Vector2 bottomRight = center + Vector2.Transform(new Vector2(width, height), rotate);

            DrawLine(sb, topLeft, topRight, clr, depth, thickness);
            DrawLine(sb, topRight, bottomRight, clr, depth, thickness);
            DrawLine(sb, bottomRight, bottomLeft, clr, depth, thickness);
            DrawLine(sb, bottomLeft, topLeft, clr, depth, thickness);
        }

        public static void DrawRectangle(SpriteBatch sb, Vector2[] corners, Color clr, float depth = 0.0f, float thickness = 1)
        {
            if (corners.Length != 4)
            {
                throw new Exception("Invalid length of the corners array! Must be 4");
            }
            DrawLine(sb, corners[0], corners[1], clr, depth, thickness);
            DrawLine(sb, corners[1], corners[2], clr, depth, thickness);
            DrawLine(sb, corners[2], corners[3], clr, depth, thickness);
            DrawLine(sb, corners[3], corners[0], clr, depth, thickness);
        }

        public static void DrawProgressBar(SpriteBatch sb, Vector2 start, Vector2 size, float progress, Color clr, float depth = 0.0f)
        {
            DrawProgressBar(sb, start, size, progress, clr, new Color(0.5f, 0.57f, 0.6f, 1.0f), depth);
        }

        public static void DrawProgressBar(SpriteBatch sb, Vector2 start, Vector2 size, float progress, Color clr, Color outlineColor, float depth = 0.0f)
        {
            DrawRectangle(sb, new Vector2(start.X, -start.Y), size, outlineColor, false, depth);

            int padding = 2;
            DrawRectangle(sb, new Rectangle((int)start.X + padding, -(int)(start.Y - padding), (int)((size.X - padding * 2) * progress), (int)size.Y - padding * 2),
                clr, true, depth);
        }

        public static bool DrawButton(SpriteBatch sb, Rectangle rect, string text, Color color, bool isHoldable = false)
        {
            bool clicked = false;

            if (rect.Contains(PlayerInput.MousePosition))
            {
                clicked = PlayerInput.PrimaryMouseButtonHeld();

                color = clicked ?
                    new Color((int)(color.R * 0.8f), (int)(color.G * 0.8f), (int)(color.B * 0.8f), color.A) :
                    new Color((int)(color.R * 1.2f), (int)(color.G * 1.2f), (int)(color.B * 1.2f), color.A);

                if (!isHoldable) clicked = PlayerInput.PrimaryMouseButtonClicked();
            }

            DrawRectangle(sb, rect, color, true);

            Vector2 origin;
            try
            {
                origin = GUIStyle.Font.MeasureString(text) / 2;
            }
            catch
            {
                origin = Vector2.Zero;
            }

            GUIStyle.Font.DrawString(sb, text, new Vector2(rect.Center.X, rect.Center.Y), Color.White, 0.0f, origin, 1.0f, SpriteEffects.None, 0.0f);

            return clicked;
        }

        private static void DrawMessages(SpriteBatch spriteBatch, Camera cam)
        {
            if (messages.Count == 0) { return; }

            bool useScissorRect = messages.Any(m => !m.WorldSpace);
            Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle;
            if (useScissorRect)
            {
                spriteBatch.End();
                spriteBatch.GraphicsDevice.ScissorRectangle = HUDLayoutSettings.MessageAreaTop;
                spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable);
            }

            foreach (GUIMessage msg in messages)
            {
                if (msg.WorldSpace) { continue; }

                Vector2 drawPos = new Vector2(HUDLayoutSettings.MessageAreaTop.Right, HUDLayoutSettings.MessageAreaTop.Center.Y);

                msg.Font.DrawString(spriteBatch, msg.Text, drawPos + msg.DrawPos + Vector2.One, Color.Black, 0, msg.Origin, 1.0f, SpriteEffects.None, 0);
                msg.Font.DrawString(spriteBatch, msg.Text, drawPos + msg.DrawPos, msg.Color, 0, msg.Origin, 1.0f, SpriteEffects.None, 0);
                break;
            }

            if (useScissorRect)
            {
                spriteBatch.End();
                spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect;
                spriteBatch.Begin(SpriteSortMode.Deferred);
            }

            foreach (GUIMessage msg in messages)
            {
                if (!msg.WorldSpace) { continue; }

                if (cam != null)
                {
                    float alpha = 1.0f;
                    if (msg.Timer < 1.0f) { alpha -= 1.0f - msg.Timer; }

                    Vector2 drawPos = cam.WorldToScreen(msg.DrawPos);
                    msg.Font.DrawString(spriteBatch, msg.Text, drawPos + Vector2.One, Color.Black * alpha, 0, msg.Origin, 1.0f, SpriteEffects.None, 0);
                    msg.Font.DrawString(spriteBatch, msg.Text, drawPos, msg.Color * alpha, 0, msg.Origin, 1.0f, SpriteEffects.None, 0);
                }
            }

            messages.RemoveAll(m => m.Timer <= 0.0f);
        }

        /// <summary>
        /// Draws a bezier curve with dots.
        /// </summary>
        public static void DrawBezierWithDots(SpriteBatch spriteBatch, Vector2 start, Vector2 end, Vector2 control, int pointCount, Color color, int dotSize = 2)
        {
            for (int i = 0; i < pointCount; i++)
            {
                float t = (float)i / (pointCount - 1);
                Vector2 pos = MathUtils.Bezier(start, control, end, t);
                ShapeExtensions.DrawPoint(spriteBatch, pos, color, dotSize);
            }
        }

        public static void DrawSineWithDots(SpriteBatch spriteBatch, Vector2 from, Vector2 dir, float amplitude, float length, float scale, int pointCount, Color color, int dotSize = 2)
        {
            Vector2 up = dir.Right();
            //DrawLine(spriteBatch, from, from + dir, GUIStyle.Red);
            //DrawLine(spriteBatch, from, from + up * dir.Length(), Color.Blue);
            for (int i = 0; i < pointCount; i++)
            {
                Vector2 pos = from;
                if (i > 0)
                {
                    float t = (float)i / (pointCount - 1);
                    float sin = (float)Math.Sin(t / length * scale) * amplitude;
                    pos += (up * sin) + (dir * t);
                }
                ShapeExtensions.DrawPoint(spriteBatch, pos, color, dotSize);
            }
        }

        private static void DrawSavingIndicator(SpriteBatch spriteBatch)
        {
            if (!IsSavingIndicatorVisible || GUIStyle.SavingIndicator == null) { return; }
            var sheet = GUIStyle.SavingIndicator;
            Vector2 pos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) - new Vector2(HUDLayoutSettings.Padding) - 2 * Scale * sheet.FrameSize.ToVector2();
            sheet.Draw(spriteBatch, (int)Math.Floor(savingIndicatorSpriteIndex), pos, savingIndicatorColor, origin: Vector2.Zero, rotate: 0.0f, scale: new Vector2(Scale));
        }
#endregion

#region Element creation

        public static Texture2D CreateCircle(int radius, bool filled = false)
        {
            int outerRadius = radius * 2 + 2; // So circle doesn't go out of bounds

            Color[] data = new Color[outerRadius * outerRadius];

            // Colour the entire texture transparent first.
            for (int i = 0; i < data.Length; i++)
                data[i] = Color.Transparent;

            if (filled)
            {
                float diameterSqr = radius * radius;
                for (int x = 0; x < outerRadius; x++)
                {
                    for (int y = 0; y < outerRadius; y++)
                    {
                        Vector2 pos = new Vector2(radius - x, radius - y);
                        if (pos.LengthSquared() <= diameterSqr)
                        {
                            TrySetArray(data, y * outerRadius + x + 1, Color.White);
                        }
                    }
                }
            }
            else
            {
                // Work out the minimum step necessary using trigonometry + sine approximation.
                double angleStep = 1f / radius;

                for (double angle = 0; angle < Math.PI * 2; angle += angleStep)
                {
                    // Use the parametric definition of a circle: http://en.wikipedia.org/wiki/Circle#Cartesian_coordinates
                    int x = (int)Math.Round(radius + radius * Math.Cos(angle));
                    int y = (int)Math.Round(radius + radius * Math.Sin(angle));

                    TrySetArray(data, y * outerRadius + x + 1, Color.White);
                }
            }

            Texture2D texture = null;
            CrossThread.RequestExecutionOnMainThread(() =>
            {
                texture = new Texture2D(GraphicsDevice, outerRadius, outerRadius);
                texture.SetData(data);
            });
            return texture;
        }

        public static Texture2D CreateCapsule(int radius, int height)
        {
            int textureWidth = Math.Max(radius * 2, 1);
            int textureHeight = Math.Max(height + radius * 2, 1);

            Color[] data = new Color[textureWidth * textureHeight];

            // Colour the entire texture transparent first.
            for (int i = 0; i < data.Length; i++)
                data[i] = Color.Transparent;

            // Work out the minimum step necessary using trigonometry + sine approximation.
            double angleStep = 1f / radius;

            for (int i = 0; i < 2; i++)
            {
                for (double angle = 0; angle < Math.PI * 2; angle += angleStep)
                {
                    // Use the parametric definition of a circle: http://en.wikipedia.org/wiki/Circle#Cartesian_coordinates
                    int x = (int)Math.Round(radius + radius * Math.Cos(angle));
                    int y = (height - 1) * i + (int)Math.Round(radius + radius * Math.Sin(angle));

                    TrySetArray(data, y * textureWidth + x, Color.White);
                }
            }

            for (int y = radius; y < textureHeight - radius; y++)
            {
                TrySetArray(data, y * textureWidth, Color.White);
                TrySetArray(data, y * textureWidth + (textureWidth - 1), Color.White);
            }

            Texture2D texture = null;
            CrossThread.RequestExecutionOnMainThread(() =>
            {
                texture = new Texture2D(GraphicsDevice, textureWidth, textureHeight);
                texture.SetData(data);
            });
            return texture;
        }

        public static Texture2D CreateRectangle(int width, int height)
        {
            width = Math.Max(width, 1);
            height = Math.Max(height, 1);
            Color[] data = new Color[width * height];

            for (int i = 0; i < data.Length; i++)
                data[i] = Color.Transparent;

            for (int y = 0; y < height; y++)
            {
                TrySetArray(data, y * width, Color.White);
                TrySetArray(data, y * width + (width - 1), Color.White);
            }

            for (int x = 0; x < width; x++)
            {
                TrySetArray(data, x, Color.White);
                TrySetArray(data, (height - 1) * width + x, Color.White);
            }

            Texture2D texture = null;
            CrossThread.RequestExecutionOnMainThread(() =>
            {
                texture = new Texture2D(GraphicsDevice, width, height);
                texture.SetData(data);
            });
            return texture;
        }

        private static bool TrySetArray(Color[] data, int index, Color value)
        {
            if (index >= 0 && index < data.Length)
            {
                data[index] = value;
                return true;
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// Creates multiple buttons with relative size and positions them automatically.
        /// </summary>
        public static List<GUIButton> CreateButtons(int count, Vector2 relativeSize, RectTransform parent,
            Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null,
            int absoluteSpacing = 0, float relativeSpacing = 0, Func<int, int> extraSpacing = null,
            int startOffsetAbsolute = 0, float startOffsetRelative = 0, bool isHorizontal = false,
            Alignment textAlignment = Alignment.Center, string style = "")
        {
            Func<RectTransform, GUIButton> constructor = rectT => new GUIButton(rectT, string.Empty, textAlignment, style);
            return CreateElements(count, relativeSize, parent, constructor, anchor, pivot, minSize, maxSize, absoluteSpacing, relativeSpacing, extraSpacing, startOffsetAbsolute, startOffsetRelative, isHorizontal);
        }

        /// <summary>
        /// Creates multiple buttons with absolute size and positions them automatically.
        /// </summary>
        public static List<GUIButton> CreateButtons(int count, Point absoluteSize, RectTransform parent,
            Anchor anchor = Anchor.TopLeft, Pivot? pivot = null,
            int absoluteSpacing = 0, float relativeSpacing = 0, Func<int, int> extraSpacing = null,
            int startOffsetAbsolute = 0, float startOffsetRelative = 0, bool isHorizontal = false,
            Alignment textAlignment = Alignment.Center, string style = "")
        {
            Func<RectTransform, GUIButton> constructor = rectT => new GUIButton(rectT, string.Empty, textAlignment, style);
            return CreateElements(count, absoluteSize, parent, constructor, anchor, pivot, absoluteSpacing, relativeSpacing, extraSpacing, startOffsetAbsolute, startOffsetRelative, isHorizontal);
        }

        /// <summary>
        /// Creates multiple elements with relative size and positions them automatically.
        /// </summary>
        public static List<T> CreateElements<T>(int count, Vector2 relativeSize, RectTransform parent, Func<RectTransform, T> constructor,
            Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null,
            int absoluteSpacing = 0, float relativeSpacing = 0, Func<int, int> extraSpacing = null,
            int startOffsetAbsolute = 0, float startOffsetRelative = 0, bool isHorizontal = false)
            where T : GUIComponent
        {
            return CreateElements(count, parent, constructor, relativeSize, null, anchor, pivot, minSize, maxSize, absoluteSpacing, relativeSpacing, extraSpacing, startOffsetAbsolute, startOffsetRelative, isHorizontal);
        }

        /// <summary>
        /// Creates multiple elements with absolute size and positions them automatically.
        /// </summary>
        public static List<T> CreateElements<T>(int count, Point absoluteSize, RectTransform parent, Func<RectTransform, T> constructor,
            Anchor anchor = Anchor.TopLeft, Pivot? pivot = null,
            int absoluteSpacing = 0, float relativeSpacing = 0, Func<int, int> extraSpacing = null,
            int startOffsetAbsolute = 0, float startOffsetRelative = 0, bool isHorizontal = false)
            where T : GUIComponent
        {
            return CreateElements(count, parent, constructor, null, absoluteSize, anchor, pivot, null, null, absoluteSpacing, relativeSpacing, extraSpacing, startOffsetAbsolute, startOffsetRelative, isHorizontal);
        }

        public static GUIComponent CreateEnumField(Enum value, int elementHeight, LocalizedString name, RectTransform parent, string toolTip = null, GUIFont font = null)
        {
            font = font ?? GUIStyle.SmallFont;
            var frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, elementHeight), parent), color: Color.Transparent);
            new GUITextBlock(new RectTransform(new Vector2(0.6f, 1), frame.RectTransform), name, font: font)
            {
                ToolTip = toolTip
            };
            GUIDropDown enumDropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1), frame.RectTransform, Anchor.TopRight),
                elementCount: Enum.GetValues(value.GetType()).Length)
            {
                ToolTip = toolTip
            };
            foreach (object enumValue in Enum.GetValues(value.GetType()))
            {
                enumDropDown.AddItem(enumValue.ToString(), enumValue);
            }
            enumDropDown.SelectItem(value);
            return frame;
        }

        public static GUIComponent CreateRectangleField(Rectangle value, int elementHeight, LocalizedString name, RectTransform parent, LocalizedString toolTip = null, GUIFont font = null)
        {
            var frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, Math.Max(elementHeight, 26)), parent), color: Color.Transparent);
            font = font ?? GUIStyle.SmallFont;
            new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), frame.RectTransform), name, font: font)
            {
                ToolTip = toolTip
            };
            var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1), frame.RectTransform, Anchor.TopRight), isHorizontal: true, childAnchor: Anchor.CenterRight)
            {
                Stretch = true,
                RelativeSpacing = 0.01f
            };
            for (int i = 3; i >= 0; i--)
            {
                var element = new GUIFrame(new RectTransform(new Vector2(0.22f, 1), inputArea.RectTransform) { MinSize = new Point(50, 0), MaxSize = new Point(150, 50) }, style: null);
                new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), RectComponentLabels[i], font: font, textAlignment: Alignment.CenterLeft);
                GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight),
                    NumberType.Int)
                {
                    Font = font
                };
                // Not sure if the min value could in any case be negative.
                numberInput.MinValueInt = 0;
                // Just something reasonable to keep the value in the input rect.
                numberInput.MaxValueInt = 9999;
                switch (i)
                {
                    case 0:
                        numberInput.IntValue = value.X;
                        break;
                    case 1:
                        numberInput.IntValue = value.Y;
                        break;
                    case 2:
                        numberInput.IntValue = value.Width;
                        break;
                    case 3:
                        numberInput.IntValue = value.Height;
                        break;
                }
            }
            return frame;
        }

        public static GUIComponent CreatePointField(Point value, int elementHeight, LocalizedString displayName, RectTransform parent, LocalizedString toolTip = null)
        {
            var frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, Math.Max(elementHeight, 26)), parent), color: Color.Transparent);
            new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont)
            {
                ToolTip = toolTip
            };
            var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1), frame.RectTransform, Anchor.TopRight), isHorizontal: true, childAnchor: Anchor.CenterRight)
            {
                Stretch = true,
                RelativeSpacing = 0.05f
            };
            for (int i = 1; i >= 0; i--)
            {
                var element = new GUIFrame(new RectTransform(new Vector2(0.45f, 1), inputArea.RectTransform), style: null);
                new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), VectorComponentLabels[i], font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft);
                GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight),
                    NumberType.Int)
                {
                    Font = GUIStyle.SmallFont
                };

                if (i == 0)
                    numberInput.IntValue = value.X;
                else
                    numberInput.IntValue = value.Y;
            }
            return frame;
        }

        public static GUIComponent CreateVector2Field(Vector2 value, int elementHeight, LocalizedString name, RectTransform parent, LocalizedString toolTip = null, GUIFont font = null, int decimalsToDisplay = 1)
        {
            font = font ?? GUIStyle.SmallFont;
            var frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, Math.Max(elementHeight, 26)), parent), color: Color.Transparent);
            new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), frame.RectTransform), name, font: font)
            {
                ToolTip = toolTip
            };
            var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1), frame.RectTransform, Anchor.TopRight), isHorizontal: true, childAnchor: Anchor.CenterRight)
            {
                Stretch = true,
                RelativeSpacing = 0.05f
            };
            for (int i = 1; i >= 0; i--)
            {
                var element = new GUIFrame(new RectTransform(new Vector2(0.45f, 1), inputArea.RectTransform), style: null);
                new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), VectorComponentLabels[i], font: font, textAlignment: Alignment.CenterLeft);
                GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), NumberType.Float) { Font = font };
                switch (i)
                {
                    case 0:
                        numberInput.FloatValue = value.X;
                        break;
                    case 1:
                        numberInput.FloatValue = value.Y;
                        break;
                }
                numberInput.DecimalsToDisplay = decimalsToDisplay;
            }
            return frame;
        }

        public static void NotifyPrompt(LocalizedString header, LocalizedString body)
        {
            GUIMessageBox msgBox = new GUIMessageBox(header, body, new[] { TextManager.Get("Ok") }, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175));
            msgBox.Buttons[0].OnClicked = delegate
            {
                msgBox.Close();
                return true;
            };
        }

        public static GUIMessageBox AskForConfirmation(LocalizedString header, LocalizedString body, Action onConfirm, Action onDeny = null)
        {
            LocalizedString[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") };
            GUIMessageBox msgBox = new GUIMessageBox(header, body, buttons, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175));

            // Cancel button
            msgBox.Buttons[1].OnClicked = delegate
            {
                onDeny?.Invoke();
                msgBox.Close();
                return true;
            };

            // Ok button
            msgBox.Buttons[0].OnClicked = delegate
            {
                onConfirm.Invoke();
                msgBox.Close();
                return true;
            };
            return msgBox;
        }

        public static GUIMessageBox PromptTextInput(LocalizedString header, string body, Action<string> onConfirm)
        {
            LocalizedString[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") };
            GUIMessageBox msgBox = new GUIMessageBox(header, string.Empty, buttons, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175));
            GUITextBox textBox = new GUITextBox(new RectTransform(Vector2.One, msgBox.Content.RectTransform), text: body)
            {
                OverflowClip = true
            };

            // Cancel button
            msgBox.Buttons[1].OnClicked = delegate
            {
                msgBox.Close();
                return true;
            };

            // Ok button
            msgBox.Buttons[0].OnClicked = delegate
            {
                onConfirm.Invoke(textBox.Text);
                msgBox.Close();
                return true;
            };
            return msgBox;
        }

#endregion

#region Element positioning
        private static List<T> CreateElements<T>(int count, RectTransform parent, Func<RectTransform, T> constructor,
            Vector2? relativeSize = null, Point? absoluteSize = null,
            Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null,
            int absoluteSpacing = 0, float relativeSpacing = 0, Func<int, int> extraSpacing = null,
            int startOffsetAbsolute = 0, float startOffsetRelative = 0, bool isHorizontal = false)
            where T : GUIComponent
        {
            var elements = new List<T>();
            int extraTotal = 0;
            for (int i = 0; i < count; i++)
            {
                if (extraSpacing != null)
                {
                    extraTotal += extraSpacing(i);
                }
                if (relativeSize.HasValue)
                {
                    var size = relativeSize.Value;
                    var offsets = CalculateOffsets(size, startOffsetRelative, startOffsetAbsolute, relativeSpacing, absoluteSpacing, i, extraTotal, isHorizontal);
                    elements.Add(constructor(new RectTransform(size, parent, anchor, pivot, minSize, maxSize)
                    {
                        RelativeOffset = offsets.Item1,
                        AbsoluteOffset = offsets.Item2
                    }));
                }
                else
                {
                    var size = absoluteSize.Value;
                    var offsets = CalculateOffsets(size, startOffsetRelative, startOffsetAbsolute, relativeSpacing, absoluteSpacing, i, extraTotal, isHorizontal);
                    elements.Add(constructor(new RectTransform(size, parent, anchor, pivot)
                    {
                        RelativeOffset = offsets.Item1,
                        AbsoluteOffset = offsets.Item2
                    }));
                }
            }
            return elements;
        }

        private static Tuple<Vector2, Point> CalculateOffsets(Vector2 relativeSize, float startOffsetRelative, int startOffsetAbsolute, float relativeSpacing, int absoluteSpacing, int counter, int extra, bool isHorizontal)
        {
            float relX = 0, relY = 0;
            int absX = 0, absY = 0;
            if (isHorizontal)
            {
                relX = CalculateRelativeOffset(startOffsetRelative, relativeSpacing, relativeSize.X, counter);
                absX = CalculateAbsoluteOffset(startOffsetAbsolute, absoluteSpacing, counter, extra);
            }
            else
            {
                relY = CalculateRelativeOffset(startOffsetRelative, relativeSpacing, relativeSize.Y, counter);
                absY = CalculateAbsoluteOffset(startOffsetAbsolute, absoluteSpacing, counter, extra);
            }
            return Tuple.Create(new Vector2(relX, relY), new Point(absX, absY));
        }

        private static Tuple<Vector2, Point> CalculateOffsets(Point absoluteSize, float startOffsetRelative, int startOffsetAbsolute, float relativeSpacing, int absoluteSpacing, int counter, int extra, bool isHorizontal)
        {
            float relX = 0, relY = 0;
            int absX = 0, absY = 0;
            if (isHorizontal)
            {
                relX = CalculateRelativeOffset(startOffsetRelative, relativeSpacing, counter);
                absX = CalculateAbsoluteOffset(startOffsetAbsolute, absoluteSpacing, absoluteSize.X, counter, extra);
            }
            else
            {
                relY = CalculateRelativeOffset(startOffsetRelative, relativeSpacing, counter);
                absY = CalculateAbsoluteOffset(startOffsetAbsolute, absoluteSpacing, absoluteSize.Y, counter, extra);
            }
            return Tuple.Create(new Vector2(relX, relY), new Point(absX, absY));
        }

        private static float CalculateRelativeOffset(float startOffset, float spacing, float size, int counter)
        {
            return startOffset + (spacing + size) * counter;
        }

        private static float CalculateRelativeOffset(float startOffset, float spacing, int counter)
        {
            return startOffset + spacing * counter;
        }

        private static int CalculateAbsoluteOffset(int startOffset, int spacing, int counter, int extra)
        {
            return startOffset + spacing * counter + extra;
        }

        private static int CalculateAbsoluteOffset(int startOffset, int spacing, int size, int counter, int extra)
        {
            return startOffset + (spacing + size) * counter + extra;
        }

        /// <summary>
        /// Attempts to move a set of UI elements further from each other to prevent them from overlapping
        /// </summary>
        /// <param name="elements">UI elements to move</param>
        /// <param name="disallowedAreas">Areas the UI elements are not allowed to overlap with (ignored if null)</param>
        /// <param name="clampArea">The elements will not be moved outside this area. If the parameter is not given, the elements are kept inside the window.</param>
        public static void PreventElementOverlap(IList<GUIComponent> elements, IList<Rectangle> disallowedAreas = null,  Rectangle? clampArea = null)
        {
            List<GUIComponent> sortedElements = elements.OrderByDescending(e => e.Rect.Width + e.Rect.Height).ToList();

            Rectangle area = clampArea ?? new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight);
            for (int i = 0; i < sortedElements.Count; i++)
            {
                Point moveAmount = Point.Zero;
                Rectangle rect1 = sortedElements[i].Rect;
                moveAmount.X += Math.Max(area.X - rect1.X, 0);
                moveAmount.X -= Math.Max(rect1.Right - area.Right, 0);
                moveAmount.Y += Math.Max(area.Y - rect1.Y, 0);
                moveAmount.Y -= Math.Max(rect1.Bottom - area.Bottom, 0);
                sortedElements[i].RectTransform.ScreenSpaceOffset += moveAmount;
            }

            bool intersections = true;
            int iterations = 0;
            while (intersections && iterations < 100)
            {
                intersections = false;
                for (int i = 0; i < sortedElements.Count; i++)
                {
                    Rectangle rect1 = sortedElements[i].Rect;
                    for (int j = i + 1; j < sortedElements.Count; j++)
                    {
                        Rectangle rect2 = sortedElements[j].Rect;
                        if (!rect1.Intersects(rect2)) { continue; }

                        intersections = true;
                        Point centerDiff = rect1.Center - rect2.Center;
                        //move the interfaces away from each other, in a random direction if they're at the same position
                        Vector2 moveAmount = centerDiff == Point.Zero ? Vector2.UnitX + Rand.Vector(0.1f) : Vector2.Normalize(centerDiff.ToVector2());

                        //if the horizontal move amount is much larger than vertical, only move horizontally
                        //(= attempt to place the elements side-by-side if they're more apart horizontally than vertically)
                        if (Math.Abs(moveAmount.X) > Math.Abs(moveAmount.Y) * 8.0f)
                        {
                            moveAmount.Y = 0.0f;
                        }
                        //same for the y-axis
                        else if (Math.Abs(moveAmount.Y) > Math.Abs(moveAmount.X) * 8.0f)
                        {
                            moveAmount.X = 0.0f;
                        }

                        //make sure we don't move the interfaces out of the screen
                        Vector2 moveAmount1 = ClampMoveAmount(rect1, area, moveAmount * 10.0f);
                        Vector2 moveAmount2 = ClampMoveAmount(rect2, area, -moveAmount * 10.0f);

                        //move by 10 units in the desired direction and repeat until nothing overlaps
                        //(or after 100 iterations, in which case we'll just give up and let them overlap)
                        sortedElements[i].RectTransform.ScreenSpaceOffset += moveAmount1.ToPoint();
                        sortedElements[j].RectTransform.ScreenSpaceOffset += moveAmount2.ToPoint();
                    }

                    if (disallowedAreas == null) { continue; }
                    foreach (Rectangle rect2 in disallowedAreas)
                    {
                        if (!rect1.Intersects(rect2)) { continue; }
                        intersections = true;

                        Point centerDiff = rect1.Center - rect2.Center;
                        //move the interface away from the disallowed area
                        Vector2 moveAmount = centerDiff == Point.Zero ? Rand.Vector(1.0f) : Vector2.Normalize(centerDiff.ToVector2());

                        //make sure we don't move the interface out of the screen
                        Vector2 moveAmount1 = ClampMoveAmount(rect1, area, moveAmount * 10.0f);

                        //move by 10 units in the desired direction and repeat until nothing overlaps
                        //(or after 100 iterations, in which case we'll just give up and let them overlap)
                        sortedElements[i].RectTransform.ScreenSpaceOffset += (moveAmount1).ToPoint();
                    }
                }
                iterations++;
            }

            static Vector2 ClampMoveAmount(Rectangle Rect, Rectangle clampTo, Vector2 moveAmount)
            {
                if (Rect.Y < clampTo.Y)
                {
                    moveAmount.Y = Math.Max(moveAmount.Y, 0.0f);
                }
                else if (Rect.Bottom > clampTo.Bottom)
                {
                    moveAmount.Y = Math.Min(moveAmount.Y, 0.0f);
                }
                if (Rect.X < clampTo.X)
                {
                    moveAmount.X = Math.Max(moveAmount.X, 0.0f);
                }
                else if (Rect.Right > clampTo.Right)
                {
                    moveAmount.X = Math.Min(moveAmount.X, 0.0f);
                }
                return moveAmount;
            }
        }

#endregion

#region Misc
        public static void TogglePauseMenu()
        {
            if (Screen.Selected == GameMain.MainMenuScreen) { return; }
            if (PreventPauseMenuToggle) { return; }

            SettingsMenuOpen = false;

            TogglePauseMenu(null, null);

            if (PauseMenuOpen)
            {
                Inventory.DraggingItems.Clear();
                Inventory.DraggingInventory = null;

                PauseMenu = new GUIFrame(new RectTransform(Vector2.One, Canvas, Anchor.Center), style: null);
                new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, PauseMenu.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker");

                var pauseMenuInner = new GUIFrame(new RectTransform(new Vector2(0.13f, 0.3f), PauseMenu.RectTransform, Anchor.Center) { MinSize = new Point(250, 300) });

                var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.6f), pauseMenuInner.RectTransform, Anchor.Center))
                {
                    Stretch = true,
                    RelativeSpacing = 0.05f
                };

                new GUIButton(new RectTransform(new Vector2(0.1f, 0.1f), pauseMenuInner.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point((int)(15 * GUI.Scale)) },
                    "", style: "GUIBugButton")
                {
                    IgnoreLayoutGroups = true,
                    ToolTip = TextManager.Get("bugreportbutton") + $" (v{GameMain.Version})",
                    OnClicked = (btn, userdata) => { GameMain.Instance.ShowBugReporter(); return true; }
                };

                CreateButton("PauseMenuResume", buttonContainer, null);
                CreateButton("PauseMenuSettings", buttonContainer, () => SettingsMenuOpen = true);

                bool IsFriendlyOutpostLevel() => GameMain.GameSession != null && Level.IsLoadedFriendlyOutpost;
                if (Screen.Selected == GameMain.GameScreen && GameMain.GameSession != null)
                {
                    if (GameMain.GameSession.GameMode is SinglePlayerCampaign spMode)
                    {
                        CreateButton("PauseMenuRetry", buttonContainer, verificationTextTag: "PauseMenuRetryVerification", action: () =>
                        {
                            if (GameMain.GameSession.RoundSummary?.Frame != null)
                            {
                                GUIMessageBox.MessageBoxes.Remove(GameMain.GameSession.RoundSummary.Frame);
                            }
                            GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction");
                            GameMain.GameSession.LoadPreviousSave();
                        });

                        if (IsFriendlyOutpostLevel())
                        {
                            CreateButton("PauseMenuSaveQuit", buttonContainer, verificationTextTag: "PauseMenuSaveAndReturnToMainMenuVerification", action: () =>
                            {
                                if (IsFriendlyOutpostLevel()) { GameMain.QuitToMainMenu(save: true); }
                            });
                        }
                    }
                    else if (GameMain.GameSession.GameMode is TestGameMode)
                    {
                        CreateButton("PauseMenuReturnToEditor", buttonContainer, action: () =>
                        {
                            GameMain.GameSession?.EndRound("");
                        });
                    }
                    else if (!GameMain.GameSession.GameMode.IsSinglePlayer && GameMain.Client != null && GameMain.Client.HasPermission(ClientPermissions.ManageRound))
                    {
                        bool canSave = GameMain.GameSession.GameMode is CampaignMode && IsFriendlyOutpostLevel();
                        if (canSave)
                        {
                            CreateButton("PauseMenuSaveQuit", buttonContainer, verificationTextTag: "PauseMenuSaveAndReturnToServerLobbyVerification", action: () =>
                            {
                                GameMain.Client?.RequestRoundEnd(save: true);
                            });
                        }

                        CreateButton(GameMain.GameSession.GameMode is CampaignMode ? "ReturnToServerlobby" : "EndRound", buttonContainer,
                            verificationTextTag: GameMain.GameSession.GameMode is CampaignMode ? "PauseMenuReturnToServerLobbyVerification" : "EndRoundSubNotAtLevelEnd",
                            action: () =>
                            {
                                GameMain.Client?.RequestRoundEnd(save: false);
                            });
                    }
                }

                if (GameMain.GameSession != null || Screen.Selected is CharacterEditorScreen || Screen.Selected is SubEditorScreen)
                {
                    CreateButton("PauseMenuQuit", buttonContainer,
                        verificationTextTag: GameMain.GameSession == null ? "PauseMenuQuitVerificationEditor" : "PauseMenuQuitVerification",
                        action: () =>
                        {
                            GameMain.QuitToMainMenu(save: false);
                        });
                }
                else
                {
                    CreateButton("PauseMenuQuit", buttonContainer, action: () => { GameMain.QuitToMainMenu(save: false); });
                }

                GUITextBlock.AutoScaleAndNormalize(buttonContainer.Children.Where(c => c is GUIButton).Select(c => ((GUIButton)c).TextBlock));
            }

            void CreateButton(string textTag, GUIComponent parent, Action action, string verificationTextTag = null)
            {
                new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), TextManager.Get(textTag))
                {
                    OnClicked = (btn, userData) =>
                    {
                        if (string.IsNullOrEmpty(verificationTextTag))
                        {
                            PauseMenuOpen = false;
                            action?.Invoke();
                        }
                        else
                        {
                            CreateVerificationPrompt(verificationTextTag, action);
                        }
                        return true;
                    }
                };
            }

            void CreateVerificationPrompt(string textTag, Action confirmAction)
            {
                var msgBox = new GUIMessageBox("", TextManager.Get(textTag),
                    new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") })
                {
                    UserData = "verificationprompt"
                };
                msgBox.Buttons[0].OnClicked = (_, __) =>
                {
                    PauseMenuOpen = false;
                    confirmAction?.Invoke();
                    return true;
                };
                msgBox.Buttons[0].OnClicked += msgBox.Close;
                msgBox.Buttons[1].OnClicked += msgBox.Close;
            }
        }

        private static bool TogglePauseMenu(GUIButton button, object obj)
        {
            PauseMenuOpen = !PauseMenuOpen;
            if (!PauseMenuOpen && PauseMenu != null)
            {
                PauseMenu.RectTransform.Parent = null;
                PauseMenu = null;
            }
            return true;
        }

        /// <summary>
        /// Displays a message at the center of the screen, automatically preventing overlapping with other centered messages. TODO: Allow to show messages at the middle of the screen (instead of the top center).
        /// </summary>
        /// 
        public static void AddMessage(LocalizedString message, Color color, float? lifeTime = null, bool playSound = true, GUIFont font = null)
        {
            AddMessage(message.Value, color, lifeTime, playSound, font);
        }

        public static void AddMessage(LocalizedString message, Color color, Vector2 pos, Vector2 velocity, float lifeTime = 3.0f, bool playSound = true, GUISoundType soundType = GUISoundType.UIMessage, int subId = -1)
        {
            AddMessage(message.Value, color, pos, velocity, lifeTime, playSound, soundType, subId);
        }

        public static void AddMessage(string message, Color color, float? lifeTime = null, bool playSound = true, GUIFont font = null)
        {
            var guiMessage = new GUIMessage(message, color, lifeTime ?? MathHelper.Clamp(message.Length / 5.0f, 3.0f, 10.0f), font ?? GUIStyle.LargeFont);
            lock (mutex)
            {
                if (messages.Any(msg => msg.Text == message)) { return; }
                messages.Add(guiMessage);
            }
            if (playSound) { SoundPlayer.PlayUISound(GUISoundType.UIMessage); }
        }

        public static void AddMessage(string message, Color color, Vector2 pos, Vector2 velocity, float lifeTime = 3.0f, bool playSound = true, GUISoundType soundType = GUISoundType.UIMessage, int subId = -1)
        {
            Submarine sub = Submarine.Loaded.FirstOrDefault(s => s.ID == subId);

            var newMessage = new GUIMessage(message, color, pos, velocity, lifeTime, Alignment.Center, GUIStyle.Font, sub: sub);
            if (playSound) { SoundPlayer.PlayUISound(soundType); }

            lock (mutex)
            {
                bool overlapFound = true;
                int tries = 0;
                while (overlapFound)
                {
                    overlapFound = false;
                    foreach (var otherMessage in messages)
                    {
                        float xDiff = otherMessage.Pos.X - newMessage.Pos.X;
                        if (Math.Abs(xDiff) > (newMessage.Size.X + otherMessage.Size.X) / 2) { continue; }
                        float yDiff = otherMessage.Pos.Y - newMessage.Pos.Y;
                        if (Math.Abs(yDiff) > (newMessage.Size.Y + otherMessage.Size.Y) / 2) { continue; }
                        Vector2 moveDir = -(new Vector2(xDiff, yDiff) + Rand.Vector(1.0f));
                        if (moveDir.LengthSquared() > 0.0001f)
                        {
                            moveDir = Vector2.Normalize(moveDir);
                        }
                        else
                        {
                            moveDir = Rand.Vector(1.0f);
                        }
                        moveDir.Y = -Math.Abs(moveDir.Y);
                        newMessage.Pos -= Vector2.UnitY * 10;
                    }
                    tries++;
                    if (tries > 20) { break; }
                }
                messages.Add(newMessage);
            }
        }

        public static void ClearMessages()
        {
            lock (mutex)
            {
                messages.Clear();
            }
        }

        public static bool IsFourByThree()
        {
            float aspectRatio = HorizontalAspectRatio;
            return aspectRatio > 1.3f && aspectRatio < 1.4f;
        }

        public static void SetSavingIndicatorState(bool enabled)
        {
            if (enabled)
            {
                timeUntilSavingIndicatorDisabled = null;
            }
            isSavingIndicatorEnabled = enabled;
        }

        public static void DisableSavingIndicatorDelayed(float delay = 3.0f)
        {
            if (!isSavingIndicatorEnabled) { return; }
            timeUntilSavingIndicatorDisabled = delay;
        }
#endregion
    }
}
