using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma.Lights;
using Microsoft.Xna.Framework;

namespace Barotrauma
{
    abstract partial class MapEntity : Entity
    {
        protected static Vector2 selectionPos = Vector2.Zero;
        protected static Vector2 selectionSize = Vector2.Zero;

        private static Vector2 startMovingPos = Vector2.Zero;

        private static float keyDelay;

        public static Vector2 StartMovingPos => startMovingPos;
        public static Vector2 SelectionPos => selectionPos;

        public event Action<Rectangle> Resized;

        public static bool Resizing { get; private set; }
        private int resizeDirX, resizeDirY;
        private Rectangle? prevRect;

        public static bool SelectionChanged;

        //which entities have been selected for editing
        public static HashSet<MapEntity> SelectedList { get; private set; } = new HashSet<MapEntity>();
        private static List<Rectangle> oldRects = new List<Rectangle>();
        private static Vector2 entityMovementNudge;

        public static List<MapEntity> CopiedList = new List<MapEntity>();

        private static List<MapEntity> highlightedInEditorList = new List<MapEntity>();

        private static float highlightTimer;

        private static GUIListBox highlightedListBox;
        public static GUIListBox HighlightedListBox
        {
            get { return highlightedListBox; }
        }


        protected static GUIComponent editingHUD;
        public static GUIComponent EditingHUD
        {
            get
            {
                return editingHUD;
            }
        }

        private static bool disableSelect;
        public static bool DisableSelect
        {
            get { return disableSelect; }
            set
            {
                disableSelect = value;
                if (disableSelect)
                {
                    StopSelection();
                }
            }
        }

        public virtual bool SelectableInEditor => true;

        public static bool SelectedAny => SelectedList.Count > 0;

        public bool IsSelected => SelectedList.Contains(this);

        public bool IsIncludedInSelection { get; set; }

        public virtual bool IsVisible(Rectangle worldView)
        {
            Rectangle worldRect = WorldRect;
            if (worldRect.X > worldView.Right || worldRect.Right < worldView.X) { return false; }
            if (worldRect.Y < worldView.Y - worldView.Height || worldRect.Y - worldRect.Height > worldView.Y) { return false; }
            //zoomed extremely far out -> no need to render
            if (Screen.Selected.Cam.Zoom < 0.05f) { return false; }
            if (worldRect.Width * Screen.Selected.Cam.Zoom < 1.0f ||
                worldRect.Height * Screen.Selected.Cam.Zoom < 1.0f)
            {
                return false;
            }
            return true;
        }

        /// <summary>
        /// Used for undo/redo to determine what this item has been replaced with
        /// </summary>
        public MapEntity ReplacedBy;

        public virtual void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { }

        public virtual float GetDrawDepth() { return 0.0f; }

        /// <summary>
        /// A method that modifies the draw depth to prevent z-fighting between entities with the same sprite depth
        /// </summary>
        public float GetDrawDepth(float baseDepth, Sprite sprite)
        {
            float depth = baseDepth
                //take texture into account to get entities with (roughly) the same base depth and texture to render consecutively to minimize texture swaps
                + (sprite?.Texture?.SortingKey ?? 0) % 100 * 0.000001f
                + ID % 100 * 0.0000001f;
            return Math.Min(depth, 1.0f);
        }

        protected Vector2 GetCollapseEffectOffset()
        {
            if (Level.Loaded?.Renderer?.CollapseEffectStrength is float collapseEffectStrength and > 0.0f && Submarine is not { Info.Type: SubmarineType.Player })
            {
                   Vector2 noisePos = new Vector2(
                    (float)PerlinNoise.GetPerlin((float)(Timing.TotalTime + ID) * 0.1f, (float)(Timing.TotalTime + ID) * 0.5f) - 0.5f,
                    (float)PerlinNoise.GetPerlin((float)(Timing.TotalTime + ID) * 0.1f, (float)(Timing.TotalTime + ID) * 0.1f) - 0.5f);
                Vector2 offsetFromOrigin = Level.Loaded.Renderer.CollapseEffectOrigin - DrawPosition;
                return offsetFromOrigin * MathF.Pow(collapseEffectStrength, MathHelper.Lerp(1, 4, ID % 1000 / 1000.0f)) + (noisePos * 100.0f * collapseEffectStrength);                
            }
            return Vector2.Zero;
        }

        /// <summary>
        /// Update the selection logic in submarine editor
        /// </summary>
        public static void UpdateSelecting(Camera cam)
        {
            if (Resizing)
            {
                if (!SelectedAny)
                {
                    Resizing = false;
                }
                return;
            }

            ClearHighlightedEntities();

            if (DisableSelect)
            {
                DisableSelect = false;
                return;
            }

            if (startMovingPos == Vector2.Zero
                && selectionPos == Vector2.Zero
                && (GUI.MouseOn != null || !PlayerInput.MouseInsideWindow))
            {
                if (highlightedListBox == null ||
                    (GUI.MouseOn != highlightedListBox && !highlightedListBox.IsParentOf(GUI.MouseOn)))
                {
                    UpdateHighlightedListBox(null, false);
                    return;
                }
            }

            if (MapEntityPrefab.Selected != null)
            {
                selectionPos = Vector2.Zero;
                SelectedList.Clear();
                return;
            }
            if (GUI.KeyboardDispatcher.Subscriber == null)
            {
                if (PlayerInput.KeyHit(Keys.Delete))
                {
                    if (SelectedAny)
                    {
                        SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List<MapEntity>(SelectedList), true));
                        SelectedList.ForEachMod(static e => { if (!e.Removed) { e.Remove(); } });
                        SelectedList.Clear();
                    }
                }

                if (PlayerInput.IsCtrlDown())
                {
#if DEBUG
                    if (PlayerInput.KeyHit(Keys.D))
                    {
                        bool terminate = false;
                        foreach (MapEntity entity in SelectedList)
                        {
                            if (entity is Item item && item.GetComponent<Planter>() is { } planter)
                            {
                                planter.Update(1.0f, cam);
                                for (var i = 0; i < planter.GrowableSeeds.Length; i++)
                                {
                                    Growable seed = planter.GrowableSeeds[i];
                                    PlantSlot slot = planter.PlantSlots.ContainsKey(i) ? planter.PlantSlots[i] : Planter.NullSlot;
                                    if (seed == null) { continue; }

                                    seed.CreateDebugHUD(planter, slot);
                                    terminate = true;
                                    break;
                                }
                            }

                            if (terminate) { break; }
                        }
                    }
#endif
                    if (PlayerInput.KeyHit(Keys.C))
                    {
                        Copy(SelectedList.ToList());
                    }
                    else if (PlayerInput.KeyHit(Keys.X))
                    {
                        Cut(SelectedList.ToList());
                    }
                    else if (PlayerInput.KeyHit(Keys.V))
                    {
                        Paste(cam.ScreenToWorld(PlayerInput.MousePosition));
                    }
                    /*else if (PlayerInput.KeyHit(Keys.G))
                    {
                        if (SelectedList.Any())
                        {
                            if (SelectionGroups.ContainsKey(SelectedList.Last()))
                            {
                                // Ungroup all selected
                                SelectedList.ForEach(e => SelectionGroups.Remove(e));
                            }
                            else
                            {
                                foreach (var entity in SelectedList)
                                {
                                    // Remove the old group, if any
                                    SelectionGroups.Remove(entity);
                                    // Create a group that can be accessed with any member
                                    SelectionGroups.Add(entity, SelectedList);
                                }
                            }
                        }
                    }*/
                }
            }

            Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition);
            MapEntity highLightedEntity = null;
            if (startMovingPos == Vector2.Zero)
            {
                List<MapEntity> highlightedEntities = new List<MapEntity>();
                if (highlightedListBox != null && highlightedListBox.IsParentOf(GUI.MouseOn))
                {
                    highLightedEntity = GUI.MouseOn.UserData as MapEntity;
                }
                else
                {
                    foreach (MapEntity e in MapEntityList)
                    {
                        if (!e.SelectableInEditor) { continue; }
                        if (e.IsMouseOn(position))
                        {
                            int i = 0;
                            while (i < highlightedEntities.Count &&
                                e.Sprite != null &&
                                (highlightedEntities[i].Sprite == null || highlightedEntities[i].SpriteDepth < e.SpriteDepth))
                            {
                                i++;
                            }
                            highlightedEntities.Insert(i, e);
                            if (i == 0) { highLightedEntity = e; }
                        }
                    }
                    UpdateHighlighting(highlightedEntities);
                }

                if (highLightedEntity != null) { highLightedEntity.IsHighlighted = true; }
            }

            if (GUI.KeyboardDispatcher.Subscriber == null)
            {
                Vector2 previousNudge = entityMovementNudge;
                entityMovementNudge = GetNudgeAmount();
                if (entityMovementNudge != Vector2.Zero)
                {
                    if (previousNudge == Vector2.Zero)
                    {
                        oldRects = SelectedList.Select(entity => entity.Rect).ToList();
                    }
                    foreach (MapEntity entityToNudge in SelectedList) { entityToNudge.Move(entityMovementNudge); }
                }
                else if (previousNudge != Vector2.Zero)
                {
                    SubEditorScreen.StoreCommand(new TransformCommand(new List<MapEntity>(SelectedList), SelectedList.Select(entity => entity.Rect).ToList(), oldRects, resized: false));
                }
            }
            else
            {
                keyDelay = 0;
            }

            bool isShiftDown = PlayerInput.IsShiftDown();

            //started moving selected entities
            if (startMovingPos != Vector2.Zero)
            {
                Item targetContainer = GetPotentialContainer(position, SelectedList);
                if (targetContainer != null) { targetContainer.IsHighlighted = true; }

                if (PlayerInput.PrimaryMouseButtonReleased())
                {
                    //mouse released -> move the entities to the new position of the mouse

                    Vector2 moveAmount = position - startMovingPos;

                    if (!isShiftDown)
                    {
                        moveAmount.X = (float)(moveAmount.X > 0.0f ? Math.Floor(moveAmount.X / Submarine.GridSize.X) : Math.Ceiling(moveAmount.X / Submarine.GridSize.X)) * Submarine.GridSize.X;
                        moveAmount.Y = (float)(moveAmount.Y > 0.0f ? Math.Floor(moveAmount.Y / Submarine.GridSize.Y) : Math.Ceiling(moveAmount.Y / Submarine.GridSize.Y)) * Submarine.GridSize.Y;
                    }

                    if (Math.Abs(moveAmount.X) >= Submarine.GridSize.X || Math.Abs(moveAmount.Y) >= Submarine.GridSize.Y || isShiftDown)
                    {
                        if (!isShiftDown) { moveAmount = Submarine.VectorToWorldGrid(moveAmount); }

                        //clone
                        if (PlayerInput.IsCtrlDown())
                        {
                            HashSet<MapEntity> clones = Clone(SelectedList.ToList()).Where(c => c != null).ToHashSet();
                            
                            if (clones.Count == 1)
                            {
                                if (clones.First() is WayPoint wayPoint && SelectedList.First() is WayPoint originalWaypoint && originalWaypoint.SpawnType == SpawnType.Path)
                                {
                                    originalWaypoint.ConnectTo(wayPoint);
                                }
                            }
                            
                            SelectedList = clones;
                            SelectedList.ForEach(c => c.Move(moveAmount));
                            SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List<MapEntity>(clones), false));
                        }
                        else // move
                        {
                            var oldRects = SelectedList.Select(e => e.Rect).ToList();
                            List<MapEntity> deposited = new List<MapEntity>();
                            foreach (MapEntity e in SelectedList)
                            {
                                e.Move(moveAmount);

                                if (isShiftDown && e is Item item && targetContainer != null)
                                {
                                    if (targetContainer.OwnInventory.TryPutItem(item, Character.Controlled))
                                    {
                                        SoundPlayer.PlayUISound(GUISoundType.DropItem);
                                        deposited.Add(item);
                                    }
                                    else
                                    {
                                        SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
                                    }
                                }
                            }

                            SubEditorScreen.StoreCommand(new TransformCommand(new List<MapEntity>(SelectedList), SelectedList.Select(entity => entity.Rect).ToList(), oldRects, resized: false));
                            if (deposited.Any() && deposited.Any(entity => entity is Item))
                            {
                                var depositedItems = deposited.Where(entity => entity is Item).Cast<Item>().ToList();
                                SubEditorScreen.StoreCommand(new InventoryPlaceCommand(targetContainer.OwnInventory, depositedItems, false));
                            }

                            deposited.ForEach(entity => { SelectedList.Remove(entity); });
                        }
                    }
                    startMovingPos = Vector2.Zero;
                }

            }
            //started dragging a "selection rectangle"
            else if (selectionPos != Vector2.Zero)
            {
                selectionSize.X = position.X - selectionPos.X;
                selectionSize.Y = selectionPos.Y - position.Y;

                foreach (MapEntity entity in MapEntityList)
                {
                    entity.IsIncludedInSelection = false;
                }

                HashSet<MapEntity> newSelection = new HashSet<MapEntity>();// FindSelectedEntities(selectionPos, selectionSize);
                if (Math.Abs(selectionSize.X) > Submarine.GridSize.X || Math.Abs(selectionSize.Y) > Submarine.GridSize.Y)
                {
                    newSelection = FindSelectedEntities(selectionPos, selectionSize);
                }
                else
                {
                    if (highLightedEntity != null)
                    {
                        if (SubEditorScreen.IsLayerLinked(highLightedEntity)/*SelectionGroups.TryGetValue(highLightedEntity, out HashSet<MapEntity> group)*/)
                        {
                            ImmutableHashSet<MapEntity> entitiesInSameLayer = SubEditorScreen.GetEntitiesInSameLayer(highLightedEntity);
                            foreach (MapEntity entity in entitiesInSameLayer.Where(e => !newSelection.Contains(e)))
                            {
                                newSelection.Add(entity);
                            }

                            foreach (MapEntity entity in entitiesInSameLayer)
                            {
                                entity.IsIncludedInSelection = true;
                            }
                        }
                        else
                        {
                            newSelection.Add(highLightedEntity);
                            highLightedEntity.IsIncludedInSelection = true;
                        }
                    }
                }

                if (PlayerInput.PrimaryMouseButtonReleased())
                {
                    if (PlayerInput.IsCtrlDown())
                    {
                        foreach (MapEntity e in newSelection)
                        {
                            if (SelectedList.Contains(e))
                            {
                                RemoveSelection(e);
                            }
                            else
                            {
                                AddSelection(e);
                            }
                        }
                    }
                    else
                    {
                        SelectedList = new HashSet<MapEntity>(newSelection);
                        //selectedList.Clear();
                        //newSelection.ForEach(e => AddSelection(e));
                        foreach (var entity in newSelection)
                        {
                            HandleDoorGapLinks(entity,
                                onGapFound: (door, gap) =>
                                {
                                    door.RefreshLinkedGap();
                                    if (!SelectedList.Contains(gap))
                                    {
                                        SelectedList.Add(gap);
                                    }
                                },
                                onDoorFound: (door, gap) =>
                                {
                                    if (!SelectedList.Contains(door.Item))
                                    {
                                        SelectedList.Add(door.Item);
                                    }
                                });
                        }
                    }

                    //select wire if both items it's connected to are selected
                    var selectedItems = SelectedList.Where(e => e is Item).Cast<Item>().ToList();
                    foreach (Item item in Item.ItemList)
                    {
                        var wire = item.GetComponent<Wire>();
                        if (wire == null) { continue; }
                        Item item0 = wire.Connections[0]?.Item;
                        Item item1 = wire.Connections[1]?.Item;

                        if (item0 == null && item1 != null)
                        {
                            item0 = Item.ItemList.Find(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false);
                        }
                        else if (item0 != null && item1 == null)
                        {
                            item1 = Item.ItemList.Find(it => it.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(wire) ?? false);
                        }
                        if (item0 != null && item1 != null && SelectedList.Contains(item0) && SelectedList.Contains(item1))
                        {
                            SelectedList.Add(item);
                        }
                    }

                    selectionPos = Vector2.Zero;
                    selectionSize = Vector2.Zero;
                    foreach (MapEntity entity in MapEntityList)
                    {
                        entity.IsIncludedInSelection = false;
                    }
                }
            }
            //default, not doing anything specific yet
            else
            {
                if (PlayerInput.PrimaryMouseButtonHeld() &&
                    PlayerInput.KeyUp(Keys.Space) &&
                    PlayerInput.KeyUp(Keys.LeftAlt) &&
                    PlayerInput.KeyUp(Keys.RightAlt) &&
                    (highlightedListBox == null || (GUI.MouseOn != highlightedListBox && !highlightedListBox.IsParentOf(GUI.MouseOn))))
                {
                    //if clicking a selected entity, start moving it
                    foreach (MapEntity e in SelectedList)
                    {
                        if (e.IsMouseOn(position)) startMovingPos = position;
                    }
                    selectionPos = position;

                    //stop camera movement to prevent accidental dragging or rect selection
                    Screen.Selected.Cam.StopMovement();
                }
            }
        }

        public static void StopSelection()
        {
            startMovingPos = Vector2.Zero;
            selectionSize = Vector2.Zero;
            selectionPos = Vector2.Zero;
        }
        
        public static Vector2 GetNudgeAmount(bool doHold = true)
        {
            Vector2 nudgeAmount = Vector2.Zero;
            if (doHold)
            {
                int up = PlayerInput.KeyDown(Keys.Up) ? 1 : 0,
                    down = PlayerInput.KeyDown(Keys.Down) ? -1 : 0,
                    left = PlayerInput.KeyDown(Keys.Left) ? -1 : 0,
                    right = PlayerInput.KeyDown(Keys.Right) ? 1 : 0;

                int xKeysDown = (left + right);
                int yKeysDown = (up + down);

                if (xKeysDown != 0 || yKeysDown != 0) { keyDelay += (float) Timing.Step; } else { keyDelay = 0; }


                if (keyDelay >= 0.5f)
                {
                    nudgeAmount.Y = yKeysDown;
                    nudgeAmount.X = xKeysDown;
                }
            }

            if (PlayerInput.KeyHit(Keys.Up)) { nudgeAmount.Y =  1f; }
            if (PlayerInput.KeyHit(Keys.Down)) { nudgeAmount.Y = -1f; }
            if (PlayerInput.KeyHit(Keys.Left)) { nudgeAmount.X = -1f;}
            if (PlayerInput.KeyHit(Keys.Right)) { nudgeAmount.X =  1f; }

            return nudgeAmount;
        }

        public MapEntity GetReplacementOrThis()
        {
            return ReplacedBy?.GetReplacementOrThis() ?? this;
        }

        public static Item GetPotentialContainer(Vector2 position, HashSet<MapEntity> entities = null)
        {
            Item targetContainer = null;
            bool isShiftDown = PlayerInput.IsShiftDown();

            if (!isShiftDown) { return null; }

            foreach (MapEntity e in MapEntityList)
            {
                if (!e.SelectableInEditor || e is not Item potentialContainer) { continue; }

                if (e.IsMouseOn(position))
                {
                    if (entities == null)
                    {
                        if (potentialContainer.OwnInventory != null && potentialContainer.ParentInventory == null && !potentialContainer.OwnInventory.IsFull(takeStacksIntoAccount: true))
                        {
                            targetContainer = potentialContainer;
                            break;
                        }
                    }
                    else
                    {
                        foreach (MapEntity selectedEntity in entities)
                        {
                            if (!(selectedEntity is Item selectedItem)) { continue; }
                            if (potentialContainer.OwnInventory != null && potentialContainer.ParentInventory == null && potentialContainer != selectedItem &&
                                potentialContainer.OwnInventory.CanBePut(selectedItem))
                            {
                                targetContainer = potentialContainer;
                                break;
                            }
                        }
                    }
                }
                if (targetContainer != null) { break; }
            }

            return targetContainer;
        }

        /// <summary>
        /// Updates the logic that runs the highlight box when the mouse is sitting still.
        /// </summary>
        /// <see cref="UpdateHighlightedListBox"/>
        /// <param name="highlightedEntities"></param>
        /// <param name="wiringMode">true to give items tooltip showing their connection</param>
        public static void UpdateHighlighting(List<MapEntity> highlightedEntities, bool wiringMode = false)
        {
            if (PlayerInput.MouseSpeed.LengthSquared() > 10)
            {
                highlightTimer = 0.0f;
            }
            else
            {
                bool mouseNearHighlightBox = false;

                if (highlightedListBox != null)
                {
                    Rectangle expandedRect = highlightedListBox.Rect;
                    expandedRect.Inflate(20, 20);
                    mouseNearHighlightBox = expandedRect.Contains(PlayerInput.MousePosition);
                    if (!mouseNearHighlightBox) highlightedListBox = null;
                }

                highlightTimer += (float)Timing.Step;
                if (highlightTimer > 1.0f)
                {
                    if (!mouseNearHighlightBox)
                    {
                        UpdateHighlightedListBox(highlightedEntities, wiringMode);
                        highlightTimer = 0.0f;
                    }
                }
            }
        }

        private static void UpdateHighlightedListBox(List<MapEntity> highlightedEntities, bool wiringMode)
        {
            if (highlightedEntities == null || highlightedEntities.Count < 2)
            {
                highlightedListBox = null;
                return;
            }
            if (highlightedListBox != null)
            {
                if (GUI.MouseOn == highlightedListBox || highlightedListBox.IsParentOf(GUI.MouseOn)) return;
                if (highlightedEntities.SequenceEqual(highlightedInEditorList)) return;
            }

            highlightedInEditorList = highlightedEntities;

            highlightedListBox = new GUIListBox(new RectTransform(new Point(180, highlightedEntities.Count * 18 + 5), GUI.Canvas)
            {
                MaxSize = new Point(int.MaxValue, 256),
                ScreenSpaceOffset =  PlayerInput.MousePosition.ToPoint() + new Point(15)
            }, style: "GUIToolTip");

            foreach (MapEntity entity in highlightedEntities)
            {
                LocalizedString tooltip = string.Empty;

                if (wiringMode && entity is Item item)
                {
                    var wire = item.GetComponent<Wire>();
                    if (wire?.Connections != null)
                    {
                        for (var i = 0; i < wire.Connections.Length; i++)
                        {
                            var conn = wire.Connections[i];
                            if (conn != null)
                            {
                                tooltip += TextManager.GetWithVariables("wirelistformat",
                                    ("[item]", conn.Item?.Name),
                                    ("[pin]", conn.Name));
                            }
                            if (i != wire.Connections.Length - 1) { tooltip += '\n'; }
                        }
                    }
                }

                var textBlock = new GUITextBlock(new RectTransform(new Point(highlightedListBox.Content.Rect.Width, 15), highlightedListBox.Content.RectTransform),
                                                 ToolBox.LimitString(entity.Name, GUIStyle.SmallFont, 140), font: GUIStyle.SmallFont)
                {
                    ToolTip = tooltip,
                    UserData = entity
                };
            }

            highlightedListBox.OnSelected = (GUIComponent component, object obj) =>
            {
                MapEntity entity = obj as MapEntity;

                if (PlayerInput.IsCtrlDown() && !wiringMode)
                {
                    if (SelectedList.Contains(entity))
                    {
                        RemoveSelection(entity);
                    }
                    else
                    {
                        AddSelection(entity);
                    }

                    return true;
                }
                SelectEntity(entity);

                return true;
            };
        }

        public static void AddSelection(MapEntity entity)
        {
            if (SelectedList.Contains(entity)) { return; }
            SelectedList.Add(entity);
            HandleDoorGapLinks(entity,
                onGapFound: (door, gap) =>
                {
                    door.RefreshLinkedGap();
                    if (!SelectedList.Contains(gap))
                    {
                        SelectedList.Add(gap);
                    }
                },
                onDoorFound: (door, gap) =>
                {
                    if (!SelectedList.Contains(door.Item))
                    {
                        SelectedList.Add(door.Item);
                    }
                });
        }

        private static void HandleDoorGapLinks(MapEntity entity, Action<Door, Gap> onGapFound, Action<Door, Gap> onDoorFound)
        {
            switch (entity)
            {
                case Item i:
                {
                    var door = i.GetComponent<Door>();
                    var gap = door?.LinkedGap;
                    if (gap != null)
                    {
                        onGapFound(door, gap);
                    }

                    break;
                }
                case Gap gap:
                {
                    var door = gap.ConnectedDoor;
                    if (door != null)
                    {
                        onDoorFound(door, gap);
                    }

                    break;
                }
            }
        }

        public static void RemoveSelection(MapEntity entity)
        {
            SelectedList.Remove(entity);
            HandleDoorGapLinks(entity,
                onGapFound: (door, gap) => SelectedList.Remove(gap),
                onDoorFound: (door, gap) => SelectedList.Remove(door.Item));
        }

        static partial void UpdateAllProjSpecific(float deltaTime)
        {
            var entitiesToRender = Submarine.VisibleEntities ?? MapEntityList;
            foreach (MapEntity me in entitiesToRender)
            {
                if (me is Item item)
                {
                    item.UpdateSpriteStates(deltaTime);
                }
                else if (me is Structure structure)
                {
                    structure.UpdateSpriteStates(deltaTime);
                }
            }
        }

        /// <summary>
        /// Draw the "selection rectangle" and outlines of entities that are being dragged (if any)
        /// </summary>
        public static void DrawSelecting(SpriteBatch spriteBatch, Camera cam)
        {
            if (Screen.Selected is SubEditorScreen subEditor)
            {
                if (subEditor.IsMouseOnEditorGUI()) { return; }
            }
            else if (GUI.MouseOn != null) 
            { 
                return; 
            }

            Vector2 position = PlayerInput.MousePosition;
            position = cam.ScreenToWorld(position);

            if (startMovingPos != Vector2.Zero)
            {
                Vector2 moveAmount = position - startMovingPos;
                moveAmount.Y = -moveAmount.Y;

                bool isShiftDown = PlayerInput.IsShiftDown();

                if (!isShiftDown)
                {
                    moveAmount.X = (float)(moveAmount.X > 0.0f ? Math.Floor(moveAmount.X / Submarine.GridSize.X) : Math.Ceiling(moveAmount.X / Submarine.GridSize.X)) * Submarine.GridSize.X;
                    moveAmount.Y = (float)(moveAmount.Y > 0.0f ? Math.Floor(moveAmount.Y / Submarine.GridSize.Y) : Math.Ceiling(moveAmount.Y / Submarine.GridSize.Y)) * Submarine.GridSize.Y;
                }

                //started moving the selected entities
                if (Math.Abs(moveAmount.X) >= Submarine.GridSize.X || Math.Abs(moveAmount.Y) >= Submarine.GridSize.Y || isShiftDown)
                {
                    foreach (MapEntity e in SelectedList)
                    {
                        SpriteEffects spriteEffects = SpriteEffects.None;
                        float spriteRotation = 0.0f;
                        float rectangleRotation = 0.0f;
                        switch (e)
                        {
                            case Item item:
                                {
                                    if (item.FlippedX && item.Prefab.CanSpriteFlipX) { spriteEffects ^= SpriteEffects.FlipHorizontally; }
                                    if (item.FlippedY && item.Prefab.CanSpriteFlipY) { spriteEffects ^= SpriteEffects.FlipVertically; }
                                    spriteRotation = MathHelper.ToRadians(item.Rotation);
                                    rectangleRotation = spriteRotation;
                                    var wire = item.GetComponent<Wire>();
                                    if (wire != null && wire.Item.body != null && !wire.Item.body.Enabled)
                                    {
                                        wire.Draw(spriteBatch, editing: false, new Vector2(moveAmount.X, -moveAmount.Y));
                                        continue;
                                    }
                                    break;
                                }
                            case Structure structure:
                                {
                                    if (structure.FlippedX && structure.Prefab.CanSpriteFlipX) { spriteEffects ^= SpriteEffects.FlipHorizontally; }
                                    if (structure.FlippedY && structure.Prefab.CanSpriteFlipY) { spriteEffects ^= SpriteEffects.FlipVertically; }
                                    rectangleRotation = MathHelper.ToRadians(structure.Rotation);

                                    spriteRotation = rectangleRotation;
                                    bool spriteIsFlippedHorizontally = structure.Sprite.effects.HasFlag(SpriteEffects.FlipHorizontally);
                                    bool spriteIsFlippedVertically = structure.Sprite.effects.HasFlag(SpriteEffects.FlipVertically);
                                    if (spriteIsFlippedHorizontally != spriteIsFlippedVertically)
                                    {
                                        spriteRotation = -spriteRotation;
                                    }

                                    if (structure.FlippedX != structure.FlippedY) { rectangleRotation = -rectangleRotation; }
                                    break;
                                }
                            case WayPoint wayPoint:
                                {
                                    Vector2 drawPos = e.WorldPosition;
                                    drawPos.Y = -drawPos.Y;
                                    drawPos += moveAmount;
                                    wayPoint.Draw(spriteBatch, drawPos);
                                    continue;
                                }
                            case LinkedSubmarine linkedSub:
                                {
                                    var ma = moveAmount;
                                    ma.Y = -ma.Y;
                                    Vector2 lPos = linkedSub.Position;
                                    lPos += ma;
                                    linkedSub.Draw(spriteBatch, lPos, alpha: 0.5f);
                                    break;
                                }
                        }
                        e.Prefab?.DrawPlacing(spriteBatch,
                            new Rectangle(e.WorldRect.Location + new Point((int)moveAmount.X, (int)-moveAmount.Y), e.WorldRect.Size), e.Scale, spriteRotation, spriteEffects: spriteEffects);
                        GUI.DrawRectangle(spriteBatch,
                            center: e.WorldRect.Center.ToVector2().FlipY() + moveAmount + new Vector2(0f, e.WorldRect.Height),
                            width: e.WorldRect.Width, height: e.WorldRect.Height,
                            rotation: rectangleRotation, clr: Color.White,
                            depth: 0f, thickness: Math.Max(3.0f / GameScreen.Selected.Cam.Zoom, 2.0f));
                    }

                    //stop dragging the "selection rectangle"
                    selectionPos = Vector2.Zero;
                }
            }
            if (selectionPos != Vector2.Zero)
            {
                DrawSelectionRect(spriteBatch, selectionPos, selectionSize, GUIStyle.Blue);
            }
        }

        public static void DrawSelectionRect(SpriteBatch spriteBatch, Vector2 pos, Vector2 size, Color color)
        {
            var (sizeX, sizeY) = size;
            var (posX, posY) = pos;

            posY = -posY;

            Vector2[] corners =
            {
                new Vector2(posX, posY),
                new Vector2(posX + sizeX, posY),
                new Vector2(posX + sizeX, posY + sizeY),
                new Vector2(posX, posY + sizeY)
            };

            float thickness = Math.Max(2f, 2f / Screen.Selected.Cam.Zoom);

            GUI.DrawFilledRectangle(spriteBatch, corners[0], size, color * 0.1f);

            Vector2 offset = new Vector2(0f, thickness / 2f);

            if (sizeY < 0) { offset.Y = -offset.Y; }

            spriteBatch.DrawLine(corners[0], corners[1], color, thickness);
            spriteBatch.DrawLine(corners[1] - offset, corners[2] + offset, color, thickness);
            spriteBatch.DrawLine(corners[2], corners[3], color, thickness);
            spriteBatch.DrawLine(corners[3] + offset, corners[0] - offset, color, thickness);
        }

        protected static void ColorFlipButton(GUIButton btn, bool flip)
        {
            var color = flip ? GUIStyle.Green : Color.White;
            var hsv = ToolBox.RGBToHSV(color);

            // Boost saturation and reduce value a bit because our default colors are too muted for this button's style
            var hsvBase = hsv;
            hsvBase.Y *= 4f;
            hsvBase.Z *= 0.8f;
            btn.Color = ToolBoxCore.HSVToRGB(hsvBase.X, hsvBase.Y, hsvBase.Z); 
            btn.SelectedColor = ToolBoxCore.HSVToRGB(hsvBase.X, hsvBase.Y, hsvBase.Z); 

            var hsvHover = hsv;
            hsvHover.Z *= 1.2f;
            btn.HoverColor = ToolBoxCore.HSVToRGB(hsvHover.X, hsvHover.Y, hsvHover.Z); 
        }

        public static List<MapEntity> FilteredSelectedList { get; private set; } = new List<MapEntity>();

        public static void UpdateEditor(Camera cam, float deltaTime)
        {
            if (highlightedListBox != null) { highlightedListBox.UpdateManually(deltaTime); }

            if (editingHUD != null)
            {
                if (FilteredSelectedList.Count == 0 || editingHUD.UserData != FilteredSelectedList[0])
                {
                    foreach (GUIComponent component in editingHUD.Children)
                    {
                        var textBox = component as GUITextBox;
                        if (textBox == null) continue;
                        textBox.Deselect();
                    }
                    editingHUD = null;
                }
            }
            FilteredSelectedList.Clear();
            if (SelectedList.Count == 0) return;
            foreach (var e in SelectedList)
            {
                if (e is Gap gap && gap.ConnectedDoor != null) { continue; }
                FilteredSelectedList.Add(e);
            }
            var first = FilteredSelectedList.FirstOrDefault();
            if (first != null)
            {
                first.UpdateEditing(cam, deltaTime);
                if (first.ResizeHorizontal || first.ResizeVertical)
                {
                    first.UpdateResizing(cam);
                }
            }

            if (PlayerInput.IsCtrlDown())
            {
                if (PlayerInput.KeyHit(Keys.N))
                {
                    MapEntity firstSelected = SelectedList.First();

                    float minX = firstSelected.WorldRect.X,
                          maxX = firstSelected.WorldRect.Right;

                    foreach (MapEntity entity in SelectedList)
                    {
                        minX = Math.Min(minX, entity.WorldRect.X);
                        maxX = Math.Max(maxX, entity.WorldRect.Right);
                    }

                    float centerX = (minX + maxX) / 2.0f;
                    foreach (MapEntity me in SelectedList)
                    {
                        me.FlipX(false);
                        me.Move(new Vector2((centerX - me.WorldPosition.X) * 2.0f, 0.0f));
                    }
                }
                else if (PlayerInput.KeyHit(Keys.M))
                {
                    MapEntity firstSelected = SelectedList.First();

                    float minY = firstSelected.WorldRect.Y - firstSelected.WorldRect.Height,
                          maxY = firstSelected.WorldRect.Y;

                    foreach (MapEntity entity in SelectedList)
                    {

                        minY = Math.Min(minY, entity.WorldRect.Y - entity.WorldRect.Height);
                        maxY = Math.Max(maxY, entity.WorldRect.Y);
                    }

                    float centerY = (minY + maxY) / 2.0f;
                    foreach (MapEntity me in SelectedList)
                    {
                        me.FlipY(false);
                        me.Move(new Vector2(0.0f, (centerY - me.WorldPosition.Y) * 2.0f));
                    }
                }
            }
        }

        public static void ResetEditingHUD()
        {
            editingHUD = null;
        }

        public static void DrawEditor(SpriteBatch spriteBatch, Camera cam)
        {
            if (SelectedList.Count == 1)
            {
                MapEntity firstSelected = SelectedList.First();
                firstSelected.DrawEditing(spriteBatch, cam);
                if (firstSelected.ResizeHorizontal || firstSelected.ResizeVertical)
                {
                    firstSelected.DrawResizing(spriteBatch, cam);
                }
            }
        }

        public static void DeselectAll()
        {
            SelectedList.Clear();
        }

        public static void SelectEntity(MapEntity entity)
        {
            DeselectAll();
            AddSelection(entity);
        }

        /// <summary>
        /// Copy the selected entities to the "clipboard" (copiedList)
        /// </summary>
        public static void Copy(List<MapEntity> entities)
        {
            if (entities.Count == 0) { return; }
            CopyEntities(entities);
        }

        /// <summary>
         /// Copy the entities to the "clipboard" (copiedList) and delete them
         /// </summary>
        public static void Cut(List<MapEntity> entities)
        {
            if (entities.Count == 0) { return; }

            CopyEntities(entities);

            SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List<MapEntity>(entities), true));

            entities.ForEach(e => { if (!e.Removed) { e.Remove(); } });
            entities.Clear();
        }

        public static void Paste(Vector2 position)
        {
            if (CopiedList.Count == 0) { return; }

            List<MapEntity> prevEntities = new List<MapEntity>(MapEntityList);
            Clone(CopiedList);

            var clones = MapEntityList.Except(prevEntities).ToList();
            var nonWireClones = clones.Where(c => !(c is Item item) || item.GetComponent<Wire>() == null);
            if (!nonWireClones.Any()) { nonWireClones = clones; }

            Vector2 center = Vector2.Zero;
            nonWireClones.ForEach(c => center += c.WorldPosition);
            center = Submarine.VectorToWorldGrid(center / nonWireClones.Count());

            Vector2 moveAmount = Submarine.VectorToWorldGrid(position - center);

            SelectedList = new HashSet<MapEntity>(clones);
            foreach (MapEntity clone in SelectedList)
            {
                clone.Move(moveAmount);
                clone.Submarine = Submarine.MainSub;
            }
            foreach (MapEntity clone in SelectedList)
            {
                (clone as Item)?.GetComponent<ItemContainer>()?.SetContainedItemPositions();
            }

            SubEditorScreen.StoreCommand(new AddOrDeleteCommand(clones, false, handleInventoryBehavior: false));
            if (Screen.Selected is SubEditorScreen subEditor) { subEditor.ReconstructLayers(); }
        }

        /// <summary>
        /// copies a list of entities to the "clipboard" (copiedList)
        /// </summary>
        public static List<MapEntity> CopyEntities(List<MapEntity> entities)
        {
            List<MapEntity> prevEntities = new List<MapEntity>(MapEntityList);

            CopiedList = Clone(entities);

            //find all new entities created during cloning
            var newEntities = MapEntityList.Except(prevEntities).ToList();

            //do a "shallow remove" (removes the entities from the game without removing links between them)
            //  -> items will stay in their containers
            newEntities.ForEach(e => e.ShallowRemove());

            return newEntities;
        }

        public virtual void AddToGUIUpdateList(int order = 0)
        {
            if (editingHUD != null && editingHUD.UserData == this) { editingHUD.AddToGUIUpdateList(order: order); }
        }

        public virtual void UpdateEditing(Camera cam, float deltaTime) { }

        protected static void PositionEditingHUD()
        {
            int maxHeight = 
                Screen.Selected == GameMain.SubEditorScreen ?
                    GameMain.GraphicsHeight - GameMain.SubEditorScreen.EntityMenu.Rect.Height - GameMain.SubEditorScreen.TopPanel.Rect.Bottom * 2 - 20 :
                    HUDLayoutSettings.InventoryAreaLower.Y - HUDLayoutSettings.CrewArea.Bottom - 10;


            var listBox = editingHUD.GetChild<GUIListBox>();
            if (listBox != null)
            {
                int padding = 20;
                int contentHeight = 0;
                foreach (GUIComponent child in listBox.Content.Children)
                {
                    contentHeight += child.Rect.Height + listBox.Spacing;
                    child.RectTransform.MaxSize = new Point(int.MaxValue, child.Rect.Height);
                    child.RectTransform.MinSize = new Point(0, child.Rect.Height);
                }

                editingHUD.RectTransform.Resize(
                    new Point(
                        editingHUD.RectTransform.NonScaledSize.X,
                        MathHelper.Clamp(contentHeight + padding * 2, 50, maxHeight)), resizeChildren: false);
                listBox.RectTransform.Resize(new Point(listBox.RectTransform.NonScaledSize.X, editingHUD.RectTransform.NonScaledSize.Y - padding * 2), resizeChildren: false);
            }
            editingHUD.RectTransform.SetPosition(Anchor.TopRight);
            if (Screen.Selected == GameMain.SubEditorScreen)
            {
                editingHUD.RectTransform.AbsoluteOffset = new Point(0, GameMain.SubEditorScreen.TopPanel.Rect.Bottom);
            }
            else
            {
                editingHUD.RectTransform.AbsoluteOffset = new Point(
                    0, 
                    HUDLayoutSettings.HealthBarAfflictionArea.Y - editingHUD.Rect.Height - GUI.IntScale(10));
            }
        }

        public virtual void DrawEditing(SpriteBatch spriteBatch, Camera cam) { }

        private Vector2 GetEditingHandlePos(int x, int y, Camera cam)
        {
            Vector2 handleDiff = new Vector2(x * (rect.Width * 0.5f), y * (rect.Height * 0.5f));
            var rotation = -RotationRad;
            handleDiff = MathUtils.RotatePoint(handleDiff, rotation);
            if (FlippedX) { handleDiff = handleDiff.FlipX(); }
            if (FlippedY) { handleDiff = handleDiff.FlipY(); }
            return cam.WorldToScreen(Position + handleDiff);
        }

        float ResizeHandleSize => 10 * GUI.Scale;
        float ResizeHandleHighlightDistance => 8 * GUI.Scale;

        private void UpdateResizing(Camera cam)
        {
            IsHighlighted = true;

            int startX = ResizeHorizontal ? -1 : 0;
            int startY = ResizeVertical ? -1 : 0;

            for (int x = startX; x < 2; x += 2)
            {
                for (int y = startY; y < 2; y += 2)
                {
                    Vector2 handlePos = GetEditingHandlePos(x, y, cam);

                    bool highlighted = Vector2.DistanceSquared(PlayerInput.MousePosition, handlePos) < ResizeHandleHighlightDistance * ResizeHandleHighlightDistance;

                    if (highlighted && PlayerInput.PrimaryMouseButtonDown())
                    {
                        selectionPos = Vector2.Zero;
                        resizeDirX = x;
                        resizeDirY = y;
                        Resizing = true;
                        startMovingPos = Vector2.Zero;
                        ClearHighlightedEntities();
                    }
                }
            }

            if (Resizing)
            {
                if (prevRect == null)
                {
                    prevRect = Rect;
                }

                Vector2 placePosition = prevRect.Value.Location.ToVector2();
                Vector2 placeSize = prevRect.Value.Size.ToVector2();

                static Vector2 flipThenRotate(Vector2 point, Vector2 center, float angle, bool flipX, bool flipY)
                {
                    if (flipX) { point = (point - center).FlipX() + center; }
                    if (flipY) { point = (point - center).FlipY() + center; }

                    point = MathUtils.RotatePointAroundTarget(point, center, angle);

                    return point;
                }
                
                static Vector2 rotateThenFlip(Vector2 point, Vector2 center, float angle, bool flipX, bool flipY)
                {
                    point = MathUtils.RotatePointAroundTarget(point, center, angle);

                    if (flipX) { point = (point - center).FlipX() + center; }
                    if (flipY) { point = (point - center).FlipY() + center; }

                    return point;
                }
                
                Vector2 mousePos = cam.ScreenToWorld(PlayerInput.MousePosition);
                Vector2 prevPos = placePosition;
                Vector2 prevOppositeCorner = prevPos + placeSize.FlipY();
                Vector2 prevCenter = placePosition + placeSize.FlipY() * 0.5f;
                mousePos = flipThenRotate(mousePos, prevCenter, RotationRad, FlippedX, FlippedY);

                if (!PlayerInput.IsShiftDown())
                {
                    mousePos = Submarine.VectorToWorldGrid(mousePos, Submarine.MainSub, round: true);
                }

                if (resizeDirX > 0)
                {
                    mousePos.X = Math.Max(mousePos.X, prevRect.Value.X + Submarine.GridSize.X);
                    placeSize.X = mousePos.X - placePosition.X;
                }
                else if (resizeDirX < 0)
                {
                    mousePos.X = Math.Min(mousePos.X, prevRect.Value.Right - Submarine.GridSize.X);

                    placeSize.X = MathF.Round((placePosition.X + placeSize.X) - mousePos.X);
                    placePosition.X = MathF.Round(mousePos.X);
                }
                if (resizeDirY < 0)
                {
                    mousePos.Y = Math.Min(mousePos.Y, prevRect.Value.Y - Submarine.GridSize.Y);
                    placeSize.Y = placePosition.Y - mousePos.Y;
                }
                else if (resizeDirY > 0)
                {
                    mousePos.Y = Math.Max(mousePos.Y, prevRect.Value.Y - prevRect.Value.Height + Submarine.GridSize.Y);

                    placeSize.Y = mousePos.Y - (prevRect.Value.Y - prevRect.Value.Height);
                    placePosition.Y = mousePos.Y;
                }

                Vector2 newPos = placePosition;
                Vector2 newOppositeCorner = placePosition + placeSize.FlipY();

                Vector2 transformedCornerDiff = rotateThenFlip(newPos-prevPos, Vector2.Zero, -RotationRad, FlippedX, FlippedY);
                Vector2 transformedOppositeCornerDiff = rotateThenFlip(newOppositeCorner-prevOppositeCorner, Vector2.Zero, -RotationRad, FlippedX, FlippedY);

                Vector2 newPosTransformed = rotateThenFlip(prevPos, prevCenter, -RotationRad, FlippedX, FlippedY)
                                            + transformedCornerDiff;
                Vector2 newOppositeTransformed = rotateThenFlip(prevOppositeCorner, prevCenter, -RotationRad, FlippedX, FlippedY)
                                                 + transformedOppositeCornerDiff;
                Vector2 newTransformedCenter = (newPosTransformed + newOppositeTransformed) * 0.5f;

                var newDiff = (newOppositeCorner - newPos) * 0.5f;
                placePosition = newTransformedCenter - newDiff;

                if ((int)placePosition.X != rect.X || (int)placePosition.Y != rect.Y || (int)placeSize.X != rect.Width || (int)placeSize.Y != rect.Height)
                {
                    Rect = new Rectangle((int)placePosition.X, (int)placePosition.Y, (int)placeSize.X, (int)placeSize.Y);
                }

                if (!PlayerInput.PrimaryMouseButtonHeld())
                {
                    Resizing = false;
                    Resized?.Invoke(rect);
                    if (prevRect != null)
                    {
                        var newData = new List<Rectangle> { Rect };
                        var oldData = new List<Rectangle> { prevRect.Value };
                        SubEditorScreen.StoreCommand(new TransformCommand(new List<MapEntity> { this }, newData, oldData, true));
                    }

                    if (this is Structure structure)
                    {
                        foreach (LightSource light in structure.Lights)
                        {
                            light.LightTextureTargetSize = Rect.Size.ToVector2();
                            light.Position = rect.Location.ToVector2();
                        }
                    }
                    prevRect = null;
                }
            }
        }

        private void DrawResizing(SpriteBatch spriteBatch, Camera cam)
        {
            IsHighlighted = true;

            int startX = ResizeHorizontal ? -1 : 0;
            int startY = ResizeVertical ? -1 : 0;

            for (int x = startX; x < 2; x += 2)
            {
                for (int y = startY; y < 2; y += 2)
                {
                    Vector2 handlePos = GetEditingHandlePos(x, y, cam);

                    bool highlighted = Vector2.DistanceSquared(PlayerInput.MousePosition, handlePos) < ResizeHandleHighlightDistance * ResizeHandleHighlightDistance;
                    var color = Color.White * (highlighted ? 1.0f : 0.6f);
                    if (highlighted && !PlayerInput.PrimaryMouseButtonHeld())
                    {
                        GUI.MouseCursor = CursorState.Hand;
                    }
                    GUI.DrawRectangle(spriteBatch,
                        handlePos - new Vector2(ResizeHandleSize / 2),
                        new Vector2(ResizeHandleSize),
                        color,
                        true, 0,
                        (int)Math.Max(1.5f / GameScreen.Selected.Cam.Zoom, 1.0f));
                }
            }
        }

        /// <summary>
        /// Find entities whose rect intersects with the "selection rect"
        /// </summary>
        public static HashSet<MapEntity> FindSelectedEntities(Vector2 pos, Vector2 size)
        {
            HashSet<MapEntity> foundEntities = new HashSet<MapEntity>();

            Rectangle selectionRect = Submarine.AbsRect(pos, size);
            Quad2D selectionQuad = Quad2D.FromSubmarineRectangle(selectionRect);

            foreach (MapEntity entity in MapEntityList)
            {
                if (!entity.SelectableInEditor) { continue; }

                Quad2D entityQuad = entity.GetTransformedQuad();

                if (selectionQuad.Intersects(entityQuad))
                {
                    foundEntities.Add(entity);
                    entity.IsIncludedInSelection = true;

                    if (SubEditorScreen.IsLayerLinked(entity))
                    {
                        ImmutableHashSet<MapEntity> entitiesInSameLayer = SubEditorScreen.GetEntitiesInSameLayer(entity);
                        foreach (MapEntity layerEntity in entitiesInSameLayer.Where(e => !foundEntities.Contains(e)))
                        {
                            foundEntities.Add(layerEntity);
                            layerEntity.IsIncludedInSelection = true;
                        }
                    }
                }
            }

            return foundEntities;
        }
    }
}
