namespace Sandbox;

/// <summary>
/// Physics for a model. This is primarily used for ragdolls and other physics driven models, otherwise you should be using a Rigidbody.
/// </summary>
[Expose]
[Title( "Model Physics" )]
[Category( "Physics" )]
[Icon( "check_box_outline_blank" )]
public sealed partial class ModelPhysics : Component, IScenePhysicsEvents, IHasModel
{
	[Obsolete( "No longer in use" )]
	public PhysicsGroup PhysicsGroup { get; private set; }

	private Model _model;
	private SkinnedModelRenderer _renderer;
	private RigidbodyFlags _rigidBodyFlags;
	private PhysicsLock _locking;
	private bool _motionEnabled = true;
	private bool _onEnabled = false;

	[Property, Hide]
	public bool PhysicsWereCreated { get; set; }

	/// <summary>
	/// Number of times the physics have been rebuilt. Debugging use only.
	/// </summary>
	internal int PhysicsRebuildCount { get; set; }

	/// <summary>
	/// Number of times the physics have been rebuilt. Debugging use only.
	/// </summary>
	internal int PhysicsDestroyCount { get; set; }

	/// <summary>
	/// The model used to generate physics bodies, collision shapes, and joints.
	/// </summary>
	[Property]
	public Model Model
	{
		get => _model;
		set
		{
			if ( _model == value )
				return;

			_model = value;

			OnModelChanged();
		}
	}

	/// <summary>
	/// The renderer that receives transform updates from physics bodies.
	/// </summary>
	[Property]
	public SkinnedModelRenderer Renderer
	{
		get => _renderer;
		set
		{
			if ( _renderer == value )
				return;

			if ( _renderer.IsValid() )
			{
				_renderer.ClearPhysicsBones();
			}

			_renderer = value;
		}
	}

	/// <summary>
	/// If true, the root physics body will not drive this component's transform.
	/// </summary>
	[Property]
	public bool IgnoreRoot { get; set; }

	/// <summary>
	/// Rigidbody flags applied to all bodies.
	/// </summary>
	[Property, Group( "Physics" )]
	public RigidbodyFlags RigidbodyFlags
	{
		get => _rigidBodyFlags;
		set
		{
			if ( _rigidBodyFlags == value )
				return;

			_rigidBodyFlags = value;

			foreach ( var body in Bodies )
			{
				if ( !body.Component.IsValid() )
					continue;

				body.Component.RigidbodyFlags = value;
			}
		}
	}

	/// <summary>
	/// Rigidbody locking applied to all bodies.
	/// </summary>
	[Property, Group( "Physics" )]
	public PhysicsLock Locking
	{
		get => _locking;
		set
		{
			_locking = value;

			foreach ( var body in Bodies )
			{
				if ( !body.Component.IsValid() )
					continue;

				body.Component.Locking = value;
			}
		}
	}

	/// <summary>
	/// All bodies will be put to sleep on start.
	/// </summary>
	[Property, Group( "Physics" )]
	public bool StartAsleep { get; set; }

	/// <summary>
	/// Enable to drive renderer from physics, disable to drive physics from renderer.
	/// </summary>
	[Property, Group( "Physics" )]
	public bool MotionEnabled
	{
		get => _motionEnabled;
		set
		{
			if ( _motionEnabled == value )
				return;

			_motionEnabled = value;

			foreach ( var body in Bodies )
			{
				if ( !body.Component.IsValid() )
					continue;

				body.Component.MotionEnabled = value;
			}
		}
	}

	/// <summary>
	/// Returns the total mass of every <see cref="Rigidbody"/>
	/// </summary>
	public float Mass => Bodies.Sum( x => x.Component?.Mass ?? default );

	/// <summary>
	/// Returns the local center of mass of every <see cref="Rigidbody"/>
	/// </summary>
	public Vector3 MassCenter
	{
		get
		{
			var mass = 0.0f;
			var center = Vector3.Zero;

			foreach ( var body in Bodies )
			{
				var rb = body.Component;
				if ( !rb.IsValid() ) continue;

				mass += rb.Mass;
				center += rb.Mass * rb.MassCenter;
			}

			return mass > 0.0f ? center / mass : Vector3.Zero;
		}
	}

	private void OnModelChanged()
	{
		if ( !_onEnabled ) return;
		if ( !Active ) return;
		if ( IsProxy ) return;
		if ( GameObject.IsDeserializing ) return;

		DestroyPhysics();
		CreatePhysics();
	}

	/// <summary>
	/// Apply body transforms to renderer bones.
	/// </summary>
	private void PositionRendererBonesFromPhysics()
	{
		var rootBody = GetComponentInChildren<Rigidbody>( true, true );

		if ( Scene.IsEditor )
		{
			if ( rootBody.IsValid() && rootBody.PhysicsBody.MotionEnabled )
			{
				WorldTransform = rootBody.WorldTransform;
			}
		}
		else if ( !IgnoreRoot && rootBody.IsValid() && rootBody.MotionEnabled )
		{
			WorldTransform = rootBody.WorldTransform;
		}

		if ( !Renderer.IsValid() )
			return;

		var so = Renderer.SceneModel;
		if ( !so.IsValid() )
			return;

		// Clear all bone overrides first.
		Renderer.ClearPhysicsBones();

		var world = WorldTransform;

		foreach ( var body in Bodies )
		{
			var rb = body.Component;
			if ( !rb.IsValid() )
				continue;

			Transform local;

			// Only drive physics from animation if we have motion disabled.
			if ( !MotionEnabled && !rb.MotionEnabled )
			{
				// Use the animation pose when body motion is disabled.
				local = so.Transform.ToLocal( so.GetWorldSpaceAnimationTransform( body.Bone ) );
				so.SetBoneOverride( body.Bone, local );

				// Move object to animation pose, physics object will move towards it in pre physics step.
				if ( rb.Transform.SetLocalTransformFast( world.ToWorld( local ) ) )
					rb.Transform.TransformChanged( true );
			}
			else
			{
				// Bone overrides are in modelspace, strip off our world transform from body world transform.
				local = world.ToLocal( rb.WorldTransform );
				so.SetBoneOverride( body.Bone, local );
			}
		}
	}

	/// <summary>
	/// Smooth move bodies to animated bone transforms for bodies that have motion disabled.
	/// </summary>
	private void MovePhysicsFromAnimation()
	{
		if ( Scene.IsEditor )
			return;

		if ( !Renderer.IsValid() )
			return;

		var so = Renderer.SceneModel;
		if ( !so.IsValid() )
			return;

		foreach ( var body in Bodies )
		{
			if ( !body.Component.IsValid() )
				continue;

			if ( body.Component.MotionEnabled )
				continue;

			if ( !body.Component.PhysicsBody.IsValid() )
				continue;

			// Move kinematic body to object transform.
			body.Component.PhysicsBody.Move( body.Component.WorldTransform, Time.Delta );
		}
	}

	/// <summary>
	/// Adjust joint points for body scaling.
	/// </summary>
	private void UpdateJointScale()
	{
		foreach ( var joint in Joints )
		{
			if ( !joint.Component.IsValid() )
				continue;

			var point1 = joint.Component.Point1;
			point1.LocalPosition = joint.LocalFrame1.Position * joint.Component.WorldTransform.UniformScale;
			joint.Component.Point1 = point1;

			var point2 = joint.Component.Point2;
			point2.LocalPosition = joint.LocalFrame2.Position * joint.Component.Body.WorldTransform.UniformScale;
			joint.Component.Point2 = point2;
		}
	}

	/// <summary>
	/// Put all bodies to sleep.
	/// </summary>
	private void Sleep()
	{
		foreach ( var body in Bodies )
		{
			if ( !body.Component.IsValid() )
				continue;

			body.Component.Sleeping = true;
		}
	}

	protected override void OnAwake()
	{
		_renderer ??= GetComponent<SkinnedModelRenderer>();

		// Auto set the model using the renderer model if it's not already set.
		if ( _model is null && _renderer.IsValid() )
		{
			_model = _renderer.Model;
		}
	}

	protected override void OnStart()
	{
		if ( StartAsleep )
		{
			Sleep();
		}
	}

	protected override void OnUpdate()
	{
		// Proxy only needs to sync renderer to physics.
		if ( IsProxy )
		{
			UpdateProxyTransforms();

			return;
		}

		PositionRendererBonesFromPhysics();

		// In editor we need to move the bodies with this transform while keeping the local pose.
		if ( Scene.IsEditor )
		{
			foreach ( var body in Bodies )
			{
				if ( !body.Component.IsValid() )
					continue;

				if ( body.Component.PhysicsBody.MotionEnabled )
				{
					// Update local transform pose when simulating physics in editor.
					// TODO
					//body.LocalTransform = WorldTransform.ToLocal( body.Component.WorldTransform );
				}
				else
				{
					// Move body when moving this gameobject in editor.
					body.Component.WorldTransform = WorldTransform.ToWorld( body.LocalTransform );
				}
			}
		}
	}

	protected override void OnFixedUpdate()
	{
		if ( IsProxy )
			return;

		UpdateJointScale();

	}

	void IScenePhysicsEvents.PrePhysicsStep()
	{
		if ( IsProxy )
		{
			// Move proxy bodies to their networked transform.
			MoveProxyBodies();
		}
		else if ( !MotionEnabled )
		{
			// Only drive physics from animation if we have motion disabled.
			MovePhysicsFromAnimation();
		}
	}

	void IScenePhysicsEvents.PostPhysicsStep()
	{
		if ( !IsProxy )
		{
			// Network body transforms.
			SetBodyTransforms();
		}
	}

	protected override void OnEnabled()
	{
		_onEnabled = true;

		CreatePhysics();
	}

	protected override void OnDisabled()
	{
		DestroyPhysics();

		_onEnabled = false;
	}

	protected override void OnDestroy()
	{
		DestroyPhysics();

		_onEnabled = false;
	}

	//
	// These are temporary holders so you can set the bone positions before the component is enabled
	// They are used once on initializing and then removed
	//
	Transform[] _rendererBonePosition;
	SkinnedModelRenderer.BoneVelocity[] _rendererBoneVelocity;

	private void PositionPhysicsFromRendererBones()
	{
		if ( _rendererBonePosition is null || _rendererBoneVelocity is null )
		{
			if ( Renderer is not null && Renderer.SceneModel.IsValid() )
			{
				_rendererBonePosition = Renderer.GetBoneTransforms( true );
				_rendererBoneVelocity = Renderer.GetBoneVelocities();
			}
		}

		if ( _rendererBonePosition is null || _rendererBoneVelocity is null )
			return;

		var boneObjects = Model.CreateBoneObjects( GameObject );

		foreach ( var pair in boneObjects )
		{
			if ( pair.Key is null )
				continue;

			if ( !pair.Value.IsValid() )
				continue;

			if ( _rendererBonePosition.Length <= pair.Key.Index )
				continue;

			if ( _rendererBoneVelocity.Length <= pair.Key.Index )
				continue;

			var tx = _rendererBonePosition[pair.Key.Index];
			var vl = _rendererBoneVelocity[pair.Key.Index];

			if ( pair.Value.GetComponent<Rigidbody>() is { } body )
			{
				body.WorldTransform = tx;
				body.Velocity = vl.Linear;
				body.AngularVelocity = vl.Angular;
			}
		}

		_rendererBoneVelocity = default;
		_rendererBonePosition = default;
	}

	/// <summary>
	/// Copy the bone positions and velocities from a different SkinnedModelRenderer
	/// </summary>
	public void CopyBonesFrom( SkinnedModelRenderer source, bool teleport )
	{
		if ( !source.IsValid() )
			return;

		_rendererBonePosition = source.GetBoneTransforms( true );
		_rendererBoneVelocity = source.GetBoneVelocities();

		PositionPhysicsFromRendererBones();
		PositionRendererBonesFromPhysics();
	}

	void IHasModel.OnModelReloaded()
	{
		OnModelReloaded();
	}
}
