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.Input.Touch;
using MLEM.Extensions;
using MLEM.Formatting.Codes;
using MLEM.Input;
using MLEM.Misc;
using MLEM.Sound;
using MLEM.Startup;
using MLEM.Textures;
using MLEM.Ui;
using MLEM.Ui.Elements;
using TouchyTickets.Attractions;
using TouchyTickets.Upgrades;
using Vector2 = Microsoft.Xna.Framework.Vector2;

namespace TouchyTickets; 

public class Ui {

    private static readonly BigInteger[] ExpoNums = Enumerable.Range(0, Localization.NumberFormat.Count).Select(i => BigInteger.Pow(1000, i + 1)).ToArray();
    private readonly UiSystem uiSystem;
    private readonly Element[] swipeRelations;
    private Element currentUi;
    private float swipeProgress;
    private bool finishingSwipe;

    public Ui(UiSystem uiSystem) {
        this.uiSystem = uiSystem;
        foreach (var modifier in AttractionModifier.Modifiers.Values)
            this.uiSystem.TextFormatter.AddImage(modifier.Name, Assets.UiTexture[modifier.Texture]);
        foreach (var upgrade in Upgrade.Upgrades.Values)
            this.uiSystem.TextFormatter.AddImage(upgrade.Name, Assets.UiTexture[upgrade.Texture]);

        // main ticket store ui
        var rainingTickets = new List<RainingTicket>();
        var main = new Group(Anchor.TopLeft, Vector2.One, false) {
            OnUpdated = (e, _) => {
                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, _, batch, alpha) => {
                batch.Draw(batch.GetBlankTexture(), e.DisplayArea, ColorHelper.FromHexRgb(0x86bccf) * 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, _ => currentNews, true) {
            OnUpdated = (e, _) => {
                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, _ => Ui.PrettyPrintNumber(GameImpl.Instance.Tickets) + "<i ticket>", true) {
            RegularFont = Assets.MonospacedFont,
            TextScale = 0.3F
        });
        ticketGroup.AddChild(new Paragraph(Anchor.AutoCenter, 1, _ => GameImpl.Instance.Map.TicketsPerSecond.ToString("#,0.##") + "<i ticket>/s", true));
        BigInteger lastTickets = 0;
        ActiveCoroutine storeWobble = null;
        var storeGroup = main.AddChild(new Group(Anchor.AutoCenter, new Vector2(1, 0.425F), false) {
            OnUpdated = (e, _) => {
                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(Ui.WobbleElement(e));
                }
            }
        });
        storeGroup.AddChild(new Image(Anchor.TopLeft, Vector2.One, Assets.UiTexture[0, 0, 2, 3]) {
            OnPressed = _ => {
                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) * Assets.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 = _ => {
                if (this.swipeProgress != 0)
                    return;
                var map = GameImpl.Instance.Map;
                var infoUi = new Group(Anchor.BottomLeft, new Vector2(1)) {CanBeMoused = false};
                Ui.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")) {
                    ActionSound = new SoundEffectInfo(Assets.PlaceSound),
                    OnPressed = _ => {
                        if (map.SelectedPosition == null)
                            return;
                        map.Remove(map.SelectedPosition.Value);
                        map.SelectedPosition = null;
                    },
                    OnUpdated = (e2, _) => ((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 Group(Anchor.TopLeft, Vector2.One, false) {
            IsHidden = true,
            OnDrawn = (e, _, batch, alpha) => batch.Draw(batch.GetBlankTexture(), e.DisplayArea, ColorHelper.FromHexRgb(0x86bccf) * alpha)
        };
        buyUi.AddChild(new Paragraph(Anchor.AutoCenter, 1, Localization.Get("Attractions"), true) {TextScale = 0.15F});
        var buyList = buyUi.AddChild(new Panel(Anchor.AutoLeft, Vector2.One, Vector2.Zero, false, true, false) {
            ChildPadding = new Padding(5, 15, 5, 5),
            PreventParentSpill = true
        });
        foreach (var attraction in AttractionType.Attractions) {
            BigInteger price = 0;
            var attractionAmount = 0;
            var button = buyList.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 40)) {
                ChildPadding = new Padding(4),
                PositionOffset = new Vector2(0, 1),
                OnPressed = _ => {
                    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 / Assets.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")) {
                        ActionSound = new SoundEffectInfo(Assets.PlaceSound),
                        OnPressed = e2 => {
                            GameImpl.Instance.Tickets -= price;

                            map.Place(map.PlacingPosition, map.PlacingAttraction);
                            map.PlacingAttraction.Wobble();
                            this.FadeUi(false, () => this.uiSystem.Remove(e2.Root.Name));
                        },
                        OnUpdated = (e2, _) => ((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 = _ => {
                    // 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 = (BigInteger) Math.Ceiling(attraction.Value.InitialPrice * Math.Pow(1 + 0.1F, attractionAmount));
                }
            });
            var image = button.AddChild(new Image(Anchor.CenterLeft, new Vector2(0.2F, 40), Assets.AttractionTexture[attraction.Value.TextureRegion]) {
                Padding = new Padding(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, _ => attraction.Value.GetGenerationRate() + "<i ticket>/s", true) {TextScale = 0.08F});
            right.AddChild(new Paragraph(Anchor.BottomRight, 1, _ => Ui.PrettyPrintNumber(price) + "<i ticket>", true));
            button.OnUpdated += (_, _) => {
                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 Group(Anchor.TopLeft, Vector2.One, false) {
            IsHidden = true,
            OnDrawn = (e, _, batch, alpha) => batch.Draw(batch.GetBlankTexture(), e.DisplayArea, ColorHelper.FromHexRgb(0x86bccf) * alpha)
        };
        modifierUi.AddChild(new Paragraph(Anchor.AutoCenter, 1, Localization.Get("Modifiers"), true) {TextScale = 0.15F});
        var modifierList = modifierUi.AddChild(new Panel(Anchor.AutoLeft, Vector2.One, Vector2.Zero, false, true, false) {
            ChildPadding = new Padding(5, 15, 5, 5),
            PreventParentSpill = true
        });
        foreach (var modifier in AttractionModifier.Modifiers.Values) {
            var button = modifierList.AddChild(new Button(Anchor.AutoLeft, new Vector2(1)) {
                SetHeightBasedOnChildren = true,
                PositionOffset = new Vector2(0, 1),
                ChildPadding = new Padding(4),
                OnPressed = _ => {
                    if (this.swipeProgress != 0)
                        return;
                    var map = GameImpl.Instance.Map;
                    map.PlacingModifier = modifier;
                    var infoUi = new Group(Anchor.BottomLeft, new Vector2(1)) {CanBeMoused = false};
                    Ui.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) {
                        ActionSound = new SoundEffectInfo(Assets.PlaceSound),
                        OnPressed = _ => {
                            if (map.SelectedPosition == null)
                                return;
                            var attraction = map.GetAttractionAt(map.SelectedPosition.Value);

                            var placeAmount = 1;
                            if (Upgrade.ModifierIncrease[2].IsActive()) {
                                placeAmount = 10;
                            } else if (Upgrade.ModifierIncrease[1].IsActive()) {
                                placeAmount = 5;
                            } else if (Upgrade.ModifierIncrease[0].IsActive()) {
                                placeAmount = 3;
                            }

                            for (var i = 0; i < placeAmount; i++) {
                                if (!map.PlacingModifier.Buy(attraction))
                                    break;
                            }
                            attraction.Wobble();
                        },
                        OnUpdated = (e2, _) => {
                            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 = _ => {
                        BigInteger 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 Ui.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), Assets.UiTexture[modifier.Texture]) {
                Padding = new Padding(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, _ => Ui.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 += (_, _) => {
                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;
            };
        }
        modifierList.OnAreaUpdated += _ => {
            // sort children by whether they should be shown or not
            modifierList.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, _, batch, alpha) => batch.Draw(batch.GetBlankTexture(), e.DisplayArea, ColorHelper.FromHexRgb(0x86bccf) * alpha)
        };
        var upgradeHeader = upgradeUi.AddChild(new Group(Anchor.AutoLeft, new Vector2(1)));
        upgradeHeader.AddChild(new Paragraph(Anchor.AutoCenter, 1, _ => 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, _) => ((Button) e).IsDisabled = GameImpl.Instance.GetBuyableStars() <= 0,
            OnPressed = _ => {
                var infoBox = new Group(Anchor.TopLeft, Vector2.One, false) {
                    OnDrawn = (e2, _, batch, _) => 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")) {
                    ActionSound = new SoundEffectInfo(Assets.BuySound),
                    OnPressed = e2 => {
                        this.uiSystem.Remove(e2.Root.Name);

                        var game = GameImpl.Instance;
                        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, _ => string.Format(Localization.Get("RequiresTickets"), Ui.PrettyPrintNumber(GameImpl.Instance.GetStarPrice())), true) {
            PositionOffset = new Vector2(0, 2)
        });
        var upgradeList = upgradeUi.AddChild(new Panel(Anchor.AutoLeft, new Vector2(1), Vector2.Zero, false, true, false) {
            ChildPadding = new Padding(5, 15, 5, 5),
            PreventParentSpill = true
        });
        Ui.PopulateUpgradeList(upgradeList);
        this.uiSystem.Add("Upgrade", upgradeUi);

        // options ui
        var optionsUi = new Group(Anchor.TopLeft, Vector2.One, false) {
            IsHidden = true,
            OnDrawn = (e, _, batch, alpha) => batch.Draw(batch.GetBlankTexture(), e.DisplayArea, ColorHelper.FromHexRgba(0x86bccf) * alpha)
        };
        optionsUi.AddChild(new Paragraph(Anchor.AutoCenter, 1, Localization.Get("Options"), true) {TextScale = 0.15F});
        var optionList = optionsUi.AddChild(new Panel(Anchor.AutoLeft, new Vector2(1), Vector2.Zero, false, true, false) {
            ChildPadding = new Padding(5, 15, 5, 5),
            PreventParentSpill = true
        });

        optionList.AddChild(new Paragraph(Anchor.AutoCenter, 1, Localization.Get("GameplayOptions"), true) {
            TextScale = 0.12F
        });
        optionList.AddChild(new Checkbox(Anchor.AutoLeft, new Vector2(1, 20), Localization.Get("AutoBuyEnabled"), Options.Instance.AutoBuyEnabled) {
            OnCheckStateChange = (_, value) => {
                Options.Instance.AutoBuyEnabled = value;
                Options.Save();
            },
            PositionOffset = new Vector2(0, 1)
        });
        optionList.AddChild(new Paragraph(Anchor.AutoLeft, 1, Localization.Get("MinTicketsForAutoBuy") + ":"));
        var num = optionList.AddChild(ElementHelper.NumberField(Anchor.AutoLeft, new Vector2(1, 20), Options.Instance.MinTicketsForAutoBuy, 1000, null, (_, value) => {
            if (int.TryParse(value, out var ret)) {
                Options.Instance.MinTicketsForAutoBuy = ret;
                Options.Save();
            }
        }));
        num.PositionOffset = new Vector2(0, 1);

        optionList.AddChild(new Paragraph(Anchor.AutoCenter, 1, Localization.Get("OtherOptions"), true) {
            PositionOffset = new Vector2(0, 10),
            TextScale = 0.12F
        });
        optionList.AddChild(new Paragraph(Anchor.AutoLeft, 1, _ => 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 = (_, v) => {
                Options.Instance.RainingTicketLimit = (int) v;
                Options.Save();
            }
        });
        optionList.AddChild(new Paragraph(Anchor.AutoLeft, 1, _ => Localization.Get("SoundVolume") + ": " + (int) (Options.Instance.SoundVolume * 100)));
        optionList.AddChild(new Slider(Anchor.AutoLeft, new Vector2(1, 20), 10, 100) {
            PositionOffset = new Vector2(0, 1),
            CurrentValue = Options.Instance.SoundVolume * 100,
            OnValueChanged = (_, v) => {
                Options.Instance.SoundVolume = v / 100;
                Options.Save();
            }
        });
        optionList.AddChild(new Checkbox(Anchor.AutoLeft, new Vector2(1, 20), Localization.Get("WhileYouWereAwayMessage"), Options.Instance.WhileYouWereAwayMessage) {
            PositionOffset = new Vector2(0, 1),
            OnCheckStateChange = (_, value) => {
                Options.Instance.WhileYouWereAwayMessage = value;
                Options.Save();
            }
        });
        optionList.AddChild(new Checkbox(Anchor.AutoLeft, new Vector2(1, 20), Localization.Get("KeepScreenOn"), Options.Instance.KeepScreenOn) {
            PositionOffset = new Vector2(0, 1),
            OnCheckStateChange = (_, value) => {
                Options.Instance.KeepScreenOn = value;
                Options.Save();
            }
        });
        this.uiSystem.Add("Options", optionsUi);

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

        var swipeTimer = 0D;
        var swipeInfo = new Group(Anchor.BottomCenter, Vector2.One) {
            PositionOffset = new Vector2(0, 10),
            SetWidthBasedOnChildren = true,
            CanBeMoused = false,
            OnUpdated = (e, _) => {
                if (this.swipeProgress == 0) {
                    if (swipeTimer > 0) {
                        swipeTimer -= 0.04F;
                    } else if (e.DrawAlpha > 0) {
                        e.DrawAlpha -= 0.05F;
                    }
                } else {
                    swipeTimer = 1;
                    if (e.DrawAlpha < 1)
                        e.DrawAlpha += 0.05F;
                }
            }
        };
        foreach (var ui in this.swipeRelations) {
            var tex = new TextureRegion(GameImpl.Instance.SpriteBatch.GetBlankTexture());
            swipeInfo.AddChild(new Image(Anchor.AutoInlineIgnoreOverflow, new Vector2(12, 6), tex) {
                MaintainImageAspect = false,
                Padding = new Padding(1),
                OnUpdated = (e, _) => ((Image) e).DrawAlpha = this.currentUi == ui ? 1 : 0.35F
            });
        }
        this.uiSystem.Add("SwipeInfo", swipeInfo).Priority = 200;
    }

    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 = Assets.Font;
        uiSystem.Style.PanelTexture = uiSystem.Style.TextFieldTexture = uiSystem.Style.ScrollBarBackground = uiSystem.Style.CheckboxTexture = new NinePatch(Assets.UiTexture[2, 1], 4);
        uiSystem.Style.ButtonTexture = uiSystem.Style.ScrollBarScrollerTexture = new NinePatch(Assets.UiTexture[3, 1], 4);
        uiSystem.Style.CheckboxCheckmark = Assets.UiTexture[4, 1];
        uiSystem.Style.TextScale = 0.1F;
        uiSystem.Style.ActionSound = new SoundEffectInfo(Assets.ClickSound, 0.5F);
        uiSystem.TextFormatter.AddImage("ticket", Assets.UiTexture[2, 0]);
        uiSystem.TextFormatter.AddImage("star", Assets.UiTexture[3, 0]);
    }

    public static IEnumerator<Wait> DisplaySplash(Action loadGame) {
        var splash = new Group(Anchor.TopLeft, Vector2.One, false) {
            OnDrawn = (e, _, batch, alpha) => batch.Draw(batch.GetBlankTexture(), e.DisplayArea, Color.Black * alpha)
        };
        var content = splash.AddChild(new Group(Anchor.TopLeft, Vector2.One, false) {DrawAlpha = 0});
        var center = content.AddChild(new Group(Anchor.Center, new Vector2(0.5F, 0.5F), false));
        center.AddChild(new Image(Anchor.AutoCenter, new Vector2(1, -1), Assets.UiTexture[4, 0]));
        center.AddChild(new Paragraph(Anchor.AutoCenter, 10000, Localization.Get("AGameByEllpeck"), true));
        content.AddChild(new Paragraph(Anchor.BottomCenter, 1, Localization.Get("TranslationBy"), true) {
            PositionOffset = new Vector2(0, 15),
            TextScale = 0.08F
        });
        GameImpl.Instance.UiSystem.Add("Splash", splash).Priority = 100000;
        while (content.DrawAlpha < 1) {
            content.DrawAlpha += 0.015F;
            yield return new Wait(CoroutineEvents.Update);
        }
        yield return new Wait(0.5);

        yield return new Wait(0.25);
        loadGame();
        yield return new Wait(0.25);
        while (content.DrawAlpha > 0) {
            content.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);
    }

    public static void DisplayWhileYouWereAway(TimeSpan passed, BigInteger ticketGen, double ticketPerSecondGen) {
        if (ticketGen <= 0 && ticketPerSecondGen <= 0)
            return;
        var infoBox = new Group(Anchor.TopLeft, Vector2.One, false) {
            OnDrawn = (e2, _, batch, _) => 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));
        var text = string.Format(Localization.Get("WhileYouWereAway"), passed.TotalMinutes.ToString("0.#"));
        if (ticketGen > 0)
            text += " " + string.Format(Localization.Get("WhileYouWereAwayTickets"), Ui.PrettyPrintNumber(ticketGen));
        if (ticketPerSecondGen > 0)
            text += " " + string.Format(Localization.Get("WhileYouWereAwayTps"), ticketPerSecondGen.ToString("#,0.##"));
        panel.AddChild(new Paragraph(Anchor.AutoLeft, 1, text));
        panel.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 30), Localization.Get("Okay")) {
            OnPressed = e2 => e2.System.Remove(e2.Root.Name)
        });
        GameImpl.Instance.UiSystem.Add("WhileYouWereAway", infoBox);
    }

    public static void DisplayRatePlease() {
        var infoBox = new Group(Anchor.TopLeft, Vector2.One, false) {
            OnDrawn = (e2, _, batch, _) => 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("RateInfo"))));
        panel.AddChild(new Button(Anchor.AutoLeft, new Vector2(0.5F, 30), Localization.Get("Back")) {
            OnPressed = e2 => e2.System.Remove(e2.Root.Name)
        });
        panel.AddChild(new Button(Anchor.AutoInlineIgnoreOverflow, new Vector2(0.5F, 30), Localization.Get("Rate")) {
            OnPressed = e2 => {
                GameImpl.Instance.Platform.OpenRateLink();
                e2.System.Remove(e2.Root.Name);
            }
        });
        GameImpl.Instance.UiSystem.Add("RatePlease", infoBox);
    }

    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, _ => {
            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, _ => {
            if (map.SelectedPosition == null)
                return string.Empty;
            var pos = map.SelectedPosition.Value;
            var attraction = map.GetAttractionAt(pos);
            // beautiful
            return string.Join(" ", Upgrade.Upgrades.Values
                .Select(u => {
                    var multiplier =
                        u is ModifierUpgrade mod ? mod.GetCurrentMultiplier(attraction.Type) :
                        u is NeighborModifierUpgrade neighbor ? neighbor.GetCurrentMultiplier(attraction, map, pos) : 1;
                    return multiplier > 1 ? $"<i {u.Name}> x{multiplier}" : null;
                }).Where(s => s != null));
        }, true));
        element.AddChild(new Paragraph(Anchor.AutoCenter, 1, _ => {
            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),
                    TextScale = 0.12F
                });
            }
            var button = upgradeList.AddChild(new Button(Anchor.AutoLeft, new Vector2(1)) {
                SetHeightBasedOnChildren = true,
                PositionOffset = new Vector2(0, 1),
                ChildPadding = new Padding(4),
                ActionSound = new SoundEffectInfo(Assets.BuySound),
                OnPressed = _ => {
                    GameImpl.Instance.Stars -= upgrade.Price;
                    GameImpl.Instance.AppliedUpgrades.Add(upgrade);
                    upgrade.OnApplied();
                    Ui.PopulateUpgradeList(upgradeList);
                }
            });

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

            button.OnUpdated += (_, _) => HideAndDisable();
            HideAndDisable();

            button.AddChild(new Image(Anchor.CenterLeft, new Vector2(0.2F, 40), Assets.UiTexture[upgrade.Texture]) {
                Padding = new Padding(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));
            if (!reachedActive)
                right.AddChild(new Paragraph(Anchor.TopRight, 1, _ => 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(Element element, float intensity = 0.02F) {
        var sin = 0F;
        while (sin < MathHelper.Pi) {
            element.ScaleTransform(1 + (float) Math.Sin(sin) * intensity);
            sin += 0.2F;
            yield return new Wait(CoroutineEvents.Update);
        }
        element.Transform = Matrix.Identity;
    }

    public static string PrettyPrintNumber(BigInteger number) {
        for (var i = 0; i < Localization.NumberFormat.Count; i++) {
            if (number < Ui.ExpoNums[i])
                return number.ToString(Localization.NumberFormat[i]);
        }
        // if the number is too large, just return the highest possible
        return number.ToString(Localization.NumberFormat.Last());
    }

}