using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
#if DEBUG
using System.IO;
#else
using Barotrauma.IO;
#endif

namespace Barotrauma
{
    class SpriteEditorScreen : EditorScreen
    {
        private GUIListBox textureList, spriteList;

        private GUIFrame topPanel;
        private GUIFrame leftPanel;
        private GUIFrame rightPanel;
        private GUIFrame bottomPanel;
        private GUIFrame backgroundColorPanel;

        private bool drawGrid, snapToGrid;

        private GUIFrame topPanelContents;
        private GUITextBlock texturePathText;
        private GUITextBlock xmlPathText;
        private GUIScrollBar zoomBar;
        private readonly List<Sprite> selectedSprites = new List<Sprite>();
        private readonly List<Sprite> dirtySprites = new List<Sprite>();
        private Texture2D SelectedTexture => lastSprite?.Texture;
        private Sprite lastSprite;
        private string selectedTexturePath;

        private Rectangle textureRect;
        private float zoom = 1;
        private const float MinZoom = 0.25f, MaxZoom = 10.0f;

        private GUITextBox filterSpritesBox;
        private GUITextBlock filterSpritesLabel;
        private GUITextBox filterTexturesBox;
        private GUITextBlock filterTexturesLabel;

        private LocalizedString originLabel, positionLabel, sizeLabel;

        private bool editBackgroundColor;
        private Color backgroundColor = new Color(0.051f, 0.149f, 0.271f, 1.0f);

        private bool ControlDown => PlayerInput.KeyDown(Keys.LeftControl) || PlayerInput.KeyDown(Keys.RightControl);

        private readonly Camera cam;
        public override Camera Cam
        {
            get { return cam; }
        }

        public GUIComponent TopPanel
        {
            get { return topPanel; }
        }

        public SpriteEditorScreen()
        {
            cam = new Camera();
            GameMain.Instance.ResolutionChanged += CreateUI;
            CreateUI();
        }

        #region Initialization
        private void CreateUI()
        {
            originLabel = TextManager.Get("charactereditor.origin");
            positionLabel = TextManager.GetWithVariable("charactereditor.position", "[coordinates]", string.Empty);
            sizeLabel = TextManager.Get("charactereditor.size");

            topPanel = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), Frame.RectTransform) { MinSize = new Point(0, 60) }, "GUIFrameTop");
            topPanelContents = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.8f), topPanel.RectTransform, Anchor.Center), style: null);

            new GUIButton(new RectTransform(new Vector2(0.14f, 0.4f), topPanelContents.RectTransform, Anchor.TopLeft)
            {
                RelativeOffset = new Vector2(0, 0.1f)
            }, TextManager.Get("spriteeditor.reloadtexture"))
            {
                OnClicked = (button, userData) =>
                {
                    var selected = selectedSprites.ToList();
                    Sprite firstSelected = selected.First();
                    selected.ForEach(s => s.ReloadTexture());
                    RefreshLists();
                    textureList.Select(firstSelected.FullPath, autoScroll: GUIListBox.AutoScroll.Disabled);
                    selected.ForEachMod(s => spriteList.Select(s, autoScroll: GUIListBox.AutoScroll.Disabled));
                    texturePathText.Text = TextManager.GetWithVariable("spriteeditor.texturesreloaded", "[filepath]", firstSelected.FilePath.Value);
                    texturePathText.TextColor = GUIStyle.Green;
                    return true;
                }
            };
            new GUIButton(new RectTransform(new Vector2(0.14f, 0.4f), topPanelContents.RectTransform, Anchor.BottomLeft)
            {
                RelativeOffset = new Vector2(0, 0.1f)
            }, TextManager.Get("spriteeditor.resetchanges"))
            {
                OnClicked = (button, userData) =>
                {
                    if (SelectedTexture == null) { return false; }
                    foreach (Sprite sprite in loadedSprites)
                    {
                        if (sprite.FullPath != selectedTexturePath) { continue; }
                        var element = sprite.SourceElement;
                        if (element == null) { continue; }
                        // Not all sprites have a sourcerect defined, in which case we'll want to use the current source rect instead of an empty rect.
                        sprite.SourceRect = element.GetAttributeRect("sourcerect", sprite.SourceRect);
                        sprite.RelativeOrigin = element.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f));
                    }
                    ResetWidgets();
                    xmlPathText.Text = TextManager.Get("spriteeditor.resetsuccessful");
                    xmlPathText.TextColor = GUIStyle.Green;
                    return true;
                }
            };
            new GUIButton(new RectTransform(new Vector2(0.14f, 0.4f), topPanelContents.RectTransform, Anchor.TopLeft)
            {
                RelativeOffset = new Vector2(0.15f, 0.1f)
            }, TextManager.Get("spriteeditor.saveselectedsprites"))
            {
                OnClicked = (button, userData) =>
                {
                    return SaveSprites(selectedSprites);
                }
            };
            new GUIButton(new RectTransform(new Vector2(0.14f, 0.4f), topPanelContents.RectTransform, Anchor.BottomLeft)
            {
                RelativeOffset = new Vector2(0.15f, 0.1f)
            }, TextManager.Get("spriteeditor.saveallsprites"))
            {
                OnClicked = (button, userData) =>
                {
                    return SaveSprites(loadedSprites);
                }
            };

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

            new GUITextBlock(new RectTransform(new Vector2(0.2f, 0.2f), topPanelContents.RectTransform, Anchor.TopCenter, Pivot.CenterRight) { RelativeOffset = new Vector2(0, 0.3f) }, TextManager.Get("spriteeditor.zoom"));
            zoomBar = new GUIScrollBar(new RectTransform(new Vector2(0.2f, 0.35f), topPanelContents.RectTransform, Anchor.TopCenter, Pivot.CenterRight)
            {
                RelativeOffset = new Vector2(0.05f, 0.3f)
            }, style: "GUISlider", barSize: 0.1f)
            {
                BarScroll = GetBarScrollValue(),
                Step = 0.01f,
                OnMoved = (scrollBar, value) =>
                {
                    zoom = MathHelper.Lerp(MinZoom, MaxZoom, value);
                    viewAreaOffset = Point.Zero;
                    return true;
                }
            };
            var resetBtn = new GUIButton(new RectTransform(new Vector2(0.05f, 0.35f), topPanelContents.RectTransform, Anchor.TopCenter, Pivot.CenterLeft) { RelativeOffset = new Vector2(0.055f, 0.3f) }, TextManager.Get("spriteeditor.resetzoom"))
            {
                OnClicked = (box, data) =>
                {
                    ResetZoom();
                    return true;
                }
            };
            resetBtn.TextBlock.AutoScaleHorizontal = true;

            new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), topPanelContents.RectTransform, Anchor.BottomCenter, Pivot.CenterRight) { RelativeOffset = new Vector2(0, 0.3f) }, TextManager.Get("spriteeditor.showgrid"))
            {
                Selected = drawGrid,
                OnSelected = (tickBox) =>
                {
                    drawGrid = tickBox.Selected;
                    return true;
                }
            };
            new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), topPanelContents.RectTransform, Anchor.BottomCenter, Pivot.CenterRight) { RelativeOffset = new Vector2(0.17f, 0.3f) }, TextManager.Get("spriteeditor.snaptogrid"))
            {
                Selected = snapToGrid,
                OnSelected = (tickBox) =>
                {
                    snapToGrid = tickBox.Selected;
                    return true;
                }
            };

            texturePathText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.4f), topPanelContents.RectTransform, Anchor.Center, Pivot.BottomCenter) { RelativeOffset = new Vector2(0.4f, 0) }, "", Color.LightGray);
            xmlPathText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.4f), topPanelContents.RectTransform, Anchor.Center, Pivot.TopCenter) { RelativeOffset = new Vector2(0.4f, 0) }, "", Color.LightGray);

            leftPanel = new GUIFrame(new RectTransform(new Vector2(0.25f, 1.0f - topPanel.RectTransform.RelativeSize.Y), Frame.RectTransform, Anchor.BottomLeft)
            { MinSize = new Point(150, 0) }, style: "GUIFrameLeft");
            var paddedLeftPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), leftPanel.RectTransform, Anchor.Center))
            { RelativeSpacing = 0.01f, Stretch = true };

            var filterArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.03f), paddedLeftPanel.RectTransform) { MinSize = new Point(0, 20) }, isHorizontal: true)
            {
                Stretch = true,
                UserData = "filterarea"
            };
            filterTexturesLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUIStyle.Font, textAlignment: Alignment.CenterLeft) { IgnoreLayoutGroups = true }; ;
            filterTexturesBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUIStyle.Font, createClearButton: true);
            filterArea.RectTransform.MinSize = filterTexturesBox.RectTransform.MinSize;
            filterTexturesBox.OnTextChanged += (textBox, text) => { FilterTextures(text); return true; };

            textureList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), paddedLeftPanel.RectTransform))
            {
                PlaySoundOnSelect = true,
                OnSelected = (listBox, userData) =>
                {
                    var newTexturePath = userData as string;
                    if (selectedTexturePath == null || selectedTexturePath != newTexturePath)
                    {
                        selectedTexturePath = newTexturePath;
                        ResetZoom();
                        spriteList.Select(loadedSprites.First(s => s.FilePath == selectedTexturePath), autoScroll: GUIListBox.AutoScroll.Disabled);
                        UpdateScrollBar(spriteList);
                    }
                    foreach (GUIComponent child in spriteList.Content.Children)
                    {
                        var textBlock = (GUITextBlock)child;
                        var sprite = (Sprite)textBlock.UserData;
                        textBlock.TextColor = new Color(textBlock.TextColor, sprite.FilePath == selectedTexturePath ? 1.0f : 0.4f);
                        if (sprite.FilePath == selectedTexturePath) { textBlock.Visible = true; }
                    }
                    texturePathText.TextColor = Color.LightGray;
                    topPanelContents.Visible = true;
                    return true;
                }
            };

            rightPanel = new GUIFrame(new RectTransform(new Vector2(0.25f, 1.0f - topPanel.RectTransform.RelativeSize.Y), Frame.RectTransform, Anchor.BottomRight) { MinSize = new Point(150, 0) }, style: "GUIFrameRight");
            var paddedRightPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), rightPanel.RectTransform, Anchor.Center))
            {
                Stretch = true,
                RelativeSpacing = 0.01f
            };

            filterArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.03f), paddedRightPanel.RectTransform) { MinSize = new Point(0, 20) }, isHorizontal: true)
            {
                Stretch = true,
                UserData = "filterarea"
            };
            filterSpritesLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUIStyle.Font, textAlignment: Alignment.CenterLeft) { IgnoreLayoutGroups = true };
            filterSpritesBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUIStyle.Font, createClearButton: true);
            filterArea.RectTransform.MinSize = filterSpritesBox.RectTransform.MinSize;
            filterSpritesBox.OnTextChanged += (textBox, text) => { FilterSprites(text); return true; };

            spriteList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), paddedRightPanel.RectTransform))
            {
                PlaySoundOnSelect = true,
                OnSelected = (listBox, userData) =>
                {
                    if (userData is Sprite sprite)
                    {
                        SelectSprite(sprite);
                        return true;
                    }
                    return false;
                }
            };

            // Background color
            bottomPanel = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.05f), Frame.RectTransform, Anchor.BottomCenter), style: null, color: Color.Black * 0.5f);
            new GUITickBox(new RectTransform(new Vector2(0.2f, 0.5f), bottomPanel.RectTransform, Anchor.Center), TextManager.Get("charactereditor.editbackgroundcolor"))
            {
                Selected = editBackgroundColor,
                OnSelected = box =>
                {
                    editBackgroundColor = box.Selected;
                    return true;
                }
            };
            backgroundColorPanel = new GUIFrame(new RectTransform(new Point(400, 80), Frame.RectTransform, Anchor.BottomCenter) { RelativeOffset = new Vector2(0, 0.1f) }, style: null, color: Color.Black * 0.4f);
            new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), backgroundColorPanel.RectTransform) { MinSize = new Point(80, 26) }, TextManager.Get("spriteeditor.backgroundcolor"), textColor: Color.WhiteSmoke);
            var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 1), backgroundColorPanel.RectTransform, Anchor.TopRight)
            {
                AbsoluteOffset = new Point(20, 0)
            }, isHorizontal: true, childAnchor: Anchor.CenterRight)
            {
                Stretch = true,
                RelativeSpacing = 0.01f
            };
            var fields = new GUIComponent[4];
            LocalizedString[] colorComponentLabels = { TextManager.Get("spriteeditor.colorcomponentr"), TextManager.Get("spriteeditor.colorcomponentg"), TextManager.Get("spriteeditor.colorcomponentb") };
            for (int i = 2; i >= 0; i--)
            {
                var element = new GUIFrame(new RectTransform(new Vector2(0.2f, 1), inputArea.RectTransform)
                {
                    MinSize = new Point(40, 0),
                    MaxSize = new Point(100, 50)
                }, style: null, color: Color.Black * 0.6f);
                var colorLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), colorComponentLabels[i],
                    font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft);
                var numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), NumberType.Int)
                {
                    Font = GUIStyle.SmallFont
                };
                numberInput.MinValueInt = 0;
                numberInput.MaxValueInt = 255;
                numberInput.Font = GUIStyle.SmallFont;
                switch (i)
                {
                    case 0:
                        colorLabel.TextColor = GUIStyle.Red;
                        numberInput.IntValue = backgroundColor.R;
                        numberInput.OnValueChanged += (numInput) => backgroundColor.R = (byte)(numInput.IntValue);
                        break;
                    case 1:
                        colorLabel.TextColor = GUIStyle.Green;
                        numberInput.IntValue = backgroundColor.G;
                        numberInput.OnValueChanged += (numInput) => backgroundColor.G = (byte)(numInput.IntValue);
                        break;
                    case 2:
                        colorLabel.TextColor = Color.DeepSkyBlue;
                        numberInput.IntValue = backgroundColor.B;
                        numberInput.OnValueChanged += (numInput) => backgroundColor.B = (byte)(numInput.IntValue);
                        break;
                }
            }
        }

        private readonly HashSet<Sprite> loadedSprites = new HashSet<Sprite>();
        private void LoadSprites()
        {
            loadedSprites.ForEach(s => s.Remove());
            loadedSprites.Clear();
            var contentPackages = ContentPackageManager.EnabledPackages.All.ToList();

#if !DEBUG
            var vanilla = GameMain.VanillaContent;
            if (vanilla != null)
            {
                contentPackages.Remove(vanilla);
            }
#endif
            foreach (var contentPackage in contentPackages)
            {
                foreach (var file in contentPackage.Files)
                {
                    if (file.Path.EndsWith(".xml"))
                    {
                        XDocument doc = XMLExtensions.TryLoadXml(file.Path);
                        if (doc != null)
                        {
                            LoadSprites(doc.Root.FromPackage(file.Path.ContentPackage));
                        }
                    }
                }
            }

            void LoadSprites(ContentXElement element)
            {
                string[] spriteElementNames = {
                    "Sprite",
                    "DeformableSprite",
                    "BackgroundSprite",
                    "BrokenSprite",
                    "ContainedSprite",
                    "InventoryIcon",
                    "Icon",
                    "VineSprite",
                    "LeafSprite",
                    "FlowerSprite",
                    "DecorativeSprite",
                    "BarrelSprite",
                    "RailSprite",
                    "ChargeSprite",
                    "SchematicSprite",
                    "WeldedSprite"
                };

                foreach (string spriteElementName in spriteElementNames)
                {
                    element.GetChildElements(spriteElementName).ForEach(s => CreateSprite(s));
                }

                element.Elements().ForEach(e => LoadSprites(e));
            }

            void CreateSprite(ContentXElement element)
            {
                //empty element, probably an item variant?
                if (element.Attributes().None()) { return; }

                string spriteFolder = "";
                ContentPath texturePath = null;
                
                if (element.GetAttribute("texture") != null)
                {
                    texturePath = element.GetAttributeContentPath("texture");
                }
                else
                {
                    if (element.Name.ToString().ToLower() == "vinesprite")
                    {
                        texturePath = element.Parent.GetAttributeContentPath("vineatlas");
                    }
                }
                if (texturePath.IsNullOrEmpty()) { return; }

                // TODO: parse and create?
                if (texturePath.Value.Contains("[GENDER]") || texturePath.Value.Contains("[HEADID]") || texturePath.Value.Contains("[RACE]") || texturePath.Value.Contains("[VARIANT]")) { return; }
                if (!texturePath.Value.Contains("/"))
                {
                    var parsedPath = element.ParseContentPathFromUri();
                    spriteFolder = Path.GetDirectoryName(parsedPath);
                }
                // Uncomment if we do multiple passes -> there can be duplicates
                //string identifier = Sprite.GetID(element);
                //if (loadedSprites.None(s => s.ID == identifier))
                //{
                //    loadedSprites.Add(new Sprite(element, spriteFolder));
                //}
                loadedSprites.Add(new Sprite(element, spriteFolder, texturePath.Value, lazyLoad: true));
            }
        }

        private bool SaveSprites(IEnumerable<Sprite> sprites)
        {
            if (SelectedTexture == null) { return false; }
            if (sprites.None()) { return false; }
            HashSet<XDocument> docsToSave = new HashSet<XDocument>();
            foreach (Sprite sprite in sprites)
            {
                if (sprite.FullPath != selectedTexturePath) { continue; }
                var element = sprite.SourceElement;
                if (element == null) { continue; }
                element.SetAttributeValue("sourcerect", XMLExtensions.RectToString(sprite.SourceRect));
                element.SetAttributeValue("origin", XMLExtensions.Vector2ToString(sprite.RelativeOrigin));

                /*if (element.Attribute("slice") != null)
                {
                    Rectangle slice = new Rectangle(
                        sprite.SourceRect.X + 5, 
                        sprite.SourceRect.Y + 5,
                        sprite.SourceRect.Right - 5,
                        sprite.SourceRect.Bottom - 5);
                    element.SetAttributeValue("slice", XMLExtensions.RectToString(slice));
                }*/
                docsToSave.Add(element.Document);
            }
            xmlPathText.Text = TextManager.Get("spriteeditor.allchangessavedto");
            foreach (XDocument doc in docsToSave)
            {
                string xmlPath = doc.ParseContentPathFromUri();
                xmlPathText.Text += "\n" + xmlPath;
#if DEBUG
                doc.Save(xmlPath);
#else
                doc.SaveSafe(xmlPath);
#endif
            }
            xmlPathText.TextColor = GUIStyle.Green;
            return true;
        }
#endregion

        #region Public methods
        public override void AddToGUIUpdateList()
        {
            leftPanel.AddToGUIUpdateList();
            rightPanel.AddToGUIUpdateList();
            topPanel.AddToGUIUpdateList();
            bottomPanel.AddToGUIUpdateList();
            if (editBackgroundColor)
            {
                backgroundColorPanel.AddToGUIUpdateList();
            }
        }

        public override void Update(double deltaTime)
        {
            base.Update(deltaTime);
            Widget.EnableMultiSelect = ControlDown;
            spriteList.SelectMultiple = Widget.EnableMultiSelect;
            // Select rects with the mouse
            if (Widget.SelectedWidgets.None() || Widget.EnableMultiSelect)
            {
                if (SelectedTexture != null && GUI.MouseOn == null)
                {
                    foreach (Sprite sprite in loadedSprites)
                    {
                        if (sprite.FullPath != selectedTexturePath) { continue; }
                        if (PlayerInput.PrimaryMouseButtonClicked())
                        {
                            var scaledRect = new Rectangle(textureRect.Location + sprite.SourceRect.Location.Multiply(zoom), sprite.SourceRect.Size.Multiply(zoom));
                            if (scaledRect.Contains(PlayerInput.MousePosition))
                            {
                                spriteList.Select(sprite, autoScroll: GUIListBox.AutoScroll.Disabled);
                                UpdateScrollBar(spriteList);
                                UpdateScrollBar(textureList);
                                // Release the keyboard so that we can nudge the source rects
                                GUI.KeyboardDispatcher.Subscriber = null;
                            }
                        }
                    }
                }
            }
            if (GUI.MouseOn == null)
            {
                if (PlayerInput.ScrollWheelSpeed != 0)
                {
                    float newZoom = MathHelper.Clamp(zoom + PlayerInput.ScrollWheelSpeed * (float)deltaTime * 0.05f * zoom, MinZoom, MaxZoom);
                    float zoomDeltaPrc = ((newZoom - zoom) / zoom);
                    zoom = newZoom;
                    zoomBar.BarScroll = GetBarScrollValue();
                    
                    // modify view area offset as well when zooming, to zoom into mouse cursor position
                    Point mouseToViewAreaScreenCenterDelta = (GetViewArea.Center - viewAreaOffset) - PlayerInput.MousePosition.ToPoint();
                    Vector2 mouseDelta = mouseToViewAreaScreenCenterDelta.ToVector2();

                    Vector2 newViewAreaOffset = viewAreaOffset.ToVector2();
                    newViewAreaOffset += (mouseDelta + newViewAreaOffset) * zoomDeltaPrc;
                    viewAreaOffset = newViewAreaOffset.ToPoint();
                }
                widgets.Values.ForEach(w => w.Update((float)deltaTime));
                if (PlayerInput.MidButtonHeld())
                {
                    // "Camera" Pan
                    Vector2 moveSpeed = PlayerInput.MouseSpeed * (float)deltaTime * 100.0f;
                    viewAreaOffset += moveSpeed.ToPoint();
                }
            }
            if (GUI.KeyboardDispatcher.Subscriber == null)
            {
                if (PlayerInput.KeyDown(Keys.LeftControl) && PlayerInput.KeyHit(Keys.C))
                {
                    string text = "";
                    if (selectedSprites.Count == 1)
                    {
                        var selectedSprite = selectedSprites.First();
                        if (selectedSprite.SourceElement != null)
                        {
                            string sourceRectText = $"sourcerect=\"{XMLExtensions.RectToString(selectedSprite.SourceRect)}\"";
                            text += $"{sourceRectText}";
                        }
                    }
                    else
                    {
                        foreach (var selectedSprite in selectedSprites)
                        {
                            if (selectedSprite.SourceElement == null) { continue; }
                            XElement xElement = new XElement(selectedSprite.SourceElement.Element);
                            xElement.SetAttributeValue("sourcerect", XMLExtensions.RectToString(selectedSprite.SourceRect));
                            xElement.SetAttributeValue("origin", XMLExtensions.Vector2ToString(selectedSprite.RelativeOrigin));
                            text += $"{xElement}";
                            if (selectedSprites.Last() != selectedSprite)
                            {
                                text += Environment.NewLine;
                            }
                        }
                    }
                    
                    Clipboard.SetText(text);
                }
                if (PlayerInput.KeyHit(Keys.Left))
                {
                    Nudge(Keys.Left);
                }
                if (PlayerInput.KeyHit(Keys.Right))
                {
                    Nudge(Keys.Right);
                }
                if (PlayerInput.KeyHit(Keys.Down))
                {
                    Nudge(Keys.Down);
                }
                if (PlayerInput.KeyHit(Keys.Up))
                {
                    Nudge(Keys.Up);
                }
                if (PlayerInput.KeyDown(Keys.Left))
                {
                    holdTimer += deltaTime;
                    if (holdTimer > holdTime)
                    {
                        Nudge(Keys.Left);
                    }
                }
                else if (PlayerInput.KeyDown(Keys.Right))
                {
                    holdTimer += deltaTime;
                    if (holdTimer > holdTime)
                    {
                        Nudge(Keys.Right);
                    }
                }
                else if (PlayerInput.KeyDown(Keys.Down))
                {
                    holdTimer += deltaTime;
                    if (holdTimer > holdTime)
                    {
                        Nudge(Keys.Down);
                    }
                }
                else if (PlayerInput.KeyDown(Keys.Up))
                {
                    holdTimer += deltaTime;
                    if (holdTimer > holdTime)
                    {
                        Nudge(Keys.Up);
                    }
                }
                else
                {
                    holdTimer = 0;
                }
                
                float moveSpeed = 600f * zoom;
                float moveSpeedDeltaTime = (float)(moveSpeed * deltaTime);
                Vector2 viewOffsetMove = Vector2.Zero;
                if (PlayerInput.KeyDown(Keys.W))
                {
                    viewOffsetMove.Y += moveSpeedDeltaTime;
                }
                if (PlayerInput.KeyDown(Keys.S))
                {
                    viewOffsetMove.Y -= moveSpeedDeltaTime;
                }
                if (PlayerInput.KeyDown(Keys.A))
                {
                    viewOffsetMove.X += moveSpeedDeltaTime;
                }
                if (PlayerInput.KeyDown(Keys.D))
                {
                    viewOffsetMove.X -= moveSpeedDeltaTime;
                }
                
                viewAreaOffset += viewOffsetMove.ToPoint();
            }            
        }

        private double holdTimer;
        private readonly float holdTime = 0.2f;
        private void Nudge(Keys key)
        {
            switch (key)
            {
                case Keys.Left:
                    foreach (var sprite in selectedSprites)
                    {
                        var newRect = sprite.SourceRect;
                        if (ControlDown)
                        {
                            newRect.Width--;
                        }
                        else
                        {
                            newRect.X--;
                        }
                        UpdateSourceRect(sprite, newRect);
                    }
                    break;
                case Keys.Right:
                    foreach (var sprite in selectedSprites)
                    {
                        var newRect = sprite.SourceRect;
                        if (ControlDown)
                        {
                            newRect.Width++;
                        }
                        else
                        {
                            newRect.X++;
                        }
                        UpdateSourceRect(sprite, newRect);
                    }
                    break;
                case Keys.Down:
                    foreach (var sprite in selectedSprites)
                    {
                        var newRect = sprite.SourceRect;
                        if (ControlDown)
                        {
                            newRect.Height++;
                        }
                        else
                        {
                            newRect.Y++;
                        }
                        UpdateSourceRect(sprite, newRect);
                    }
                    break;
                case Keys.Up:
                    foreach (var sprite in selectedSprites)
                    {
                        var newRect = sprite.SourceRect;
                        if (ControlDown)
                        {
                            newRect.Height--;
                        }
                        else
                        {
                            newRect.Y--;
                        }
                        UpdateSourceRect(sprite, newRect);
                    }
                    break;
            }
        }


        public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch)
        {
            graphics.Clear(backgroundColor);
            spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable, samplerState: SamplerState.PointClamp);

            var viewArea = GetViewArea;

            if (SelectedTexture != null)
            {
                textureRect = new Rectangle(
                    (int)(viewArea.Center.X - SelectedTexture.Bounds.Width / 2f * zoom),
                    (int)(viewArea.Center.Y - SelectedTexture.Bounds.Height / 2f * zoom),
                    (int)(SelectedTexture.Bounds.Width * zoom),
                    (int)(SelectedTexture.Bounds.Height * zoom));

                spriteBatch.Draw(SelectedTexture,
                    viewArea.Center.ToVector2(),
                    sourceRectangle: null,
                    color: Color.White,
                    rotation: 0.0f,
                    origin: new Vector2(SelectedTexture.Bounds.Width / 2.0f, SelectedTexture.Bounds.Height / 2.0f),
                    scale: zoom,
                    effects: SpriteEffects.None,
                    layerDepth: 0);

                //GUI.DrawRectangle(spriteBatch, viewArea, Color.Green, isFilled: false);
                GUI.DrawRectangle(spriteBatch, textureRect, Color.Gray, isFilled: false);

                if (drawGrid)
                {
                    DrawGrid(spriteBatch, textureRect, zoom, Submarine.GridSize);
                }

                foreach (GUIComponent element in spriteList.Content.Children)
                {
                    if (!(element.UserData is Sprite sprite)) { continue; }
                    if (sprite.FullPath != selectedTexturePath) { continue; }

                    Rectangle sourceRect = new Rectangle(
                        textureRect.X + (int)(sprite.SourceRect.X * zoom),
                        textureRect.Y + (int)(sprite.SourceRect.Y * zoom),
                        (int)(sprite.SourceRect.Width * zoom),
                        (int)(sprite.SourceRect.Height * zoom));

                    bool isSelected = selectedSprites.Contains(sprite);
                    GUI.DrawRectangle(spriteBatch, sourceRect, isSelected ? GUIStyle.Orange : GUIStyle.Red * 0.5f, thickness: isSelected ? 2 : 1);

                    Identifier id = sprite.Identifier;
                    if (!id.IsEmpty)
                    {
                        int widgetSize = 10;
                        Vector2 GetTopLeft() => sprite.SourceRect.Location.ToVector2();
                        Vector2 GetTopRight() => new Vector2(GetTopLeft().X + sprite.SourceRect.Width, GetTopLeft().Y);
                        Vector2 GetBottomRight() => new Vector2(GetTopRight().X, GetTopRight().Y + sprite.SourceRect.Height);
                        var originWidget = GetWidget($"{id}_origin", sprite, widgetSize, WidgetShape.Cross, initMethod: w =>
                        {
                            w.Tooltip = TextManager.AddPunctuation(':', originLabel, sprite.RelativeOrigin.FormatDoubleDecimal());
                            w.MouseHeld += dTime =>
                            {
                                w.DrawPos = PlayerInput.MousePosition.Clamp(textureRect.Location.ToVector2() + GetTopLeft() * zoom, textureRect.Location.ToVector2() + GetBottomRight() * zoom);
                                sprite.Origin = (w.DrawPos - textureRect.Location.ToVector2() - sprite.SourceRect.Location.ToVector2() * zoom) / zoom;
                                w.Tooltip = TextManager.AddPunctuation(':', originLabel, sprite.RelativeOrigin.FormatDoubleDecimal());
                            };
                            w.Refresh = () =>
                                w.DrawPos = (textureRect.Location.ToVector2() + (sprite.Origin + sprite.SourceRect.Location.ToVector2()) * zoom)
                                    .Clamp(textureRect.Location.ToVector2() + GetTopLeft() * zoom, textureRect.Location.ToVector2() + GetBottomRight() * zoom);
                        });
                        var positionWidget = GetWidget($"{id}_position", sprite, widgetSize, WidgetShape.Rectangle, initMethod: w =>
                        {
                            w.Tooltip = positionLabel + sprite.SourceRect.Location;
                            w.MouseHeld += dTime =>
                            {
                                w.DrawPos = (drawGrid && snapToGrid) ?
                                    SnapToGrid(PlayerInput.MousePosition, textureRect, zoom, Submarine.GridSize, Submarine.GridSize.X / 4.0f * zoom) :
                                    PlayerInput.MousePosition;
                                w.DrawPos = new Vector2((float)Math.Ceiling(w.DrawPos.X), (float)Math.Ceiling(w.DrawPos.Y));
                                sprite.SourceRect = new Rectangle(((w.DrawPos - textureRect.Location.ToVector2()) / zoom).ToPoint(), sprite.SourceRect.Size);
                                if (spriteList.SelectedComponent is GUITextBlock textBox)
                                {
                                    // TODO: cache the sprite name?
                                    textBox.Text = GetSpriteName(sprite) + " " + sprite.SourceRect;
                                }
                                w.Tooltip = positionLabel + sprite.SourceRect.Location;
                            };
                            w.Refresh = () => w.DrawPos = textureRect.Location.ToVector2() + sprite.SourceRect.Location.ToVector2() * zoom;
                        });
                        var sizeWidget = GetWidget($"{id}_size", sprite, widgetSize, WidgetShape.Rectangle, initMethod: w =>
                        {
                            w.Tooltip = TextManager.AddPunctuation(':', sizeLabel, sprite.SourceRect.Size.ToString());
                            w.MouseHeld += dTime =>
                            {
                                w.DrawPos = (drawGrid && snapToGrid) ?
                                    SnapToGrid(PlayerInput.MousePosition, textureRect, zoom, Submarine.GridSize, Submarine.GridSize.X / 4.0f * zoom) :
                                    PlayerInput.MousePosition;
                                w.DrawPos = new Vector2((float)Math.Ceiling(w.DrawPos.X), (float)Math.Ceiling(w.DrawPos.Y));
                                sprite.SourceRect = new Rectangle(sprite.SourceRect.Location, ((w.DrawPos - positionWidget.DrawPos) / zoom).ToPoint());
                                // TODO: allow to lock the origin
                                sprite.RelativeOrigin = sprite.RelativeOrigin;
                                if (spriteList.SelectedComponent is GUITextBlock textBox)
                                {
                                    // TODO: cache the sprite name?
                                    textBox.Text = GetSpriteName(sprite) + " " + sprite.SourceRect;
                                }
                                w.Tooltip = TextManager.AddPunctuation(':', sizeLabel, sprite.SourceRect.Size.ToString());
                            };
                            w.Refresh = () => w.DrawPos = textureRect.Location.ToVector2() + new Vector2(sprite.SourceRect.Right, sprite.SourceRect.Bottom) * zoom;
                        });
                        originWidget.MouseDown += () => GUI.KeyboardDispatcher.Subscriber = null;
                        positionWidget.MouseDown += () => GUI.KeyboardDispatcher.Subscriber = null;
                        sizeWidget.MouseDown += () => GUI.KeyboardDispatcher.Subscriber = null;
                        if (isSelected)
                        {
                            positionWidget.Draw(spriteBatch, (float)deltaTime);
                            sizeWidget.Draw(spriteBatch, (float)deltaTime);
                            originWidget.Draw(spriteBatch, (float)deltaTime);
                        }
                    }
                }
            }

            GUI.Draw(Cam, spriteBatch);

            spriteBatch.End();
        }

        private void DrawGrid(SpriteBatch spriteBatch, Rectangle gridArea, float zoom, Vector2 gridSize)
        {
            gridSize *= zoom;
            if (gridSize.X < 1.0f) { return; }
            if (gridSize.Y < 1.0f) { return; }
            int xLines = (int)(gridArea.Width / gridSize.X);
            int yLines = (int)(gridArea.Height / gridSize.Y);

            for (int x = 0; x <= xLines; x++)
            {
                GUI.DrawLine(spriteBatch,
                    new Vector2(gridArea.X + x * gridSize.X, gridArea.Y),
                    new Vector2(gridArea.X + x * gridSize.X, gridArea.Bottom),
                    Color.White * 0.25f);
            }
            for (int y = 0; y <= yLines; y++)
            {
                GUI.DrawLine(spriteBatch,
                    new Vector2(gridArea.X, gridArea.Y + y * gridSize.Y),
                    new Vector2(gridArea.Right, gridArea.Y + y * gridSize.Y),
                    Color.White * 0.25f);
            }
        }

        private Vector2 SnapToGrid(Vector2 position, Rectangle gridArea, float zoom, Vector2 gridSize, float tolerance)
        {
            gridSize *= zoom;
            if (gridSize.X < 1.0f) { return position; }
            if (gridSize.Y < 1.0f) { return position; }

            Vector2 snappedPos = position;
            snappedPos.X -= gridArea.X;
            snappedPos.Y -= gridArea.Y;

            Vector2 gridPos = new Vector2(
                MathUtils.RoundTowardsClosest(snappedPos.X, gridSize.X),
                MathUtils.RoundTowardsClosest(snappedPos.Y, gridSize.Y));

            if (Math.Abs(gridPos.X - snappedPos.X) < tolerance)
            {
                snappedPos.X = gridPos.X;
            }
            if (Math.Abs(gridPos.Y - snappedPos.Y) < tolerance)
            {
                snappedPos.Y = gridPos.Y;
            }

            snappedPos.X += gridArea.X;
            snappedPos.Y += gridArea.Y;
            return snappedPos;
        }

        private void FilterTextures(string text)
        {
            if (string.IsNullOrWhiteSpace(text))
            {
                filterTexturesLabel.Visible = true;
                textureList.Content.Children.ForEach(c => c.Visible = true);
                return;
            }
            text = text.ToLower();
            filterTexturesLabel.Visible = false;
            foreach (GUIComponent child in textureList.Content.Children)
            {
                if (!(child is GUITextBlock textBlock)) { continue; }
                textBlock.Visible = textBlock.Text.Contains(text, StringComparison.OrdinalIgnoreCase);
            }
        }
        private void FilterSprites(string text)
        {
            if (string.IsNullOrWhiteSpace(text))
            {
                filterSpritesLabel.Visible = true;
                spriteList.Content.Children.ForEach(c => c.Visible = true);
                return;
            }
            text = text.ToLower();
            filterSpritesLabel.Visible = false;
            foreach (GUIComponent child in spriteList.Content.Children)
            {
                if (!(child is GUITextBlock textBlock)) { continue; }
                textBlock.Visible = textBlock.Text.Contains(text, StringComparison.OrdinalIgnoreCase);
            }
        }

        public override void Select()
        {
            base.Select();
            LoadSprites();
            RefreshLists();
            spriteList.Select(0, autoScroll: GUIListBox.AutoScroll.Disabled);            
        }

        protected override void DeselectEditorSpecific()
        {
            loadedSprites.ForEach(s => s.Remove());
            loadedSprites.Clear();
            ResetWidgets();
            // Automatically reload all sprites that have been selected at least once (and thus might have been edited)
            var reloadedSprites = new List<Sprite>();
            foreach (var sprite in dirtySprites)
            {
                foreach (var s in Sprite.LoadedSprites)
                {
                    if (s.FullPath == sprite.FullPath && !reloadedSprites.Contains(s))
                    {
                        s.ReloadXML();
                        reloadedSprites.Add(s);
                    }
                }
            }
            dirtySprites.Clear();
            filterSpritesBox.Text = "";
            filterTexturesBox.Text = "";
        }

        public void SelectSprite(Sprite sprite)
        {
            lastSprite = sprite;
            if (!loadedSprites.Contains(sprite))
            {
                loadedSprites.Add(sprite);
                RefreshLists();
            }
            if (selectedSprites.Any(s => s.FullPath != selectedTexturePath))
            {
                ResetWidgets();
            }
            if (Widget.EnableMultiSelect)
            {
                if (selectedSprites.Contains(sprite))
                {
                    selectedSprites.Remove(sprite);
                }
                else
                {
                    selectedSprites.Add(sprite);
                    dirtySprites.Add(sprite);
                }
            }
            else
            {
                selectedSprites.Clear();
                selectedSprites.Add(sprite);
                dirtySprites.Add(sprite);
            }
            if (sprite.FullPath != selectedTexturePath)
            {
                textureList.Select(sprite.FullPath, autoScroll: GUIListBox.AutoScroll.Disabled);
                UpdateScrollBar(textureList);
            }
            xmlPathText.Text = string.Empty;
            foreach (var s in selectedSprites)
            {
                texturePathText.Text = s.FilePath.Value;
                var element = s.SourceElement;
                if (element != null)
                {
                    string xmlPath = element.ParseContentPathFromUri();
                    if (!xmlPathText.Text.Contains(xmlPath))
                    {
                        xmlPathText.Text += "\n" + xmlPath;
                    }
                }
            }
            xmlPathText.TextColor = Color.LightGray;
        }

        public void RefreshLists()
        {
            selectedSprites.Clear();
            textureList.ClearChildren();
            spriteList.ClearChildren();
            ResetWidgets();
            HashSet<string> textures = new HashSet<string>();
            // Create texture list
            foreach (Sprite sprite in loadedSprites.OrderBy(s => Path.GetFileNameWithoutExtension(s.FilePath.Value)))
            {
                //ignore sprites that don't have a file path (e.g. submarine pics)
                if (sprite.FilePath.IsNullOrEmpty()) { continue; }
                string normalizedFilePath = sprite.FilePath.FullPath;
                if (!textures.Contains(normalizedFilePath))
                {
                    new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), textureList.Content.RectTransform) { MinSize = new Point(0, 20) },
                        Path.GetFileName(sprite.FilePath.Value))
                    {
                        ToolTip = sprite.FilePath.Value,
                        UserData = sprite.FullPath
                    };
                    textures.Add(normalizedFilePath);
                }
            }
            // Create sprite list
            // TODO: allow the user to choose whether to sort by file name or by texture sheet
            //foreach (Sprite sprite in loadedSprites.OrderBy(s => GetSpriteName(s)))
            foreach (Sprite sprite in loadedSprites.OrderBy(s => s.SourceElement.GetAttributeContentPath("texture")?.Value ?? string.Empty))
            {
                string elementLocalName = sprite.SourceElement.Element.Name.LocalName;
                string text = $"{GetSpriteName(sprite)} ({sprite.SourceRect.X}, {sprite.SourceRect.Y}, {sprite.SourceRect.Width}, {sprite.SourceRect.Height}) [{elementLocalName}]";
                if (string.Equals(elementLocalName, "sprite", StringComparison.InvariantCultureIgnoreCase))
                {
                    text = $"{GetSpriteName(sprite)} ({sprite.SourceRect.X}, {sprite.SourceRect.Y}, {sprite.SourceRect.Width}, {sprite.SourceRect.Height})";
                }
                
                new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), spriteList.Content.RectTransform) { MinSize = new Point(0, 20) }, text: text)
                {
                    UserData = sprite
                };
            }
            topPanelContents.Visible = false;
        }

        public void ResetZoom()
        {
            if (SelectedTexture == null) { return; }
            var viewArea = GetViewArea;
            float width = viewArea.Width / (float)SelectedTexture.Width;
            float height = viewArea.Height / (float)SelectedTexture.Height;
            zoom = Math.Min(1, Math.Min(width, height));
            zoomBar.BarScroll = GetBarScrollValue();
            viewAreaOffset = Point.Zero;
        }
#endregion

        #region Helpers
        private Point viewAreaOffset;
        private Rectangle GetViewArea
        {
            get
            {
                int margin = 20;
                var viewArea = new Rectangle(leftPanel.Rect.Right + margin + viewAreaOffset.X, topPanel.Rect.Bottom + margin + viewAreaOffset.Y, rightPanel.Rect.Left - leftPanel.Rect.Right - margin * 2, Frame.Rect.Height - topPanel.Rect.Height - margin * 2);
                return viewArea;
            }
        }

        private float GetBarScrollValue() => MathHelper.Lerp(0, 1, MathUtils.InverseLerp(MinZoom, MaxZoom, zoom));

        private string GetSpriteName(Sprite sprite)
        {
            var sourceElement = sprite.SourceElement;
            if (sourceElement == null) { return string.Empty; }
            string name = sprite.Name;
            if (string.IsNullOrWhiteSpace(name))
            {
                name = sourceElement.Parent?.GetAttributeString("identifier", string.Empty);
            }
            if (string.IsNullOrEmpty(name))
            {
                name = sourceElement.Parent?.GetAttributeString("name", string.Empty);
            }
            return string.IsNullOrEmpty(name) ? Path.GetFileNameWithoutExtension(sprite.FilePath.Value) : name;
        }

        private void UpdateScrollBar(GUIListBox listBox)
        {
            var sb = listBox.ScrollBar;
            sb.BarScroll = MathHelper.Clamp(MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, listBox.Content.CountChildren - 1, listBox.SelectedIndex)), sb.MinValue, sb.MaxValue);
        }

        private void UpdateSourceRect(Sprite sprite, Rectangle newRect)
        {
            sprite.SourceRect = newRect;
            // Keeps the relative origin unchanged. The absolute origin will be recalculated.
            sprite.RelativeOrigin = sprite.RelativeOrigin;
        }
#endregion

        #region Widgets
        private Dictionary<string, Widget> widgets = new Dictionary<string, Widget>();

        private Widget GetWidget(string id, Sprite sprite, int size = 5, WidgetShape shape = WidgetShape.Rectangle, Action<Widget> initMethod = null)
        {
            if (!widgets.TryGetValue(id, out Widget widget))
            {
                int selectedSize = (int)Math.Round(size * 1.5f);
                widget = new Widget(id, size, shape)
                {
                    Data = sprite,
                    Color = Color.Yellow,
                    SecondaryColor = Color.Gray,
                    TooltipOffset = new Vector2(selectedSize / 2 + 5, -10)
                };
                widget.PreDraw += (sp, dTime) =>
                {
                    if (!widget.IsControlled)
                    {
                        widget.Refresh();
                    }
                };
                widget.PreUpdate += dTime => widget.Enabled = selectedSprites.Contains(sprite);
                widget.PostUpdate += dTime =>
                {
                    widget.InputAreaMargin = widget.IsControlled ? 1000 : 0;
                    widget.Size = widget.IsSelected ? selectedSize : size;
                    widget.IsFilled = widget.IsControlled;
                };
                widgets.Add(id, widget);
                initMethod?.Invoke(widget);
            }
            return widget;
        }

        private void ResetWidgets()
        {
            widgets.Clear();
            Widget.SelectedWidgets.Clear();
        }
#endregion
    }
}
