code/BaseVoxelVolume/BaseVoxelVolume.Query.cs
namespace Boxfish;

public abstract partial class BaseVoxelVolume<T, U>
{
	/// <summary>
	/// The result of queries giving information about a voxel, it's position, etc.. 
	/// </summary>
	public struct VoxelQueryData
	{
		public T Voxel;
		public bool HasVoxel;
		public (byte x, byte y, byte z) LocalPosition;
		public Vector3Int GlobalPosition;
		public Chunk Chunk;
	}

	/// <summary>
	/// Apply our GameObject's transform to a 3D-vector.
	/// </summary>
	/// <param name="vector"></param>
	/// <returns></returns>
	public Vector3 ApplyWorldTransform( Vector3 vector )
		=> new Vector3( (vector - WorldPosition) * WorldRotation.Inverse / WorldScale + WorldPosition );

	/// <summary>
	/// Converts a world position to a VoxelVolume position.
	/// </summary>
	/// <param name="position"></param>
	/// <param name="transformed"></param>
	/// <returns></returns>
	public Vector3Int WorldToVoxel( Vector3 position, bool transformed = true )
	{
		var relative = transformed 
			? ApplyWorldTransform( position ) - WorldPosition
			: position;

		return new Vector3Int(
			(relative.x / Scale).FloorToInt(),
			(relative.y / Scale).FloorToInt(),
			(relative.z / Scale).FloorToInt()
		);
	}

	/// <summary>
	/// Converts a VoxelVolume position to a world position.
	/// </summary>
	/// <param name="position"></param>
	/// <param name="transformed"></param>
	/// <returns></returns>
	public Vector3 VoxelToWorld( Vector3Int position, bool transformed = true )
	{
		if ( !transformed )
			return position * Scale;

		return position * Scale * WorldRotation * WorldScale + WorldPosition;
	}

	/// <summary>
	/// Converts global VoxelVolume coordinates to local, also outs the chunk.
	/// </summary>
	/// <param name="x"></param>
	/// <param name="y"></param>
	/// <param name="z"></param>
	/// <param name="chunk"></param>
	/// <param name="relative"></param>
	/// <returns>The position local to the chunk.</returns>
	public (byte x, byte y, byte z) GetLocalSpace( int x, int y, int z, out Chunk chunk, Chunk relative = null )
	{
		var position = new Vector3Int(
			((float)(x + (relative?.X ?? 0) * VoxelUtils.CHUNK_SIZE) / VoxelUtils.CHUNK_SIZE).FloorToInt(),
			((float)(y + (relative?.Y ?? 0) * VoxelUtils.CHUNK_SIZE) / VoxelUtils.CHUNK_SIZE).FloorToInt(),
			((float)(z + (relative?.Z ?? 0) * VoxelUtils.CHUNK_SIZE) / VoxelUtils.CHUNK_SIZE).FloorToInt()
		);

		chunk = null;
		_ = Chunks?.TryGetValue( position, out chunk );

		return (
			(byte)((x % VoxelUtils.CHUNK_SIZE + VoxelUtils.CHUNK_SIZE) % VoxelUtils.CHUNK_SIZE),
			(byte)((y % VoxelUtils.CHUNK_SIZE + VoxelUtils.CHUNK_SIZE) % VoxelUtils.CHUNK_SIZE),
			(byte)((z % VoxelUtils.CHUNK_SIZE + VoxelUtils.CHUNK_SIZE) % VoxelUtils.CHUNK_SIZE)
		);
	}

	/// <summary>
	/// Converts local coordinates of a chunk to global coordinates in a VoxelWorld.
	/// <para>NOTE: The position range expected is 0-15.</para>
	/// </summary>
	/// <param name="x"></param>
	/// <param name="y"></param>
	/// <param name="z"></param>
	/// <param name="relative"></param>
	/// <returns></returns>
	public static Vector3Int GetGlobalSpace( byte x, byte y, byte z, Chunk relative )
	{
		return new Vector3Int(
			x + (relative?.X ?? 0) * VoxelUtils.CHUNK_SIZE,
			y + (relative?.Y ?? 0) * VoxelUtils.CHUNK_SIZE,
			z + (relative?.Z ?? 0) * VoxelUtils.CHUNK_SIZE
		);
	}

	/// <summary>
	/// Gets VoxelQueryData by offset relative to a chunk, or Chunks[0, 0, 0].
	/// </summary>
	/// <param name="x"></param>
	/// <param name="y"></param>
	/// <param name="z"></param>
	/// <param name="relative"></param>
	/// <returns></returns>
	public VoxelQueryData Query( int x, int y, int z, Chunk relative = null )
	{
		// Get the new chunk's position based on the offset.
		var position = new Vector3Int(
			(relative?.X ?? 0) + ((x + 1) / (float)VoxelUtils.CHUNK_SIZE - 1).CeilToInt(),
			(relative?.Y ?? 0) + ((y + 1) / (float)VoxelUtils.CHUNK_SIZE - 1).CeilToInt(),
			(relative?.Z ?? 0) + ((z + 1) / (float)VoxelUtils.CHUNK_SIZE - 1).CeilToInt()
		);

		// Calculate new voxel position.
		var chunk = (Chunk)null;
		_ = Chunks?.TryGetValue( position, out chunk );

		var vx = (byte)((x % VoxelUtils.CHUNK_SIZE + VoxelUtils.CHUNK_SIZE) % VoxelUtils.CHUNK_SIZE);
		var vy = (byte)((y % VoxelUtils.CHUNK_SIZE + VoxelUtils.CHUNK_SIZE) % VoxelUtils.CHUNK_SIZE);
		var vz = (byte)((z % VoxelUtils.CHUNK_SIZE + VoxelUtils.CHUNK_SIZE) % VoxelUtils.CHUNK_SIZE);

		var voxel = chunk?.GetVoxel( vx, vy, vz );
		return new VoxelQueryData
		{
			Chunk = chunk,
			Voxel = voxel ?? default,
			HasVoxel = IsValidVoxel( voxel ?? default ),
			LocalPosition = (vx, vy, vz),
			GlobalPosition = new Vector3Int(
				x + (relative?.X ?? 0) * (VoxelUtils.CHUNK_SIZE - 1), 
				y + (relative?.Y ?? 0) * (VoxelUtils.CHUNK_SIZE - 1), 
				z + (relative?.Z ?? 0) * (VoxelUtils.CHUNK_SIZE - 1) 
			)
		};
	}

	/// <inheritdoc cref="Query( int, int, int, Chunk )"/>
	public VoxelQueryData Query( Vector3Int offset, Chunk relative = null )
		=> Query( offset.x, offset.y, offset.z, relative );
}