Editor/MovieMaker/BlockDisplay/CurveBlockItem.cs
using System.Linq;

namespace Editor.MovieMaker.BlockDisplays;

#nullable enable

public interface ICurveBlockItem
{
	public IReadOnlyList<Element> Elements { get; }
	public IReadOnlyList<(float Min, float Max)> Ranges { get; }
}

public record struct Element( string Name, Color Color, float? Min = null, float? Max = null );

public abstract partial class CurveBlockItem<T> : PropertyBlockItem<T>, ICurveBlockItem
{
	private readonly (float Min, float Max)[] _ranges;

	private int _lastRangeDataHash;
	private int _lastPaintHash;

	public IReadOnlyList<Element> Elements { get; }

	public IReadOnlyList<(float Min, float Max)> Ranges
	{
		get
		{
			var dataHash = DataHash;

			if ( dataHash != _lastRangeDataHash )
			{
				_lastRangeDataHash = dataHash;
				UpdateRanges();
			}

			return _ranges;
		}
	}

	protected int PaintHash
	{
		get
		{
			var hash = new HashCode();
			var elementCount = Elements.Count;

			hash.Add( DataHash );

			foreach ( var preview in Parent.BlockItems )
			{
				if ( preview is not ICurveBlockItem { Ranges: { } curveRanges } ) continue;
				if ( curveRanges.Count != elementCount ) continue;

				foreach ( var (min, max) in curveRanges )
				{
					hash.Add( min );
					hash.Add( max );
				}
			}

			return hash.ToHashCode();
		}
	}

	protected CurveBlockItem( params Element[] elements )
	{
		Elements = elements;

		_ranges = new (float, float)[elements.Length];
	}

	protected abstract void Decompose( T value, Span<float> result );
}

#region Scalars

public sealed class BooleanBlockItem() : CurveBlockItem<bool>(
	new Element( "X", Color.White, 0f, 1f ) )
{
	protected override void Decompose( bool value, Span<float> result )
	{
		result[0] = value ? 1f : 0f;
	}
}

public sealed class FloatBlockItem() : CurveBlockItem<float>(
	new Element( "X", Color.White ) )
{
	protected override void Decompose( float value, Span<float> result )
	{
		result[0] = value;
	}
}

#endregion

#region Vectors

public sealed class Vector2BlockItem() : CurveBlockItem<Vector2>(
	new Element( "X", Theme.Red ),
	new Element( "Y", Theme.Green ) )
{
	protected override void Decompose( Vector2 value, Span<float> result )
	{
		result[0] = value.x;
		result[1] = value.y;
	}
}

public sealed class Vector3BlockItem() : CurveBlockItem<Vector3>(
	new Element( "X", Theme.Red ),
	new Element( "Y", Theme.Green ),
	new Element( "Z", Theme.Blue ) )
{
	protected override void Decompose( Vector3 value, Span<float> result )
	{
		result[0] = value.x;
		result[1] = value.y;
		result[2] = value.z;
	}
}

public sealed class Vector4BlockItem() : CurveBlockItem<Vector4>(
	new Element( "X", Theme.Red ),
	new Element( "Y", Theme.Green ),
	new Element( "Z", Theme.Blue ),
	new Element( "W", Color.White ) )
{
	protected override void Decompose( Vector4 value, Span<float> result )
	{
		result[0] = value.x;
		result[1] = value.y;
		result[2] = value.z;
		result[3] = value.w;
	}
}

#endregion Vectors

#region Rotation

public sealed class AnglesBlockItem() : CurveBlockItem<Angles>(
	new Element( "P", Theme.Red, -180f, 180f ),
	new Element( "Y", Theme.Green, -180f, 180f ),
	new Element( "R", Theme.Blue, -180f, 180f ) )
{
	protected override void Decompose( Angles value, Span<float> result )
	{
		result[0] = value.pitch;
		result[1] = value.yaw;
		result[2] = value.roll;
	}
}

public sealed class RotationBlockItem() : CurveBlockItem<Rotation>(
	new Element( "X", Theme.Red, -1f, 1f ),
	new Element( "Y", Theme.Green, -1f, 1f ),
	new Element( "Z", Theme.Blue, -1f, 1f ),
	new Element( "W", Color.White, -1f, 1f ) )
{
	protected override void Decompose( Rotation value, Span<float> result )
	{
		// Decompose it as the forward vector + how much the right vector is pointing up,
		// because that looks nice and smooth

		var forward = value.Forward;
		var right = value.Right;

		result[0] = forward.x;
		result[1] = forward.y;
		result[2] = forward.z;
		result[3] = right.z;
	}
}

#endregion

#region Color

public sealed class ColorBlockItem() : CurveBlockItem<Color>(
	new Element( "R", Color.Red, 0f, 1f ),
	new Element( "G", Color.Green, 0f, 1f ),
	new Element( "B", Color.Blue, 0f, 1f ),
	new Element( "A", Color.White, 0f, 1f ) )
{
	protected override void Decompose( Color value, Span<float> result )
	{
		result[0] = value.r;
		result[1] = value.g;
		result[2] = value.b;
		result[3] = value.a;
	}
}

#endregion