Code/Binder/Target.cs
using System;

namespace Sandbox.MovieMaker;

#nullable enable

/// <summary>
/// <para>
/// Something in the scene that is being controlled by an <see cref="ITrack"/>.
/// This could be a <see cref="GameObject"/> or <see cref="Component"/> reference, or a property contained
/// within another <see cref="ITrackTarget"/>.
/// </para>
/// <para>
/// These targets are created using <see cref="TrackBinder.Get(ITrack)"/>.
/// </para>
/// <para>
/// If <see cref="IsBound"/> is true, this target is connected to a live instance of something in the scene,
/// so accessing it will affect that connected instance.
/// </para>
/// </summary>
public interface ITrackTarget
{
	/// <summary>
	/// Name of this target, for debugging and editing.
	/// </summary>
	string Name { get; }

	/// <summary>
	/// Value type of this target.
	/// </summary>
	Type TargetType { get; }

	/// <summary>
	/// If true, this target is connected to a real object in the scene, so can be accessed.
	/// </summary>
	bool IsBound { get; }

	/// <summary>
	/// If true, the target is bound and active in the scene hierarchy.
	/// </summary>
	bool IsActive { get; }

	/// <summary>
	/// If bound, the current value of this target in the scene.
	/// </summary>
	object? Value { get; }

	/// <summary>
	/// Component / game object / property that contains this target, if from a nested track.
	/// </summary>
	ITrackTarget? Parent { get; }
}

/// <inheritdoc cref="ITrackTarget"/>
/// <typeparam name="T">Target value type.</typeparam>
public interface ITrackTarget<out T> : ITrackTarget
{
	/// <inheritdoc cref="ITrackTarget.Value"/>
	new T Value { get; }

	Type ITrackTarget.TargetType => typeof(T);
	object? ITrackTarget.Value => Value;
}

/// <summary>
/// A target referencing a <see cref="GameObject"/> or <see cref="Component"/> in the scene.
/// </summary>
public interface ITrackReference : ITrackTarget
{
	/// <summary>
	/// The <see cref="IReferenceTrack.Id"/> of the reference track this target was created from.
	/// </summary>
	Guid Id { get; }

	/// <summary>
	/// Optional game object target that contains this one, if from a nested track.
	/// </summary>
	new ITrackReference<GameObject>? Parent { get; }

	/// <summary>
	/// Explicitly this reference to a particular object in the scene, or null to force it to stay unbound.
	/// </summary>
	void Bind( IValid? value );

	/// <summary>
	/// Clear any explicit binding, so this reference will auto-bind based on its name, type, and parent.
	/// </summary>
	void Reset();

	ITrackTarget? ITrackTarget.Parent => Parent;
	bool ITrackTarget.IsBound => (Value as IValid).IsValid();
}

/// <inheritdoc cref="ITrackReference"/>
/// <typeparam name="T">Reference value type.</typeparam>
public interface ITrackReference<T> : ITrackReference, ITrackTarget<T?>
	where T : class, IValid
{
	/// <inheritdoc cref="ITrackReference.Bind"/>
	void Bind( T? value );

	void ITrackReference.Bind( IValid? value ) => Bind( (T?)value );
}

/// <summary>
/// A target referencing a member property or field of another target.
/// </summary>
public partial interface ITrackProperty : ITrackTarget
{
	/// <summary>
	/// Target that this member belongs to.
	/// </summary>
	new ITrackTarget Parent { get; }

	/// <summary>
	/// False if this member is readonly.
	/// </summary>
	bool CanWrite => true;

	/// <summary>
	/// If bound, gets or sets the current value of this member.
	/// </summary>
	new object? Value { get; set; }

	/// <summary>
	/// If bound and writable, update this property's value from the
	/// given <paramref name="track"/> at the given <paramref name="time"/>.
	/// </summary>
	bool Update( IPropertyTrack track, MovieTime time );

	bool ITrackTarget.IsBound => Parent.IsBound;
	bool ITrackTarget.IsActive => Parent.IsActive;
	ITrackTarget ITrackTarget.Parent => Parent;
}

/// <inheritdoc cref="ITrackProperty"/>
/// <typeparam name="T">Property value type.</typeparam>
public partial interface ITrackProperty<T> : ITrackProperty, ITrackTarget<T>
{
	/// <inheritdoc cref="ITrackProperty.Value"/>
	new T Value { get; set; }

	/// <inheritdoc cref="ITrackProperty.Update"/>
	public bool Update( IPropertyTrack<T> track, MovieTime time )
	{
		if ( !IsBound || !CanWrite ) return false;
		if ( !track.TryGetValue( time, out var value ) ) return false;

		Value = value;

		return true;
	}

	T ITrackTarget<T>.Value => Value;

	bool ITrackProperty.Update( IPropertyTrack track, MovieTime time ) =>
		track is IPropertyTrack<T> typedTrack && Update( typedTrack, time );

	object? ITrackProperty.Value
	{
		get => Value;
		set => Value = (T)value!;
	}
}