using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using Coroutine;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input.Touch;
using MLEM.Extensions;
using MLEM.Font;
using MLEM.Formatting.Codes;
using MLEM.Input;
using MLEM.Misc;
using MLEM.Startup;
using MLEM.Textures;
using MLEM.Ui;
using MLEM.Ui.Elements;
using TouchyTickets.Attractions;

namespace TouchyTickets {
    public class Ui {

        public static readonly UniformTextureAtlas Texture = new UniformTextureAtlas(MlemGame.LoadContent<Texture2D>("Textures/Ui"), 16, 16);
        private readonly UiSystem uiSystem;
        private readonly Element[] swipeRelations;
        private Element currentUi;
        private float swipeProgress;
        private bool finishingSwipe;

        public Ui(UiSystem uiSystem) {
            this.uiSystem = uiSystem;

            // main ticket store ui
            var rainingTickets = new List<RainingTicket>();
            var main = new Group(Anchor.TopLeft, Vector2.One, false) {
                OnUpdated = (e, time) => {
                    if (e.IsHidden)
                        return;
                    for (var i = rainingTickets.Count - 1; i >= 0; i--) {
                        if (rainingTickets[i].Update())
                            rainingTickets.RemoveAt(i);
                    }
                    while (rainingTickets.Count < Math.Min(GameImpl.Instance.Map.TicketsPerSecond / 30, Options.Instance.RainingTicketLimit))
                        rainingTickets.Add(new RainingTicket());
                },
                OnDrawn = (e, time, batch, alpha) => {
                    batch.Draw(batch.GetBlankTexture(), e.DisplayArea, ColorExtensions.FromHex(0xff86bccf) * alpha);
                    foreach (var ticket in rainingTickets)
                        ticket.Draw(batch, e.DisplayArea.Size, e.Scale, Color.White * alpha);
                }
            };
            var currentNews = Localization.GetRandomNews();
            var newsTicker = main.AddChild(new Panel(Anchor.AutoCenter, new Vector2(1, 0.05F), Vector2.Zero));
            newsTicker.AddChild(new Paragraph(Anchor.CenterLeft, float.MaxValue, p => currentNews, true) {
                OnUpdated = (e, time) => {
                    e.PositionOffset -= new Vector2(1, 0);
                    if (e.PositionOffset.X <= -e.DisplayArea.Width / e.Scale - 20) {
                        e.PositionOffset = new Vector2(e.Parent.DisplayArea.Width / e.Scale + 20, 0);
                        currentNews = Localization.GetRandomNews();
                    }
                }
            });
            var ticketGroup = main.AddChild(new Group(Anchor.AutoCenter, new Vector2(1, 0.175F), false));
            ticketGroup.AddChild(new Paragraph(Anchor.AutoCenter, 10000, p => PrettyPrintNumber(GameImpl.Instance.Tickets) + "<i ticket>", true) {
                TextScale = 0.3F
            });
            ticketGroup.AddChild(new Paragraph(Anchor.AutoCenter, 1, p => GameImpl.Instance.Map.TicketsPerSecond.ToString("#,0.##") + "<i ticket>/s", true) {
                PositionOffset = new Vector2(0, -5)
            });
            BigInteger lastTickets = 0;
            ActiveCoroutine storeWobble = null;
            var storeGroup = main.AddChild(new CustomDrawGroup(Anchor.AutoCenter, new Vector2(1, 0.425F), null, null, false) {
                OnUpdated = (e, time) => {
                    if (lastTickets != GameImpl.Instance.Tickets) {
                        lastTickets = GameImpl.Instance.Tickets;
                        // only wobble if we're not already wobbling
                        if (storeWobble == null || storeWobble.IsFinished)
                            storeWobble = CoroutineHandler.Start(WobbleElement((CustomDrawGroup) e));
                    }
                }
            });
            storeGroup.AddChild(new Image(Anchor.TopLeft, Vector2.One, Texture[0, 0, 2, 3]) {
                OnPressed = e => {
                    var rate = 1;
                    if (Upgrade.TapIncrease[2].IsActive()) {
                        rate = 50;
                    } else if (Upgrade.TapIncrease[1].IsActive()) {
                        rate = 10;
                    } else if (Upgrade.TapIncrease[0].IsActive()) {
                        rate = 5;
                    }
                    #if DEBUG
                    rate = 500000;
                    #endif
                    GameImpl.Instance.Tickets += rate;
                },
                CanBeSelected = true,
                CanBeMoused = true
            });
            main.AddChild(new Group(Anchor.AutoLeft, new Vector2(1, 0.35F), false) {
                Padding = new Padding(6, 6, 12, 6),
                OnDrawn = (e, time, batch, alpha) => {
                    var map = GameImpl.Instance.Map;
                    var mapSize = new Vector2(map.Width, map.Height) * Attraction.TileSize;
                    var (scaleX, scaleY) = e.DisplayArea.Size / mapSize;
                    var scale = Math.Min(scaleX, scaleY);
                    var pos = e.DisplayArea.Location + (e.DisplayArea.Size - mapSize * scale) / 2;
                    batch.Draw(this.uiSystem.Style.PanelTexture, new RectangleF(pos - new Vector2(2) * e.Scale, mapSize * scale + new Vector2(4) * e.Scale), Color.White * alpha, e.Scale);
                    map.Draw(time, batch, pos, scale, alpha, false, new RectangleF(Vector2.Zero, mapSize * scale));
                },
                OnPressed = e => {
                    if (this.swipeProgress != 0)
                        return;
                    var map = GameImpl.Instance.Map;
                    var infoUi = new Group(Anchor.BottomLeft, new Vector2(1)) {CanBeMoused = false};
                    AddSelectedAttractionInfo(infoUi);
                    infoUi.AddChild(new Button(Anchor.AutoLeft, new Vector2(0.5F, 30), Localization.Get("Back")) {
                        OnPressed = e2 => this.FadeUi(false, () => this.uiSystem.Remove(e2.Root.Name))
                    });
                    infoUi.AddChild(new Button(Anchor.AutoInlineIgnoreOverflow, new Vector2(0.5F, 30), Localization.Get("Remove")) {
                        OnPressed = e2 => {
                            if (map.SelectedPosition == null)
                                return;
                            map.Remove(map.SelectedPosition.Value);
                            map.SelectedPosition = null;
                        },
                        OnUpdated = (e2, time) => ((Button) e2).IsDisabled = map.SelectedPosition == null
                    });
                    // we want this to render below the main ui while it fades away
                    this.uiSystem.Add("MapViewInfo", infoUi).Priority = -100;

                    this.FadeUi(true);
                }
            });
            this.currentUi = main;
            this.uiSystem.Add("Main", main);

            // buy ui
            var buyUi = new Panel(Anchor.TopLeft, Vector2.One, Vector2.Zero, false, true, new Point(10, 30), false) {
                ChildPadding = new Padding(5, 15, 5, 5),
                IsHidden = true
            };
            foreach (var attraction in AttractionType.Attractions) {
                long price = 0;
                var attractionAmount = 0;
                var button = buyUi.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 40)) {
                    ChildPadding = new Vector2(4),
                    PositionOffset = new Vector2(0, 1),
                    OnPressed = e => {
                        if (this.swipeProgress != 0)
                            return;
                        var map = GameImpl.Instance.Map;
                        map.PlacingAttraction = attraction.Value.Create();
                        // set placing position to center of camera's view
                        var (posX, posY) = (GameImpl.Instance.Camera.LookingPosition / Attraction.TileSize).ToPoint();
                        map.PlacingPosition = new Point(MathHelper.Clamp(posX, 0, map.Width - attraction.Value.Width), MathHelper.Clamp(posY, 0, map.Height - attraction.Value.Height));

                        var yesNoUi = new Group(Anchor.BottomLeft, new Vector2(1));
                        yesNoUi.AddChild(new Button(Anchor.AutoInlineIgnoreOverflow, new Vector2(0.5F, 30), Localization.Get("Back")) {
                            OnPressed = e2 => this.FadeUi(false, () => this.uiSystem.Remove(e2.Root.Name))
                        });
                        yesNoUi.AddChild(new Button(Anchor.AutoInlineIgnoreOverflow, new Vector2(0.5F, 30), Localization.Get("Place")) {
                            OnPressed = e2 => {
                                GameImpl.Instance.Tickets -= price;
                                GameImpl.Instance.Platform.AddResourceEvent(true, "Tickets", price, "Attraction", attraction.Key);

                                map.Place(map.PlacingPosition, map.PlacingAttraction);
                                map.PlacingAttraction.Wobble();
                                this.FadeUi(false, () => this.uiSystem.Remove(e2.Root.Name));
                            },
                            OnUpdated = (e2, time) => ((Button) e2).IsDisabled = !map.CanPlace(map.PlacingPosition, map.PlacingAttraction)
                        });
                        // we want this to render below the main ui while it fades away
                        this.uiSystem.Add("PlacingYesNo", yesNoUi).Priority = -100;

                        this.FadeUi(true);
                    },
                    OnAreaUpdated = e => {
                        // only update the price when the area updates, since it won't change while we're in the buy ui
                        attractionAmount = GameImpl.Instance.Map.GetAttractionAmount(attraction.Value);
                        // yay compound interest
                        price = (long) Math.Ceiling(attraction.Value.InitialPrice * Math.Pow(1 + 0.1F, attractionAmount));
                    }
                });
                var image = button.AddChild(new Image(Anchor.CenterLeft, new Vector2(0.2F, 40), attraction.Value.TextureRegion) {
                    Padding = new Vector2(4)
                });
                var right = button.AddChild(new Group(Anchor.TopRight, new Vector2(0.8F, 1), false) {CanBeMoused = false});
                var name = right.AddChild(new Paragraph(Anchor.TopLeft, 1, Localization.Get(attraction.Key), true));
                var hiddenName = right.AddChild(new Paragraph(Anchor.TopLeft, 1, "?????", true) {TextColor = Color.Gray});
                var genRate = right.AddChild(new Paragraph(Anchor.BottomLeft, 1, p => attraction.Value.GetGenerationRate() + "<i ticket>/s", true) {TextScale = 0.08F});
                right.AddChild(new Paragraph(Anchor.BottomRight, 1, p => PrettyPrintNumber(price) + "<i ticket>", true));
                button.OnUpdated += (e, time) => {
                    button.IsDisabled = GameImpl.Instance.Tickets < price;
                    // we only want to show the info if we have enough tickets or we've placed the thing before
                    var shouldShow = !button.IsDisabled || attractionAmount > 0;
                    image.Color = shouldShow ? Color.White : Color.Black;
                    name.IsHidden = !shouldShow;
                    genRate.IsHidden = !shouldShow;
                    hiddenName.IsHidden = shouldShow;
                };
            }
            this.uiSystem.Add("Buy", buyUi);

            // modifier ui
            var modifierUi = new Panel(Anchor.TopLeft, Vector2.One, Vector2.Zero, false, true, new Point(10, 30), false) {
                ChildPadding = new Padding(5, 15, 5, 5),
                IsHidden = true
            };
            foreach (var modifier in AttractionModifier.Modifiers.Values) {
                var button = modifierUi.AddChild(new Button(Anchor.AutoLeft, new Vector2(1)) {
                    SetHeightBasedOnChildren = true,
                    PositionOffset = new Vector2(0, 1),
                    ChildPadding = new Vector2(4),
                    OnPressed = e => {
                        if (this.swipeProgress != 0)
                            return;
                        var map = GameImpl.Instance.Map;
                        map.PlacingModifier = modifier;
                        var infoUi = new Group(Anchor.BottomLeft, new Vector2(1)) {CanBeMoused = false};
                        AddSelectedAttractionInfo(infoUi);
                        infoUi.AddChild(new Button(Anchor.AutoLeft, new Vector2(0.5F, 30), Localization.Get("Back")) {
                            OnPressed = e2 => this.FadeUi(false, () => this.uiSystem.Remove(e2.Root.Name))
                        });
                        var addButton = infoUi.AddChild(new Button(Anchor.AutoInlineIgnoreOverflow, new Vector2(0.5F, 30), string.Empty) {
                            OnPressed = e2 => {
                                if (map.SelectedPosition == null)
                                    return;
                                var attraction = map.GetAttractionAt(map.SelectedPosition.Value);

                                var price = attraction.GetModifierPrice(map.PlacingModifier);
                                GameImpl.Instance.Tickets -= price;
                                GameImpl.Instance.Platform.AddResourceEvent(true, "Tickets", price, "Modifier", modifier.Name);

                                attraction.ApplyModifier(map.PlacingModifier);
                                attraction.Wobble();
                            },
                            OnUpdated = (e2, time) => {
                                var disabled = map.SelectedPosition == null;
                                if (!disabled) {
                                    var attraction = map.GetAttractionAt(map.SelectedPosition.Value);
                                    disabled = attraction == null || !map.PlacingModifier.IsAffected(attraction) || GameImpl.Instance.Tickets < attraction.GetModifierPrice(map.PlacingModifier);
                                }
                                ((Button) e2).IsDisabled = disabled;
                            }
                        });
                        addButton.Text.GetTextCallback = p => {
                            var price = map.PlacingModifier.InitialPrice;
                            if (map.SelectedPosition != null) {
                                var attraction = map.GetAttractionAt(map.SelectedPosition.Value);
                                if (attraction != null && map.PlacingModifier.IsAffected(attraction))
                                    price = attraction.GetModifierPrice(map.PlacingModifier);
                            }
                            return PrettyPrintNumber(price) + "<i ticket>";
                        };
                        // we want this to render below the main ui while it fades away
                        this.uiSystem.Add("ModifierInfo", infoUi).Priority = -100;

                        this.FadeUi(true);
                    }
                });
                var image = button.AddChild(new Image(Anchor.CenterLeft, new Vector2(0.2F, 40), modifier.Texture) {
                    Padding = new Vector2(4)
                });
                var right = button.AddChild(new Group(Anchor.TopRight, new Vector2(0.8F, 1)) {CanBeMoused = false});
                var name = right.AddChild(new Paragraph(Anchor.TopLeft, 1, Localization.Get(modifier.Name), true));
                var hiddenName = right.AddChild(new Paragraph(Anchor.TopLeft, 1, "?????", true) {TextColor = Color.Gray});
                var desc = right.AddChild(new Paragraph(Anchor.AutoLeft, 1, Localization.Get(modifier.Name + "Description"), true) {TextScale = 0.08F});
                right.AddChild(new Paragraph(Anchor.AutoRight, 1, p => PrettyPrintNumber(modifier.InitialPrice) + "<i ticket>", true));
                var genRate = right.AddChild(new Paragraph(Anchor.BottomLeft, 1, $"x{modifier.Multiplier}<i ticket>", true) {TextScale = 0.08F});

                var shouldShow = false;
                button.OnAreaUpdated += e => {
                    // this is a pretty expensive operation, so only do it when the area changes
                    shouldShow = GameImpl.Instance.Map.IsAnyAttractionAffected(modifier);
                    e.SetData("Show", shouldShow);
                };
                button.OnUpdated += (e, time) => {
                    button.IsDisabled = !shouldShow || GameImpl.Instance.Tickets < modifier.InitialPrice;
                    image.Color = shouldShow ? Color.White : Color.Black;
                    name.IsHidden = !shouldShow;
                    hiddenName.IsHidden = shouldShow;
                    genRate.IsHidden = !shouldShow;
                    desc.IsHidden = !shouldShow;
                };
            }
            modifierUi.OnAreaUpdated += e => {
                // sort children by whether they should be shown or not
                modifierUi.ReorderChildren((e1, e2) => Comparer<bool>.Default.Compare(e2.GetData<bool>("Show"), e1.GetData<bool>("Show")));
            };
            this.uiSystem.Add("Modifiers", modifierUi);

            // upgrade ui
            var upgradeUi = new Group(Anchor.TopLeft, Vector2.One, false) {
                IsHidden = true,
                OnDrawn = (e, time, batch, alpha) => batch.Draw(batch.GetBlankTexture(), e.DisplayArea, ColorExtensions.FromHex(0xff86bccf) * alpha)
            };
            var upgradeHeader = upgradeUi.AddChild(new Group(Anchor.AutoLeft, new Vector2(1)));
            upgradeHeader.AddChild(new Paragraph(Anchor.AutoCenter, 1, p => GameImpl.Instance.Stars + "<i star>", true) {TextScale = 0.3F});
            upgradeHeader.AddChild(new Button(Anchor.AutoCenter, new Vector2(0.8F, 30), Localization.Get("EarnStar")) {
                PositionOffset = new Vector2(0, 4),
                OnUpdated = (e, time) => ((Button) e).IsDisabled = GameImpl.Instance.Tickets < GameImpl.Instance.GetStarPrice(),
                OnPressed = e => {
                    var infoBox = new Group(Anchor.TopLeft, Vector2.One, false) {
                        OnDrawn = (e2, time, batch, alpha) => batch.Draw(batch.GetBlankTexture(), e2.DisplayArea, Color.Black * 0.35F)
                    };
                    var panel = infoBox.AddChild(new Panel(Anchor.Center, new Vector2(0.8F), Vector2.Zero, true));
                    panel.AddChild(new Paragraph(Anchor.AutoLeft, 1, string.Format(Localization.Get("ReallyEarnStar"), GameImpl.Instance.GetBuyableStars())));
                    panel.AddChild(new Button(Anchor.AutoLeft, new Vector2(0.5F, 30), Localization.Get("Back")) {
                        OnPressed = e2 => this.uiSystem.Remove(e2.Root.Name)
                    });
                    panel.AddChild(new Button(Anchor.AutoInlineIgnoreOverflow, new Vector2(0.5F, 30), Localization.Get("Yes")) {
                        OnPressed = e2 => {
                            this.uiSystem.Remove(e2.Root.Name);

                            var game = GameImpl.Instance;
                            game.Platform.AddResourceEvent(true, "Tickets", (long) game.Tickets, "Restart", "Restart" + game.TimesRestarted);
                            game.Platform.AddResourceEvent(false, "Stars", game.GetBuyableStars(), "Restart", "Restart" + game.TimesRestarted);

                            game.Stars += game.GetBuyableStars();
                            game.TimesRestarted++;
                            game.Tickets = 0;
                            game.Map = new ParkMap(game.Map.Width, game.Map.Height);
                        }
                    });
                    this.uiSystem.Add("ReallyEarnStarBox", infoBox);
                }
            });
            upgradeHeader.AddChild(new Paragraph(Anchor.AutoCenter, 1, p => string.Format(Localization.Get("RequiresTickets"), PrettyPrintNumber(GameImpl.Instance.GetStarPrice()), GameImpl.Instance.GetMaxStars()), true) {
                PositionOffset = new Vector2(0, 2)
            });
            var upgradeList = upgradeUi.AddChild(new Panel(Anchor.AutoLeft, new Vector2(1), Vector2.Zero, false, true, new Point(10, 30), false) {
                ChildPadding = new Padding(5, 15, 5, 5)
            });
            upgradeHeader.OnAreaUpdated += e => upgradeList.Size = new Vector2(1, (upgradeUi.DisplayArea.Height - upgradeHeader.DisplayArea.Height) / e.Scale);
            PopulateUpgradeList(upgradeList);
            this.uiSystem.Add("Upgrade", upgradeUi);

            // options ui
            var optionsUi = new Group(Anchor.TopLeft, Vector2.One, false) {
                IsHidden = true,
                OnDrawn = (e, time, batch, alpha) => batch.Draw(batch.GetBlankTexture(), e.DisplayArea, ColorExtensions.FromHex(0xff86bccf) * alpha)
            };
            optionsUi.AddChild(new Paragraph(Anchor.AutoCenter, 1, Localization.Get("Options"), true) {TextScale = 0.2F});
            var optionList = optionsUi.AddChild(new Panel(Anchor.AutoLeft, new Vector2(1), Vector2.Zero, false, true, new Point(10, 30), false) {
                ChildPadding = new Padding(5, 15, 5, 5),
                PreventParentSpill = true
            });
            optionList.AddChild(new Paragraph(Anchor.AutoLeft, 1, p => Localization.Get("RainingTicketLimit") + ": " + Options.Instance.RainingTicketLimit));
            optionList.AddChild(new Slider(Anchor.AutoLeft, new Vector2(1, 20), 10, 500) {
                PositionOffset = new Vector2(0, 1),
                CurrentValue = Options.Instance.RainingTicketLimit,
                OnValueChanged = (s, v) => {
                    Options.Instance.RainingTicketLimit = (int) v;
                    Options.Save();
                }
            });
            this.uiSystem.Add("Options", optionsUi);

            this.swipeRelations = new Element[] {optionsUi, upgradeUi, main, buyUi, modifierUi};
        }

        public void Update(GameTime time) {
            // swiping between tabs
            if (!this.currentUi.IsHidden && this.uiSystem.Controls.HandleTouch && !this.currentUi.GetChildren<ScrollBar>(c => c.Horizontal && c.IsBeingScrolled, true).Any()) {
                if (MlemGame.Input.GetGesture(GestureType.HorizontalDrag, out var gesture)) {
                    this.swipeProgress -= gesture.Delta.X / this.currentUi.DisplayArea.Width;
                } else if (!this.finishingSwipe && this.swipeProgress != 0 && !MlemGame.Input.TouchState.Any()) {
                    // if we're not dragging or holding, we need to move back to our current ui
                    this.swipeProgress -= Math.Sign(this.swipeProgress) * 0.03F;
                    if (this.swipeProgress.Equals(0, 0.03F)) {
                        this.currentUi.Root.Transform = Matrix.Identity;
                        this.finishingSwipe = false;
                        this.swipeProgress = 0;
                    }
                }

                var next = -1;
                if (this.swipeProgress != 0) {
                    // actual swipe reaction logic
                    var curr = Array.IndexOf(this.swipeRelations, this.currentUi);
                    next = curr + Math.Sign(this.swipeProgress);
                    if (next >= 0 && next < this.swipeRelations.Length) {
                        this.swipeRelations[next].IsHidden = false;
                        // if we've swiped a bit, we count this as a success and move to the next ui
                        this.finishingSwipe = Math.Abs(this.swipeProgress) >= 0.15F;
                        // if we're in the process of finishing a swipe, move without requiring input
                        if (this.finishingSwipe) {
                            if (!MlemGame.Input.TouchState.Any())
                                this.swipeProgress += Math.Sign(this.swipeProgress) * 0.05F;
                            // we're done with the swipe, so switch the active ui
                            if (this.swipeProgress.Equals(Math.Sign(this.swipeProgress), 0.05F)) {
                                this.currentUi = this.swipeRelations[next];
                                this.currentUi.Root.Transform = Matrix.Identity;
                                this.finishingSwipe = false;
                                this.swipeProgress = 0;
                            }
                        }
                    } else {
                        // when we're swiping into the void, we want to stop at the max
                        this.swipeProgress = MathHelper.Clamp(this.swipeProgress, -1, 1);
                    }

                    // update element positions
                    this.currentUi.Root.Transform = Matrix.CreateTranslation(-this.swipeProgress * this.currentUi.DisplayArea.Width, 0, 0);
                    if (next >= 0 && next < this.swipeRelations.Length)
                        this.swipeRelations[next].Root.Transform = Matrix.CreateTranslation((Math.Sign(this.swipeProgress) - this.swipeProgress) * this.swipeRelations[next].DisplayArea.Width, 0, 0);
                }
                for (var i = 0; i < this.swipeRelations.Length; i++)
                    this.swipeRelations[i].IsHidden = i != next && this.swipeRelations[i] != this.currentUi;
            }
        }

        public static void SetupUiSystem(UiSystem uiSystem) {
            InputHandler.EnableGestures(GestureType.HorizontalDrag, GestureType.FreeDrag, GestureType.Pinch, GestureType.Tap);
            uiSystem.GlobalScale = 4;
            uiSystem.AutoScaleWithScreen = true;
            uiSystem.AutoScaleReferenceSize = new Point(720, 1280);
            uiSystem.Style.Font = new GenericSpriteFont(MlemGame.LoadContent<SpriteFont>("Fonts/Regular"));
            uiSystem.Style.PanelTexture = uiSystem.Style.ScrollBarBackground = new NinePatch(Texture[2, 1], 4);
            uiSystem.Style.ButtonTexture = uiSystem.Style.ScrollBarScrollerTexture = new NinePatch(Texture[3, 1], 4);
            uiSystem.Style.TextScale = 0.1F;
            uiSystem.TextFormatter.AddImage("ticket", Texture[2, 0]);
            uiSystem.TextFormatter.AddImage("star", Texture[3, 0]);
            foreach (var modifier in AttractionModifier.Modifiers.Values)
                uiSystem.TextFormatter.AddImage(modifier.Name, modifier.Texture);
        }

        public static IEnumerator<Wait> DisplaySplash(Action loadGame) {
            var splash = new Group(Anchor.TopLeft, Vector2.One, false) {
                OnDrawn = (e, time, batch, alpha) => batch.Draw(batch.GetBlankTexture(), e.DisplayArea, Color.Black * alpha)
            };
            var center = splash.AddChild(new Group(Anchor.Center, new Vector2(0.5F, 0.5F), false) {DrawAlpha = 0});
            center.AddChild(new Image(Anchor.AutoCenter, new Vector2(1, -1), Texture[4, 0]));
            center.AddChild(new Paragraph(Anchor.AutoCenter, 10000, "A game by Ellpeck", true));
            GameImpl.Instance.UiSystem.Add("Splash", splash).Priority = 100000;
            while (center.DrawAlpha < 1) {
                center.DrawAlpha += 0.015F;
                yield return new Wait(CoroutineEvents.Update);
            }
            yield return new Wait(0.5);

            var analyticsFlag = new FileInfo(Path.Combine(SaveHandler.GetGameDirectory(true).FullName, "_ReadGdpr"));
            if (!analyticsFlag.Exists) {
                var evt = new Event();
                var panel = splash.AddChild(new Panel(Anchor.Center, new Vector2(0.8F), Vector2.Zero, true));
                panel.AddChild(new Paragraph(Anchor.AutoLeft, 1, Localization.Get("GDPRInfo")));
                panel.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 30), Localization.Get("Okay")) {
                    OnPressed = e2 => {
                        // create the (empty) flag file
                        using (analyticsFlag.Create()) {
                        }
                        splash.RemoveChild(panel);
                        CoroutineHandler.RaiseEvent(evt);
                    }
                });
                yield return new Wait(evt);
            }

            yield return new Wait(0.25);
            loadGame();
            yield return new Wait(0.25);
            while (center.DrawAlpha > 0) {
                center.DrawAlpha -= 0.015F;
                yield return new Wait(CoroutineEvents.Update);
            }
            while (splash.DrawAlpha > 0) {
                splash.DrawAlpha -= 0.015F;
                yield return new Wait(CoroutineEvents.Update);
            }
            splash.System.Remove(splash.Root.Name);
        }

        private void FadeUi(bool fadeOut, Action after = null) {
            IEnumerator<Wait> Impl() {
                // disable input handling during fade
                this.uiSystem.Controls.HandleTouch = false;
                GameImpl.Instance.DrawMap = true;
                this.currentUi.IsHidden = false;
                var alpha = 1F;
                while (alpha > 0) {
                    alpha -= 0.03F;
                    this.currentUi.DrawAlpha = !fadeOut ? 1 - alpha : alpha;
                    yield return new Wait(CoroutineEvents.Update);
                }
                this.uiSystem.Controls.HandleTouch = true;
                GameImpl.Instance.DrawMap = fadeOut;
                this.currentUi.IsHidden = fadeOut;
                // disable horizontal and vertical drag on map view to allow free drag to take priority
                InputHandler.SetGesturesEnabled(!fadeOut, GestureType.HorizontalDrag, GestureType.VerticalDrag);
                if (!fadeOut) {
                    var map = GameImpl.Instance.Map;
                    map.PlacingAttraction = null;
                    map.SelectedPosition = null;
                    map.PlacingModifier = null;
                }
                after?.Invoke();
            }

            CoroutineHandler.Start(Impl());
        }

        private static void AddSelectedAttractionInfo(Element element) {
            var map = GameImpl.Instance.Map;
            element.AddChild(new Paragraph(Anchor.AutoCenter, 1, p => {
                if (map.SelectedPosition == null)
                    return string.Empty;
                var attraction = map.GetAttractionAt(map.SelectedPosition.Value);
                return string.Join(" ", attraction.Modifiers.Select(m => $"{m.Amount}<i {m.Modifier.Name}>"));
            }, true));
            element.AddChild(new Paragraph(Anchor.AutoCenter, 1, p => {
                if (map.SelectedPosition == null)
                    return string.Empty;
                var pos = map.SelectedPosition.Value;
                var attraction = map.GetAttractionAt(pos);
                return attraction.GetGenerationRate(map, pos).ToString("#,0.##") + "<i ticket>/s";
            }, true));
        }

        private static void PopulateUpgradeList(Element upgradeList) {
            upgradeList.RemoveChildren(c => !(c is ScrollBar));
            var reachedActive = false;
            foreach (var upgrade in Upgrade.Upgrades.Values.OrderBy(u => u.IsActive())) {
                // the first active upgrade should be preceded by a section separator paragraph
                if (!reachedActive && upgrade.IsActive()) {
                    reachedActive = true;
                    upgradeList.AddChild(new Paragraph(Anchor.AutoCenter, 1, Localization.Get("AppliedUpgrades"), true) {
                        PositionOffset = new Vector2(0, 4)
                    });
                }
                var button = upgradeList.AddChild(new Button(Anchor.AutoLeft, new Vector2(1)) {
                    SetHeightBasedOnChildren = true,
                    PositionOffset = new Vector2(0, 1),
                    ChildPadding = new Vector2(4),
                    OnPressed = e => {
                        GameImpl.Instance.Stars -= upgrade.Price;
                        GameImpl.Instance.Platform.AddResourceEvent(true, "Stars", upgrade.Price, "Upgrade", upgrade.Name);

                        GameImpl.Instance.AppliedUpgrades.Add(upgrade);
                        upgrade.OnApplied();
                        PopulateUpgradeList(upgradeList);
                    }
                });

                void HideAndDisable() {
                    button.IsHidden = upgrade.Dependencies.Any(u => !u.IsActive());
                    button.IsDisabled = upgrade.IsActive() || GameImpl.Instance.Stars < upgrade.Price;
                }

                button.OnUpdated += (e, time) => HideAndDisable();
                HideAndDisable();

                button.AddChild(new Image(Anchor.CenterLeft, new Vector2(0.2F, 40), upgrade.Texture) {
                    Padding = new Vector2(4)
                });
                var right = button.AddChild(new Group(Anchor.TopRight, new Vector2(0.8F, 1)) {CanBeMoused = false});
                right.AddChild(new Paragraph(Anchor.TopLeft, 1, Localization.Get(upgrade.Name), true));
                right.AddChild(new Paragraph(Anchor.TopRight, 1, p => upgrade.Price + "<i star>", true));
                right.AddChild(new Paragraph(Anchor.AutoLeft, 1, Localization.Get(upgrade.Name + "Description"), true) {TextScale = 0.08F});
            }
        }

        private static IEnumerator<Wait> WobbleElement(CustomDrawGroup element, float intensity = 0.02F) {
            var sin = 0F;
            while (sin < MathHelper.Pi) {
                element.ScaleOrigin(1 + (float) Math.Sin(sin) * intensity);
                sin += 0.2F;
                yield return new Wait(CoroutineEvents.Update);
            }
            element.Transform = Matrix.Identity;
        }

        private static string PrettyPrintNumber(BigInteger number) {
            if (number < 1000)
                return number.ToString();
            // thousands
            if (number < 1000000)
                return number.ToString("0,.##K");
            // millions
            if (number < 1000000000)
                return number.ToString("0,,.##M");
            // billions
            if (number < 1000000000000)
                return number.ToString("0,,,.##B");
            // trillions
            if (number < 1000000000000000)
                return number.ToString("0,,,,.##T");
            return number.ToString("0,,,,,.##Q");
        }

    }
}