JsonSerializationException: Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'System.Collections.Generic.Dictionary`2[System.String,BlockFetcher+ModelElement]' because the type requires a JSON object (e.g. {"name":"value"}) to deserialize correctly.
To fix this error either change the JSON to a JSON object (e.g. {"name":"value"}) or change the deserialized type to an array or a type that implements a collection interface (e.g. ICollection, IList) like List<T> that can be deserialized from a JSON array. JsonArrayAttribute can also be added to the type to force it to deserialize from a JSON array.
Path 'bamboo1_age0.elements', line 413, position 17.
Newtonsoft.Json.Serialization.JsonSerializerInternalReader.EnsureArrayContract (Newtonsoft.Json.JsonReader reader, System.Type objectType, Newtonsoft.Json.Serialization.JsonContract contract) (at <761cf2a144514d2291a678c334d49e9b>:0)
Newtonsoft.Json.Serialization.JsonSerializerInternalRea

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
using Newtonsoft.Json;
using PimDeWitte.UnityMainThreadDispatcher;
using System.IO;

public class BlockFetcher : MonoBehaviour
{
    private const string ASSETS_BASE_URL = "https://raw.githubusercontent.com/PrismarineJS/minecraft-assets/master/data/1.20.2";
    private const string BLOCKS_TEXTURES_URL = $"{ASSETS_BASE_URL}/blocks_textures.json";
    private const string BLOCKS_MODELS_URL = $"{ASSETS_BASE_URL}/blocks_models.json";
    private const string BLOCKS_URL = "http://localhost:8000/blocks";
    private const int MAX_VERTICES_PER_CHUNK = 65000;
    private const int ATLAS_SIZE = 2048;

    public Material blockMaterial;
    private List<MeshChunk> meshChunks = new();
    private Dictionary<string, BlockModel> blockModels = new(StringComparer.OrdinalIgnoreCase);
    private Dictionary<string, BlockTextureMapping> textureMapping = new(StringComparer.OrdinalIgnoreCase);
    private Dictionary<string, int> textureIndexMap = new(StringComparer.OrdinalIgnoreCase);
    private Dictionary<int, Vector2[]> uvCache = new();

    private Texture2D textureAtlas;
    private Rect[] atlasRects;

    [Serializable]
    private class BlockTextureMapping
    {
        public string particle;
        public Dictionary<string, string> textures;
    }

    [Serializable]
    private class BlockModel
    {
        public string parent;
        public Dictionary<string, ModelElement> elements;
        public Dictionary<string, string> textures;
    }

    [Serializable]
    private class ModelElement
    {
        public float[] from;
        public float[] to;
        public Dictionary<string, Face> faces;
    }

    [Serializable]
    private class Face
    {
        public float[] uv;
        public string texture;
        public int rotation;
    }

    private class MeshChunk
    {
        public List<Vector3> vertices = new(MAX_VERTICES_PER_CHUNK);
        public List<int> triangles = new();
        public List<Vector2> uvs = new();
        public bool isFull => vertices.Count + 4 >= MAX_VERTICES_PER_CHUNK;
    }

    [Serializable]
    private class BlockData
    {
        public string block_name;
        public int x, y, z;
        public string direction;
    }

    private async void Start()
    {
        await InitializeAssets();
        await FetchAndProcessBlocks();
    }

    private async Task InitializeAssets()
    {
        await Task.WhenAll(
            LoadBlockModels(),
            LoadTextureMapping(),
            CreateTextureAtlas()
        );
    }

    private async Task LoadBlockModels()
    {
        string json = await FetchJsonData(BLOCKS_MODELS_URL);
        blockModels = JsonConvert.DeserializeObject<Dictionary<string, BlockModel>>(json);
    }

    private async Task LoadTextureMapping()
    {
        string json = await FetchJsonData(BLOCKS_TEXTURES_URL);
        textureMapping = JsonConvert.DeserializeObject<Dictionary<string, BlockTextureMapping>>(json);
    }

    private async Task CreateTextureAtlas()
    {
        var textures = new List<Texture2D>();
        var textureUrls = new HashSet<string>();

        // Collect all unique texture URLs
        foreach (var mapping in textureMapping.Values)
        {
            if (mapping.textures != null)
            {
                foreach (var texture in mapping.textures.Values)
                {
                    textureUrls.Add($"{ASSETS_BASE_URL}/blocks/{texture}.png");
                }
            }
        }

        // Load all textures
        foreach (var url in textureUrls)
        {
            var texture = await LoadTextureFromUrl(url);
            if (texture != null)
            {
                textures.Add(texture);
                textureIndexMap[Path.GetFileNameWithoutExtension(url)] = textures.Count - 1;
            }
        }

        // Create atlas on main thread
        await CreateAtlasOnMainThread(textures);
    }

    private async Task CreateAtlasOnMainThread(List<Texture2D> textures)
    {
        var tcs = new TaskCompletionSource<bool>();

        UnityMainThreadDispatcher.Instance().Enqueue(() =>
        {
            textureAtlas = new Texture2D(ATLAS_SIZE, ATLAS_SIZE, TextureFormat.RGBA32, false)
            {
                filterMode = FilterMode.Point
            };

            atlasRects = textureAtlas.PackTextures(textures.ToArray(), 0, ATLAS_SIZE);
            textureAtlas.Apply(false, true);
            blockMaterial.mainTexture = textureAtlas;

            tcs.SetResult(true);
        });

        await tcs.Task;
    }

    private async Task<Texture2D> LoadTextureFromUrl(string url)
    {
        using var request = UnityWebRequestTexture.GetTexture(url);
        await request.SendWebRequest();

        if (request.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError($"Failed to load texture: {url}");
            return null;
        }

        var texture = DownloadHandlerTexture.GetContent(request);
        texture.filterMode = FilterMode.Point;
        return texture;
    }

    private async Task<string> FetchJsonData(string url)
    {
        using var request = UnityWebRequest.Get(url);
        await request.SendWebRequest();

        if (request.result != UnityWebRequest.Result.Success)
        {
            Debug.LogError($"Failed to fetch JSON data: {url}");
            return "{}";
        }

        return request.downloadHandler.text;
    }

    private async Task FetchAndProcessBlocks()
    {
        string jsonResponse = await FetchJsonData(BLOCKS_URL);
        var blockDataList = JsonConvert.DeserializeObject<List<BlockData>>(jsonResponse);

        UnityMainThreadDispatcher.Instance().Enqueue(() =>
            StartCoroutine(ProcessBlocksCoroutine(blockDataList)));
    }

    private IEnumerator ProcessBlocksCoroutine(List<BlockData> blockDataList)
    {
        const int BATCH_SIZE = 1000;
        int processedBlocks = 0;

        while (processedBlocks < blockDataList.Count)
        {
            int blocksToProcess = Mathf.Min(BATCH_SIZE, blockDataList.Count - processedBlocks);

            for (int i = 0; i < blocksToProcess; i++)
            {
                ProcessBlock(blockDataList[processedBlocks + i]);
            }

            processedBlocks += blocksToProcess;
            yield return null;
        }

        CreateMeshes();
    }

    private void ProcessBlock(BlockData block)
    {
        if (!blockModels.TryGetValue(block.block_name, out var model))
        {
            Debug.LogWarning($"No model found for block: {block.block_name}");
            return;
        }

        var position = new Vector3(block.x, block.y, block.z);
        AddBlockToMesh(position, model, block.direction);
    }

    private void AddBlockToMesh(Vector3 position, BlockModel model, string direction)
    {
        if (model.elements == null) return;

        foreach (var element in model.elements.Values)
        {
            var chunk = GetCurrentChunk();
            AddElementToChunk(chunk, position, element, model.textures, direction);
        }
    }

    private void AddElementToChunk(MeshChunk chunk, Vector3 position, ModelElement element, Dictionary<string, string> textures, string direction)
    {
        Vector3 from = new(
            element.from[0] / 16f - 0.5f,
            element.from[1] / 16f - 0.5f,
            element.from[2] / 16f - 0.5f
        );

        Vector3 to = new(
            element.to[0] / 16f - 0.5f,
            element.to[1] / 16f - 0.5f,
            element.to[2] / 16f - 0.5f
        );

        foreach (var face in element.faces)
        {
            AddFaceToChunk(chunk, position, from, to, face.Value, textures, direction);
        }
    }

    private void AddFaceToChunk(MeshChunk chunk, Vector3 position, Vector3 from, Vector3 to, Face face, Dictionary<string, string> textures, string direction)
    {
        // Get texture name from mapping
        string textureName = face.texture;
        if (textureName.StartsWith("#"))
        {
            textureName = textures[textureName.Substring(1)];
        }

        // Get UVs for texture
        var uvs = GetUVsForTexture(textureName);
        if (face.rotation != 0)
        {
            uvs = RotateUVs(uvs, face.rotation);
        }

        // Add vertices
        int vertStart = chunk.vertices.Count;
        AddFaceVertices(chunk, position, from, to, direction);

        // Add UVs
        for (int i = 0; i < 4; i++)
        {
            chunk.uvs.Add(uvs[i]);
        }

        // Add triangles
        chunk.triangles.AddRange(new[] {
            vertStart, vertStart + 2, vertStart + 1,
            vertStart, vertStart + 3, vertStart + 2
        });
    }

    private void AddFaceVertices(MeshChunk chunk, Vector3 position, Vector3 from, Vector3 to, string direction)
    {
        // Apply rotation based on direction
        Quaternion rotation = GetRotationFromDirection(direction);

        Vector3[] vertices = {
            rotation * (from + position),
            rotation * (new Vector3(to.x, from.y, from.z) + position),
            rotation * (new Vector3(to.x, to.y, from.z) + position),
            rotation * (new Vector3(from.x, to.y, from.z) + position)
        };

        chunk.vertices.AddRange(vertices);
    }

    private Quaternion GetRotationFromDirection(string direction)
    {
        return direction?.ToLower() switch
        {
            "north" => Quaternion.Euler(0, 0, 0),
            "south" => Quaternion.Euler(0, 180, 0),
            "east" => Quaternion.Euler(0, 90, 0),
            "west" => Quaternion.Euler(0, 270, 0),
            _ => Quaternion.identity
        };
    }

    private Vector2[] RotateUVs(Vector2[] uvs, int rotation)
    {
        var rotated = new Vector2[4];
        int offset = rotation / 90;
        for (int i = 0; i < 4; i++)
        {
            rotated[i] = uvs[(i + offset) % 4];
        }
        return rotated;
    }

    private MeshChunk GetCurrentChunk()
    {
        if (meshChunks.Count == 0 || meshChunks[^1].isFull)
        {
            meshChunks.Add(new MeshChunk());
        }
        return meshChunks[^1];
    }

    private Vector2[] GetUVsForTexture(string textureName)
    {
        if (!textureIndexMap.TryGetValue(textureName, out int textureIndex))
        {
            Debug.LogWarning($"Texture not found: {textureName}");
            textureIndex = 0;
        }

        if (!uvCache.TryGetValue(textureIndex, out Vector2[] uvs))
        {
            var uvRect = atlasRects[textureIndex];
            uvs = new[] {
                new Vector2(uvRect.xMin, uvRect.yMin),
                new Vector2(uvRect.xMax, uvRect.yMin),
                new Vector2(uvRect.xMax, uvRect.yMax),
                new Vector2(uvRect.xMin, uvRect.yMax)
            };
            uvCache[textureIndex] = uvs;
        }

        return uvs;
    }

    private void CreateMeshes()
    {
        var chunksParent = new GameObject("ChunkedMeshes");

        for (int i = 0; i < meshChunks.Count; i++)
        {
            var chunk = meshChunks[i];
            var chunkObject = new GameObject($"MeshChunk_{i}")
            {
                transform = { parent = chunksParent.transform }
            };

            var mesh = new Mesh { indexFormat = UnityEngine.Rendering.IndexFormat.UInt32 };
            mesh.SetVertices(chunk.vertices);
            mesh.SetTriangles(chunk.triangles, 0);
            mesh.SetUVs(0, chunk.uvs);
            mesh.RecalculateNormals();

            var meshFilter = chunkObject.AddComponent<MeshFilter>();
            meshFilter.mesh = mesh;

            var meshRenderer = chunkObject.AddComponent<MeshRenderer>();
            meshRenderer.material = blockMaterial;

            var meshCollider = chunkObject.AddComponent<MeshCollider>();
            meshCollider.sharedMesh = mesh;
        }
    }
}