using UnityEngine;

namespace InfiniteWorld.VoxelWorld
{
    public sealed partial class VoxelWorldGenerator
    {
        private void RenderChunk(Vector2Int coord)
        {
            if (!chunks.TryGetValue(coord, out ChunkRuntime runtime) || !runtime.HasData || atlas == null)
            {
                return;
            }

            runtime.EnsureCreated(coord, chunkRoot, chunkSize);
            ChunkMeshBuild meshBuild = BuildChunkMesh(coord, runtime.Heights, runtime.BiomeIndices);
            runtime.ApplyRenderData(meshBuild.RenderSnapshot);
            EnqueueColliderApply(coord, runtime.Version, runtime.RuntimeId, meshBuild.ColliderMesh);
            runtime.State = ChunkState.Rendered;
            MarkRegionDirty(coord);
        }

        private ChunkMeshBuild BuildChunkMesh(Vector2Int coord, int[] heights, byte[] biomeIndices)
        {
            MeshBuffers renderBuffers = new MeshBuffers();
            MeshBuffers colliderBuffers = new MeshBuffers();

            BuildGroundSurface(heights, biomeIndices, renderBuffers);
            BuildMountainTops(coord, heights, biomeIndices, renderBuffers, colliderBuffers);
            BuildMountainSides(coord, heights, biomeIndices, renderBuffers, colliderBuffers);

            ChunkRenderSnapshot renderSnapshot = renderBuffers.ToSnapshot();
            return new ChunkMeshBuild(colliderBuffers.ToMesh($"Collider_{coord.x}_{coord.y}"), renderSnapshot);
        }

        private void BuildGroundSurface(int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers)
        {
            bool[,] visited = new bool[chunkSize, chunkSize];
            for (int z = 0; z < chunkSize; z++)
            {
                for (int x = 0; x < chunkSize; x++)
                {
                    int index = z * chunkSize + x;
                    if (visited[x, z] || heights[index] > 0)
                    {
                        continue;
                    }

                    byte biomeIndex = biomeIndices[index];
                    int width = 1;
                    while (x + width < chunkSize && !visited[x + width, z] && heights[z * chunkSize + x + width] == 0 && biomeIndices[z * chunkSize + x + width] == biomeIndex)
                    {
                        width++;
                    }

                    int depth = 1;
                    bool canGrow = true;
                    while (z + depth < chunkSize && canGrow)
                    {
                        for (int ix = 0; ix < width; ix++)
                        {
                            int candidateIndex = (z + depth) * chunkSize + x + ix;
                            if (visited[x + ix, z + depth] || heights[candidateIndex] > 0 || biomeIndices[candidateIndex] != biomeIndex)
                            {
                                canGrow = false;
                                break;
                            }
                        }

                        if (canGrow)
                        {
                            depth++;
                        }
                    }

                    for (int dz = 0; dz < depth; dz++)
                    {
                        for (int dx = 0; dx < width; dx++)
                        {
                            visited[x + dx, z + dz] = true;
                        }
                    }

                    AddTopQuad(renderBuffers, x, z, width, depth, 0f, atlas.GetTextureLayer(biomeIndex, VoxelSurfaceType.WalkableSurface));
                }
            }
        }

        private void BuildMountainTops(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers)
        {
            bool[,] visited = new bool[chunkSize, chunkSize];
            for (int z = 0; z < chunkSize; z++)
            {
                for (int x = 0; x < chunkSize; x++)
                {
                    int height = heights[z * chunkSize + x];
                    if (height <= 0 || visited[x, z])
                    {
                        continue;
                    }

                    int index = z * chunkSize + x;
                    VoxelSurfaceType surfaceType = IsCliffTop(ChunkToWorldCell(coord, x, z), height) ? VoxelSurfaceType.CliffTop : VoxelSurfaceType.WalkableSurface;
                    byte biomeIndex = biomeIndices[index];

                    int width = 1;
                    while (x + width < chunkSize && !visited[x + width, z])
                    {
                        int candidateIndex = z * chunkSize + x + width;
                        if (heights[candidateIndex] != height || biomeIndices[candidateIndex] != biomeIndex)
                        {
                            break;
                        }

                        VoxelSurfaceType candidateSurface = IsCliffTop(ChunkToWorldCell(coord, x + width, z), height) ? VoxelSurfaceType.CliffTop : VoxelSurfaceType.WalkableSurface;
                        if (candidateSurface != surfaceType)
                        {
                            break;
                        }

                        width++;
                    }

                    int depth = 1;
                    bool canGrow = true;
                    while (z + depth < chunkSize && canGrow)
                    {
                        for (int ix = 0; ix < width; ix++)
                        {
                            if (visited[x + ix, z + depth])
                            {
                                canGrow = false;
                                break;
                            }

                            int candidateIndex = (z + depth) * chunkSize + x + ix;
                            if (heights[candidateIndex] != height || biomeIndices[candidateIndex] != biomeIndex)
                            {
                                canGrow = false;
                                break;
                            }

                            VoxelSurfaceType candidateSurface = IsCliffTop(ChunkToWorldCell(coord, x + ix, z + depth), height) ? VoxelSurfaceType.CliffTop : VoxelSurfaceType.WalkableSurface;
                            if (candidateSurface != surfaceType)
                            {
                                canGrow = false;
                                break;
                            }
                        }

                        if (canGrow)
                        {
                            depth++;
                        }
                    }

                    for (int dz = 0; dz < depth; dz++)
                    {
                        for (int dx = 0; dx < width; dx++)
                        {
                            visited[x + dx, z + dz] = true;
                        }
                    }

                    int textureLayer = atlas.GetTextureLayer(biomeIndex, surfaceType);
                    AddTopQuad(renderBuffers, x, z, width, depth, height, textureLayer);
                    AddTopQuad(colliderBuffers, x, z, width, depth, height, 0, false);
                }
            }
        }

        private void BuildMountainSides(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers)
        {
            for (int z = 0; z < chunkSize; z++)
            {
                BuildNorthSouthFaceSlice(coord, heights, biomeIndices, renderBuffers, colliderBuffers, z, true);
                BuildNorthSouthFaceSlice(coord, heights, biomeIndices, renderBuffers, colliderBuffers, z, false);
            }

            for (int x = 0; x < chunkSize; x++)
            {
                BuildEastWestFaceSlice(coord, heights, biomeIndices, renderBuffers, colliderBuffers, x, true);
                BuildEastWestFaceSlice(coord, heights, biomeIndices, renderBuffers, colliderBuffers, x, false);
            }
        }

        private void BuildNorthSouthFaceSlice(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers, int z, bool north)
        {
            bool[,] visited = new bool[chunkSize, maxMountainHeight];
            for (int x = 0; x < chunkSize; x++)
            {
                for (int y = 0; y < maxMountainHeight; y++)
                {
                    if (visited[x, y] || !TryGetNorthSouthFace(coord, heights, biomeIndices, x, z, y, north, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer))
                    {
                        continue;
                    }

                    int width = 1;
                    while (x + width < chunkSize && !visited[x + width, y] && TryGetNorthSouthFace(coord, heights, biomeIndices, x + width, z, y, north, out byte candidateBiome, out VoxelSurfaceType candidateSurface, out int candidateTexture) && candidateBiome == biomeIndex && candidateSurface == surfaceType && candidateTexture == textureLayer)
                    {
                        width++;
                    }

                    int height = 1;
                    bool canGrow = true;
                    while (y + height < maxMountainHeight && canGrow)
                    {
                        for (int dx = 0; dx < width; dx++)
                        {
                            if (visited[x + dx, y + height] || !TryGetNorthSouthFace(coord, heights, biomeIndices, x + dx, z, y + height, north, out byte candidateBiome, out VoxelSurfaceType candidateSurface, out int candidateTexture) || candidateBiome != biomeIndex || candidateSurface != surfaceType || candidateTexture != textureLayer)
                            {
                                canGrow = false;
                                break;
                            }
                        }

                        if (canGrow)
                        {
                            height++;
                        }
                    }

                    for (int dy = 0; dy < height; dy++)
                    {
                        for (int dx = 0; dx < width; dx++)
                        {
                            visited[x + dx, y + dy] = true;
                        }
                    }

                    float faceZ = north ? z + 1f : z;
                    Vector3 bl = new Vector3(x, y, faceZ);
                    Vector3 br = new Vector3(x + width, y, faceZ);
                    Vector3 tr = new Vector3(x + width, y + height, faceZ);
                    Vector3 tl = new Vector3(x, y + height, faceZ);

                    if (north)
                    {
                        AddVerticalQuad(renderBuffers, bl, br, tr, tl, width, height, textureLayer, Vector3.forward);
                        AddVerticalQuad(colliderBuffers, bl, br, tr, tl, width, height, 0, Vector3.forward, false);
                    }
                    else
                    {
                        AddVerticalQuad(renderBuffers, br, bl, tl, tr, width, height, textureLayer, Vector3.back);
                        AddVerticalQuad(colliderBuffers, br, bl, tl, tr, width, height, 0, Vector3.back, false);
                    }
                }
            }
        }

        private void BuildEastWestFaceSlice(Vector2Int coord, int[] heights, byte[] biomeIndices, MeshBuffers renderBuffers, MeshBuffers colliderBuffers, int x, bool east)
        {
            bool[,] visited = new bool[chunkSize, maxMountainHeight];
            for (int z = 0; z < chunkSize; z++)
            {
                for (int y = 0; y < maxMountainHeight; y++)
                {
                    if (visited[z, y] || !TryGetEastWestFace(coord, heights, biomeIndices, x, z, y, east, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer))
                    {
                        continue;
                    }

                    int width = 1;
                    while (z + width < chunkSize && !visited[z + width, y] && TryGetEastWestFace(coord, heights, biomeIndices, x, z + width, y, east, out byte candidateBiome, out VoxelSurfaceType candidateSurface, out int candidateTexture) && candidateBiome == biomeIndex && candidateSurface == surfaceType && candidateTexture == textureLayer)
                    {
                        width++;
                    }

                    int height = 1;
                    bool canGrow = true;
                    while (y + height < maxMountainHeight && canGrow)
                    {
                        for (int dz = 0; dz < width; dz++)
                        {
                            if (visited[z + dz, y + height] || !TryGetEastWestFace(coord, heights, biomeIndices, x, z + dz, y + height, east, out byte candidateBiome, out VoxelSurfaceType candidateSurface, out int candidateTexture) || candidateBiome != biomeIndex || candidateSurface != surfaceType || candidateTexture != textureLayer)
                            {
                                canGrow = false;
                                break;
                            }
                        }

                        if (canGrow)
                        {
                            height++;
                        }
                    }

                    for (int dy = 0; dy < height; dy++)
                    {
                        for (int dz = 0; dz < width; dz++)
                        {
                            visited[z + dz, y + dy] = true;
                        }
                    }

                    float faceX = east ? x + 1f : x;
                    Vector3 bl = new Vector3(faceX, y, z);
                    Vector3 br = new Vector3(faceX, y, z + width);
                    Vector3 tr = new Vector3(faceX, y + height, z + width);
                    Vector3 tl = new Vector3(faceX, y + height, z);

                    if (east)
                    {
                        AddVerticalQuad(renderBuffers, br, bl, tl, tr, width, height, textureLayer, Vector3.right);
                        AddVerticalQuad(colliderBuffers, br, bl, tl, tr, width, height, 0, Vector3.right, false);
                    }
                    else
                    {
                        AddVerticalQuad(renderBuffers, bl, br, tr, tl, width, height, textureLayer, Vector3.left);
                        AddVerticalQuad(colliderBuffers, bl, br, tr, tl, width, height, 0, Vector3.left, false);
                    }
                }
            }
        }

        private bool TryGetNorthSouthFace(Vector2Int coord, int[] heights, byte[] biomeIndices, int x, int z, int y, bool north, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer)
        {
            int current = heights[z * chunkSize + x];
            int worldX = coord.x * chunkSize + x;
            int worldZ = coord.y * chunkSize + z;
            int neighbor = GetHeightAtWorldCell(new Vector2Int(worldX, north ? worldZ + 1 : worldZ - 1));
            if (y >= current || y < neighbor)
            {
                biomeIndex = 0;
                surfaceType = VoxelSurfaceType.Dirt;
                textureLayer = 0;
                return false;
            }

            biomeIndex = biomeIndices[z * chunkSize + x];
            surfaceType = y == current - 1 ? VoxelSurfaceType.CliffSide : VoxelSurfaceType.Dirt;
            textureLayer = atlas.GetTextureLayer(biomeIndex, surfaceType);
            return true;
        }

        private bool TryGetEastWestFace(Vector2Int coord, int[] heights, byte[] biomeIndices, int x, int z, int y, bool east, out byte biomeIndex, out VoxelSurfaceType surfaceType, out int textureLayer)
        {
            int current = heights[z * chunkSize + x];
            int worldX = coord.x * chunkSize + x;
            int worldZ = coord.y * chunkSize + z;
            int neighbor = GetHeightAtWorldCell(new Vector2Int(east ? worldX + 1 : worldX - 1, worldZ));
            if (y >= current || y < neighbor)
            {
                biomeIndex = 0;
                surfaceType = VoxelSurfaceType.Dirt;
                textureLayer = 0;
                return false;
            }

            biomeIndex = biomeIndices[z * chunkSize + x];
            surfaceType = y == current - 1 ? VoxelSurfaceType.CliffSide : VoxelSurfaceType.Dirt;
            textureLayer = atlas.GetTextureLayer(biomeIndex, surfaceType);
            return true;
        }

        private void AddTopQuad(MeshBuffers buffers, float x, float z, float width, float depth, float height, int textureLayer, bool withUv = true)
        {
            int baseIndex = buffers.Vertices.Count;
            buffers.Vertices.Add(new Vector3(x, height, z));
            buffers.Vertices.Add(new Vector3(x + width, height, z));
            buffers.Vertices.Add(new Vector3(x + width, height, z + depth));
            buffers.Vertices.Add(new Vector3(x, height, z + depth));
            buffers.Normals.Add(Vector3.up);
            buffers.Normals.Add(Vector3.up);
            buffers.Normals.Add(Vector3.up);
            buffers.Normals.Add(Vector3.up);

            if (withUv)
            {
                buffers.Uvs.Add(new Vector2(0f, 0f));
                buffers.Uvs.Add(new Vector2(width, 0f));
                buffers.Uvs.Add(new Vector2(width, depth));
                buffers.Uvs.Add(new Vector2(0f, depth));
                Vector2 textureData = new Vector2(textureLayer, 0f);
                buffers.TextureData.Add(textureData);
                buffers.TextureData.Add(textureData);
                buffers.TextureData.Add(textureData);
                buffers.TextureData.Add(textureData);
            }

            buffers.Triangles.Add(baseIndex);
            buffers.Triangles.Add(baseIndex + 2);
            buffers.Triangles.Add(baseIndex + 1);
            buffers.Triangles.Add(baseIndex);
            buffers.Triangles.Add(baseIndex + 3);
            buffers.Triangles.Add(baseIndex + 2);
        }

        private void AddVerticalQuad(MeshBuffers buffers, Vector3 bottomLeft, Vector3 bottomRight, Vector3 topRight, Vector3 topLeft, float width, float height, int textureLayer, Vector3 normal, bool withUv = true)
        {
            int baseIndex = buffers.Vertices.Count;
            buffers.Vertices.Add(bottomLeft);
            buffers.Vertices.Add(bottomRight);
            buffers.Vertices.Add(topRight);
            buffers.Vertices.Add(topLeft);
            buffers.Normals.Add(normal);
            buffers.Normals.Add(normal);
            buffers.Normals.Add(normal);
            buffers.Normals.Add(normal);

            if (withUv)
            {
                buffers.Uvs.Add(new Vector2(0f, 0f));
                buffers.Uvs.Add(new Vector2(width, 0f));
                buffers.Uvs.Add(new Vector2(width, height));
                buffers.Uvs.Add(new Vector2(0f, height));
                Vector2 textureData = new Vector2(textureLayer, 0f);
                buffers.TextureData.Add(textureData);
                buffers.TextureData.Add(textureData);
                buffers.TextureData.Add(textureData);
                buffers.TextureData.Add(textureData);
            }

            buffers.Triangles.Add(baseIndex);
            buffers.Triangles.Add(baseIndex + 1);
            buffers.Triangles.Add(baseIndex + 2);
            buffers.Triangles.Add(baseIndex);
            buffers.Triangles.Add(baseIndex + 2);
            buffers.Triangles.Add(baseIndex + 3);
        }

        private bool IsCliffTop(Vector2Int worldCell, int currentHeight)
        {
            return GetHeightAtWorldCell(worldCell + Vector2Int.up) < currentHeight ||
                   GetHeightAtWorldCell(worldCell + Vector2Int.right) < currentHeight ||
                   GetHeightAtWorldCell(worldCell + Vector2Int.down) < currentHeight ||
                   GetHeightAtWorldCell(worldCell + Vector2Int.left) < currentHeight;
        }

        private int GetHeightAtWorldCell(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 GetSampledFinalHeightAtWorldCell(worldCell);
            }

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

            return runtime.Heights[localZ * chunkSize + localX];
        }
    }
}
