using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Tilemaps;

namespace InfiniteWorld
{
    public class InfiniteWorldGenerator : MonoBehaviour
    {
        [Header("References")]
        [SerializeField] private Transform player;
        [SerializeField] private WorldAutotileProfile profile;

        [Header("Chunk Settings")]
        [SerializeField] private int chunkSize = 16;
        [SerializeField] private int generationRadius = 2;
        [SerializeField] private int seed = 12345;

        [Header("Rock Noise")]
        [SerializeField] private float macroNoiseScale = 0.05f;
        [SerializeField] private float detailNoiseScale = 0.12f;
        [SerializeField] private float ridgeNoiseScale = 0.18f;
        [SerializeField] private float wallThreshold = 0.6f;
        [SerializeField] private float rockBias = 0.04f;
        [SerializeField] private int smoothingPasses = 2;

        [Header("Global Passes")]
        [SerializeField] private float passNoiseScale = 0.018f;
        [SerializeField] private float passDetailScale = 0.041f;
        [SerializeField] private float passThreshold = 0.22f;
        [SerializeField] private float passFeather = 0.12f;

        [Header("Environment")]
        [SerializeField] private float environmentNoiseScale = 0.19f;
        [SerializeField] private float environmentThreshold = 0.7f;

        [Header("Random Objects")]
        [SerializeField, Range(0f, 1f)] private float randomPrefabChance = 0.06f;
        [SerializeField] private float randomPrefabZOffset = -0.1f;

        [Header("Streaming")]
        [SerializeField, Min(1)] private int maxAsyncChunkJobs = 2;
        [SerializeField, Min(1)] private int maxChunkRendersPerFrame = 1;
        [SerializeField, Min(0)] private int blockingGenerationRadius = 0;

        private readonly Dictionary<Vector2Int, ChunkRuntime> chunks = new Dictionary<Vector2Int, ChunkRuntime>();
        private readonly Queue<ChunkBuildResult> completedBuilds = new Queue<ChunkBuildResult>();
        private readonly object generationLock = new object();
        private static readonly Vector2Int[] NeighborOffsets =
        {
            Vector2Int.up,
            Vector2Int.right,
            Vector2Int.down,
            Vector2Int.left
        };

        private Grid grid;
        private Transform chunkRoot;
        private Vector2Int lastGeneratedCenter = new Vector2Int(int.MinValue, int.MinValue);
        private WorldAutotileProfile runtimeFallbackProfile;
        private int activeGenerationJobs;
        private bool isLoadingPaused;
        private float pausedTimeScale = 1f;
        private TileBase cachedGroundTile;
        private TileBase[] cachedGroundTiles;

        private void Awake()
        {
            EnsureSceneInfrastructure();
            EnsureRuntimeData();
            TryFindPlayer();
        }

        private void Update()
        {
            if (player == null && !TryFindPlayer())
            {
                return;
            }

            Vector2Int playerChunk = WorldToChunk(player.position);
            if (playerChunk != lastGeneratedCenter)
            {
                lastGeneratedCenter = playerChunk;
                UnloadDistantChunks(playerChunk);
            }

            DrainCompletedBuilds(isLoadingPaused ? int.MaxValue : maxChunkRendersPerFrame);
            ScheduleChunkGeneration(playerChunk);
            EnsureBlockingChunksLoaded(playerChunk);
        }

        private void OnDisable()
        {
            SetLoadingPaused(false);
        }

        public bool UsesProfile(WorldAutotileProfile candidateProfile)
        {
            return profile == candidateProfile;
        }

        public void EditorRefreshFromProfile()
        {
            EnsureSceneInfrastructure();
            EnsureRuntimeData();
            cachedGroundTile = null;
            cachedGroundTiles = null;

            List<Vector2Int> coords = new List<Vector2Int>(chunks.Keys);
            for (int i = 0; i < coords.Count; i++)
            {
                if (!chunks.TryGetValue(coords[i], out ChunkRuntime runtime) || !runtime.HasData)
                {
                    continue;
                }

                runtime.State = ChunkState.ReadyToRender;
                RenderChunk(coords[i]);
            }

            for (int i = 0; i < coords.Count; i++)
            {
                RefreshNeighborBorders(coords[i]);
            }
        }

        private void EnsureSceneInfrastructure()
        {
            grid = GetComponentInChildren<Grid>();
            if (grid == null)
            {
                GameObject gridObject = new GameObject("Grid", typeof(Grid));
                gridObject.transform.SetParent(transform, false);
                grid = gridObject.GetComponent<Grid>();
            }

            Transform existingChunkRoot = grid.transform.Find("Chunks");
            if (existingChunkRoot == null)
            {
                GameObject root = new GameObject("Chunks");
                root.transform.SetParent(grid.transform, false);
                chunkRoot = root.transform;
            }
            else
            {
                chunkRoot = existingChunkRoot;
            }
        }

        private void EnsureRuntimeData()
        {
            if (profile == null || !profile.HasAnyAssignedTiles())
            {
                runtimeFallbackProfile = RuntimeWorldProfileFactory.CreateFallbackProfile();
                return;
            }

            runtimeFallbackProfile = null;
        }

        private bool TryFindPlayer()
        {
            if (player != null)
            {
                return true;
            }

            SimplePlayerInputMover mover = FindFirstObjectByType<SimplePlayerInputMover>();
            if (mover == null)
            {
                return false;
            }

            player = mover.transform;
            return true;
        }

        private void ScheduleChunkGeneration(Vector2Int centerChunk)
        {
            List<Vector2Int> coords = GetCoordsByPriority(centerChunk, generationRadius);
            for (int i = 0; i < coords.Count; i++)
            {
                Vector2Int coord = coords[i];
                ChunkRuntime runtime = GetOrCreateChunkRuntime(coord);
                if (runtime.IsRendered || runtime.HasData || runtime.State == ChunkState.Generating)
                {
                    continue;
                }

                if (IsWithinRadius(coord, centerChunk, blockingGenerationRadius))
                {
                    continue;
                }

                if (!TryReserveGenerationSlot())
                {
                    break;
                }

                runtime.State = ChunkState.Generating;
                runtime.Version++;
                GenerateChunkDataAsync(coord, runtime.Version).Forget();
            }
        }

        private void UnloadDistantChunks(Vector2Int centerChunk)
        {
            List<Vector2Int> coordsToRemove = new List<Vector2Int>();
            foreach (KeyValuePair<Vector2Int, ChunkRuntime> pair in chunks)
            {
                if (IsWithinActiveRadius(pair.Key, centerChunk))
                {
                    continue;
                }

                coordsToRemove.Add(pair.Key);
            }

            for (int i = 0; i < coordsToRemove.Count; i++)
            {
                Vector2Int coord = coordsToRemove[i];
                if (!chunks.TryGetValue(coord, out ChunkRuntime chunk))
                {
                    continue;
                }

                chunks.Remove(coord);
                if (chunk.Chunk.Root != null)
                {
                    Destroy(chunk.Chunk.Root.gameObject);
                }

                RefreshNeighborBorders(coord);
            }
        }

        private bool IsWithinActiveRadius(Vector2Int coord, Vector2Int centerChunk)
        {
            int dx = Mathf.Abs(coord.x - centerChunk.x);
            int dy = Mathf.Abs(coord.y - centerChunk.y);
            return dx <= generationRadius && dy <= generationRadius;
        }

        private GeneratedChunk CreateChunk(Vector2Int coord)
        {
            GameObject chunkObject = new GameObject($"Chunk_{coord.x}_{coord.y}");
            chunkObject.transform.SetParent(chunkRoot, false);
            chunkObject.transform.localPosition = new Vector3(coord.x * chunkSize, coord.y * chunkSize, 0f);

            Tilemap ground = CreateTilemap("Ground", chunkObject.transform, 0, false);
            Tilemap walls = CreateTilemap("Walls", chunkObject.transform, 1, true);
            Tilemap environment = CreateTilemap("Environment", chunkObject.transform, 2, false);
            Transform objectsRoot = new GameObject("Objects").transform;
            objectsRoot.SetParent(chunkObject.transform, false);

            return new GeneratedChunk(chunkObject.transform, ground, walls, environment, objectsRoot);
        }

        private Tilemap CreateTilemap(string name, Transform parent, int sortingOrder, bool addCollision)
        {
            GameObject tilemapObject = new GameObject(name, typeof(Tilemap), typeof(TilemapRenderer));
            tilemapObject.transform.SetParent(parent, false);

            TilemapRenderer renderer = tilemapObject.GetComponent<TilemapRenderer>();
            renderer.sortingOrder = sortingOrder;

            if (addCollision)
            {
                Rigidbody2D rb = tilemapObject.GetComponent<Rigidbody2D>();
                if (rb == null)
                {
                    rb = tilemapObject.AddComponent<Rigidbody2D>();
                }
                rb.bodyType = RigidbodyType2D.Static;

                CompositeCollider2D composite = tilemapObject.GetComponent<CompositeCollider2D>();
                if (composite == null)
                {
                    composite = tilemapObject.AddComponent<CompositeCollider2D>();
                }
                composite.geometryType = CompositeCollider2D.GeometryType.Polygons;

                TilemapCollider2D collider = tilemapObject.GetComponent<TilemapCollider2D>();
                if (collider == null)
                {
                    collider = tilemapObject.AddComponent<TilemapCollider2D>();
                }
                collider.compositeOperation = Collider2D.CompositeOperation.Merge;
            }

            return tilemapObject.GetComponent<Tilemap>();
        }

        private ChunkBuildResult GenerateChunkData(Vector2Int coord, int version)
        {
            int margin = Mathf.Max(2, smoothingPasses + 1);
            int sampleSize = chunkSize + margin * 2;
            bool[,] sampled = new bool[sampleSize, sampleSize];

            for (int y = 0; y < sampleSize; y++)
            {
                for (int x = 0; x < sampleSize; x++)
                {
                    int localX = x - margin;
                    int localY = y - margin;
                    Vector2Int worldCell = ChunkToWorldCell(coord, localX, localY);
                    sampled[x, y] = SampleRock(worldCell);
                }
            }

            for (int pass = 0; pass < smoothingPasses; pass++)
            {
                sampled = SmoothSampledMask(sampled);
            }

            bool[,] wallMask = new bool[chunkSize, chunkSize];
            for (int y = 0; y < chunkSize; y++)
            {
                for (int x = 0; x < chunkSize; x++)
                {
                    wallMask[x, y] = sampled[x + margin, y + margin];
                }
            }

            bool[,] environmentMask = BuildEnvironment(coord, wallMask);
            return new ChunkBuildResult(coord, wallMask, environmentMask, version);
        }

        private bool[,] SmoothSampledMask(bool[,] source)
        {
            int width = source.GetLength(0);
            int height = source.GetLength(1);
            bool[,] result = new bool[width, height];

            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    int solidNeighbors = CountSampledWallNeighbors(source, x, y);
                    if (solidNeighbors >= 5)
                    {
                        result[x, y] = true;
                    }
                    else if (solidNeighbors <= 2)
                    {
                        result[x, y] = false;
                    }
                    else
                    {
                        result[x, y] = source[x, y];
                    }
                }
            }

            return result;
        }

        private int CountSampledWallNeighbors(bool[,] sampled, int x, int y)
        {
            int width = sampled.GetLength(0);
            int height = sampled.GetLength(1);
            int count = 0;

            for (int oy = -1; oy <= 1; oy++)
            {
                for (int ox = -1; ox <= 1; ox++)
                {
                    if (ox == 0 && oy == 0)
                    {
                        continue;
                    }

                    int nx = x + ox;
                    int ny = y + oy;
                    if (nx < 0 || ny < 0 || nx >= width || ny >= height)
                    {
                        count++;
                        continue;
                    }

                    if (sampled[nx, ny])
                    {
                        count++;
                    }
                }
            }

            return count;
        }

        private bool SampleRock(Vector2Int worldCell)
        {
            float macro = Mathf.PerlinNoise((worldCell.x + seed * 0.13f) * macroNoiseScale, (worldCell.y - seed * 0.17f) * macroNoiseScale);
            float detail = Mathf.PerlinNoise((worldCell.x - seed * 0.23f) * detailNoiseScale, (worldCell.y + seed * 0.19f) * detailNoiseScale);
            float ridge = 1f - Mathf.Abs(Mathf.PerlinNoise((worldCell.x + seed * 0.31f) * ridgeNoiseScale, (worldCell.y + seed * 0.29f) * ridgeNoiseScale) * 2f - 1f);

            float rockValue = macro * 0.62f + detail * 0.18f + ridge * 0.20f + rockBias;
            if (IsInsideGlobalPass(worldCell))
            {
                rockValue -= 0.45f;
            }

            return rockValue >= wallThreshold;
        }

        private bool IsInsideGlobalPass(Vector2Int worldCell)
        {
            float primary = Mathf.PerlinNoise((worldCell.x + seed * 0.41f) * passNoiseScale, (worldCell.y - seed * 0.43f) * passNoiseScale);
            float detail = Mathf.PerlinNoise((worldCell.x - seed * 0.17f) * passDetailScale, (worldCell.y + seed * 0.23f) * passDetailScale);
            float ridged = Mathf.Abs(primary * 2f - 1f);
            float warped = Mathf.Lerp(ridged, Mathf.Abs(detail * 2f - 1f), 0.35f);
            return warped <= passThreshold + passFeather * detail;
        }

        private bool[,] BuildEnvironment(Vector2Int coord, bool[,] wallMask)
        {
            bool[,] environmentMask = new bool[chunkSize, chunkSize];
            for (int y = 0; y < chunkSize; y++)
            {
                for (int x = 0; x < chunkSize; x++)
                {
                    if (wallMask[x, y])
                    {
                        continue;
                    }

                    if (HasAdjacentOpenTiles(wallMask, x, y, 1))
                    {
                        continue;
                    }

                    Vector2Int worldCell = ChunkToWorldCell(coord, x, y);
                    float noise = Mathf.PerlinNoise((worldCell.x + seed * 0.53f) * environmentNoiseScale, (worldCell.y - seed * 0.61f) * environmentNoiseScale);
                    environmentMask[x, y] = noise >= environmentThreshold;
                }
            }

            return environmentMask;
        }

        private bool HasAdjacentOpenTiles(bool[,] wallMask, int x, int y, int radius)
        {
            for (int oy = -radius; oy <= radius; oy++)
            {
                for (int ox = -radius; ox <= radius; ox++)
                {
                    int nx = x + ox;
                    int ny = y + oy;
                    if (nx < 0 || ny < 0 || nx >= chunkSize || ny >= chunkSize)
                    {
                        continue;
                    }

                    if (!wallMask[nx, ny])
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        private void RenderChunk(Vector2Int coord)
        {
            if (!chunks.TryGetValue(coord, out ChunkRuntime runtime) || !runtime.HasData)
            {
                return;
            }

            WorldAutotileProfile activeProfile = profile != null && profile.HasAnyAssignedTiles() ? profile : runtimeFallbackProfile;
            if (activeProfile == null)
            {
                return;
            }

            if (runtime.Chunk.Root == null)
            {
                runtime.Chunk = CreateChunk(coord);
            }

            GeneratedChunk chunk = runtime.Chunk;
            int tileCount = chunkSize * chunkSize;
            TileBase[] groundTiles = GetGroundTiles(activeProfile.baseGroundTile, tileCount);
            TileBase[] wallTiles = new TileBase[tileCount];
            TileBase[] environmentTiles = new TileBase[tileCount];

            int index = 0;
            for (int y = 0; y < chunkSize; y++)
            {
                for (int x = 0; x < chunkSize; x++)
                {
                    if (runtime.WallMask[x, y])
                    {
                        Vector2Int worldCell = ChunkToWorldCell(coord, x, y);
                        AutoTileShape shape = ResolveWallShape(worldCell);
                        wallTiles[index] = activeProfile.GetWallTile(shape);
                    }
                    else if (runtime.EnvironmentMask[x, y])
                    {
                        TileBase tile = PickEnvironmentTile(ChunkToWorldCell(coord, x, y), activeProfile);
                        environmentTiles[index] = tile;
                    }

                    index++;
                }
            }

            BoundsInt bounds = new BoundsInt(0, 0, 0, chunkSize, chunkSize, 1);
            chunk.Ground.SetTilesBlock(bounds, groundTiles);
            chunk.Walls.SetTilesBlock(bounds, wallTiles);
            chunk.Environment.SetTilesBlock(bounds, environmentTiles);
            RenderRandomPrefabs(coord, runtime, activeProfile);
            runtime.State = ChunkState.Rendered;
        }

        private void RefreshNeighborBorders(Vector2Int coord)
        {
            for (int i = 0; i < NeighborOffsets.Length; i++)
            {
                RenderChunk(coord + NeighborOffsets[i]);
            }
        }

        private AutoTileShape ResolveWallShape(Vector2Int worldCell)
        {
            bool top = HasWallAt(worldCell + Vector2Int.up);
            bool right = HasWallAt(worldCell + Vector2Int.right);
            bool bottom = HasWallAt(worldCell + Vector2Int.down);
            bool left = HasWallAt(worldCell + Vector2Int.left);
            bool topLeft = HasWallAt(worldCell + new Vector2Int(-1, 1));
            bool topRight = HasWallAt(worldCell + new Vector2Int(1, 1));
            bool bottomRight = HasWallAt(worldCell + new Vector2Int(1, -1));
            bool bottomLeft = HasWallAt(worldCell + new Vector2Int(-1, -1));

            if (!top && !left)
            {
                return AutoTileShape.OuterTopLeft;
            }

            if (!top && !right)
            {
                return AutoTileShape.OuterTopRight;
            }

            if (!bottom && !right)
            {
                return AutoTileShape.OuterBottomRight;
            }

            if (!bottom && !left)
            {
                return AutoTileShape.OuterBottomLeft;
            }

            if (top && left && !topLeft)
            {
                return AutoTileShape.InnerTopLeft;
            }

            if (top && right && !topRight)
            {
                return AutoTileShape.InnerTopRight;
            }

            if (bottom && right && !bottomRight)
            {
                return AutoTileShape.InnerBottomRight;
            }

            if (bottom && left && !bottomLeft)
            {
                return AutoTileShape.InnerBottomLeft;
            }

            if (!top)
            {
                return AutoTileShape.Top;
            }

            if (!right)
            {
                return AutoTileShape.Right;
            }

            if (!bottom)
            {
                return AutoTileShape.Bottom;
            }

            if (!left)
            {
                return AutoTileShape.Left;
            }

            return AutoTileShape.Center;
        }

        private bool HasWallAt(Vector2Int worldCell)
        {
            Vector2Int coord = new Vector2Int(Mathf.FloorToInt(worldCell.x / (float)chunkSize), Mathf.FloorToInt(worldCell.y / (float)chunkSize));
            if (!chunks.TryGetValue(coord, out ChunkRuntime runtime) || !runtime.HasData)
            {
                return SampleRock(worldCell);
            }

            int localX = worldCell.x - coord.x * chunkSize;
            int localY = worldCell.y - coord.y * chunkSize;
            if (localX < 0 || localY < 0 || localX >= chunkSize || localY >= chunkSize)
            {
                return SampleRock(worldCell);
            }

            return runtime.WallMask[localX, localY];
        }

        private TileBase PickEnvironmentTile(Vector2Int worldCell, WorldAutotileProfile activeProfile)
        {
            if (activeProfile.environmentTiles == null || activeProfile.environmentTiles.Count == 0)
            {
                return null;
            }

            float total = 0f;
            for (int i = 0; i < activeProfile.environmentTiles.Count; i++)
            {
                EnvironmentTileEntry entry = activeProfile.environmentTiles[i];
                if (entry != null && entry.tile != null)
                {
                    total += Mathf.Max(0.01f, entry.weight);
                }
            }

            if (total <= 0f)
            {
                return null;
            }

            float selector = Hash01(worldCell.x, worldCell.y, seed + 701);
            float threshold = selector * total;
            float cumulative = 0f;

            for (int i = 0; i < activeProfile.environmentTiles.Count; i++)
            {
                EnvironmentTileEntry entry = activeProfile.environmentTiles[i];
                if (entry == null || entry.tile == null)
                {
                    continue;
                }

                cumulative += Mathf.Max(0.01f, entry.weight);
                if (threshold <= cumulative)
                {
                    return entry.tile;
                }
            }

            return activeProfile.environmentTiles[0].tile;
        }

        private void RenderRandomPrefabs(Vector2Int coord, ChunkRuntime runtime, WorldAutotileProfile activeProfile)
        {
            if (runtime.Chunk.ObjectsRoot == null)
            {
                return;
            }

            ClearSpawnedObjects(runtime.Chunk.ObjectsRoot);
            if (randomPrefabChance <= 0f || activeProfile.randomPrefabs == null || activeProfile.randomPrefabs.Count == 0)
            {
                return;
            }

            for (int y = 0; y < chunkSize; y++)
            {
                for (int x = 0; x < chunkSize; x++)
                {
                    if (runtime.WallMask[x, y] || runtime.EnvironmentMask[x, y])
                    {
                        continue;
                    }

                    Vector2Int worldCell = ChunkToWorldCell(coord, x, y);
                    if (Hash01(worldCell.x, worldCell.y, seed + 1103) > randomPrefabChance)
                    {
                        continue;
                    }

                    GameObject prefab = PickRandomPrefab(worldCell, activeProfile);
                    if (prefab == null)
                    {
                        continue;
                    }

                    GameObject instance = Instantiate(prefab, runtime.Chunk.ObjectsRoot);
                    instance.transform.localPosition = new Vector3(x + 0.5f, y + 0.5f, randomPrefabZOffset);
                    instance.transform.localRotation = Quaternion.identity;
                    instance.name = prefab.name;
                }
            }
        }

        private GameObject PickRandomPrefab(Vector2Int worldCell, WorldAutotileProfile activeProfile)
        {
            float total = 0f;
            for (int i = 0; i < activeProfile.randomPrefabs.Count; i++)
            {
                RandomPrefabEntry entry = activeProfile.randomPrefabs[i];
                if (entry != null && entry.prefab != null)
                {
                    total += Mathf.Max(0.01f, entry.weight);
                }
            }

            if (total <= 0f)
            {
                return null;
            }

            float selector = Hash01(worldCell.x, worldCell.y, seed + 1409) * total;
            float cumulative = 0f;
            for (int i = 0; i < activeProfile.randomPrefabs.Count; i++)
            {
                RandomPrefabEntry entry = activeProfile.randomPrefabs[i];
                if (entry == null || entry.prefab == null)
                {
                    continue;
                }

                cumulative += Mathf.Max(0.01f, entry.weight);
                if (selector <= cumulative)
                {
                    return entry.prefab;
                }
            }

            for (int i = 0; i < activeProfile.randomPrefabs.Count; i++)
            {
                RandomPrefabEntry entry = activeProfile.randomPrefabs[i];
                if (entry != null && entry.prefab != null)
                {
                    return entry.prefab;
                }
            }

            return null;
        }

        private static void ClearSpawnedObjects(Transform root)
        {
            for (int i = root.childCount - 1; i >= 0; i--)
            {
                UnityEngine.Object.Destroy(root.GetChild(i).gameObject);
            }
        }

        private Vector2Int WorldToChunk(Vector3 position)
        {
            return new Vector2Int(
                Mathf.FloorToInt(position.x / chunkSize),
                Mathf.FloorToInt(position.y / chunkSize));
        }

        private Vector2Int ChunkToWorldCell(Vector2Int coord, int localX, int localY)
        {
            return new Vector2Int(coord.x * chunkSize + localX, coord.y * chunkSize + localY);
        }

        private static int Hash(int x, int y, int seed)
        {
            int hash = x;
            hash = hash * 397 ^ y;
            hash = hash * 397 ^ seed;
            hash = (hash << 13) ^ hash;
            return hash * (hash * hash * 15731 + 789221) + 1376312589;
        }

        private static float Hash01(int x, int y, int seed)
        {
            return (Hash(x, y, seed) & int.MaxValue) / (float)int.MaxValue;
        }

        private async UniTaskVoid GenerateChunkDataAsync(Vector2Int coord, int version)
        {
            try
            {
                ChunkBuildResult result = await UniTask.RunOnThreadPool(() => GenerateChunkData(coord, version));
                lock (generationLock)
                {
                    completedBuilds.Enqueue(result);
                    activeGenerationJobs = Mathf.Max(0, activeGenerationJobs - 1);
                }
            }
            catch (Exception exception)
            {
                lock (generationLock)
                {
                    activeGenerationJobs = Mathf.Max(0, activeGenerationJobs - 1);
                }

                Debug.LogException(exception, this);
            }
        }

        private void DrainCompletedBuilds(int maxRenders)
        {
            int renders = 0;
            while (renders < maxRenders)
            {
                ChunkBuildResult result;
                lock (generationLock)
                {
                    if (completedBuilds.Count == 0)
                    {
                        break;
                    }

                    result = completedBuilds.Dequeue();
                }

                if (!ApplyBuildResult(result))
                {
                    continue;
                }

                RenderChunk(result.Coord);
                RefreshNeighborBorders(result.Coord);
                renders++;
            }
        }

        private bool ApplyBuildResult(ChunkBuildResult result)
        {
            if (!chunks.TryGetValue(result.Coord, out ChunkRuntime runtime))
            {
                return false;
            }

            if (result.Version != runtime.Version)
            {
                return false;
            }

            runtime.WallMask = result.WallMask;
            runtime.EnvironmentMask = result.EnvironmentMask;
            if (!runtime.IsRendered)
            {
                runtime.State = ChunkState.ReadyToRender;
            }

            return !runtime.IsRendered;
        }

        private void EnsureBlockingChunksLoaded(Vector2Int centerChunk)
        {
            List<Vector2Int> requiredCoords = GetCoordsByPriority(centerChunk, blockingGenerationRadius);
            bool isMissingRequiredChunk = false;

            for (int i = 0; i < requiredCoords.Count; i++)
            {
                Vector2Int coord = requiredCoords[i];
                ChunkRuntime runtime = GetOrCreateChunkRuntime(coord);
                if (runtime.IsRendered)
                {
                    continue;
                }

                isMissingRequiredChunk = true;
                if (runtime.State == ChunkState.Generating)
                {
                    continue;
                }

                runtime.Version++;
                runtime.State = ChunkState.SyncBuilding;
                ApplyBuildResult(GenerateChunkData(coord, runtime.Version));
                RenderChunk(coord);
                RefreshNeighborBorders(coord);
            }

            DrainCompletedBuilds(isMissingRequiredChunk ? int.MaxValue : maxChunkRendersPerFrame);
            SetLoadingPaused(HasMissingRequiredChunks(requiredCoords));
        }

        private bool HasMissingRequiredChunks(List<Vector2Int> requiredCoords)
        {
            for (int i = 0; i < requiredCoords.Count; i++)
            {
                if (!chunks.TryGetValue(requiredCoords[i], out ChunkRuntime runtime) || !runtime.IsRendered)
                {
                    return true;
                }
            }

            return false;
        }

        private bool TryReserveGenerationSlot()
        {
            lock (generationLock)
            {
                if (activeGenerationJobs >= maxAsyncChunkJobs)
                {
                    return false;
                }

                activeGenerationJobs++;
                return true;
            }
        }

        private List<Vector2Int> GetCoordsByPriority(Vector2Int centerChunk, int radius)
        {
            List<Vector2Int> coords = new List<Vector2Int>((radius * 2 + 1) * (radius * 2 + 1));
            for (int y = -radius; y <= radius; y++)
            {
                for (int x = -radius; x <= radius; x++)
                {
                    coords.Add(new Vector2Int(centerChunk.x + x, centerChunk.y + y));
                }
            }

            coords.Sort((left, right) => CompareChunkPriority(centerChunk, left, right));
            return coords;
        }

        private static int CompareChunkPriority(Vector2Int centerChunk, Vector2Int left, Vector2Int right)
        {
            int leftDx = Mathf.Abs(left.x - centerChunk.x);
            int leftDy = Mathf.Abs(left.y - centerChunk.y);
            int rightDx = Mathf.Abs(right.x - centerChunk.x);
            int rightDy = Mathf.Abs(right.y - centerChunk.y);

            int leftChebyshev = Mathf.Max(leftDx, leftDy);
            int rightChebyshev = Mathf.Max(rightDx, rightDy);
            int chebyshevCompare = leftChebyshev.CompareTo(rightChebyshev);
            if (chebyshevCompare != 0)
            {
                return chebyshevCompare;
            }

            int leftDistance = leftDx * leftDx + leftDy * leftDy;
            int rightDistance = rightDx * rightDx + rightDy * rightDy;
            int distanceCompare = leftDistance.CompareTo(rightDistance);
            if (distanceCompare != 0)
            {
                return distanceCompare;
            }

            int yCompare = left.y.CompareTo(right.y);
            return yCompare != 0 ? yCompare : left.x.CompareTo(right.x);
        }

        private ChunkRuntime GetOrCreateChunkRuntime(Vector2Int coord)
        {
            if (!chunks.TryGetValue(coord, out ChunkRuntime runtime))
            {
                runtime = new ChunkRuntime();
                chunks.Add(coord, runtime);
            }

            return runtime;
        }

        private bool IsWithinRadius(Vector2Int coord, Vector2Int centerChunk, int radius)
        {
            int dx = Mathf.Abs(coord.x - centerChunk.x);
            int dy = Mathf.Abs(coord.y - centerChunk.y);
            return dx <= radius && dy <= radius;
        }

        private TileBase[] GetGroundTiles(TileBase groundTile, int tileCount)
        {
            if (cachedGroundTiles == null || cachedGroundTiles.Length != tileCount || cachedGroundTile != groundTile)
            {
                cachedGroundTiles = new TileBase[tileCount];
                cachedGroundTile = groundTile;
                if (groundTile != null)
                {
                    Array.Fill(cachedGroundTiles, groundTile);
                }
            }

            return cachedGroundTiles;
        }

        private void SetLoadingPaused(bool pause)
        {
            if (pause == isLoadingPaused)
            {
                return;
            }

            if (pause)
            {
                pausedTimeScale = Time.timeScale > 0f ? Time.timeScale : 1f;
                Time.timeScale = 0f;
            }
            else
            {
                Time.timeScale = pausedTimeScale;
            }

            isLoadingPaused = pause;
        }

        private readonly struct GeneratedChunk
        {
            public GeneratedChunk(Transform root, Tilemap ground, Tilemap walls, Tilemap environment, Transform objectsRoot)
            {
                Root = root;
                Ground = ground;
                Walls = walls;
                Environment = environment;
                ObjectsRoot = objectsRoot;
            }

            public Transform Root { get; }
            public Tilemap Ground { get; }
            public Tilemap Walls { get; }
            public Tilemap Environment { get; }
            public Transform ObjectsRoot { get; }
        }

        private sealed class ChunkRuntime
        {
            public GeneratedChunk Chunk;
            public bool[,] WallMask;
            public bool[,] EnvironmentMask;
            public ChunkState State;
            public int Version;

            public bool HasData => WallMask != null && EnvironmentMask != null;
            public bool IsRendered => State == ChunkState.Rendered && Chunk.Root != null;
        }

        private readonly struct ChunkBuildResult
        {
            public ChunkBuildResult(Vector2Int coord, bool[,] wallMask, bool[,] environmentMask, int version)
            {
                Coord = coord;
                WallMask = wallMask;
                EnvironmentMask = environmentMask;
                Version = version;
            }

            public Vector2Int Coord { get; }
            public bool[,] WallMask { get; }
            public bool[,] EnvironmentMask { get; }
            public int Version { get; }
        }

        private enum ChunkState
        {
            None,
            Generating,
            SyncBuilding,
            ReadyToRender,
            Rendered
        }
    }
}
