using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;

namespace Barotrauma
{
    partial class Submarine : Entity, IServerPositionSync
    {
        //drawing ----------------------------------------------------
        private static readonly HashSet<Submarine> visibleSubs = new HashSet<Submarine>();

        private static double prevCullTime;
        private static Rectangle prevCullArea;
        /// <summary>
        /// Interval at which we force culled entites to be updated, regardless if the camera has moved
        /// </summary>
        private const float CullInterval = 0.25f;
        /// <summary>
        /// Margin applied around the view area when culling entities (i.e. entities that are this far outside the view are still considered visible)
        /// </summary>
        private const int CullMargin = 50;
        /// <summary>
        /// Update entity culling when any corner of the view has moved more than this
        /// </summary>
        private const int CullMoveThreshold = 50;

        public static void CullEntities(Camera cam)
        {
            Rectangle camView = cam.WorldView;
            camView = new Rectangle(camView.X - CullMargin, camView.Y + CullMargin, camView.Width + CullMargin * 2, camView.Height + CullMargin * 2);

            if (Level.Loaded?.Renderer?.CollapseEffectStrength is > 0.0f)
            {
                //force everything to be visible when the collapse effect (which moves everything to a single point) is active
                camView = Rectangle.Union(AbsRect(camView.Location.ToVector2(), camView.Size.ToVector2()), new Rectangle(Point.Zero, Level.Loaded.Size));
                camView.Y += camView.Height;
            }

            if (Math.Abs(camView.X - prevCullArea.X) < CullMoveThreshold &&
                Math.Abs(camView.Y - prevCullArea.Y) < CullMoveThreshold &&
                Math.Abs(camView.Right - prevCullArea.Right) < CullMoveThreshold &&
                Math.Abs(camView.Bottom - prevCullArea.Bottom) < CullMoveThreshold &&
                prevCullTime > Timing.TotalTime - CullInterval)
            {
                return;
            }
            
            visibleSubs.Clear();
            foreach (Submarine sub in Loaded)
            {
                if (Level.Loaded != null && sub.WorldPosition.Y < Level.MaxEntityDepth) { continue; }

                Rectangle worldBorders = new Rectangle(
                    sub.VisibleBorders.X + (int)sub.WorldPosition.X,
                    sub.VisibleBorders.Y + (int)sub.WorldPosition.Y,
                    sub.VisibleBorders.Width,
                    sub.VisibleBorders.Height);

                if (RectsOverlap(worldBorders, camView))
                {
                    visibleSubs.Add(sub);
                }
            }

            if (visibleEntities == null)
            {
                visibleEntities = new List<MapEntity>(MapEntity.MapEntityList.Count);
            }
            else
            {
                visibleEntities.Clear();
            }

            foreach (MapEntity entity in MapEntity.MapEntityList)
            {
                if (entity == null || entity.Removed) { continue; }
                if (entity.Submarine != null)
                {
                    if (!visibleSubs.Contains(entity.Submarine)) { continue; }
                }
                if (entity.IsVisible(camView)) { visibleEntities.Add(entity); }
            }

            prevCullArea = camView;
            prevCullTime = Timing.TotalTime;
        }

        public static void ForceVisibilityRecheck()
        {
            prevCullTime = 0;
        }

        public static void ForceRemoveFromVisibleEntities(MapEntity entity)
        {
            visibleEntities?.Remove(entity);
        }

        public static void Draw(SpriteBatch spriteBatch, bool editing = false)
        {
            var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;

            foreach (MapEntity e in entitiesToRender)
            {
                e.Draw(spriteBatch, editing);
            }
        }

        public static void DrawFront(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null)
        {
            var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;

            foreach (MapEntity e in entitiesToRender)
            {
                if (!e.DrawOverWater) { continue; }

                if (predicate != null)
                {
                    if (!predicate(e)) { continue; }
                }

                e.Draw(spriteBatch, editing, false);
            }

            if (GameMain.DebugDraw)
            {
                foreach (Submarine sub in Loaded)
                {
                    Rectangle worldBorders = sub.Borders;
                    worldBorders.Location += (sub.DrawPosition + sub.HiddenSubPosition).ToPoint();
                    worldBorders.Y = -worldBorders.Y;

                    GUI.DrawRectangle(spriteBatch, worldBorders, Color.White, false, 0, 5);

                    if (sub.SubBody == null || sub.subBody.PositionBuffer.Count < 2) continue;

                    Vector2 prevPos = ConvertUnits.ToDisplayUnits(sub.subBody.PositionBuffer[0].Position);
                    prevPos.Y = -prevPos.Y;

                    for (int i = 1; i < sub.subBody.PositionBuffer.Count; i++)
                    {
                        Vector2 currPos = ConvertUnits.ToDisplayUnits(sub.subBody.PositionBuffer[i].Position);
                        currPos.Y = -currPos.Y;

                        GUI.DrawRectangle(spriteBatch, new Rectangle((int)currPos.X - 10, (int)currPos.Y - 10, 20, 20), Color.Blue * 0.6f, true, 0.01f);
                        GUI.DrawLine(spriteBatch, prevPos, currPos, Color.Cyan * 0.5f, 0, 5);

                        prevPos = currPos;
                    }
                }
            }
        }

        public static float DamageEffectCutoff;

        private static readonly List<Structure> depthSortedDamageable = new List<Structure>();

        public static void DrawDamageable(SpriteBatch spriteBatch, Effect damageEffect, bool editing = false, Predicate<MapEntity> predicate = null)
        {
            var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;

            depthSortedDamageable.Clear();

            //insertion sort according to draw depth
            foreach (MapEntity e in entitiesToRender)
            {
                if (e is Structure structure && structure.DrawDamageEffect)
                {
                    if (predicate != null)
                    {
                        if (!predicate(e)) { continue; }
                    }
                    float drawDepth = structure.GetDrawDepth();
                    int i = 0;
                    while (i < depthSortedDamageable.Count)
                    {
                        float otherDrawDepth = depthSortedDamageable[i].GetDrawDepth();
                        if (otherDrawDepth < drawDepth) { break; }
                        i++;
                    }
                    depthSortedDamageable.Insert(i, structure);
                }
            }

            foreach (Structure s in depthSortedDamageable)
            {
                s.DrawDamage(spriteBatch, damageEffect, editing);
            }
        }

        public static void DrawPaintedColors(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null)
        {
            var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;

            foreach (MapEntity e in entitiesToRender)
            {
                if (e is Hull hull)
                {
                    if (hull.SupportsPaintedColors)
                    {
                        if (predicate != null)
                        {
                            if (!predicate(e)) { continue; }
                        }
                        hull.DrawSectionColors(spriteBatch);
                    }
                }
            }
        }

        public static void DrawBack(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null)
        {
            var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;

            foreach (MapEntity e in entitiesToRender)
            {
                if (!e.DrawBelowWater) continue;

                if (predicate != null)
                {
                    if (!predicate(e)) continue;
                }

                e.Draw(spriteBatch, editing, true);
            }
        }

        public static void DrawGrid(SpriteBatch spriteBatch, int gridCells, Vector2 gridCenter, Vector2 roundedGridCenter, float alpha = 1.0f, Color? color = null)
        {
            Vector2 topLeft = roundedGridCenter - Vector2.One * GridSize * gridCells / 2;
            Vector2 bottomRight = roundedGridCenter + Vector2.One * GridSize * gridCells / 2;

            for (int i = 0; i < gridCells; i++)
            {
                float middleIndex = (gridCells - 1) / 2.0f;
                float normalizedPos = Math.Abs((i - middleIndex) / middleIndex);
                float expandX = MathHelper.Lerp(30.0f, 0.0f, normalizedPos);
                float expandY = expandX;

                Color lineColor = color ?? Color.White;

                GUI.DrawLine(spriteBatch,
                    new Vector2(topLeft.X - expandX, -bottomRight.Y + i * GridSize.Y),
                    new Vector2(bottomRight.X + expandX, -bottomRight.Y + i * GridSize.Y),
                    lineColor * (1.0f - normalizedPos) * alpha, depth: 0.6f, width: 3);
                GUI.DrawLine(spriteBatch,
                    new Vector2(topLeft.X + i * GridSize.X, -topLeft.Y + expandY),
                    new Vector2(topLeft.X + i * GridSize.X, -bottomRight.Y - expandY),
                    lineColor * (1.0f - normalizedPos) * alpha, depth: 0.6f, width: 3);
            }
        }

        // TODO remove
        [Obsolete("Use MiniMap.CreateMiniMap()")]
        public void CreateMiniMap(GUIComponent parent, IEnumerable<Entity> pointsOfInterest = null, bool ignoreOutpost = false)
        {
            Rectangle worldBorders = GetDockedBorders();
            worldBorders.Location += WorldPosition.ToPoint();

            //create a container that has the same "aspect ratio" as the sub
            float aspectRatio = worldBorders.Width / (float)worldBorders.Height;
            float parentAspectRatio = parent.Rect.Width / (float)parent.Rect.Height;

            float scale = 0.9f;

            GUIFrame hullContainer = new GUIFrame(new RectTransform(
                (parentAspectRatio > aspectRatio ? new Vector2(aspectRatio / parentAspectRatio, 1.0f) : new Vector2(1.0f, parentAspectRatio / aspectRatio)) * scale,
                parent.RectTransform, Anchor.Center),
                style: null)
            {
                UserData = "hullcontainer"
            };

            var connectedSubs = GetConnectedSubs();

            HashSet<Hull> hullList = Hull.HullList.Where(hull => hull.Submarine == this || connectedSubs.Contains(hull.Submarine)).Where(hull => !ignoreOutpost || IsEntityFoundOnThisSub(hull, true)).ToHashSet();

            Dictionary<Hull, HashSet<Hull>> combinedHulls = new Dictionary<Hull, HashSet<Hull>>();

            foreach (Hull hull in hullList)
            {
                if (combinedHulls.ContainsKey(hull) || combinedHulls.Values.Any(hh => hh.Contains(hull))) { continue; }

                List<Hull> linkedHulls = new List<Hull>();
                hull.GetLinkedHulls(linkedHulls);

                linkedHulls.Remove(hull);

                foreach (Hull linkedHull in linkedHulls)
                {
                    if (!combinedHulls.ContainsKey(hull))
                    {
                        combinedHulls.Add(hull, new HashSet<Hull>());
                    }
                    combinedHulls[hull].Add(linkedHull);
                }
            }

            foreach (Hull hull in hullList)
            {
                Vector2 relativeHullPos = new Vector2(
                    (hull.WorldRect.X - worldBorders.X) / (float)worldBorders.Width,
                    (worldBorders.Y - hull.WorldRect.Y) / (float)worldBorders.Height);
                Vector2 relativeHullSize = new Vector2(hull.Rect.Width / (float)worldBorders.Width, hull.Rect.Height / (float)worldBorders.Height);

                bool hideHull = combinedHulls.ContainsKey(hull) || combinedHulls.Values.Any(hh => hh.Contains(hull));

                if (hideHull) { continue; }

                Color color = Color.DarkCyan * 0.8f;

                var hullFrame = new GUIFrame(new RectTransform(relativeHullSize, hullContainer.RectTransform) { RelativeOffset = relativeHullPos }, style: "MiniMapRoom", color: color)
                {
                    UserData = hull
                };

                new GUIFrame(new RectTransform(Vector2.One, hullFrame.RectTransform), style: "ScanLines", color: color);
            }

            foreach (var (mainHull, linkedHulls) in combinedHulls)
            {
                MiniMapHullData data = ConstructLinkedHulls(mainHull, linkedHulls, hullContainer, worldBorders);

                Vector2 relativeHullPos = new Vector2(
                    (data.Bounds.X - worldBorders.X) / worldBorders.Width,
                    (worldBorders.Y - data.Bounds.Y) / worldBorders.Height);

                Vector2 relativeHullSize = new Vector2(data.Bounds.Width / worldBorders.Width, data.Bounds.Height / worldBorders.Height);

                Color color = Color.DarkCyan * 0.8f;

                float highestY = 0f,
                      highestX = 0f;

                foreach (var (r, _) in data.RectDatas)
                {
                    float y = r.Y - -r.Height,
                          x = r.X;

                    if (y > highestY) { highestY = y; }
                    if (x > highestX) { highestX = x; }
                }

                HashSet<GUIFrame> frames = new HashSet<GUIFrame>();

                foreach (var (snappredRect, hull) in data.RectDatas)
                {
                    RectangleF rect = snappredRect;
                    rect.Height = -rect.Height;
                    rect.Y -= rect.Height;

                    var (parentW, parentH) = hullContainer.Rect.Size.ToVector2();
                    Vector2 size = new Vector2(rect.Width / parentW, rect.Height / parentH);
                    // TODO this won't be required if we some day switch RectTransform to use RectangleF
                    Vector2 pos = new Vector2(rect.X / parentW, rect.Y / parentH);

                    GUIFrame hullFrame = new GUIFrame(new RectTransform(size, hullContainer.RectTransform) { RelativeOffset = pos }, style: "ScanLinesSeamless", color: color)
                    {
                        UserData = hull,
                        UVOffset = new Vector2(highestX - rect.X, highestY - rect.Y)
                    };

                    frames.Add(hullFrame);
                }

                new GUICustomComponent(new RectTransform(relativeHullSize, hullContainer.RectTransform) { RelativeOffset = relativeHullPos }, (spriteBatch, component) =>
                {
                    foreach (List<Vector2> list in data.Polygon)
                    {
                        spriteBatch.DrawPolygonInner(hullContainer.Rect.Location.ToVector2(), list, component.Color, 2f);
                    }
                }, (deltaTime, component) =>
                {
                    if (component.Parent.Rect.Size != data.ParentSize)
                    {
                        data = ConstructLinkedHulls(mainHull, linkedHulls, hullContainer, worldBorders);
                    }
                })
                {
                    UserData = frames,
                    Color = color,
                    CanBeFocused = false
                };
            }

            if (pointsOfInterest != null)
            {
                foreach (Entity entity in pointsOfInterest)
                {
                    Vector2 relativePos = new Vector2(
                        (entity.WorldPosition.X - worldBorders.X) / worldBorders.Width,
                        (worldBorders.Y - entity.WorldPosition.Y) / worldBorders.Height);
                    new GUIFrame(new RectTransform(new Point(1, 1), hullContainer.RectTransform) { RelativeOffset = relativePos }, style: null)
                    {
                        CanBeFocused = false,
                        UserData = entity
                    };
                }
            }
        }

        public static MiniMapHullData ConstructLinkedHulls(Hull mainHull, HashSet<Hull> linkedHulls, GUIComponent parent, Rectangle worldBorders)
        {
            Rectangle parentRect = parent.Rect;

            Dictionary<Hull, Rectangle> rects = new Dictionary<Hull, Rectangle>();
            Rectangle worldRect = mainHull.WorldRect;
            worldRect.Y = -worldRect.Y;

            rects.Add(mainHull, worldRect);

            foreach (Hull hull in linkedHulls)
            {
                Rectangle rect = hull.WorldRect;
                rect.Y = -rect.Y;

                worldRect = Rectangle.Union(worldRect, rect);
                rects.Add(hull, rect);
            }

            worldRect.Y = -worldRect.Y;

            List<RectangleF> normalizedRects = new List<RectangleF>();
            List<Hull> hullRefs = new List<Hull>();
            foreach (var (hull, rect) in rects)
            {
                Rectangle wRect = rect;
                wRect.Y = -wRect.Y;

                var (posX, posY) = new Vector2(
                    (wRect.X - worldBorders.X) / (float)worldBorders.Width,
                    (worldBorders.Y - wRect.Y) / (float)worldBorders.Height);

                var (scaleX, scaleY) = new Vector2(wRect.Width / (float)worldBorders.Width, wRect.Height / (float)worldBorders.Height);

                RectangleF newRect = new RectangleF(posX * parentRect.Width, posY * parentRect.Height, scaleX * parentRect.Width, scaleY * parentRect.Height);

                normalizedRects.Add(newRect);
                hullRefs.Add(hull);
            }

            ImmutableArray<RectangleF> snappedRectangles = ToolBox.SnapRectangles(normalizedRects, treshold: 1);

            List<List<Vector2>> polygon = ToolBox.CombineRectanglesIntoShape(snappedRectangles);

            List<List<Vector2>> scaledPolygon = new List<List<Vector2>>();

            foreach (List<Vector2> list in polygon)
            {
                var (polySizeX, polySizeY) = ToolBox.GetPolygonBoundingBoxSize(list);
                float sizeX = polySizeX - 1f,
                      sizeY = polySizeY - 1f;

                scaledPolygon.Add(ToolBox.ScalePolygon(list, new Vector2(sizeX / polySizeX, sizeY / polySizeY)));
            }

            return new MiniMapHullData(scaledPolygon, worldRect, parentRect.Size, snappedRectangles, hullRefs.ToImmutableArray());
        }

        public void CheckForErrors()
        {
            List<string> errorMsgs = new List<string>();
            List<SubEditorScreen.WarningType> warnings = new List<SubEditorScreen.WarningType>();

            if (!Hull.HullList.Any())
            {
                if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoWaypoints))
                {
                    errorMsgs.Add(TextManager.Get("NoHullsWarning").Value);
                    warnings.Add(SubEditorScreen.WarningType.NoHulls);
                }
            }

            if (Info.Type != SubmarineType.OutpostModule || 
                (Info.OutpostModuleInfo?.ModuleFlags.Any(f => f != "hallwayvertical" && f != "hallwayhorizontal") ?? true))
            {
                if (!WayPoint.WayPointList.Any(wp => wp.ShouldBeSaved && wp.SpawnType == SpawnType.Path))
                {
                    if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoWaypoints))
                    {
                        errorMsgs.Add(TextManager.Get("NoWaypointsWarning").Value);
                        warnings.Add(SubEditorScreen.WarningType.NoWaypoints);
                    }
                }
            }

            if (Hull.HullList.Any(h => h.WaterVolume > 0.0f))
            {
                errorMsgs.Add(TextManager.Get("WaterInHullsWarning").Value);
                warnings.Add(SubEditorScreen.WarningType.WaterInHulls);
                Hull.ShowHulls = true;
            }
            
            if (Info.IsWreck)
            {
                Point vanillaBrainSize = new Point(204, 204);
                if (WreckAI.GetPotentialBrainRooms(this, WreckAIConfig.GetRandom(), minSize: vanillaBrainSize).None())
                {
                    errorMsgs.Add(TextManager.Get("NoSuitableBrainRoomsWarning").Value);
                    warnings.Add(SubEditorScreen.WarningType.NoSuitableBrainRooms);
                }
            }

            if (!IsWarningSuppressed(SubEditorScreen.WarningType.NotEnoughContainers))
            {
                HashSet<ContainerTagPrefab> missingContainerTags = new();
                foreach (var prefab in ContainerTagPrefab.Prefabs)
                {
                    if (!prefab.IsRecommendedForSub(this) || !prefab.WarnIfLess) { continue; }
                
                    int count = Item.ItemList.Count(i => i.HasTag(prefab.Identifier));
                    if (count < prefab.RecommendedAmount)
                    {
                        missingContainerTags.Add(prefab);
                    }
                }

                if (missingContainerTags.Any())
                {
                    StringBuilder sb = new();
                    int count = 0;
                    foreach (var tag in missingContainerTags)
                    {
                        sb.AppendLine($"- {tag.Name}");
                        count++;
                        if (missingContainerTags.Count > count && count >= 3)
                        {
                            var moreIndicator = TextManager.GetWithVariable(
                                "upgradeuitooltip.moreindicator",
                                "[amount]",
                                (missingContainerTags.Count - count).ToString()).Value;
                            sb.AppendLine(moreIndicator);
                            break;
                        }
                    }

                    errorMsgs.Add(TextManager.GetWithVariable(
                        "ContainerTagUI.CountWarning",
                        "[tags]",
                        sb.ToString()).Value);
                    warnings.Add(SubEditorScreen.WarningType.NotEnoughContainers);
                }
            }
            
            if (Info.Type == SubmarineType.Player)
            {
                foreach (Item item in Item.ItemList)
                {
                    if (item.GetComponent<Vent>() == null) { continue; }
                    if (!item.linkedTo.Any())
                    {
                        if (!IsWarningSuppressed(SubEditorScreen.WarningType.DisconnectedVents))
                        {
                            errorMsgs.Add(TextManager.Get("DisconnectedVentsWarning").Value);
                            warnings.Add(SubEditorScreen.WarningType.DisconnectedVents);
                        }
                        break;
                    }
                }
                foreach (Item item in Item.ItemList)
                {
                    if (item.GetComponent<OxygenGenerator>() is not OxygenGenerator oxygenGenerator) { continue; }

                    oxygenGenerator.GetVents();

                    Dictionary<Hull, float> hullOxygenFlow = new Dictionary<Hull, float>();

                    foreach (var linkedTo in item.linkedTo)
                    {
                        if (linkedTo is not Item linkedItem || linkedItem.GetComponent<Vent>() is not Vent vent) { continue; }
                        if (vent.Item.CurrentHull == null)
                        {
                            vent.Item.FindHull();
                            if (vent.Item.CurrentHull == null) { continue; }
                        }                        
                        float oxygenFlow = oxygenGenerator.GetVentOxygenFlow(vent);
                        if (!hullOxygenFlow.ContainsKey(vent.Item.CurrentHull))
                        {
                            hullOxygenFlow[vent.Item.CurrentHull] = oxygenFlow;
                        }
                        else
                        {
                            hullOxygenFlow[vent.Item.CurrentHull] += oxygenFlow;
                        }                        
                    }
                    foreach ((Hull hull, float oxygenFlow) in hullOxygenFlow)
                    {
                        if (oxygenFlow < Hull.OxygenConsumptionSpeed)
                        {
                            errorMsgs.Add(TextManager.GetWithVariable("LowOxygenOutputWarning", "[roomname]",
                                hull.DisplayName).Value);
                            warnings.Add(SubEditorScreen.WarningType.LowOxygenOutputWarning);
                        }
                    }
                }
                if (!WayPoint.WayPointList.Any(wp => wp.ShouldBeSaved && wp.SpawnType == SpawnType.Human))
                {
                    if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoHumanSpawnpoints))
                    {
                        errorMsgs.Add(TextManager.Get("NoHumanSpawnpointWarning").Value);
                        warnings.Add(SubEditorScreen.WarningType.NoHumanSpawnpoints);
                    }
                }
                if (WayPoint.WayPointList.Find(wp => wp.SpawnType == SpawnType.Cargo) == null)
                {
                    if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoCargoSpawnpoints))
                    {
                        errorMsgs.Add(TextManager.Get("NoCargoSpawnpointWarning").Value);
                        warnings.Add(SubEditorScreen.WarningType.NoCargoSpawnpoints);
                    }
                }
                if (Item.ItemList.None(it => it.GetComponent<Pump>() != null && it.HasTag(Tags.Ballast)))
                {
                    if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoBallastTag))
                    {
                        errorMsgs.Add(TextManager.Get("NoBallastTagsWarning").Value);
                        warnings.Add(SubEditorScreen.WarningType.NoBallastTag);
                    }
                }
                if (Item.ItemList.None(it => it.HasTag(Tags.HiddenItemContainer)))
                {
                    if (!IsWarningSuppressed(SubEditorScreen.WarningType.NoHiddenContainers))
                    {
                        errorMsgs.Add(TextManager.Get("NoHiddenContainersWarning").Value);
                        warnings.Add(SubEditorScreen.WarningType.NoHiddenContainers);
                    }
                }
                if (Info.Dimensions.X * Physics.DisplayToRealWorldRatio > 80 ||
                    Info.Dimensions.Y * Physics.DisplayToRealWorldRatio > 32)
                {
                    if (!IsWarningSuppressed(SubEditorScreen.WarningType.TooLargeForEndGame))
                    {
                        errorMsgs.Add(TextManager.Get("TooLargeForEndGameWarning").Value);
                        warnings.Add(SubEditorScreen.WarningType.TooLargeForEndGame);
                    }
                }
            }
            else if (Info.Type == SubmarineType.OutpostModule)
            {
                foreach (Item item in Item.ItemList)
                {
                    var junctionBox = item.GetComponent<PowerTransfer>();
                    if (junctionBox == null) { continue; }
                    int doorLinks =
                        item.linkedTo.Count(lt => lt is Gap || (lt is Item it2 && it2.GetComponent<Door>() != null)) +
                        Item.ItemList.Count(it2 => it2.linkedTo.Contains(item) && !item.linkedTo.Contains(it2));
                    for (int i = 0; i < item.Connections.Count; i++)
                    {
                        int wireCount = item.Connections[i].Wires.Count;
                        if (doorLinks + wireCount > item.Connections[i].MaxWires)
                        {
                            errorMsgs.Add(TextManager.GetWithVariables("InsufficientFreeConnectionsWarning",
                                ("[doorcount]", doorLinks.ToString()),
                                ("[freeconnectioncount]", (item.Connections[i].MaxWires - wireCount).ToString())).Value);
                            warnings.Add(SubEditorScreen.WarningType.InsufficientFreeConnectionsWarning);
                            break;
                        }
                    }
                }
            }

            if (Gap.GapList.Any(g => g.linkedTo.Count == 0))
            {
                if (!IsWarningSuppressed(SubEditorScreen.WarningType.NonLinkedGaps))
                {
                    errorMsgs.Add(TextManager.Get("NonLinkedGapsWarning").Value);
                    warnings.Add(SubEditorScreen.WarningType.NonLinkedGaps);
                }
            }

            float entityCountWarningThreshold = 0.75f;

            if (Item.ItemList.Count > SubEditorScreen.MaxItems * entityCountWarningThreshold)
            {
                if (!IsWarningSuppressed(SubEditorScreen.WarningType.ItemCount))
                {
                    errorMsgs.Add(TextManager.Get("subeditor.itemcountwarning").Value);
                    warnings.Add(SubEditorScreen.WarningType.ItemCount);
                }
            }

            if ((MapEntity.MapEntityList.Count - Item.ItemList.Count - Hull.HullList.Count - WayPoint.WayPointList.Count - Gap.GapList.Count) > SubEditorScreen.MaxStructures * entityCountWarningThreshold)
            {
                if (!IsWarningSuppressed(SubEditorScreen.WarningType.StructureCount))
                {
                    errorMsgs.Add(TextManager.Get("subeditor.structurecountwarning").Value);
                    warnings.Add(SubEditorScreen.WarningType.StructureCount);
                }
            }

            if (Structure.WallList.Count > SubEditorScreen.MaxStructures * entityCountWarningThreshold)
            {
                if (!IsWarningSuppressed(SubEditorScreen.WarningType.WallCount))
                {
                    errorMsgs.Add(TextManager.Get("subeditor.wallcountwarning").Value);
                    warnings.Add(SubEditorScreen.WarningType.WallCount);
                }
            }

            if (GetLightCount() > SubEditorScreen.MaxLights * entityCountWarningThreshold)
            {
                if (!IsWarningSuppressed(SubEditorScreen.WarningType.LightCount))
                {
                    errorMsgs.Add(TextManager.Get("subeditor.lightcountwarning").Value);
                    warnings.Add(SubEditorScreen.WarningType.LightCount);
                }
            }

            if (GetShadowCastingLightCount() > SubEditorScreen.MaxShadowCastingLights * entityCountWarningThreshold)
            {
                if (!IsWarningSuppressed(SubEditorScreen.WarningType.ShadowCastingLightCount))
                {
                    errorMsgs.Add(TextManager.Get("subeditor.shadowcastinglightswarning").Value);
                    warnings.Add(SubEditorScreen.WarningType.ShadowCastingLightCount);
                }
            }

            if (errorMsgs.Any())
            {
                GUIMessageBox msgBox = new GUIMessageBox(TextManager.Get("Warning"), string.Empty, new Vector2(0.25f, 0.0f), minSize: new Point(GUI.IntScale(650), GUI.IntScale(650)));
                if (warnings.Any())
                {
                    var textListBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.75f), msgBox.Content.RectTransform));
                    var text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textListBox.Content.RectTransform), string.Join("\n\n", errorMsgs), wrap: true)
                    {
                        CanBeFocused = false
                    };
                    text.RectTransform.MinSize = new Point(0, (int)text.TextSize.Y);

                    Point size = msgBox.RectTransform.NonScaledSize;
                    GUITickBox suppress = new GUITickBox(new RectTransform(new Vector2(1f, 0.33f), msgBox.Content.RectTransform), TextManager.Get("editor.suppresswarnings"));
                    msgBox.RectTransform.NonScaledSize = new Point(size.X, size.Y + suppress.RectTransform.NonScaledSize.Y);

                    msgBox.Buttons[0].OnClicked += (button, obj) =>
                    {
                        if (suppress.Selected)
                        {
                            foreach (SubEditorScreen.WarningType warning in warnings.Where(warning => !SubEditorScreen.SuppressedWarnings.Contains(warning)))
                            {
                                SubEditorScreen.SuppressedWarnings.Add(warning);
                            }
                        }
                        return true;
                    };
                }
            }

            foreach (MapEntity e in MapEntity.MapEntityList)
            {
                if (Vector2.Distance(e.Position, HiddenSubPosition) > 20000)
                {
                    //move disabled items (wires, items inside containers) inside the sub
                    if (e is Item item && item.body != null && !item.body.Enabled)
                    {
                        item.SetTransform(ConvertUnits.ToSimUnits(HiddenSubPosition), 0.0f);
                    }
                }
            }

            foreach (MapEntity e in MapEntity.MapEntityList)
            {
                if (Vector2.Distance(e.Position, HiddenSubPosition) > 20000)
                {
                    var msgBox = new GUIMessageBox(
                        TextManager.Get("Warning"),
                        TextManager.Get("FarAwayEntitiesWarning"),
                        new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") });

                    msgBox.Buttons[0].OnClicked += (btn, obj) =>
                    {
                        GameMain.SubEditorScreen.Cam.Position = e.WorldPosition;
                        return true;
                    };
                    msgBox.Buttons[0].OnClicked += msgBox.Close;
                    msgBox.Buttons[1].OnClicked += msgBox.Close;

                    break;

                }
            }

            bool IsWarningSuppressed(SubEditorScreen.WarningType type)
            {
                return SubEditorScreen.SuppressedWarnings.Contains(type);
            }
        }

        public static int GetLightCount()
        {
            int disabledItemLightCount = 0;
            foreach (Item item in Item.ItemList)
            {
                if (item.ParentInventory == null) { continue; }
                disabledItemLightCount += item.GetComponents<Items.Components.LightComponent>().Count();
            }
            return GameMain.LightManager.Lights.Count() - disabledItemLightCount;
        }

        public static int GetShadowCastingLightCount()
        {
            int disabledItemLightCount = 0;
            foreach (Item item in Item.ItemList)
            {
                if (item.ParentInventory == null) { continue; }
                disabledItemLightCount += item.GetComponents<Items.Components.LightComponent>().Count();
            }
            return GameMain.LightManager.Lights.Count(l => l.CastShadows && !l.IsBackground) - disabledItemLightCount;
        }

        public static Vector2 MouseToWorldGrid(Camera cam, Submarine sub, Vector2? mousePos = null, bool round = false)
        {
            Vector2 position = mousePos ?? PlayerInput.MousePosition;
            position = cam.ScreenToWorld(position);

            Vector2 worldGridPos = VectorToWorldGrid(position, sub, round);

            return worldGridPos;
        }

        public void ClientReadPosition(IReadMessage msg, float sendingTime)
        {
            var posInfo = PhysicsBody.ClientRead(msg, sendingTime, parentDebugName: Info.Name);
            msg.ReadPadBits();

            if (posInfo != null)
            {
                int index = 0;
                while (index < subBody.PositionBuffer.Count && sendingTime > subBody.PositionBuffer[index].Timestamp)
                {
                    index++;
                }

                subBody.PositionBuffer.Insert(index, posInfo);
            }
        }

        public void ClientEventRead(IReadMessage msg, float sendingTime)
        {
            Identifier layerIdentifier = msg.ReadIdentifier();
            bool enabled = msg.ReadBoolean();
            SetLayerEnabled(layerIdentifier, enabled);
        }
    }
}
