Voxel/VoxelChunk.cs
namespace Monolith;

/// <summary>
/// A fixed 32x32x32 block of voxels stored as a bitset. One chunk is 4 KB of state, which is
/// what makes a 16.7M cube monolith cheap to hold, snapshot and send over the network.
/// </summary>
public sealed class VoxelChunk
{
	public const int Size = Tuning.ChunkSize;
	public const int SizeSq = Size * Size;
	public const int VoxelCount = Size * Size * Size;
	public const int WordCount = VoxelCount / 64;

	/// <summary>Bit set means solid. Index is x + y*Size + z*Size*Size.</summary>
	public readonly ulong[] Bits = new ulong[WordCount];

	/// <summary>Number of solid voxels, kept incrementally so we never have to popcount.</summary>
	public int SolidCount;

	/// <summary>Chunk coordinate within the world grid.</summary>
	public readonly Vector3Int Coord;

	/// <summary>Set when geometry no longer matches <see cref="Bits"/>.</summary>
	public bool MeshDirty;

	public VoxelChunk( Vector3Int coord )
	{
		Coord = coord;
	}

	public bool IsEmpty => SolidCount == 0;
	public bool IsFull => SolidCount == VoxelCount;

	public static int LocalIndex( int x, int y, int z ) => x + y * Size + z * SizeSq;

	public bool Get( int x, int y, int z )
	{
		var i = LocalIndex( x, y, z );
		return (Bits[i >> 6] & (1UL << (i & 63))) != 0;
	}

	public bool Get( int index )
	{
		return (Bits[index >> 6] & (1UL << (index & 63))) != 0;
	}

	/// <summary>Clears a voxel. Returns true if it was solid, so callers can count removals.</summary>
	public bool Clear( int x, int y, int z )
	{
		var i = LocalIndex( x, y, z );
		var word = i >> 6;
		var mask = 1UL << (i & 63);

		if ( (Bits[word] & mask) == 0 )
			return false;

		Bits[word] &= ~mask;
		SolidCount--;
		MeshDirty = true;
		return true;
	}

	public bool Set( int x, int y, int z )
	{
		var i = LocalIndex( x, y, z );
		var word = i >> 6;
		var mask = 1UL << (i & 63);

		if ( (Bits[word] & mask) != 0 )
			return false;

		Bits[word] |= mask;
		SolidCount++;
		MeshDirty = true;
		return true;
	}

	public void FillAll()
	{
		for ( int i = 0; i < WordCount; i++ )
			Bits[i] = ulong.MaxValue;

		SolidCount = VoxelCount;
		MeshDirty = true;
	}

	public void ClearAll()
	{
		Array.Clear( Bits, 0, WordCount );
		SolidCount = 0;
		MeshDirty = true;
	}

	/// <summary>Recount solid voxels from scratch. Only needed after a bulk bit write.</summary>
	public void RecountSolid()
	{
		int n = 0;
		for ( int i = 0; i < WordCount; i++ )
			n += PopCount( Bits[i] );

		SolidCount = n;
	}

	/// <summary>
	/// Hand rolled rather than System.Numerics.BitOperations, which is not worth betting the
	/// build on given the s&amp;box API whitelist. This runs once per chunk per snapshot.
	/// </summary>
	private static int PopCount( ulong v )
	{
		v -= (v >> 1) & 0x5555555555555555UL;
		v = (v & 0x3333333333333333UL) + ((v >> 2) & 0x3333333333333333UL);
		v = (v + (v >> 4)) & 0x0f0f0f0f0f0f0f0fUL;
		return (int)((v * 0x0101010101010101UL) >> 56);
	}

	// ---------------------------------------------------------------- serialisation

	public const byte FormatEmpty = 0;
	public const byte FormatFull = 1;
	public const byte FormatRaw = 2;

	/// <summary>
	/// Compact form for network snapshots. Most chunks in a monolith are either untouched
	/// (full) or completely mined out (empty), so those cost a single byte.
	/// </summary>
	public byte[] Serialize()
	{
		if ( IsEmpty ) return new[] { FormatEmpty };
		if ( IsFull ) return new[] { FormatFull };

		var bytes = new byte[1 + WordCount * 8];
		bytes[0] = FormatRaw;
		Buffer.BlockCopy( Bits, 0, bytes, 1, WordCount * 8 );
		return bytes;
	}

	public void Deserialize( byte[] data )
	{
		if ( data == null || data.Length == 0 )
			return;

		switch ( data[0] )
		{
			case FormatEmpty:
				ClearAll();
				return;

			case FormatFull:
				FillAll();
				return;

			default:
				Buffer.BlockCopy( data, 1, Bits, 0, WordCount * 8 );
				RecountSolid();
				MeshDirty = true;
				return;
		}
	}
}