code/Library/VoxelVolume/VoxelVolume.Updates.cs
namespace Boxfish.Library;

partial class VoxelVolume
{
	/// <summary>
	/// The frequency of chunk mesh updates, should be somewhere around 1 / 30f (used for <see cref="TickUpdate"/>).
	/// <para>NOTE: Set to 0 to completely ignore updates.</para>
	/// </summary>
	public virtual float UpdateFrequency { get; } = 1 / 30f;

	/// <summary>
	/// The time passed since <see cref="TickUpdate"/> has been called.
	/// </summary>
	protected TimeSince SinceUpdate { get; private set; }

	/// <summary>
	/// Which chunks have been tracked to change.
	/// </summary>
	protected readonly Dictionary<Chunk, bool> TrackedChanges = new();

	/// <summary>
	/// Track a voxel change, we will go through these in <see cref="TickUpdate"/>.
	/// </summary>
	/// <param name="chunk"></param>
	/// <param name="changed"></param>
	/// <param name="offset"></param>
	protected void TrackChange( Chunk chunk, bool changed, (byte x, byte y, byte z)? offset = null )
	{
		// Updates are disabled.
		if ( UpdateFrequency == 0 ) return;

		if ( chunk == null )
			return;

		// Add tracked chunk.
		if ( TrackedChanges.ContainsKey( chunk ) )
		{
			TrackedChanges[chunk] |= changed;
		}
		else
		{
			TrackedChanges.Add( chunk, changed );
		}

		// Add neighbors to rebuild visual mesh.
		if ( offset.HasValue )
		{
			var pos = offset.Value;
			var neighbors = chunk.GetNeighbors( pos.x, pos.y, pos.z, false );
			foreach ( var neighbor in neighbors )
			{
				if ( neighbor == null )
					continue;

				TrackedChanges.TryAdd( neighbor, false );
			}

			return;
		}
	}

	/// <summary>
	/// Set a voxel and track the change, this will automatically regenerate chunks if you have ticked updates enabled.
	/// </summary>
	/// <param name="position"></param>
	/// <param name="voxel"></param>
	/// <param name="relative"></param>
	public virtual void SetTrackedVoxel( Vector3Int position, Voxel voxel, Chunk relative = null )
	{
		// Get local position to a chunk, set the voxel.
		var pos = GetLocalSpace( position.x, position.y, position.z, out var chunk, relative );
		if ( chunk is null )
		{
			if ( !voxel.Valid )
				return;

			chunk = GetOrCreateChunk( position, pos, relative );
		}

		if ( chunk is null )
		{
			Log.Warning( $"Failed to GetOrCreate chunk, this shouldn't happen." );
			return;
		}

		var previous = chunk.GetVoxel( pos.x, pos.y, pos.z );
		chunk.SetVoxel( pos.x, pos.y, pos.z, voxel );

		// Track change.
		var prevValid = previous.Valid;
		var newValid = voxel.Valid;
		var changed = (!prevValid && newValid) || (prevValid && !newValid);
		TrackChange( chunk, changed, pos );
	}

	/// <inheritdoc cref="SetTrackedVoxel(Vector3Int, Voxel, BaseVoxelVolume{Voxel, VoxelVertex}.Chunk)"/>
	public void SetTrackedVoxel( int x, int y, int z, Voxel voxel, Chunk relative = null )
	{
		var position = new Vector3Int( x, y, z );
		SetTrackedVoxel( position, voxel, relative );
	}

	/// <summary>
	/// Tick update the changed chunks.
	/// <para>NOTE: This will update the chunks directly, you shouldn't call this multiple times a frame.</para>
	/// </summary>
	public void TickUpdate()
	{
		if ( TrackedChanges.Any() )
		{
			var chunks = new KeyValuePair<Chunk, bool>[TrackedChanges.Count];
			(TrackedChanges as ICollection<KeyValuePair<Chunk, bool>>).CopyTo( chunks, 0 );
			TrackedChanges.Clear();

			_ = Task.RunInThreadAsync( () => _ = GenerateMeshes( chunks ) );
			SinceUpdate = 0f;
		}
	}

	protected override void OnUpdate()
	{
		base.OnUpdate();

		// Tick updates.
		if ( SinceUpdate >= UpdateFrequency )
			TickUpdate();
	}
}