using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

namespace UNSLOW.ShaderBasics
{
    /// <summary>小さな草地区画を、株ごとのGameObjectを作らずに描画する。</summary>
    [ExecuteAlways]
    public sealed class GrasslandPatch : MonoBehaviour
    {
        [SerializeField] private Mesh bladeMesh;
        [SerializeField] private Material grassMaterial;
        [SerializeField, Min(1f)] private float width = 8f;
        [SerializeField, Range(0.04f, 0.5f)] private float spacing = 0.16f;
        [SerializeField] private int seed = 2409;

        private readonly List<Batch> batches = new List<Batch>();
        private int plantCount;

        public int PlantCount => plantCount;

        public static float GroundHeight(float x, float z)
        {
            return 0.08f * Mathf.Sin(0.45f * x)
                 + 0.06f * Mathf.Cos(0.38f * z)
                 + 0.04f * Mathf.Sin(0.8f * x + 0.6f * z);
        }

        public void Configure(Mesh mesh, Material material)
        {
            bladeMesh = mesh;
            grassMaterial = material;
            spacing = 0.16f;
            Rebuild();
        }

        private void OnEnable() => Rebuild();

        private void OnValidate() => Rebuild();

        private void Update()
        {
            if (grassMaterial == null || !SystemInfo.supportsInstancing)
                return;

            if (batches.Count == 0)
                Rebuild();

            var parameters = new RenderParams(grassMaterial)
            {
                shadowCastingMode = ShadowCastingMode.Off,
                receiveShadows = false
            };

            foreach (var batch in batches)
                Graphics.RenderMeshInstanced(parameters, batch.Mesh, 0, batch.Matrices);
        }

        private void Rebuild()
        {
            batches.Clear();
            plantCount = 0;
            if (bladeMesh == null || grassMaterial == null)
                return;

            const int tilesPerSide = 2;
            var cellsPerSide = Mathf.CeilToInt(width / spacing);
            var groups = new List<Matrix4x4>[tilesPerSide * tilesPerSide];
            for (var i = 0; i < groups.Length; i++)
                groups[i] = new List<Matrix4x4>();

            var random = new System.Random(seed);
            for (var z = 0; z < cellsPerSide; z++)
            for (var x = 0; x < cellsPerSide; x++)
            {
                var px = ((x + 0.5f) / cellsPerSide - 0.5f) * width;
                var pz = ((z + 0.5f) / cellsPerSide - 0.5f) * width;
                px += ((float)random.NextDouble() - 0.5f) * spacing * 0.55f;
                pz += ((float)random.NextDouble() - 0.5f) * spacing * 0.55f;

                var cluster = Mathf.PerlinNoise((px + 18f) * 0.42f, (pz + 18f) * 0.42f);
                if (random.NextDouble() > 0.56f + 0.42f * cluster)
                    continue;

                var scale = 0.82f + (float)random.NextDouble() * 0.5f;
                var angle = (float)random.NextDouble() * 360f;
                var position = new Vector3(px, GroundHeight(px, pz), pz);
                var upright = Quaternion.Euler(0f, angle, 0f);
                var matrix = Matrix4x4.TRS(position, upright,
                    Vector3.one * scale);
                var tileX = x < cellsPerSide / 2 ? 0 : 1;
                var tileZ = z < cellsPerSide / 2 ? 0 : 1;
                var group = tileZ * tilesPerSide + tileX;
                groups[group].Add(matrix);
                plantCount++;
            }

            for (var i = 0; i < groups.Length; i++)
            {
                var matrices = groups[i];
                for (var start = 0; start < matrices.Count; start += 500)
                {
                    var count = Mathf.Min(500, matrices.Count - start);
                    var copy = new Matrix4x4[count];
                    matrices.CopyTo(start, copy, 0, count);
                    batches.Add(new Batch(bladeMesh, copy));
                }
            }
        }

        private readonly struct Batch
        {
            public readonly Mesh Mesh;
            public readonly Matrix4x4[] Matrices;

            public Batch(Mesh mesh, Matrix4x4[] matrices)
            {
                Mesh = mesh;
                Matrices = matrices;
            }
        }
    }
}
