using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using UnityEngine;
using UnityEngine.Serialization;
using Random = UnityEngine.Random;

public class GrassField : MonoBehaviour {
    public int size = 100;
    public int numChunks = 5;
    public int bladesPerMeter = 100;
    public GrassChunk chunkPrefab;
    public bool ignoreHeightForLOD = true;
    [Header("LOD 0")]
    public Material grassMaterialLOD0;
    [Range(0f, 1f)] public float densityLOD0 = 1f;
    public bool enableShadowsLOD0 = true;
    [Header("LOD 1")]
    public Material grassMaterialLOD1;
    [Range(0f, 1f)] public float densityLOD1 = 0.5f;
    public bool enableShadowsLOD1 = true;
    [Header("LOD 2")]
    public Material grassMaterialLOD2;
    [Range(0f, 1f)] public float densityLOD2 = 0.2f;
    public bool enableShadowsLOD2 = false;

    // a 2D array would be cleaner, but it can't be serialized
    // serialize it, because the chunks are generated offline and used during runtime
    [SerializeField, HideInInspector] private GrassChunk[] _chunks;

    // ReSharper disable Unity.PerformanceAnalysis
    public void GenerateGrassField() {
        Debug.Log("Generating grass field... ");

        var bladesLOD0 = GrassMeshGeneration.GenerateBladesRandom(size, size * size * (int)(bladesPerMeter * densityLOD0));
        // var bladesLOD1 = GenerateBladesRandom(size, size * size * (int)(bladesPerMeter * densityLOD1));
        // var bladesLOD2 = GenerateBladesRandom(size, size * size * (int)(bladesPerMeter * densityLOD2));
        var bladesLOD1 = GrassMeshGeneration.GenerateBladesReduced(bladesLOD0, densityLOD1);
        var bladesLOD2 = GrassMeshGeneration.GenerateBladesReduced(bladesLOD1, densityLOD2 / densityLOD1);
        CreateChunks(bladesLOD0, bladesLOD1, bladesLOD2);

        Debug.Log("Generating grass field done");
    }

    private void OnEnable() {
        if (_chunks.Length != numChunks * numChunks) {
            Debug.LogError(this.name + ": Existing chunks does not match numChunks.");
            gameObject.SetActive(false);
        }
    }

    private void Update() {
        UpdateLODs();
    }

    private void UpdateLODs() {
        var cam = Camera.main;
        if (!cam) return;

        if (_chunks.Length != numChunks * numChunks) return;

        var camPos = cam.transform.position - transform.position; // cam position relative to grass field
        if (ignoreHeightForLOD) {
            camPos.y = 0; // ignore height
        }
        
        var chunkSize = ((float)size) / numChunks;
        var bounds = new Bounds(
            new Vector3(0.5f * chunkSize, 0f, 0.5f * chunkSize),
            new Vector3(chunkSize, 0f, chunkSize)
        );
        
        for (int x = 0; x < numChunks; x++)
        for (int y = 0; y < numChunks; y++) {
            var chunkPos = new Vector3(x, 0f, y) * chunkSize;
            var localCamPos = camPos - chunkPos; // cam position relative to chunk

            var sqrCamDist = bounds.SqrDistance(localCamPos);
            if (sqrCamDist > 15f * 15f) {
                _chunks[x + y * numChunks].LOD = 2;
            }
            else if (sqrCamDist > 7.5f * 7.5f) {
                _chunks[x + y * numChunks].LOD = 1;
            }
            else {
                _chunks[x + y * numChunks].LOD = 0;
            }
        }
    }

    private void CreateChunks(BladeData[] bladesLOD0, BladeData[] bladesLOD1, BladeData[] bladesLOD2) {
        GameObject chunksContainer;
        // If a child called "Chunks" exists, destroy it -> children are also destroyed
        var chunksTransform = gameObject.transform.Find("Chunks");
        if (chunksTransform != null) {
            // Destroy(obj) does not work in edit mode
            DestroyImmediate(chunksTransform.gameObject);
        }
        // create GameObject "Chunks" as a container for all GrassChunk objects
        chunksContainer = new GameObject("Chunks");
        chunksContainer.transform.SetParent(this.transform, false);

        var chunkSize = ((float)size) / numChunks;
        // bounds of the chunk (relative to chunk) - do not consider y position
        var chunkBounds = new Bounds(
            new Vector3(0.5f * chunkSize, 0.0f, 0.5f * chunkSize),
            new Vector3(chunkSize, 10000f, chunkSize)
        );
        _chunks = new GrassChunk[numChunks * numChunks];
        for (int x = 0; x < numChunks; x++)
        for (int y = 0; y < numChunks; y++) {
            var chunkPos = new Vector3(x, 0.0f, y) * chunkSize;

            var meshLOD0 = GenerateChunkMesh(chunkPos, chunkBounds, bladesLOD0, "grass_chunk_" + x + "_" + y + "_lod0");
            var meshLOD1 = GenerateChunkMesh(chunkPos, chunkBounds, bladesLOD1, "grass_chunk_" + x + "_" + y + "_lod1");
            var meshLOD2 = GenerateChunkMesh(chunkPos, chunkBounds, bladesLOD2, "grass_chunk_" + x + "_" + y + "_lod2");

            var chunk = Instantiate(chunkPrefab, chunksContainer.transform);
            chunk.name = "Chunk [" + x + ", " + y +"]";
            chunk.transform.localPosition = chunkPos;

            chunk.MaterialLOD0 = grassMaterialLOD0;
            chunk.MaterialLOD1 = grassMaterialLOD1;
            chunk.MaterialLOD2 = grassMaterialLOD2;
            chunk.MeshLOD0 = meshLOD0;
            chunk.MeshLOD1 = meshLOD1;
            chunk.MeshLOD2 = meshLOD2;
            chunk.EnableShadowsLOD0 = enableShadowsLOD0;
            chunk.EnableShadowsLOD1 = enableShadowsLOD1;
            chunk.EnableShadowsLOD2 = enableShadowsLOD2;

            _chunks[x + y * numChunks] = chunk;
        }

        UpdateLODs();
    }

    private Mesh GenerateChunkMesh(Vector3 chunkPos, Bounds chunkBounds, BladeData[] blades, string meshName) {
        List<Vector3> rootPositions = new(); // vertices
        List<Vector3> tipOffsets = new(); // uv0
        List<int> indices = new();
        foreach (var blade in blades) {
            // position relative to chunk
            var localRootPos = blade.rootPosition - chunkPos;
            if (chunkBounds.Contains(localRootPos)) {
                rootPositions.Add(localRootPos);
                tipOffsets.Add(blade.tipOffset);
                indices.Add(rootPositions.Count - 1);
            }
        }

        var mesh = new Mesh();
        mesh.name = meshName;
        if (rootPositions.Count > 65536) {
            // 16 bit (default) supports 65536 vertices, 32 bit supports 4 billion
            mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
            Debug.LogWarning(meshName + " has more than 65536 grass blades. Mesh.indexFormat set to UInt32.");
        }
        mesh.SetVertices(rootPositions);
        mesh.SetUVs(0, tipOffsets); // Vector3 UVs
        // AABB gets calculated automatically (considers all root positions)
        mesh.SetIndices(indices, MeshTopology.Points, 0);
        // update the AABB to match the actual grass (not just the root positions)
        // use mesh.bounds rather than meshRenderer.localBounds,
        // because the latter gets overwritten automatically
        var previousBounds = mesh.bounds;
        const float width = 1f; // a blade should never bend sideways farther than this
        const float height = 1f; // a blade should not be higher than this
        mesh.bounds = new Bounds(
            previousBounds.center + new Vector3(0f, height * 0.5f, 0f),
            previousBounds.size + new Vector3(2f*width, height, 2f*width)
        );

        return mesh;
    }
    
    private void OnDrawGizmos() {
        var chunkSize = ((float)size) / numChunks;
        var tPos = transform.position;

        for (int i = 0; i < numChunks + 1; i++) {
            Gizmos.color = new Color(0f, 0.3f, 1f, 1f);
            Gizmos.DrawLine(
                tPos + new Vector3(chunkSize * i, 0.01f, 0),
                tPos + new Vector3(chunkSize * i, 0.01f, 0) + new Vector3(0, 0, size)
            );
            Gizmos.DrawLine(
                tPos + new Vector3(0, 0.01f, chunkSize * i),
                tPos + new Vector3(0, 0.01f, chunkSize * i) + new Vector3(size, 0, 0)
            );
        }

        var cam = Camera.main;
        if (!cam) return;
        var center = cam.transform.position;
        if (ignoreHeightForLOD) {
            center.y = 0f;
        }
        Gizmos.color = new Color(1.0f, 0.0f, 1f, 2f);
        Gizmos.DrawWireSphere(center, 7.5f);
    }
}
