Code/RigControl/RigTrackName.cs
namespace Marionette;

/// <summary>
/// How a track names the thing it drives when a clip animates more than one object.
///
/// A CLIP IS A SCENE, NOT A MODEL. A reload is the arms, the weapon and the magazine; opening a
/// fridge is the hand and the door. Each of those can carry its own skeleton, and two of them can
/// each have a bone called "root". So a track's name is the object it belongs to plus the bone
/// inside it, joined by a slash: "magazine/latch".
///
/// THE MAIN MODEL'S BONES KEEP THEIR BARE NAMES - it is the object with no name, so "hand_R" is
/// still "hand_R". That is not tidiness: every .riganim ever saved names its bones that way, and a
/// scheme that qualified them would read every existing clip as animating bones that don't exist.
///
/// Slash because a bone name cannot contain one (they come from the model's skeleton, which is
/// compiled from a DMX/FBX node name) and because it is the separator every path-shaped name in
/// the engine already uses, so it reads as a path without being explained.
/// </summary>
public static class RigTrackName
{
	public const char Separator = '/';

	/// <summary>The main model - the object a clip has always had. Empty rather than a word like
	/// "root", so it can never collide with a prop somebody names.</summary>
	public const string RootSubject = "";

	/// <summary>Joins an object and a bone into the name a track is stored under.</summary>
	public static string Qualify( string subject, string bone ) =>
		string.IsNullOrEmpty( subject ) ? bone : $"{subject}{Separator}{bone}";

	/// <summary>Splits a track name back into the object and the bone. A bare name is the main
	/// model's, which is what every pre-existing clip contains.</summary>
	public static (string Subject, string Bone) Split( string name )
	{
		if ( string.IsNullOrEmpty( name ) )
			return (RootSubject, "");

		var slash = name.IndexOf( Separator );

		return slash < 0
			? (RootSubject, name)
			: (name[..slash], name[(slash + 1)..]);
	}

	/// <summary>The object a track belongs to - empty for the main model.</summary>
	public static string SubjectOf( string name ) => Split( name ).Subject;

	/// <summary>The bone alone, for anything that has to match the model's own skeleton.</summary>
	public static string BoneOf( string name ) => Split( name ).Bone;

	/// <summary>
	/// What to show a person: the last segment, with everything above it in brackets.
	///
	/// SPLIT AT THE LAST SEPARATOR, not the first, which is the one place these two differ. A name
	/// can be three deep - "door/handle/screw" is a bone inside a part of an object - and the
	/// useful half of that is the leaf. Split answers "which object owns this", which is the first
	/// segment; Display answers "what is this called", which is the last.
	///
	/// A bare name is left exactly as it is, so the common case reads no differently than before.
	/// </summary>
	public static string Display( string name )
	{
		if ( string.IsNullOrEmpty( name ) )
			return "";

		var slash = name.LastIndexOf( Separator );

		return slash < 0 ? name : $"{name[(slash + 1)..]} ({name[..slash]})";
	}
}