using UnityEngine;
using UnityEditor;

[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
public class MeshGenerator : MonoBehaviour
{
	public Texture2D heightMap;
	public Vector3 size = new Vector3(10, 3, 10);
	public Vector2Int sampleCount = new Vector2Int(25, 25);
	public LayerMask terrainLayer;
	[HideInInspector] public MeshData meshData;
	[HideInInspector] public Mesh mesh;
	[HideInInspector] public RenderTexture colorTexture;

	void OnEnable()
	{
		var colorCameraObject = new GameObject("Color Camera");
		colorCameraObject.transform.parent = transform;
		colorCameraObject.transform.localPosition = Vector3.zero;
		var offset = new Vector3(0, size.y, 0);
		var rotation = Quaternion.Euler(90, 0, 0);
		colorCameraObject.transform.SetLocalPositionAndRotation(offset, rotation);

		var colorCamera = colorCameraObject.AddComponent<Camera>();
		colorCamera.orthographic = true;
		colorCamera.orthographicSize = size.z / 2;
		colorCamera.aspect = size.x / size.z;
		colorCamera.nearClipPlane = 0;
		colorCamera.clearFlags = CameraClearFlags.Nothing;
		colorCamera.depthTextureMode = DepthTextureMode.None;
		colorCamera.cullingMask = 1 << (int)Mathf.Log(terrainLayer.value, 2);
		colorTexture = new RenderTexture(1024, 1024, 0, RenderTextureFormat.ARGB32);
		colorCamera.targetTexture = colorTexture;
	}

	public void GenerateTerrainMesh()
	{
		// Check if the height map is null
		if (heightMap == null)
		{
			Debug.LogError("Height map is null");
			return;
		}

		// Initialize the mesh data
		meshData = new MeshData(sampleCount.x, sampleCount.y);

		// Loop through each vertex
		var index = 0;
		for (var y = 0; y < sampleCount.y; y++)
		{
			for (var x = 0; x < sampleCount.x; x++)
			{
				// Calculate the uv, vertex position
				var uv = new Vector2(x, y) / (sampleCount - Vector2.one);
				var vertex = (uv - Vector2.one / 2) * new Vector2(size.x, size.z);
				var heightValue = GetTrueHeight(uv);

				// Set the vertex position and uv
				meshData.vertices[index] = new Vector3(vertex.x, heightValue, vertex.y);
				meshData.uvs[index] = uv;

				// Add triangles if not at the edge
				if (x < sampleCount.x - 1 && y < sampleCount.y - 1)
				{
					meshData.AddTriangle(index, index + sampleCount.x + 1, index + sampleCount.x);
					meshData.AddTriangle(index + sampleCount.x + 1, index, index + 1);
				}

				// Increment the index
				index++;
			}
		}

		// Create the mesh
		mesh = meshData.CreateMesh();

		var meshFilter = GetComponent<MeshFilter>();
		if (meshFilter != null)
			meshFilter.sharedMesh = mesh;
		else
			Debug.LogError("Mesh filter is null");

		var meshCollider = GetComponent<MeshCollider>();
		if (meshCollider != null)
			meshCollider.sharedMesh = mesh;
	}

	// Get the height of the terrain at the given uv from the mesh
	public float GetMeshHeight(Vector2 uv)
	{
		// Calculate the precise indices
		float xIndexPrecise = uv.x * (sampleCount.x - 1);
		float zIndexPrecise = uv.y * (sampleCount.y - 1);

		// Calculate the indices of the vertices around the point
		var xIndex = Mathf.FloorToInt(uv.x * (sampleCount.x - 1));
		var zIndex = Mathf.FloorToInt(uv.y * (sampleCount.y - 1));

		// Ensure indices are within the bounds of the mesh
		xIndex = Mathf.Clamp(xIndex, 0, sampleCount.x - 2);
		zIndex = Mathf.Clamp(zIndex, 0, sampleCount.y - 2);

		// Get the four vertices of the square (assuming row major and clockwise order)
		var v1 = meshData.vertices[zIndex * sampleCount.x + xIndex];
		var v2 = meshData.vertices[zIndex * sampleCount.x + xIndex + 1];
		var v3 = meshData.vertices[(zIndex + 1) * sampleCount.x + xIndex];
		var v4 = meshData.vertices[(zIndex + 1) * sampleCount.x + xIndex + 1];

		// Bilinear interpolation
		var xRem = xIndexPrecise - xIndex;
		var zRem = zIndexPrecise - zIndex;
		var height1 = Mathf.Lerp(v1.y, v2.y, xRem);
		var height2 = Mathf.Lerp(v3.y, v4.y, xRem);
		return Mathf.Lerp(height1, height2, zRem);
	}

	// Get the true height of the terrain at the given uv from the height map
	public float GetTrueHeight(Vector2 uv)
	{
		var x = Mathf.RoundToInt(uv.x * heightMap.width);
		var y = Mathf.RoundToInt(uv.y * heightMap.height);
		return heightMap.GetPixel(x, y).grayscale * size.y; ;
	}

	// Check if the mesh and mesh data are generated
	public bool IsMeshGenerated()
	{
		return mesh != null && meshData != null;
	}
}

// MeshData class which allows for easy mesh creation
public class MeshData
{
	public Vector3[] vertices;
	public int[] triangles;
	public Vector2[] uvs;

	int triangleIndex;

	public MeshData(int meshWidth, int meshHeight)
	{
		vertices = new Vector3[meshWidth * meshHeight];
		uvs = new Vector2[meshWidth * meshHeight];
		triangles = new int[(meshWidth - 1) * (meshHeight - 1) * 6];
	}

	public void AddTriangle(int a, int b, int c)
	{
		triangles[triangleIndex] = a;
		triangles[triangleIndex + 1] = c;
		triangles[triangleIndex + 2] = b;
		triangleIndex += 3;
	}

	public Mesh CreateMesh()
	{
		var mesh = new Mesh
		{
			vertices = vertices,
			triangles = triangles,
			uv = uvs
		};
		mesh.RecalculateNormals();
		return mesh;
	}
}

#if UNITY_EDITOR
// Custom editor which adds a button to generate the terrain
[CustomEditor(typeof(MeshGenerator))]
public class MeshGeneratorEditor : Editor
{
	public override void OnInspectorGUI()
	{
		var script = (MeshGenerator)target;
		DrawDefaultInspector();
		GUILayout.Space(10);
		if (GUILayout.Button("Generate Terrain"))
			script.GenerateTerrainMesh();
	}
}
#endif
