using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using Robust.Shared.Collections;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Map.Enumerators;
using Robust.Shared.Map.Events;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Timing;
using Robust.Shared.Utility;

namespace Robust.Shared.GameObjects;

public abstract partial class SharedMapSystem
{
    #region Chunk helpers

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Vector2i GetChunkIndices(Vector2 tile, int chunkSize)
    {
        return new Vector2i ((int) Math.Floor(tile.X / chunkSize), (int) Math.Floor(tile.Y / chunkSize));
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Vector2i GetChunkIndices(Vector2 tile, byte chunkSize)
    {
        return new Vector2i ((int) Math.Floor(tile.X / chunkSize), (int) Math.Floor(tile.Y / chunkSize));
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Vector2i GetChunkIndices(Vector2i tile, int chunkSize)
    {
        return new Vector2i ((int) Math.Floor(tile.X / (float) chunkSize), (int) Math.Floor(tile.Y / (float) chunkSize));
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Vector2i GetChunkIndices(Vector2i tile, byte chunkSize)
    {
        return new Vector2i ((int) Math.Floor(tile.X / (float) chunkSize), (int) Math.Floor(tile.Y / (float) chunkSize));
    }

    /// <summary>
    /// Returns the tile offset to a chunk origin based on the provided size.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Vector2i GetChunkRelative(Vector2 tile, int chunkSize)
    {
        var x = MathHelper.Mod((int) Math.Floor(tile.X), chunkSize);
        var y = MathHelper.Mod((int) Math.Floor(tile.Y), chunkSize);
        return new Vector2i(x, y);
    }

    /// <summary>
    /// Returns the tile offset to a chunk origin based on the provided size.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Vector2i GetChunkRelative(Vector2 tile, byte chunkSize)
    {
        var x = MathHelper.Mod((int) Math.Floor(tile.X), chunkSize);
        var y = MathHelper.Mod((int) Math.Floor(tile.Y), chunkSize);
        return new Vector2i(x, y);
    }

    /// <summary>
    /// Returns the tile offset to a chunk origin based on the provided size.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Vector2i GetChunkRelative(Vector2i tile, int chunkSize)
    {
        var x = MathHelper.Mod(tile.X, chunkSize);
        var y = MathHelper.Mod(tile.Y, chunkSize);
        return new Vector2i(x, y);
    }

    /// <summary>
    /// Returns the tile offset to a chunk origin based on the provided size.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Vector2i GetChunkRelative(Vector2i tile, byte chunkSize)
    {
        var x = MathHelper.Mod(tile.X, chunkSize);
        var y = MathHelper.Mod(tile.Y, chunkSize);
        return new Vector2i(x, y);
    }

    #endregion

    public static Vector2i GetDirection(Vector2i position, Direction dir, int dist = 1)
    {
        switch (dir)
        {
            case Direction.East:
                return position + new Vector2i(dist, 0);
            case Direction.SouthEast:
                return position + new Vector2i(dist, -dist);
            case Direction.South:
                return position + new Vector2i(0, -dist);
            case Direction.SouthWest:
                return position + new Vector2i(-dist, -dist);
            case Direction.West:
                return position + new Vector2i(-dist, 0);
            case Direction.NorthWest:
                return position + new Vector2i(-dist, dist);
            case Direction.North:
                return position + new Vector2i(0, dist);
            case Direction.NorthEast:
                return position + new Vector2i(dist, dist);
            default:
                throw new NotImplementedException();
        }
    }

    private void InitializeGrid()
    {
        SubscribeLocalEvent<MapGridComponent, ComponentGetState>(OnGridGetState);
        SubscribeLocalEvent<MapGridComponent, ComponentHandleState>(OnGridHandleState);
        SubscribeLocalEvent<MapGridComponent, ComponentAdd>(OnGridAdd);
        SubscribeLocalEvent<MapGridComponent, ComponentInit>(OnGridInit);
        SubscribeLocalEvent<MapGridComponent, ComponentStartup>(OnGridStartup);
        SubscribeLocalEvent<MapGridComponent, ComponentShutdown>(OnGridRemove);
        SubscribeLocalEvent<MapGridComponent, MoveEvent>(OnGridMove);
    }

    /// <summary>
    /// <see cref="GetGridPosition(Robust.Shared.GameObjects.Entity{Robust.Shared.Physics.Components.PhysicsComponent?},System.Numerics.Vector2,Robust.Shared.Maths.Angle)"/>
    /// </summary>
    public Vector2 GetGridPosition(Entity<PhysicsComponent?> grid, Vector2 worldPos, Angle worldRot)
    {
        if (!Resolve(grid.Owner, ref grid.Comp))
            return Vector2.Zero;

        return worldPos + worldRot.RotateVec(grid.Comp.LocalCenter);
    }

    /// <summary>
    /// Gets the mapgrid's position considering its local physics center.
    /// </summary>
    public Vector2 GetGridPosition(Entity<PhysicsComponent?, TransformComponent?> grid)
    {
        if (!Resolve(grid.Owner, ref grid.Comp1, ref grid.Comp2))
            return Vector2.Zero;

        var (worldPos, worldRot) = _transform.GetWorldPositionRotation(grid.Comp2);

        return GetGridPosition((grid.Owner, grid.Comp1), worldPos, worldRot);
    }

    private void OnGridBoundsChange(EntityUid uid, MapGridComponent component)
    {
        // Just MapLoader things.
        if (component.MapProxy == DynamicTree.Proxy.Free) return;

        var xform = Comp<TransformComponent>(uid);
        var aabb = GetWorldAABB(uid, component, xform);

        if (TryComp<GridTreeComponent>(xform.MapUid, out var gridTree))
        {
            gridTree.Tree.MoveProxy(component.MapProxy, in aabb);
        }

        _physics.MovedGrids.Add(uid);
    }

    private void OnGridMove(EntityUid uid, MapGridComponent component, ref MoveEvent args)
    {
        if (args.ParentChanged)
        {
            OnParentChange(uid, component, ref args);
            return;
        }

        // Just maploader / test things
        if (component.MapProxy == DynamicTree.Proxy.Free) return;

        var xform = args.Component;
        var aabb = GetWorldAABB(uid, component, xform);

        if (TryComp<GridTreeComponent>(xform.MapUid, out var gridTree))
        {
            gridTree.Tree.MoveProxy(component.MapProxy, in aabb);
        }

        _physics.MovedGrids.Add(uid);
    }

    private void OnParentChange(EntityUid uid, MapGridComponent component, ref MoveEvent args)
    {
        UpdatePvsChunks(args.Entity);

        var (_, xform, meta) = args.Entity;

        // oh boy
        // Want gridinit to handle this hence specialcase those situations.
        // oh boy oh boy, its even worse now.
        // transform now raises parent change events on startup, because container code is a POS.
        if (meta.EntityLifeStage < EntityLifeStage.Initialized || args.Component.LifeStage == ComponentLifeStage.Starting)
            return;

        // yipeee grids are being spontaneously moved to nullspace.
        Log.Info($"Grid {ToPrettyString(uid, meta)} changed parent. Old parent: {ToPrettyString(args.OldPosition.EntityId)}. New parent: {ToPrettyString(xform.ParentUid)}");
        if (xform.MapUid == null && meta.EntityLifeStage < EntityLifeStage.Terminating && _netManager.IsServer)
            Log.Error($"Grid {ToPrettyString(uid, meta)} was moved to nullspace! AAAAAAAAAAAAAAAAAAAAAAAAA! {Environment.StackTrace}");

        DebugTools.Assert(!_mapQuery.HasComponent(uid));

        if (xform.ParentUid != xform.MapUid && meta.EntityLifeStage < EntityLifeStage.Terminating  && _netManager.IsServer)
        {
            Log.Error($"Grid {ToPrettyString(uid, meta)} is parented to {ToPrettyString(xform._parent)} which is not a map.  y'all need jesus. {Environment.StackTrace}");
            return;
        }

        // Make sure we cleanup old map for moved grid stuff.
        var oldMap = _transform.ToMapCoordinates(args.OldPosition);
        var oldMapUid = GetMapOrInvalid(oldMap.MapId);
        if (component.MapProxy != DynamicTree.Proxy.Free)
        {
            _physics.MovedGrids.Remove(uid);
            RemoveGrid(uid, component, oldMapUid);
        }

        DebugTools.Assert(component.MapProxy == DynamicTree.Proxy.Free);
        if (xform.MapUid != null)
        {
            _physics.MovedGrids.Add(uid);
            AddGrid(uid, component);
        }
    }

    protected virtual void UpdatePvsChunks(Entity<TransformComponent, MetaDataComponent> grid)
    {
    }

    private void OnGridHandleState(EntityUid uid, MapGridComponent component, ref ComponentHandleState args)
    {
        switch (args.Current)
        {
            case MapGridComponentDeltaState delta:
            {
                DebugTools.Assert(component.ChunkSize == delta.ChunkSize || component.Chunks.Count == 0,
                    "Can't modify chunk size of an existing grid.");

                component.ChunkSize = delta.ChunkSize;
                if (delta.ChunkData == null)
                    return;

                foreach (var (index, chunkData) in delta.ChunkData)
                {
                    ApplyChunkData(uid, component, index, chunkData);
                }

                component.LastTileModifiedTick = delta.LastTileModifiedTick;
                break;
            }
            case MapGridComponentState state:
            {
                DebugTools.Assert(component.ChunkSize == state.ChunkSize || component.Chunks.Count == 0,
                    "Can't modify chunk size of an existing grid.");

                component.LastTileModifiedTick = state.LastTileModifiedTick;
                component.ChunkSize = state.ChunkSize;

                foreach (var index in component.Chunks.Keys)
                {
                    if (!state.FullGridData.ContainsKey(index))
                        ApplyChunkData(uid, component, index, ChunkDatum.Empty);
                }

                foreach (var (index, data) in state.FullGridData)
                {
                    DebugTools.Assert(!data.IsDeleted());
                    ApplyChunkData(uid, component, index, data);
                }

                break;
            }
            default:
                return;
        }

        RegenerateAabb(component);
        OnGridBoundsChange(uid, component);

#if DEBUG
        foreach (var chunk in component.Chunks.Values)
        {
            chunk.ValidateChunk();
            DebugTools.Assert(chunk.FilledTiles > 0);
        }
#endif
    }

    private void ApplyChunkData(
        EntityUid uid,
        MapGridComponent component,
        Vector2i index,
        ChunkDatum data)
    {
        var counter = 0;
        var gridEnt = new Entity<MapGridComponent>(uid, component);

        if (data.IsDeleted())
        {
            if (!component.Chunks.Remove(index, out var deletedChunk))
                return;

            // Deleted chunks still need to raise tile-changed events.
            deletedChunk.SuppressCollisionRegeneration = true;
            for (ushort x = 0; x < component.ChunkSize; x++)
            {
                for (ushort y = 0; y < component.ChunkSize; y++)
                {
                    if (!deletedChunk.TrySetTile(x, y, Tile.Empty, out var oldTile, out _))
                        continue;

                    var gridIndices = deletedChunk.ChunkTileToGridTile((x, y));
                    var newTileRef = new TileRef(uid, gridIndices, Tile.Empty);
                    _mapInternal.RaiseOnTileChanged(gridEnt, newTileRef, oldTile, index);
                }
            }

            deletedChunk.CachedBounds = Box2i.Empty;
            deletedChunk.SuppressCollisionRegeneration = false;
            return;
        }

        var chunk = GetOrAddChunk(uid, component, index);
        chunk.Fixtures.Clear();
        chunk.Fixtures.UnionWith(data.Fixtures);

        chunk.SuppressCollisionRegeneration = true;
        DebugTools.Assert(data.TileData.Any(x => !x.IsEmpty));
        DebugTools.Assert(data.TileData.Length == component.ChunkSize * component.ChunkSize);
        var changedEntry = new ValueList<TileChangedEntry>();

        for (ushort x = 0; x < component.ChunkSize; x++)
        {
            for (ushort y = 0; y < component.ChunkSize; y++)
            {
                var tile = data.TileData[counter++];
                if (!chunk.TrySetTile(x, y, tile, out var oldTile, out _))
                    continue;


                var chunkIndex = new Vector2i(x, y);
                var gridIndices = chunk.ChunkTileToGridTile(chunkIndex);
                changedEntry.Add(new TileChangedEntry(tile, oldTile, chunk.Indices, gridIndices));
            }
        }

        var ev = new TileChangedEvent(gridEnt, changedEntry.ToArray());
        EntityManager.EventBus.RaiseLocalEvent(gridEnt.Owner, ref ev, true);
        DebugTools.Assert(chunk.Fixtures.SetEquals(data.Fixtures));

        // These should never refer to the same object
        DebugTools.AssertNotEqual(chunk.Fixtures, data.Fixtures);

        chunk.CachedBounds = data.CachedBounds!.Value;
        chunk.SuppressCollisionRegeneration = false;
    }

    private void OnGridGetState(EntityUid uid, MapGridComponent component, ref ComponentGetState args)
    {
        if (args.FromTick <= component.CreationTick)
        {
            GetFullState(uid, component, ref args);
            return;
        }

        Dictionary<Vector2i, ChunkDatum>? chunkData;
        var fromTick = args.FromTick;

        if (component.LastTileModifiedTick < fromTick)
        {
            chunkData = null;
        }
        else
        {
            chunkData = new Dictionary<Vector2i, ChunkDatum>();

            foreach (var (tick, indices) in component.ChunkDeletionHistory)
            {
                if (tick < fromTick && fromTick != GameTick.Zero)
                    continue;

                // Chunk may have been re-added sometime after it was deleted, but before deletion history was culled.
                if (!component.Chunks.TryGetValue(indices, out var chunk))
                {
                    chunkData.Add(indices, ChunkDatum.Empty);
                    continue;
                }

                if (chunk.LastTileModifiedTick < fromTick)
                    Log.Error($"Encountered un-deleted chunk with an old last-modified tick on grid {ToPrettyString(uid)}");
            }

            foreach (var (index, chunk) in GetMapChunks(uid, component))
            {
                if (chunk.LastTileModifiedTick < fromTick)
                    continue;

                var tileBuffer = new Tile[component.ChunkSize * (uint) component.ChunkSize];

                // Flatten the tile array.
                // NetSerializer doesn't do multi-dimensional arrays.
                // This is probably really expensive.
                for (var x = 0; x < component.ChunkSize; x++)
                {
                    for (var y = 0; y < component.ChunkSize; y++)
                    {
                        tileBuffer[x * component.ChunkSize + y] = chunk.GetTile((ushort)x, (ushort)y);
                    }
                }

                // The client needs to clone the fixture set instead of storing a reference.
                // TODO Game State
                // Force the client to serialize & de-serialize implicitly generated component states.
                var fixtures = chunk.Fixtures;
                if (_netManager.IsClient)
                    fixtures = new(fixtures);

                chunkData.Add(index, ChunkDatum.CreateModified(tileBuffer, fixtures, chunk.CachedBounds));
            }
        }

        args.State = new MapGridComponentDeltaState(component.ChunkSize, chunkData, component.LastTileModifiedTick);

#if DEBUG
        if (chunkData == null)
            return;

        HashSet<Vector2> keys = new();
        foreach (var (index, chunk) in chunkData)
        {
            if (chunk.IsDeleted())
                continue;

            DebugTools.Assert(keys.Add(index), "Duplicate chunk");
            DebugTools.Assert(chunk.TileData.Any(x => !x.IsEmpty), "Empty non-deleted chunk");
        }
#endif
    }

    private void GetFullState(EntityUid uid, MapGridComponent component, ref ComponentGetState args)
    {
        var chunkData = new Dictionary<Vector2i, ChunkDatum>();

        foreach (var (index, chunk) in GetMapChunks(uid, component))
        {
            var tileBuffer = new Tile[component.ChunkSize * (uint)component.ChunkSize];

            for (var x = 0; x < component.ChunkSize; x++)
            {
                for (var y = 0; y < component.ChunkSize; y++)
                {
                    tileBuffer[x * component.ChunkSize + y] = chunk.GetTile((ushort)x, (ushort)y);
                }
            }

            // The client needs to clone the fixture set instead of storing a reference.
            // TODO Game State
            // Force the client to serialize & de-serialize implicitly generated component states.
            var fixtures = chunk.Fixtures;
            if (_netManager.IsClient)
                fixtures = new(fixtures);

            chunkData.Add(index, ChunkDatum.CreateModified(tileBuffer, fixtures, chunk.CachedBounds));
        }

        args.State = new MapGridComponentState(component.ChunkSize, chunkData, component.LastTileModifiedTick);

#if DEBUG
        foreach (var chunk in chunkData.Values)
        {
            DebugTools.Assert(chunk.TileData!.Any(x => !x.IsEmpty));
        }
#endif
    }

    private void OnGridAdd(EntityUid uid, MapGridComponent component, ComponentAdd args)
    {
        var msg = new GridAddEvent(uid);
        RaiseLocalEvent(uid, msg, true);
    }

    private void OnGridInit(EntityUid uid, MapGridComponent component, ComponentInit args)
    {
        var xform = _xformQuery.GetComponent(uid);

        // Force networkedmapmanager to send it due to non-ECS legacy code.
        var curTick = _timing.CurTick;

        foreach (var chunk in component.Chunks.Values)
        {
            chunk.LastTileModifiedTick = curTick;
        }

        component.LastTileModifiedTick = curTick;

        if (xform.MapUid != null && xform.MapUid != uid)
            _transform.SetParent(uid, xform, xform.MapUid.Value);

        if (!_mapQuery.HasComponent(uid))
        {
            var aabb = GetWorldAABB(uid, component);

            if (TryComp<GridTreeComponent>(xform.MapUid, out var gridTree))
            {
                var proxy = gridTree.Tree.CreateProxy(in aabb, uint.MaxValue, (uid, _fixturesQuery.Comp(uid), component));
                DebugTools.Assert(component.MapProxy == DynamicTree.Proxy.Free);
                component.MapProxy = proxy;
            }

            if (xform.MapUid != null)
            {
                _physics.MovedGrids.Add(uid);
            }
        }

        var msg = new GridInitializeEvent(uid);
        RaiseLocalEvent(uid, msg, true);
    }

    private void OnGridStartup(EntityUid uid, MapGridComponent component, ComponentStartup args)
    {
        var msg = new GridStartupEvent(uid);
        RaiseLocalEvent(uid, msg, true);
    }

    private void OnGridRemove(EntityUid uid, MapGridComponent component, ComponentShutdown args)
    {
        Log.Info($"Removing grid {ToPrettyString(uid)}");
        if (TryComp(uid, out TransformComponent? xform) && xform.MapUid != null)
        {
            RemoveGrid(uid, component, xform.MapUid.Value);
        }

        component.MapProxy = DynamicTree.Proxy.Free;
        RaiseLocalEvent(uid, new GridRemovalEvent(uid), true);
    }

    private Box2 GetWorldAABB(EntityUid uid, MapGridComponent grid, TransformComponent? xform = null)
    {
        if (!Resolve(uid, ref xform))
            return new Box2();

        var (worldPos, worldRot) = _transform.GetWorldPositionRotation(xform);
        var aabb = grid.LocalAABB.Translated(worldPos);

        return new Box2Rotated(aabb, worldRot, worldPos).CalcBoundingBox();
    }

    private void AddGrid(EntityUid uid, MapGridComponent grid)
    {
        DebugTools.Assert(!_mapQuery.HasComponent(uid));
        var aabb = GetWorldAABB(uid, grid);

        if (!_xformQuery.TryGetComponent(uid, out var xform))
            return;

        if (TryComp<GridTreeComponent>(xform.MapUid, out var gridTree))
        {
            var proxy = gridTree.Tree.CreateProxy(in aabb, uint.MaxValue, (uid, _fixturesQuery.Comp(uid), grid));
            DebugTools.Assert(grid.MapProxy == DynamicTree.Proxy.Free);
            grid.MapProxy = proxy;
        }

        if (xform.MapUid != null)
        {
            _physics.MovedGrids.Add(uid);
        }
    }

    private void RemoveGrid(EntityUid uid, MapGridComponent grid, EntityUid mapUid)
    {
        if (grid.MapProxy != DynamicTree.Proxy.Free && TryComp<GridTreeComponent>(mapUid, out var gridTree))
        {
            gridTree.Tree.DestroyProxy(grid.MapProxy);
        }

        grid.MapProxy = DynamicTree.Proxy.Free;

        if (mapUid.IsValid())
        {
            _physics.MovedGrids.Remove(uid);
        }
    }

    private void RemoveChunk(EntityUid uid, MapGridComponent grid, Vector2i origin)
    {
        if (!grid.Chunks.TryGetValue(origin, out var chunk))
            return;

        if (_netManager.IsServer)
            grid.ChunkDeletionHistory.Add((_timing.CurTick, chunk.Indices));

        chunk.Fixtures.Clear();
        grid.Chunks.Remove(origin);

        if (grid.Chunks.Count == 0)
            RaiseLocalEvent(uid, new EmptyGridEvent { GridId = uid }, true);
    }

    /// <summary>
    /// Regenerates the chunk local bounds of this chunk.
    /// </summary>
    private void RegenerateCollision(EntityUid uid, MapGridComponent grid, MapChunk mapChunk)
    {
        RegenerateCollision(uid, grid, new HashSet<MapChunk> { mapChunk });
    }

    /// <summary>
    /// Regenerate collision for multiple chunks at once; faster than doing it individually.
    /// </summary>
    internal void RegenerateCollision(EntityUid uid, MapGridComponent grid, IReadOnlySet<MapChunk> chunks)
    {
        if (HasComp<MapComponent>(uid))
        {
            ClearEmptyMapChunks(uid, grid, chunks);
            return;
        }

        var chunkRectangles = new Dictionary<MapChunk, List<Box2i>>(chunks.Count);
        var removedChunks = new List<MapChunk>();

        foreach (var mapChunk in chunks)
        {
            // Even if the chunk is still removed still need to make sure bounds are updated (for now...)
            // generate collision rectangles for this chunk based on filled tiles.
            GridChunkPartition.PartitionChunk(mapChunk, out var localBounds, out var rectangles);
            mapChunk.CachedBounds = localBounds;

            if (mapChunk.FilledTiles > 0)
                chunkRectangles.Add(mapChunk, rectangles);
            else
            {
                // Gone. Reduced to atoms
                // Need to do this before RemoveChunk because it clears fixtures.
                FixturesComponent? manager = null;
                PhysicsComponent? body = null;
                TransformComponent? xform = null;

                foreach (var id in mapChunk.Fixtures)
                {
                    mapChunk.Fixtures.Remove(id);
                    _fixtures.DestroyFixture(uid, id, false, manager: manager, body: body, xform: xform);
                }

                RemoveChunk(uid, grid, mapChunk.Indices);
                DebugTools.AssertEqual(mapChunk.Fixtures.Count, 0);
                removedChunks.Add(mapChunk);
            }
        }

        RegenerateAabb(grid);

        // May have been deleted from the bulk update above!
        if (Deleted(uid))
            return;

        _physics.WakeBody(uid);
        OnGridBoundsChange(uid, grid);
        var ev = new RegenerateGridBoundsEvent(uid, chunkRectangles, removedChunks);
        RaiseLocalEvent(ref ev);
    }

    private void RegenerateAabb(MapGridComponent grid)
    {
        grid.LocalAABB = new Box2();

        foreach (var chunk in grid.Chunks.Values)
        {
            var chunkBounds = chunk.CachedBounds;

            if (chunkBounds.Size.Equals(Vector2i.Zero))
                continue;

            if (grid.LocalAABB.Size == Vector2.Zero)
            {
                var gridBounds = chunkBounds.Translated(chunk.Indices * chunk.ChunkSize);
                grid.LocalAABB = gridBounds;
            }
            else
            {
                var gridBounds = chunkBounds.Translated(chunk.Indices * chunk.ChunkSize);
                grid.LocalAABB = grid.LocalAABB.Union(gridBounds);
            }
        }
    }

    /// <summary>
    /// Variation of <see cref="RegenerateCollision(Robust.Shared.GameObjects.EntityUid,Robust.Shared.Map.Components.MapGridComponent,Robust.Shared.Map.MapChunk)"/>
    /// that only simply removes empty chunks. Intended for use with "planet-maps", which have no grid fixtures.
    /// </summary>
    private void ClearEmptyMapChunks(EntityUid uid, MapGridComponent grid, IReadOnlySet<MapChunk> modified)
    {
        foreach (var chunk in modified)
        {
            DebugTools.Assert(chunk.FilledTiles >= 0);
            if (chunk.FilledTiles > 0)
                continue;

            DebugTools.AssertEqual(chunk.Fixtures.Count, 0, "maps should not have grid-chunk fixtures");
            RemoveChunk(uid, grid, chunk.Indices);
        }
    }

    #region TileAccess

    public TileRef GetTileRef(Entity<MapGridComponent> grid, MapCoordinates coords)
    {
        return GetTileRef(grid.Owner, grid.Comp, coords);
    }

    public TileRef GetTileRef(EntityUid uid, MapGridComponent grid, MapCoordinates coords)
    {
        return GetTileRef(uid, grid, CoordinatesToTile(uid, grid, coords));
    }

    public TileRef GetTileRef(Entity<MapGridComponent> grid, EntityCoordinates coords)
    {
        return GetTileRef(grid.Owner, grid.Comp, coords);
    }

    public TileRef GetTileRef(EntityUid uid, MapGridComponent grid, EntityCoordinates coords)
    {
        return GetTileRef(uid, grid, CoordinatesToTile(uid, grid, coords));
    }

    public TileRef GetTileRef(Entity<MapGridComponent> grid, Vector2i tileCoordinates)
    {
        return GetTileRef(grid.Owner, grid.Comp, tileCoordinates);
    }

    public TileRef GetTileRef(EntityUid uid, MapGridComponent grid, Vector2i tileCoordinates)
    {
        var chunkIndices = GridTileToChunkIndices(uid, grid, tileCoordinates);

        if (!grid.Chunks.TryGetValue(chunkIndices, out var output))
        {
            // Chunk doesn't exist, return a tileRef to an empty (space) tile.
            return new TileRef(uid, tileCoordinates.X, tileCoordinates.Y, default);
        }

        var chunkTileIndices = output.GridTileToChunkTile(tileCoordinates);
        return GetTileRef(uid, grid, output, (ushort)chunkTileIndices.X, (ushort)chunkTileIndices.Y);
    }

    /// <summary>
    ///     Returns the tile at the given chunk indices.
    /// </summary>
    /// <param name="mapChunk"></param>
    /// <param name="xIndex">The X tile index relative to the chunk origin.</param>
    /// <param name="yIndex">The Y tile index relative to the chunk origin.</param>
    /// <returns>A reference to a tile.</returns>
    internal TileRef GetTileRef(EntityUid uid, MapGridComponent grid, MapChunk mapChunk, ushort xIndex, ushort yIndex)
    {
        if (xIndex >= mapChunk.ChunkSize)
            throw new ArgumentOutOfRangeException(nameof(xIndex), "Tile indices out of bounds.");

        if (yIndex >= mapChunk.ChunkSize)
            throw new ArgumentOutOfRangeException(nameof(yIndex), "Tile indices out of bounds.");

        var indices = mapChunk.ChunkTileToGridTile(new Vector2i(xIndex, yIndex));
        return new TileRef(uid, indices, mapChunk.GetTile(xIndex, yIndex));
    }

    public IEnumerable<TileRef> GetAllTiles(EntityUid uid, MapGridComponent grid, bool ignoreEmpty = true)
    {
        foreach (var chunk in grid.Chunks.Values)
        {
            for (ushort x = 0; x < grid.ChunkSize; x++)
            {
                for (ushort y = 0; y < grid.ChunkSize; y++)
                {
                    var tile = chunk.GetTile(x, y);

                    if (ignoreEmpty && tile.IsEmpty)
                        continue;

                    var (gridX, gridY) = new Vector2i(x, y) + chunk.Indices * grid.ChunkSize;
                    yield return new TileRef(uid, gridX, gridY, tile);
                }
            }
        }
    }

    /// <summary>
    /// Returns the total number of tiles on a grid.
    /// by summing the counts of filled tiles in each chunk.
    /// </summary>
    /// <param name="ent">The target map grid entity</param>
    /// <returns>The total number of filled tiles in <paramref name="ent"/>.</returns>
    [Pure]
    public int GetFilledTileCount(Entity<MapGridComponent> ent)
    {
        return ent.Comp.Chunks.Values.Sum(chunk => chunk.FilledTiles);
    }

    public GridTileEnumerator GetAllTilesEnumerator(EntityUid uid, MapGridComponent grid, bool ignoreEmpty = true)
    {
        return new GridTileEnumerator(uid, grid.Chunks.GetEnumerator(), grid.ChunkSize, ignoreEmpty);
    }

    public void SetTile(Entity<MapGridComponent> grid, EntityCoordinates coordinates, Tile tile)
    {
        SetTile(grid.Owner, grid.Comp, coordinates, tile);
    }

    public void SetTile(Entity<MapGridComponent> grid, Vector2i gridIndices, Tile tile)
    {
        SetTile(grid.Owner, grid.Comp, gridIndices, tile);
    }

    public void SetTiles(Entity<MapGridComponent> grid, List<(Vector2i GridIndices, Tile Tile)> tiles)
    {
        SetTiles(grid.Owner, grid.Comp, tiles);
    }

    public void SetTile(EntityUid uid, MapGridComponent grid, EntityCoordinates coords, Tile tile)
    {
        var localTile = CoordinatesToTile(uid, grid, coords);
        SetTile(uid, grid, new Vector2i(localTile.X, localTile.Y), tile);
    }

    public void SetTile(EntityUid uid, MapGridComponent grid, Vector2i gridIndices, Tile tile)
    {
        var chunkIndex = GridTileToChunkIndices(uid, grid, gridIndices);
        if (!grid.Chunks.TryGetValue(chunkIndex, out var chunk))
        {
            if (tile.IsEmpty)
                return;

            grid.Chunks[chunkIndex] = chunk = new MapChunk(chunkIndex.X, chunkIndex.Y, grid.ChunkSize)
            {
                LastTileModifiedTick = _timing.CurTick
            };
        }

        var offset = chunk.GridTileToChunkTile(gridIndices);
        SetChunkTile(uid, grid, chunk, (ushort)offset.X, (ushort)offset.Y, tile, out _);
    }

    public void SetTiles(EntityUid uid, MapGridComponent grid, List<(Vector2i GridIndices, Tile Tile)> tiles)
    {
        if (tiles.Count == 0)
            return;

        var modified = new HashSet<MapChunk>(Math.Max(1, tiles.Count / grid.ChunkSize));
        var tileChanges = new ValueList<TileChangedEntry>(tiles.Count);

        // Suppress sending out events for each tile changed
        // We're going to send them all out together at the end
        MapManager.SuppressOnTileChanged = true;

        foreach (var (gridIndices, tile) in tiles)
        {
            var chunkIndex = GridTileToChunkIndices(uid, grid, gridIndices);
            if (!grid.Chunks.TryGetValue(chunkIndex, out var chunk))
            {
                if (tile.IsEmpty)
                    continue;

                grid.Chunks[chunkIndex] = chunk = new MapChunk(chunkIndex.X, chunkIndex.Y, grid.ChunkSize)
                {
                    LastTileModifiedTick = _timing.CurTick
                };
            }

            var offset = chunk.GridTileToChunkTile(gridIndices);
            chunk.SuppressCollisionRegeneration = true;
            if (SetChunkTile(uid, grid, chunk, (ushort)offset.X, (ushort)offset.Y, tile, out var oldTile))
            {
                modified.Add(chunk);
                tileChanges.Add(new TileChangedEntry(tile, oldTile, offset, gridIndices));
            }
        }

        foreach (var chunk in modified)
        {
            chunk.SuppressCollisionRegeneration = false;
        }

        // Notify of all tile changes in one event
        var ev = new TileChangedEvent((uid, grid), tileChanges.ToArray());
        RaiseLocalEvent(uid, ref ev, true);

        RegenerateCollision(uid, grid, modified);

        // Back to normal
        MapManager.SuppressOnTileChanged = false;
    }

    public TilesEnumerator GetLocalTilesEnumerator(EntityUid uid, MapGridComponent grid, Box2 aabb,
        bool ignoreEmpty = true,
        Predicate<TileRef>? predicate = null)
    {
        var enumerator = new TilesEnumerator(this, ignoreEmpty, predicate, uid, grid, aabb);
        return enumerator;
    }

    public TilesEnumerator GetTilesEnumerator(EntityUid uid, MapGridComponent grid, Box2 aabb, bool ignoreEmpty = true,
        Predicate<TileRef>? predicate = null)
    {
        var invMatrix = _transform.GetInvWorldMatrix(uid);
        var localAABB = invMatrix.TransformBox(aabb);
        var enumerator = new TilesEnumerator(this, ignoreEmpty, predicate, uid, grid, localAABB);
        return enumerator;
    }

    public TilesEnumerator GetTilesEnumerator(EntityUid uid, MapGridComponent grid, Box2Rotated bounds, bool ignoreEmpty = true,
        Predicate<TileRef>? predicate = null)
    {
        var invMatrix = _transform.GetInvWorldMatrix(uid);
        var localAABB = invMatrix.TransformBox(bounds);
        var enumerator = new TilesEnumerator(this, ignoreEmpty, predicate, uid, grid, localAABB);
        return enumerator;
    }

    public IEnumerable<TileRef> GetLocalTilesIntersecting(EntityUid uid, MapGridComponent grid, Box2 localAABB, bool ignoreEmpty = true,
        Predicate<TileRef>? predicate = null)
    {
        var enumerator = new TilesEnumerator(this, ignoreEmpty, predicate, uid, grid, localAABB);

        while (enumerator.MoveNext(out var tileRef))
        {
            yield return tileRef;
        }
    }

    public IEnumerable<TileRef> GetLocalTilesIntersecting(EntityUid uid, MapGridComponent grid, Box2Rotated localArea, bool ignoreEmpty = true,
        Predicate<TileRef>? predicate = null)
    {
        var localAABB = localArea.CalcBoundingBox();

        var enumerator = new TilesEnumerator(this, ignoreEmpty, predicate, uid, grid, localAABB);

        while (enumerator.MoveNext(out var tileRef))
        {
            yield return tileRef;
        }
    }

    public IEnumerable<TileRef> GetTilesIntersecting(EntityUid uid, MapGridComponent grid, Box2Rotated worldArea, bool ignoreEmpty = true,
        Predicate<TileRef>? predicate = null)
    {
        var matrix = _transform.GetInvWorldMatrix(uid);
        var localArea = matrix.TransformBox(worldArea);

        var enumerator = new TilesEnumerator(this, ignoreEmpty, predicate, uid, grid, localArea);

        while (enumerator.MoveNext(out var tileRef))
        {
            yield return tileRef;
        }
    }

    public IEnumerable<TileRef> GetTilesIntersecting(EntityUid uid, MapGridComponent grid, Box2 worldArea, bool ignoreEmpty = true,
        Predicate<TileRef>? predicate = null)
    {
        var matrix = _transform.GetInvWorldMatrix(uid);
        var localArea = matrix.TransformBox(worldArea);

        var enumerator = new TilesEnumerator(this, ignoreEmpty, predicate, uid, grid, localArea);

        while (enumerator.MoveNext(out var tileRef))
        {
            yield return tileRef;
        }
    }

    public IEnumerable<TileRef> GetLocalTilesIntersecting(EntityUid uid, MapGridComponent grid, Circle localCircle, bool ignoreEmpty = true,
        Predicate<TileRef>? predicate = null)
    {
        var aabb = new Box2(localCircle.Position.X - localCircle.Radius, localCircle.Position.Y - localCircle.Radius,
            localCircle.Position.X + localCircle.Radius, localCircle.Position.Y + localCircle.Radius);

        var tileEnumerator = GetLocalTilesEnumerator(uid, grid, aabb, ignoreEmpty, predicate);

        while (tileEnumerator.MoveNext(out var tile))
        {
            var tileCenter = tile.GridIndices + grid.TileSizeHalfVector;
            var direction = tileCenter - localCircle.Position;

            if (direction.IsShorterThanOrEqualTo(localCircle.Radius))
            {
                yield return tile;
            }
        }
    }

    public IEnumerable<TileRef> GetTilesIntersecting(EntityUid uid, MapGridComponent grid, Circle worldArea, bool ignoreEmpty = true,
        Predicate<TileRef>? predicate = null)
    {
        var aabb = new Box2(worldArea.Position.X - worldArea.Radius, worldArea.Position.Y - worldArea.Radius,
            worldArea.Position.X + worldArea.Radius, worldArea.Position.Y + worldArea.Radius);
        var circleGridPos = new EntityCoordinates(uid, WorldToLocal(uid, grid, worldArea.Position));

        foreach (var tile in GetTilesIntersecting(uid, grid, aabb, ignoreEmpty, predicate))
        {
            var local = GridTileToLocal(uid, grid, tile.GridIndices);

            if (!local.TryDistance(EntityManager, _transform, circleGridPos, out var distance))
            {
                continue;
            }

            if (distance <= worldArea.Radius)
            {
                yield return tile;
            }
        }
    }

    private bool TryGetTile(EntityUid uid, MapGridComponent grid, Vector2i indices, bool ignoreEmpty, [NotNullWhen(true)] out TileRef? tileRef, Predicate<TileRef>? predicate = null)
    {
        // Similar to TryGetTileRef but for the tiles intersecting iterators.
        var gridChunk = GridTileToChunkIndices(uid, grid, indices);

        if (grid.Chunks.TryGetValue(gridChunk, out var chunk))
        {
            var chunkTile = chunk.GridTileToChunkTile(indices);
            var tile = GetTileRef(uid, grid, chunk, (ushort)chunkTile.X, (ushort)chunkTile.Y);

            if (ignoreEmpty && tile.Tile.IsEmpty)
            {
                tileRef = null;
                return false;
            }

            if (predicate == null || predicate(tile))
            {
                tileRef = tile;
                return true;
            }
        }
        else if (!ignoreEmpty)
        {
            var tile = new TileRef(uid, indices.X, indices.Y, Tile.Empty);

            if (predicate == null || predicate(tile))
            {
                tileRef = tile;
                return true;
            }
        }

        tileRef = null;
        return false;
    }

    #endregion TileAccess

    #region ChunkAccess

    internal MapChunk GetOrAddChunk(EntityUid uid, MapGridComponent grid, int xIndex, int yIndex)
    {
        return GetOrAddChunk(uid, grid, new Vector2i(xIndex, yIndex));
    }

    internal bool TryGetChunk(EntityUid uid, MapGridComponent grid, Vector2i chunkIndices, [NotNullWhen(true)] out MapChunk? chunk)
    {
        return grid.Chunks.TryGetValue(chunkIndices, out chunk);
    }

    internal MapChunk GetOrAddChunk(EntityUid uid, MapGridComponent grid, Vector2i chunkIndices)
    {
        if (grid.Chunks.TryGetValue(chunkIndices, out var output))
            return output;

        var newChunk = new MapChunk(chunkIndices.X, chunkIndices.Y, grid.ChunkSize)
        {
            LastTileModifiedTick = _timing.CurTick
        };

        return grid.Chunks[chunkIndices] = newChunk;
    }

    public bool HasChunk(EntityUid uid, MapGridComponent grid, Vector2i chunkIndices)
    {
        return grid.Chunks.ContainsKey(chunkIndices);
    }

    internal IReadOnlyDictionary<Vector2i, MapChunk> GetMapChunks(EntityUid uid, MapGridComponent grid)
    {
        return grid.Chunks;
    }

    internal ChunkEnumerator GetMapChunks(EntityUid uid, MapGridComponent grid, Box2 worldAABB)
    {
        var localAABB = _transform.GetInvWorldMatrix(uid).TransformBox(worldAABB);
        return GetLocalMapChunks(uid, grid, localAABB);
    }

    internal ChunkEnumerator GetMapChunks(EntityUid uid, MapGridComponent grid, Box2Rotated worldArea)
    {
        var matrix = _transform.GetInvWorldMatrix(uid);
        var localArea = matrix.TransformBox(worldArea);
        return GetLocalMapChunks(uid, grid, localArea);
    }

    internal ChunkEnumerator GetLocalMapChunks(EntityUid uid, MapGridComponent grid, Box2 localAABB)
    {
        Box2 compAABB;

        // The entire area intersects.
        if (_mapQuery.HasComponent(uid))
        {
            compAABB = localAABB;
        }
        else
        {
            compAABB = grid.LocalAABB.Intersect(localAABB);
        }

        return new ChunkEnumerator(grid.Chunks, compAABB, grid.ChunkSize);
    }

    #endregion ChunkAccess

    #region SnapGridAccess

    public int AnchoredEntityCount(EntityUid uid, MapGridComponent grid, Vector2i pos)
    {
        var gridChunkPos = GridTileToChunkIndices(uid, grid, pos);

        if (!grid.Chunks.TryGetValue(gridChunkPos, out var chunk))
            return 0;

        var (x, y) = chunk.GridTileToChunkTile(pos);
        return chunk.GetSnapGrid((ushort)x, (ushort)y)?.Count ?? 0; // ?
    }

    public IEnumerable<EntityUid> GetAnchoredEntities(Entity<MapGridComponent> grid, MapCoordinates coords)
    {
        return GetAnchoredEntities(grid.Owner, grid.Comp, coords);
    }

    public IEnumerable<EntityUid> GetAnchoredEntities(EntityUid uid, MapGridComponent grid, MapCoordinates coords)
    {
        return GetAnchoredEntities(uid, grid, TileIndicesFor(uid, grid, coords));
    }

    public IEnumerable<EntityUid> GetAnchoredEntities(Entity<MapGridComponent> grid, EntityCoordinates coords)
    {
        return GetAnchoredEntities(grid.Owner, grid.Comp, coords);
    }

    public IEnumerable<EntityUid> GetAnchoredEntities(EntityUid uid, MapGridComponent grid, EntityCoordinates coords)
    {
        return GetAnchoredEntities(uid, grid, TileIndicesFor(uid, grid, coords));
    }

    public IEnumerable<EntityUid> GetAnchoredEntities(Entity<MapGridComponent> grid, Vector2i pos)
    {
        return GetAnchoredEntities(grid.Owner, grid.Comp, pos);
    }

    public IEnumerable<EntityUid> GetAnchoredEntities(EntityUid uid, MapGridComponent grid, Vector2i pos)
    {
        // Because some content stuff checks neighboring tiles (which may not actually exist) we won't just
        // create an entire chunk for it.
        var gridChunkPos = GridTileToChunkIndices(uid, grid, pos);

        if (!grid.Chunks.TryGetValue(gridChunkPos, out var chunk))
            return Enumerable.Empty<EntityUid>();

        var chunkTile = chunk.GridTileToChunkTile(pos);
        return chunk.GetSnapGridCell((ushort)chunkTile.X, (ushort)chunkTile.Y);
    }

    public void GetAnchoredEntities(Entity<MapGridComponent> grid, Vector2i pos, List<EntityUid> list)
    {
        var gridChunkPos = GridTileToChunkIndices(grid.Owner, grid.Comp, pos);
        if (!grid.Comp.Chunks.TryGetValue(gridChunkPos, out var chunk))
            return;

        var chunkTile = chunk.GridTileToChunkTile(pos);
        var anchored = chunk.GetSnapGrid((ushort) chunkTile.X, (ushort) chunkTile.Y);
        if (anchored != null)
            list.AddRange(anchored);
    }

    public AnchoredEntitiesEnumerator GetAnchoredEntitiesEnumerator(EntityUid uid, MapGridComponent grid, Vector2i pos)
    {
        var gridChunkPos = GridTileToChunkIndices(uid, grid, pos);

        if (!grid.Chunks.TryGetValue(gridChunkPos, out var chunk)) return AnchoredEntitiesEnumerator.Empty;

        var chunkTile = chunk.GridTileToChunkTile(pos);
        var snapgrid = chunk.GetSnapGrid((ushort)chunkTile.X, (ushort)chunkTile.Y);

        return snapgrid == null
            ? AnchoredEntitiesEnumerator.Empty
            : new AnchoredEntitiesEnumerator(snapgrid.GetEnumerator());
    }

    public IEnumerable<EntityUid> GetLocalAnchoredEntities(EntityUid uid, MapGridComponent grid, Box2 localAABB)
    {
        var enumerator = new TilesEnumerator(this, true, null, uid, grid, localAABB);

        while (enumerator.MoveNext(out var tileRef))
        {
            var anchoredEnumerator = GetAnchoredEntitiesEnumerator(uid, grid, tileRef.GridIndices);

            while (anchoredEnumerator.MoveNext(out var ent))
            {
                yield return ent.Value;
            }
        }
    }

    public IEnumerable<EntityUid> GetAnchoredEntities(EntityUid uid, MapGridComponent grid, Box2 worldAABB)
    {
        var invWorldMatrix = _transform.GetInvWorldMatrix(uid);
        var localAABB = invWorldMatrix.TransformBox(worldAABB);
        var enumerator = new TilesEnumerator(this, true, null, uid, grid, localAABB);

        while (enumerator.MoveNext(out var tileRef))
        {
            var anchoredEnumerator = GetAnchoredEntitiesEnumerator(uid, grid, tileRef.GridIndices);

            while (anchoredEnumerator.MoveNext(out var ent))
            {
                yield return ent.Value;
            }
        }
    }

    public IEnumerable<EntityUid> GetAnchoredEntities(EntityUid uid, MapGridComponent grid, Box2Rotated worldBounds)
    {
        foreach (var tile in GetTilesIntersecting(uid, grid, worldBounds))
        {
            foreach (var ent in GetAnchoredEntities(uid, grid, tile.GridIndices))
            {
                yield return ent;
            }
        }
    }

    public Vector2i TileIndicesFor(EntityUid uid, MapGridComponent grid, EntityCoordinates coords)
    {
#if DEBUG
        var mapId = _xformQuery.GetComponent(uid).MapID;
        DebugTools.Assert(mapId == _transform.GetMapId(coords));
#endif

        return SnapGridLocalCellFor(uid, grid, LocalToGrid(uid, grid, coords));
    }

    public Vector2i TileIndicesFor(Entity<MapGridComponent> grid, EntityCoordinates coords)
    {
        return TileIndicesFor(grid.Owner, grid.Comp, coords);
    }

    public Vector2i TileIndicesFor(EntityUid uid, MapGridComponent grid, MapCoordinates worldPos)
    {
#if DEBUG
        var mapId = _xformQuery.GetComponent(uid).MapID;
        DebugTools.Assert(mapId == worldPos.MapId);
#endif

        var localPos = WorldToLocal(uid, grid, worldPos.Position);
        return SnapGridLocalCellFor(uid, grid, localPos);
    }

    public Vector2i TileIndicesFor(Entity<MapGridComponent> grid, MapCoordinates coords)
    {
        return TileIndicesFor(grid.Owner, grid.Comp, coords);
    }

    private Vector2i SnapGridLocalCellFor(EntityUid uid, MapGridComponent grid, Vector2 localPos)
    {
        var x = (int)Math.Floor(localPos.X / grid.TileSize);
        var y = (int)Math.Floor(localPos.Y / grid.TileSize);
        return new Vector2i(x, y);
    }

    public bool IsAnchored(EntityUid uid, MapGridComponent grid, EntityCoordinates coords, EntityUid euid)
    {
        var tilePos = TileIndicesFor(uid, grid, coords);

        if (!TryChunkAndOffsetForTile(uid, grid, tilePos, out var chunk, out var chunkTile))
            return false;

        var snapgrid = chunk.GetSnapGrid((ushort)chunkTile.X, (ushort)chunkTile.Y);
        return snapgrid?.Contains(euid) == true;
    }

    public bool AddToSnapGridCell(EntityUid gridUid, MapGridComponent grid, Vector2i pos, EntityUid euid)
    {
        if (!TryChunkAndOffsetForTile(gridUid, grid, pos, out var chunk, out var chunkTile))
            return false;

        if (chunk.GetTile((ushort)chunkTile.X, (ushort)chunkTile.Y).IsEmpty)
            return false;

        chunk.AddToSnapGridCell((ushort)chunkTile.X, (ushort)chunkTile.Y, euid);
        return true;
    }

    public bool AddToSnapGridCell(EntityUid gridUid, MapGridComponent grid, EntityCoordinates coords, EntityUid euid)
    {
        return AddToSnapGridCell(gridUid, grid, TileIndicesFor(gridUid, grid, coords), euid);
    }

    public void RemoveFromSnapGridCell(EntityUid gridUid, MapGridComponent grid, Vector2i pos, EntityUid euid)
    {
        var gridChunkIndices = GridTileToChunkIndices(gridUid, grid, pos);

        if (!grid.Chunks.TryGetValue(gridChunkIndices, out var chunk))
            return;

        var chunkTile = chunk.GridTileToChunkTile(pos);
        chunk.RemoveFromSnapGridCell((ushort)chunkTile.X, (ushort)chunkTile.Y, euid);
    }

    public void RemoveFromSnapGridCell(EntityUid gridUid, MapGridComponent grid, EntityCoordinates coords, EntityUid euid)
    {
        RemoveFromSnapGridCell(gridUid, grid, TileIndicesFor(gridUid, grid, coords), euid);
    }

    private bool TryChunkAndOffsetForTile(EntityUid uid, MapGridComponent grid, Vector2i pos,
        [NotNullWhen(true)]out MapChunk? chunk, out Vector2i offset)
    {
        var gridChunkIndices = GridTileToChunkIndices(uid, grid, pos);
        if (!grid.Chunks.TryGetValue(gridChunkIndices, out chunk))
        {
            offset = default;
            return false;
        }

        offset = chunk.GridTileToChunkTile(pos);
        return true;
    }

    public IEnumerable<EntityUid> GetInDir(EntityUid uid, MapGridComponent grid, EntityCoordinates position, Direction dir)
    {
        var pos = GetDirection(TileIndicesFor(uid, grid, position), dir);
        return GetAnchoredEntities(uid, grid, pos);
    }

    public IEnumerable<EntityUid> GetOffset(EntityUid uid, MapGridComponent grid, EntityCoordinates coords, Vector2i offset)
    {
        var pos = TileIndicesFor(uid, grid, coords) + offset;
        return GetAnchoredEntities(uid, grid, pos);
    }

    public IEnumerable<EntityUid> GetLocal(EntityUid uid, MapGridComponent grid, EntityCoordinates coords)
    {
        return GetAnchoredEntities(uid, grid, TileIndicesFor(uid, grid, coords));
    }

    public EntityCoordinates DirectionToGrid(EntityUid uid, MapGridComponent grid, EntityCoordinates coords, Direction direction)
    {
        return GridTileToLocal(uid, grid, GetDirection(TileIndicesFor(uid, grid, coords), direction));
    }

    public IEnumerable<EntityUid> GetCardinalNeighborCells(EntityUid uid, MapGridComponent grid, EntityCoordinates coords)
    {
        var position = TileIndicesFor(uid, grid, coords);
        foreach (var cell in GetAnchoredEntities(uid, grid, position))
            yield return cell;
        foreach (var cell in GetAnchoredEntities(uid, grid, position + new Vector2i(0, 1)))
            yield return cell;
        foreach (var cell in GetAnchoredEntities(uid, grid, position + new Vector2i(0, -1)))
            yield return cell;
        foreach (var cell in GetAnchoredEntities(uid, grid, position + new Vector2i(1, 0)))
            yield return cell;
        foreach (var cell in GetAnchoredEntities(uid, grid, position + new Vector2i(-1, 0)))
            yield return cell;
    }

    public IEnumerable<EntityUid> GetCellsInSquareArea(EntityUid uid, MapGridComponent grid, EntityCoordinates coords, int n)
    {
        var position = TileIndicesFor(uid, grid, coords);

        for (var y = -n; y <= n; ++y)
        for (var x = -n; x <= n; ++x)
        {
            var enumerator = GetAnchoredEntitiesEnumerator(uid, grid, position + new Vector2i(x, y));

            while (enumerator.MoveNext(out var cell))
            {
                yield return cell.Value;
            }
        }
    }

    #endregion

    #region Transforms

    public Vector2 WorldToLocal(EntityUid uid, MapGridComponent grid, Vector2 posWorld)
    {
        var matrix = _transform.GetInvWorldMatrix(uid);
        return Vector2.Transform(posWorld, matrix);
    }

    public EntityCoordinates MapToGrid(EntityUid uid, MapCoordinates posWorld)
    {
        var mapId = _xformQuery.GetComponent(uid).MapID;

        if (posWorld.MapId != mapId)
            throw new ArgumentException(
                $"Grid {uid} is on map {mapId}, but coords are on map {posWorld.MapId}.",
                nameof(posWorld));

        if (!_gridQuery.TryGetComponent(uid, out var grid))
        {
            return new EntityCoordinates(GetMapOrInvalid(posWorld.MapId), new Vector2(posWorld.X, posWorld.Y));
        }

        return new EntityCoordinates(uid, WorldToLocal(uid, grid, posWorld.Position));
    }

    public Vector2 LocalToWorld(EntityUid uid, MapGridComponent grid, Vector2 posLocal)
    {
        var matrix = _transform.GetWorldMatrix(uid);
        return Vector2.Transform(posLocal, matrix);
    }

    public Vector2i WorldToTile(EntityUid uid, MapGridComponent grid, Vector2 posWorld)
    {
        var local = WorldToLocal(uid, grid, posWorld);
        var x = (int)Math.Floor(local.X / grid.TileSize);
        var y = (int)Math.Floor(local.Y / grid.TileSize);
        return new Vector2i(x, y);
    }

    public Vector2i LocalToTile(EntityUid uid, MapGridComponent grid, EntityCoordinates coordinates)
    {
        var position = LocalToGrid(uid, grid, coordinates);
        return new Vector2i((int) Math.Floor(position.X / grid.TileSize), (int) Math.Floor(position.Y / grid.TileSize));
    }

        public Vector2i CoordinatesToTile(EntityUid uid, MapGridComponent grid, MapCoordinates coords)
    {
#if DEBUG
        var mapId = _xformQuery.GetComponent(uid).MapID;
        DebugTools.Assert(mapId == coords.MapId);
#endif

        var local = WorldToLocal(uid, grid, coords.Position);

        var x = (int)Math.Floor(local.X / grid.TileSize);
        var y = (int)Math.Floor(local.Y / grid.TileSize);
        return new Vector2i(x, y);
    }

    public Vector2i CoordinatesToTile(EntityUid uid, MapGridComponent grid, EntityCoordinates coords)
    {
#if DEBUG
        var mapId = _xformQuery.GetComponent(uid).MapID;
        DebugTools.Assert(mapId == _transform.GetMapId(coords));
#endif
        var local = LocalToGrid(uid, grid, coords);

        var x = (int)Math.Floor(local.X / grid.TileSize);
        var y = (int)Math.Floor(local.Y / grid.TileSize);
        return new Vector2i(x, y);
    }

    public Vector2i LocalToChunkIndices(EntityUid uid, MapGridComponent grid, EntityCoordinates gridPos)
    {
        var local = LocalToGrid(uid, grid, gridPos);

        var x = (int)Math.Floor(local.X / (grid.TileSize * grid.ChunkSize));
        var y = (int)Math.Floor(local.Y / (grid.TileSize * grid.ChunkSize));
        return new Vector2i(x, y);
    }

    public Vector2 LocalToGrid(EntityUid uid, MapGridComponent grid, EntityCoordinates position)
    {
        return position.EntityId == uid
            ? position.Position
            : WorldToLocal(uid, grid, _transform.ToMapCoordinates(position).Position);
    }

    public bool CollidesWithGrid(EntityUid uid, MapGridComponent grid, Vector2i indices)
    {
        var chunkIndices = GridTileToChunkIndices(uid, grid, indices);
        if (!grid.Chunks.TryGetValue(chunkIndices, out var chunk))
            return false;

        var cTileIndices = chunk.GridTileToChunkTile(indices);
        return chunk.GetTile((ushort)cTileIndices.X, (ushort)cTileIndices.Y).TypeId != Tile.Empty.TypeId;
    }

    public Vector2i GridTileToChunkIndices(EntityUid uid, MapGridComponent grid, Vector2i gridTile)
        => GridTileToChunkIndices(grid, gridTile);

    public Vector2i GridTileToChunkIndices(MapGridComponent grid, Vector2i gridTile)
    {
        var x = (int)Math.Floor(gridTile.X / (float) grid.ChunkSize);
        var y = (int)Math.Floor(gridTile.Y / (float) grid.ChunkSize);

        return new Vector2i(x, y);
    }

    public EntityCoordinates GridTileToLocal(EntityUid uid, MapGridComponent grid, Vector2i gridTile)
    {
        var position = TileCenterToVector(uid, grid, gridTile);

        return new(uid, position);
    }

    /// <summary>
    /// Turns a gridtile origin into a Vector2, accounting for tile size.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Vector2 TileToVector(Entity<MapGridComponent> grid, Vector2i gridTile)
    {
        return new Vector2(gridTile.X * grid.Comp.TileSize, gridTile.Y * grid.Comp.TileSize);
    }

    /// <summary>
    /// Turns a gridtile center into a Vector2, accounting for tile size.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Vector2 TileCenterToVector(EntityUid uid, MapGridComponent grid, Vector2i gridTile)
    {
        return TileCenterToVector((uid, grid), gridTile);
    }

    /// <summary>
    /// Turns a gridtile center into a Vector2, accounting for tile size.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Vector2 TileCenterToVector(Entity<MapGridComponent> grid, Vector2i gridTile)
    {
        return new Vector2(gridTile.X * grid.Comp.TileSize, gridTile.Y * grid.Comp.TileSize) + grid.Comp.TileSizeHalfVector;
    }

    public Vector2 GridTileToWorldPos(EntityUid uid, MapGridComponent grid, Vector2i gridTile)
    {
        var locX = gridTile.X * grid.TileSize + (grid.TileSize / 2f);
        var locY = gridTile.Y * grid.TileSize + (grid.TileSize / 2f);

        return Vector2.Transform(new Vector2(locX, locY), _transform.GetWorldMatrix(uid));
    }

    public MapCoordinates GridTileToWorld(EntityUid uid, MapGridComponent grid, Vector2i gridTile)
    {
        var parentMapId = _xformQuery.GetComponent(uid).MapID;

        return new(GridTileToWorldPos(uid, grid, gridTile), parentMapId);
    }

    public bool TryGetTileRef(EntityUid uid, MapGridComponent grid, Vector2i indices, out TileRef tile)
    {
        var chunkIndices = GridTileToChunkIndices(uid, grid, indices);
        if (!grid.Chunks.TryGetValue(chunkIndices, out var chunk))
        {
            tile = default;
            return false;
        }

        var cTileIndices = chunk.GridTileToChunkTile(indices);
        tile = GetTileRef(uid, grid, chunk, (ushort)cTileIndices.X, (ushort)cTileIndices.Y);
        return true;
    }

    public bool TryGetTile(MapGridComponent grid, Vector2i indices, out Tile tile)
    {
        var chunkIndices = GridTileToChunkIndices(grid, indices);
        if (!grid.Chunks.TryGetValue(chunkIndices, out var chunk))
        {
            tile = default;
            return false;
        }

        var cTileIndices = chunk.GridTileToChunkTile(indices);
        tile = chunk.Tiles[cTileIndices.X, cTileIndices.Y];
        return true;
    }

    /// <summary>
    /// Attempts to get the <see cref="ITileDefinition"/> for the tile at the given grid indices. This will throw an
    /// exception if the tile at this location has no registered tile definition.
    /// </summary>
    public bool TryGetTileDef(MapGridComponent grid, Vector2i indices, [NotNullWhen(true)] out ITileDefinition? tileDef)
    {
        if (!TryGetTile(grid, indices, out var tile))
        {
            tileDef = null;
            return false;
        }

        tileDef = _tileMan[tile.TypeId];
        return true;
    }

    public bool TryGetTileRef(EntityUid uid, MapGridComponent grid, EntityCoordinates coords, out TileRef tile)
    {
        return TryGetTileRef(uid, grid, CoordinatesToTile(uid, grid, coords), out tile);
    }

    public bool TryGetTileRef(EntityUid uid, MapGridComponent grid, Vector2 worldPos, out TileRef tile)
    {
        return TryGetTileRef(uid, grid, WorldToTile(uid, grid, worldPos), out tile);
    }

    #endregion Transforms

    /// <summary>
    /// Calculate the world space AABB for this chunk.
    /// </summary>
    internal Box2 CalcWorldAABB(EntityUid uid, MapGridComponent grid, MapChunk mapChunk)
    {
        var (position, rotation) =
            _transform.GetWorldPositionRotation(uid);

        var chunkPosition = mapChunk.Indices;
        var tileScale = grid.TileSize;
        var chunkScale = mapChunk.ChunkSize;

        var worldPos = position + rotation.RotateVec(chunkPosition * tileScale * chunkScale);

        return new Box2Rotated(
            ((Box2)mapChunk.CachedBounds
                .Scale(tileScale))
            .Translated(worldPos),
            rotation, worldPos).CalcBoundingBox();
    }

    private void OnTileModified(EntityUid uid, MapGridComponent grid, MapChunk mapChunk, Vector2i tileIndices, Tile newTile, Tile oldTile,
        bool shapeChanged)
    {
        // As the collision regeneration can potentially delete the chunk we'll notify of the tile changed first.
        var gridTile = mapChunk.ChunkTileToGridTile(tileIndices);
        mapChunk.LastTileModifiedTick = _timing.CurTick;
        grid.LastTileModifiedTick = _timing.CurTick;
        Dirty(uid, grid);

        // The map serializer currently sets tiles of unbound grids as part of the deserialization process
        // It properly sets SuppressOnTileChanged so that the event isn't spammed for every tile on the grid.
        // ParentMapId is not able to be accessed on unbound grids, so we can't even call this function for unbound grids.
        if (!MapManager.SuppressOnTileChanged)
        {
            var newTileRef = new TileRef(uid, gridTile, newTile);
            _mapInternal.RaiseOnTileChanged((uid, grid), newTileRef, oldTile, mapChunk.Indices);
        }

        if (shapeChanged && !mapChunk.SuppressCollisionRegeneration)
        {
            RegenerateCollision(uid, grid, mapChunk);
        }
    }

    /// <summary>
    /// Iterates the local tiles of the specified data.
    /// </summary>
    public struct TilesEnumerator
    {
        private readonly SharedMapSystem _mapSystem;

        private readonly EntityUid _uid;
        private readonly MapGridComponent _grid;
        private readonly bool _ignoreEmpty;
        private readonly Predicate<TileRef>? _predicate;

        private readonly int _lowerY;
        private readonly int _upperX;
        private readonly int _upperY;

        private int _x;
        private int _y;

        public TilesEnumerator(
            SharedMapSystem mapSystem,
            bool ignoreEmpty,
            Predicate<TileRef>? predicate,
            EntityUid uid,
            MapGridComponent grid,
            Box2 aabb)
        {
            _mapSystem = mapSystem;

            _uid = uid;
            _grid = grid;
            _ignoreEmpty = ignoreEmpty;
            _predicate = predicate;

            // TODO: Should move the intersecting calls onto mapmanager system and then allow people to pass in xform / xformquery
            // that way we can avoid the GetComp here.
            var gridTileLb = new Vector2i((int)Math.Floor(aabb.Left), (int)Math.Floor(aabb.Bottom));
            // If we have 20.1 we want to include that tile but if we have 20 then we don't.
            var gridTileRt = new Vector2i((int)Math.Ceiling(aabb.Right), (int)Math.Ceiling(aabb.Top));

            _x = gridTileLb.X;
            _y = gridTileLb.Y;
            _lowerY = gridTileLb.Y;
            _upperX = gridTileRt.X;
            _upperY = gridTileRt.Y;
        }

        public bool MoveNext(out TileRef tile)
        {
            while (true)
            {
                if (_x >= _upperX)
                {
                    tile = TileRef.Zero;
                    return false;
                }

                var gridTile = new Vector2i(_x, _y);

                _y++;

                if (_y >= _upperY)
                {
                    _x++;
                    _y = _lowerY;
                }

                var gridChunk = _mapSystem.GridTileToChunkIndices(_uid, _grid, gridTile);

                if (_grid.Chunks.TryGetValue(gridChunk, out var chunk))
                {
                    var chunkTile = chunk.GridTileToChunkTile(gridTile);
                    tile = _mapSystem.GetTileRef(_uid, _grid, chunk, (ushort)chunkTile.X, (ushort)chunkTile.Y);

                    if (_ignoreEmpty && tile.Tile.IsEmpty)
                        continue;

                    if (_predicate == null || _predicate(tile))
                    {
                        return true;
                    }
                }
                else if (!_ignoreEmpty)
                {
                    tile = new TileRef(_uid, gridTile.X, gridTile.Y, Tile.Empty);

                    if (_predicate == null || _predicate(tile))
                    {
                        return true;
                    }
                }
            }
        }
    }
}
