BaseVoxelVolume/BaseVoxelVolume.Mesh.cs
namespace Boxfish;

partial class BaseVoxelVolume<T, U>
{
	/// <summary>
	/// Do we want to store chunks while in editor?
	/// <para>NOTE: You'd also want to implement <see cref="Component.ExecuteInEditor"/> on your component.</para>
	/// </summary>
	public virtual bool StoreEditorChunks { get; } = false;

	/// <summary>
	/// Requires for <see cref="StoreEditorChunks"/> to be true.
	/// <para>NOTE: You will still have to manually generate chunks with <see cref="GenerateMesh(Chunk, bool)"/> for this to store anything.</para>
	/// </summary>
	public IReadOnlyDictionary<Vector3Int, Model> EditorChunks => _editorChunks;
	private Dictionary<Vector3Int, Model> _editorChunks = new();

	// TODO: Does not work perfectly, we need to check if we are out of actual VoxelBounds first...
	/// <summary>
	/// Should we ignore out of bounds faces? 
	/// <para>NOTE: This does not affect the +Z faces.</para>
	/// <para>NOTE: This does not work 100% well with varying elevation.</para>
	/// </summary>
	public virtual bool IgnoreOOBFaces { get; } = false;

	/// <summary>
	/// Shrimple structure containing voxel mesh information such as vertices.
	/// </summary>
	protected struct VoxelMesh
	{
		public List<U> Vertices { get; } = new();
		public List<int> Indices { get; } = new();
		public Mesh Mesh { get; set; }

		public int Offset { get; set; }
		public bool Valid { get; set; }

		public VoxelMesh() { }

		public void CreateBuffers( VertexAttribute[] layout )
		{
			if ( !Indices.Any() )
				return;

			Valid = true;
			Mesh.CreateVertexBuffer<U>( Vertices.Count, layout, Vertices.ToArray() );
			Mesh.CreateIndexBuffer( Indices.Count, Indices.ToArray() );
		}
	}

	/// <summary>
	/// Our method that creates a single vertex based on the parameters.
	/// </summary>
	/// <param name="position"></param>
	/// <param name="vertexIndex"></param>
	/// <param name="face"></param>
	/// <param name="ao"></param>
	/// <param name="voxel"></param>
	/// <returns></returns>
	public abstract U CreateVertex( Vector3Int position, int vertexIndex, int face, int ao, T voxel );

	/// <summary>
	/// Builds multiple chunk meshes.
	/// </summary>
	/// <param name="chunks"></param>
	/// <param name="physics"></param>
	/// <returns></returns>
	public async Task GenerateMeshes( IEnumerable<Chunk> chunks, bool physics = true )
	{
		Sandbox.Utility.Parallel.ForEach( chunks, chunk => _ = GenerateMesh( chunk, physics ) );
		await Task.CompletedTask;
	}

	/// <summary>
	/// Builds multiple chunk meshes with each one optionally getting physics.
	/// </summary>
	/// <param name="chunks"></param>
	/// <returns></returns>
	public async Task GenerateMeshes( IEnumerable<KeyValuePair<Chunk, bool>> chunks )
	{
		Sandbox.Utility.Parallel.ForEach( chunks, kvp => _ = GenerateMesh( kvp.Key, kvp.Value ) );
		await Task.CompletedTask;
	}

	/// <summary>
	/// Build a chunk mesh.
	/// </summary>
	/// <param name="chunk"></param>
	/// <param name="physics"></param>
	/// <returns></returns>
	public virtual async Task GenerateMesh( Chunk chunk, bool physics = true )
	{
		if ( chunk == null 
		  || Chunks == null 
		  || !this.IsValid()
		  || !Task.IsValid ) return;

		await Task.MainThread();
		var chunkObject = !Scene.IsEditor ? GetChunkObject( chunk ) : null;
		await Task.WorkerThread();

		// Let's create a VoxelMesh.
		var voxelMesh = new VoxelMesh();
		voxelMesh.Mesh = new Mesh( Material );

		var tested = new bool[VoxelUtils.CHUNK_SIZE, VoxelUtils.CHUNK_SIZE, VoxelUtils.CHUNK_SIZE];
		var collisionBuffer = new Utility.VertexBuffer();
		collisionBuffer.Init( true );

		chunk.Empty = true;
		physics &= Collisions;

		for ( byte x = 0; x < VoxelUtils.CHUNK_SIZE; x++ )
			for ( byte y = 0; y < VoxelUtils.CHUNK_SIZE; y++ )
				for ( byte z = 0; z < VoxelUtils.CHUNK_SIZE; z++ )
				{
					var voxel = chunk.GetVoxel( x, y, z );
					if ( !IsValidVoxel( voxel ) )
						continue;

					chunk.Empty = false;

					var position = new Vector3Int( x, y, z );

					// Let's start checking for collisions.
					if ( !tested[x, y, z] && !Scene.IsEditor && physics )
					{
						tested[x, y, z] = true;

						var start = (x: x, y: y, z: z);
						var size = (x: 1, y: 1, z: 1);
						var canSpread = (x: true, y: true, z: true);

						// Calculate how much we can fill.
						while ( canSpread.x || canSpread.y || canSpread.z )
						{
							canSpread.x = trySpreadX( chunk, canSpread.x, ref tested, start, ref size );
							canSpread.y = trySpreadY( chunk, canSpread.y, ref tested, start, ref size );
							canSpread.z = trySpreadZ( chunk, canSpread.z, ref tested, start, ref size );
						}

						var scale = new Vector3( size.x, size.y, size.z ) * Scale;
						var pos = new Vector3( start.x, start.y, start.z ) * Scale
							+ scale / 2f
							- Scale;

						collisionBuffer.AddCube( pos, scale, Rotation.Identity );
					}

					// Build vertices.
					var drawCount = 0;
					var isOpaque = IsOpaqueVoxel( voxel );
					for ( var i = 0; i < 6; i++ )
					{
						var direction = VoxelUtils.Directions[i];
						var neighbour = Query( position.x + direction.x, position.y + direction.y, position.z + direction.z, chunk );
						
						var ignoreFace = IgnoreOOBFaces && neighbour.Chunk == null && direction.z != 1;
						var shouldDraw = !isOpaque && IsOpaqueVoxel( neighbour.Voxel );

						if ( (neighbour.HasVoxel && !shouldDraw) || ignoreFace )
							continue;

						for ( var j = 0; j < 4; ++j )
						{
							var vertexIndex = VoxelUtils.FaceIndices[(i * 4) + j];
							var ao = Utility.AmbientOcclusion.Fetch( chunk, position, i, j );

							var vertex = CreateVertex( position, vertexIndex, (byte)i, ao, voxel );
							voxelMesh.Vertices.Add( vertex );
						}

						voxelMesh.Indices.Add( voxelMesh.Offset + drawCount * 4 + 0 );
						voxelMesh.Indices.Add( voxelMesh.Offset + drawCount * 4 + 2 );
						voxelMesh.Indices.Add( voxelMesh.Offset + drawCount * 4 + 1 );
						voxelMesh.Indices.Add( voxelMesh.Offset + drawCount * 4 + 2 );
						voxelMesh.Indices.Add( voxelMesh.Offset + drawCount * 4 + 0 );
						voxelMesh.Indices.Add( voxelMesh.Offset + drawCount * 4 + 3 );

						drawCount++;
					}

					voxelMesh.Offset += 4 * drawCount;
				}

		await Task.MainThread();

		if ( !this.IsValid() )
			return;

		// Check if we actually end up with vertices.
		if ( !chunk.Empty )
		{
			var builder = Model.Builder;
			voxelMesh.CreateBuffers( Layout );

			if ( voxelMesh.Valid )
				builder.AddMesh( voxelMesh.Mesh );
			else
			{
				// _ = builder.Create();

				if ( chunkObject == null )
				{
					if ( _editorChunks.ContainsKey( chunk.Position ) )
						_editorChunks.Remove( chunk.Position );
				}
				else
				{
					chunkObject.Rebuild( null, physics );
				}

				return;
			}

			if ( chunkObject == null ) // We are in editor.
			{
				if ( StoreEditorChunks )
				{
					var model = builder.Create();

					if ( _editorChunks.ContainsKey( chunk.Position ) )
						_editorChunks.Remove( chunk.Position );

					_editorChunks.Add( chunk.Position, model );
				}

				return;
			}

			if ( physics )
				builder = builder.AddCollisionMesh( collisionBuffer.Vertices.ToArray(), collisionBuffer.Indices.ToArray() );

			chunkObject.Rebuild( builder.Create(), physics );

			return;
		}

		// Remove the Gizmo chunk.
		else if ( chunkObject == null && _editorChunks.ContainsKey( chunk.Position ) )
			_editorChunks.Remove( chunk.Position );

		// Let's remove our empty chunk!
		_objects.Remove( chunk );
		_chunks.Remove( chunk.Position );

		chunkObject?.Dispose();
	}
}