Code/ShrimpleRagdoll.PartialRagdoll.cs
namespace ShrimpleRagdolls;

public partial class ShrimpleRagdoll
{
	/// <summary>
	/// Bones that currently have a partial ragdoll override
	/// </summary>
	public IReadOnlyDictionary<int, RagdollMode> PartialRagdollOverrides => _partialRagdollOverrides;
	private readonly Dictionary<int, RagdollMode> _partialRagdollOverrides = new();

	/// <summary>
	/// Ragdolls a single bone and optionally all its children
	/// </summary>
	/// <param name="rootBone">The target bone</param>
	/// <param name="mode">Which mode to set the bone (Only works for Enabled and Motor)</param>
	/// <param name="includeChildren">Include all children of the target bone</param>
	public void RagdollBone( BoneCollection.Bone rootBone, RagdollMode mode = RagdollMode.Enabled, bool includeChildren = true )
	{
		if ( Mode == RagdollMode.None )
			return;

		if ( !Renderer.IsValid() || !Renderer.Model.IsValid() || rootBone == null )
			return;

		var bones = includeChildren ? GetDescendantBones( rootBone ) : new[] { rootBone };
		var indices = bones.Select( b => b.Index ).ToHashSet();

		foreach ( var body in Bodies )
		{
			if ( !indices.Contains( body.Bone ) || !body.Component.IsValid() )
				continue;

			foreach ( var collider in body.Component.GameObject.GetComponents<Collider>() )
				collider.Enabled = true;

			body.Component.Enabled = true;
			body.Component.MotionEnabled = true;
			body.Component.Gravity = Gravity; // We reset this for active mode so reenable it
		}

		// Gotta enable joints last otherwise they error out
		foreach ( var joint in GetJointsForBones( indices ) )
		{
			joint.Component.Enabled = true;

			if ( joint.Component is BallJoint ballJoint )
			{
				ballJoint.Motor = BallJoint.MotorMode.Disabled;
				ballJoint.Frequency = 0f;
			}
			else if ( joint.Component is HingeJoint hingeJoint )
			{
				hingeJoint.Motor = HingeJoint.MotorMode.Disabled;
				hingeJoint.Frequency = 0f;
			}
		}

		foreach ( var index in indices )
			_partialRagdollOverrides[index] = mode;
	}

	public void RagdollBone( string boneName, RagdollMode mode = RagdollMode.Enabled, bool includeChildren = true )
		=> RagdollBone( Renderer?.Model?.Bones?.GetBone( boneName ), mode, includeChildren );

	public void RagdollBone( int boneIndex, RagdollMode mode = RagdollMode.Enabled, bool includeChildren = true )
		=> RagdollBone( Renderer?.Model?.Bones?.AllBones[boneIndex], mode, includeChildren );

	/// <summary>
	/// Unragdoll the bone optionally all its children
	/// </summary>
	public void UnragdollBone( BoneCollection.Bone rootBone, bool includeChildren = true )
	{
		if ( !Renderer.IsValid() || !Renderer.Model.IsValid() || rootBone == null )
			return;

		var bones = includeChildren ? GetDescendantBones( rootBone ) : new[] { rootBone };
		var indices = bones.Select( b => b.Index ).ToHashSet();

		var modeWantsRigidbodies = Mode != RagdollMode.None;
		var modeWantsMotion = Mode == RagdollMode.Enabled || Mode == RagdollMode.Active || Mode == RagdollMode.Motor;
		var modeWantsJoints = Mode != RagdollMode.None && Mode != RagdollMode.Passive;
		var modeWantsGravity = Mode != RagdollMode.Active && Gravity;

		// Gotta disable joints first otherwise they error out
		foreach ( var joint in GetJointsForBones( indices ) )
		{
			if ( joint.Component.IsValid() )
				joint.Component.Enabled = modeWantsJoints;
		}

		foreach ( var body in Bodies )
		{
			if ( !indices.Contains( body.Bone ) || !body.Component.IsValid() )
				continue;

			body.Component.MotionEnabled = modeWantsMotion;
			body.Component.Enabled = modeWantsRigidbodies;
			body.Component.Gravity = modeWantsGravity;

			foreach ( var collider in body.Component.GameObject.GetComponents<Collider>() )
				collider.Enabled = modeWantsRigidbodies;
		}

		foreach ( var index in indices )
			_partialRagdollOverrides.Remove( index );
	}

	public void UnragdollBone( string boneName, bool includeChildren = true )
		=> UnragdollBone( Renderer?.Model?.Bones?.GetBone( boneName ), includeChildren );

	public void UnragdollBone( int boneIndex, bool includeChildren = true )
		=> UnragdollBone( Renderer?.Model?.Bones?.AllBones[boneIndex], includeChildren );

	/// <summary>
	/// Clear all partial ragdoll overrides, returning full control to the global mode.
	/// </summary>
	public void ClearPartialRagdoll() => _partialRagdollOverrides.Clear();

	/// <summary>
	/// Bone overrides to apply on start and whenever this property is changed.
	/// Key is the bone name, value is the ragdoll mode.
	/// </summary>
	[Advanced, Property, Group( "Partial Ragdoll" )]
	public Dictionary<string, RagdollMode> PartialRagdollConfig
	{
		get;
		set
		{
			field = value;
			ApplyPartialRagdollConfig();
		}
	} = new();

	private void ApplyPartialRagdollConfig()
	{
		ClearPartialRagdoll();

		if ( PartialRagdollConfig == null )
			return;

		foreach ( var (boneName, mode) in PartialRagdollConfig )
			RagdollBone( boneName, mode );
	}

	public bool IsBonePartiallyRagdolled( BoneCollection.Bone bone )
		=> bone != null && _partialRagdollOverrides.ContainsKey( bone.Index );

	public bool IsBonePartiallyRagdolled( string boneName )
		=> IsBonePartiallyRagdolled( Renderer?.Model?.Bones?.GetBone( boneName ) );

	public bool IsBonePartiallyRagdolled( int boneIndex )
		=> _partialRagdollOverrides.ContainsKey( boneIndex );

	public RagdollMode? GetBoneOverrideMode( BoneCollection.Bone bone )
		=> bone != null && _partialRagdollOverrides.TryGetValue( bone.Index, out var m ) ? m : null;

	private IEnumerable<ModelPhysics.Joint> GetJointsForBones( HashSet<int> boneIndices )
		=> Joints?.Where( j => j.Component.IsValid() && boneIndices.Contains( j.Body2.Bone ) )
		   ?? Enumerable.Empty<ModelPhysics.Joint>();
}