Editor/MovieMaker/Project/Extensions.cs
using Sandbox.MovieMaker;
using System.Linq;
using Sandbox.MovieMaker.Compiled;

namespace Editor.MovieMaker;

#nullable enable

/// <summary>
/// Helper methods for working with <see cref="MovieProject"/>, <see cref="ProjectTrack"/>, or <see cref="ProjectBlock"/>.
/// </summary>
public static class ProjectExtensions
{
	public static MovieProject FromCompiled( this MovieClip clip ) =>
		new MovieProject( clip );

	public static IEnumerable<IProjectPropertyBlock> GetBlocks( this IProjectPropertyTrack track, MovieTimeRange timeRange )
	{
		return track.Blocks.Where( x => x.TimeRange.Intersect( timeRange ) is not null );
	}

	public static IEnumerable<PropertyBlock<T>> GetBlocks<T>( this ProjectPropertyTrack<T> track, MovieTimeRange timeRange )
	{
		return track.Blocks.Where( x => x.TimeRange.Intersect( timeRange ) is not null );
	}

	public static T GetValue<T>( this IReadOnlyList<IPropertyBlock<T>> blocks, MovieTime time )
	{
		return blocks.GetLastBlock( time ).GetValue( time );
	}

	public static T GetLastBlock<T>( this IReadOnlyList<T> blocks, MovieTime time )
		where T : ITrackBlock
	{
		if ( blocks.Count == 0 ) throw new ArgumentException( "Expected at least one block.", nameof( blocks ) );

		if ( time <= blocks[0].TimeRange.Start ) return blocks[0];
		if ( time >= blocks[^1].TimeRange.End ) return blocks[^1];

		// TODO: binary search?

		// We go backwards because if we're exactly on a block boundary, we want to use the later block

		for ( var i = blocks.Count - 1; i >= 0; --i )
		{
			var block = blocks[i];

			if ( block.TimeRange.Start > time ) continue;

			return block;
		}

		return blocks[0];
	}

	public static T? GetBlock<T>( this IReadOnlyList<T> blocks, MovieTime time )
		where T : ITrackBlock
	{
		if ( blocks.Count == 0 ) return default;

		var block = blocks.GetLastBlock( time );

		return block.TimeRange.Contains( time ) ? block : default;
	}
}