namespace Fusion.Addons.KCC
{
	using System;
	using UnityEngine;
	// This file contains penetration solver.
	public partial class KCC
	{
		// PUBLIC METHODS
		[Obsolete("ResolvePenetration now works directly with KCCData.BasePosition and KCCData.TargetPosition. Please switch to ResolvePenetration(KCCOverlapInfo overlapInfo, KCCData data, int maxSteps, bool probeGrounding, bool resolveTriggers).")]
		public Vector3 ResolvePenetration(KCCOverlapInfo overlapInfo, KCCData data, Vector3 basePosition, Vector3 targetPosition, bool probeGrounding, int maxSteps, int resolverIterations, bool resolveTriggers)
		{
			data.BasePosition   = basePosition;
			data.TargetPosition = targetPosition;
			ResolvePenetration(overlapInfo, data, maxSteps, probeGrounding, resolveTriggers);
			return data.TargetPosition;
		}
		/// 
		/// Computes penetration vectors for given OverlapInfo and moves the KCC out of collision.
		/// It is safe to pass OverlapInfo with results from inflated capsule overlap. Penetration info is stored in overlap hits.
		/// 
		/// Info with overlap hits.
		/// KCCData instance. Results are stored here.
		/// How many times the penetration is calculated.
		/// Make extra ground check after depenetration.
		/// Whether the penetration should be checked against triggers.
		public void ResolvePenetration(KCCOverlapInfo overlapInfo, KCCData data, int maxSteps, bool probeGrounding, bool resolveTriggers)
		{
			if (_settings.SuppressConvexMeshColliders == true)
			{
				overlapInfo.ToggleConvexMeshColliders(false);
			}
			if (overlapInfo.ColliderHitCount == 1)
			{
				DepenetrateSingle(overlapInfo, data, probeGrounding);
			}
			else if (overlapInfo.ColliderHitCount > 1)
			{
				DepenetrateMultiple(overlapInfo, data, probeGrounding, maxSteps);
			}
			RecalculateGroundProperties(data);
			if (resolveTriggers == true)
			{
				for (int i = 0; i < overlapInfo.TriggerHitCount; ++i)
				{
					KCCOverlapHit hit = overlapInfo.TriggerHits[i];
					hit.Transform.GetPositionAndRotation(out hit.CachedPosition, out hit.CachedRotation);
					bool hasPenetration = Physics.ComputePenetration(_collider.Collider, data.TargetPosition, Quaternion.identity, hit.Collider, hit.CachedPosition, hit.CachedRotation, out Vector3 direction, out float distance);
					hit.IsWithinExtent = hasPenetration;
					hit.HasPenetration = hasPenetration;
					hit.MaxPenetration = default;
					hit.CollisionType  = hasPenetration == true ? ECollisionType.Trigger : ECollisionType.None;
					if (distance > hit.MaxPenetration)
					{
						hit.MaxPenetration = distance;
					}
				}
			}
			if (_settings.SuppressConvexMeshColliders == true)
			{
				overlapInfo.ToggleConvexMeshColliders(true);
			}
		}
		// PRIVATE METHODS
		private void DepenetrateSingle(KCCOverlapInfo overlapInfo, KCCData data, bool probeGrounding)
		{
			float minGroundDot = Mathf.Cos(Mathf.Clamp(data.MaxGroundAngle, 0.0f, 90.0f) * Mathf.Deg2Rad);
			KCCOverlapHit hit = overlapInfo.ColliderHits[0];
			hit.IsWithinExtent = default;
			hit.HasPenetration = default;
			hit.MaxPenetration = default;
			hit.UpDirectionDot = default;
			hit.Transform.GetPositionAndRotation(out hit.CachedPosition, out hit.CachedRotation);
			hit.HasPenetration = Physics.ComputePenetration(_collider.Collider, data.TargetPosition, Quaternion.identity, hit.Collider, hit.CachedPosition, hit.CachedRotation, out Vector3 direction, out float distance);
			if (hit.HasPenetration == true)
			{
				hit.IsWithinExtent = true;
				hit.MaxPenetration = distance;
				hit.UpDirectionDot = Vector3.Dot(direction, Vector3.up);
				float minWallDot = -Mathf.Cos(Mathf.Clamp(90.0f - data.MaxWallAngle, 0.0f, 90.0f) * Mathf.Deg2Rad);
				float minHangDot = -Mathf.Cos(Mathf.Clamp(90.0f - data.MaxHangAngle, 0.0f, 90.0f) * Mathf.Deg2Rad);
				if (hit.UpDirectionDot >= minGroundDot)
				{
					hit.CollisionType = ECollisionType.Ground;
					data.IsGrounded     = true;
					data.GroundNormal   = direction;
					data.GroundDistance = default;
					data.GroundAngle    = Vector3.Angle(direction, Vector3.up);
				}
				else if (hit.UpDirectionDot > -minWallDot)
				{
					hit.CollisionType = ECollisionType.Slope;
				}
				else if (hit.UpDirectionDot >= minWallDot)
				{
					hit.CollisionType = ECollisionType.Wall;
				}
				else if (hit.UpDirectionDot >= minHangDot)
				{
					hit.CollisionType = ECollisionType.Hang;
				}
				else
				{
					hit.CollisionType = ECollisionType.Top;
				}
				if (hit.UpDirectionDot > 0.0f && hit.UpDirectionDot < minGroundDot)
				{
					if (distance >= 0.000001f && data.DynamicVelocity.y <= 0.0f)
					{
						Vector3 positionDelta = data.TargetPosition - data.BasePosition;
						float movementDot = Vector3.Dot(positionDelta.OnlyXZ(), direction.OnlyXZ());
						if (movementDot < 0.0f)
						{
							KCCPhysicsUtility.ProjectVerticalPenetration(ref direction, ref distance);
						}
					}
				}
				data.TargetPosition += direction * distance;
			}
			if (data.IsGrounded == true)
			{
				data.GroundPosition = data.TargetPosition + (Vector3.up - data.GroundNormal) * _settings.Radius;
				data.GroundAngle    = Vector3.Angle(data.GroundNormal, Vector3.up);
			}
			if (data.IsGrounded == false && probeGrounding == true)
			{
				bool isGrounded = KCCPhysicsUtility.CheckGround(_collider.Collider, data.TargetPosition, hit.Collider, hit.CachedPosition, hit.CachedRotation, _settings.Radius, _settings.Height, _settings.Extent, minGroundDot, out Vector3 checkGroundPosition, out Vector3 checkGroundNormal, out float checkGroundDistance, out bool isWithinExtent);
				if (isGrounded == true)
				{
					data.IsGrounded     = true;
					data.GroundNormal   = checkGroundNormal;
					data.GroundPosition = checkGroundPosition;
					data.GroundDistance = checkGroundDistance;
					data.GroundAngle    = Vector3.Angle(checkGroundNormal, Vector3.up);
					hit.IsWithinExtent = true;
					hit.CollisionType  = ECollisionType.Ground;
				}
				else if (isWithinExtent == true)
				{
					hit.IsWithinExtent = true;
					if (hit.CollisionType == ECollisionType.None)
					{
						hit.CollisionType = ECollisionType.Slope;
					}
				}
			}
		}
		private void DepenetrateMultiple(KCCOverlapInfo overlapInfo, KCCData data, bool probeGrounding, int maxSteps)
		{
			if (maxSteps <= 0)
			{
				maxSteps = 1;
			}
			float   minGroundDot        = Mathf.Cos(Mathf.Clamp(data.MaxGroundAngle, 0.0f, 90.0f) * Mathf.Deg2Rad);
			float   minWallDot          = -Mathf.Cos(Mathf.Clamp(90.0f - data.MaxWallAngle, 0.0f, 90.0f) * Mathf.Deg2Rad);
			float   minHangDot          = -Mathf.Cos(Mathf.Clamp(90.0f - data.MaxHangAngle, 0.0f, 90.0f) * Mathf.Deg2Rad);
			float   maxGroundDot        = default;
			Vector3 maxGroundNormal     = default;
			Vector3 averageGroundNormal = default;
			Vector3 positionDelta       = data.TargetPosition - data.BasePosition;
			Vector3 positionDeltaXZ     = positionDelta.OnlyXZ();
			for (int i = 0; i < overlapInfo.ColliderHitCount; ++i)
			{
				KCCOverlapHit hit = overlapInfo.ColliderHits[i];
				hit.IsWithinExtent = default;
				hit.HasPenetration = default;
				hit.MaxPenetration = default;
				hit.UpDirectionDot = float.MinValue;
				hit.Transform.GetPositionAndRotation(out hit.CachedPosition, out hit.CachedRotation);
			}
			if (maxSteps > 1)
			{
				float minStepDistance = 0.001f;
				float targetDistance  = Vector3.Magnitude(positionDelta);
				if (targetDistance < maxSteps * minStepDistance)
				{
					maxSteps = Mathf.Max(1, (int)(targetDistance / minStepDistance));
				}
			}
			int remainingSteps = maxSteps;
			while (remainingSteps > 0)
			{
				--remainingSteps;
				_resolver.Reset();
				for (int i = 0; i < overlapInfo.ColliderHitCount; ++i)
				{
					KCCOverlapHit hit = overlapInfo.ColliderHits[i];
					bool hasPenetration = Physics.ComputePenetration(_collider.Collider, data.TargetPosition, Quaternion.identity, hit.Collider, hit.CachedPosition, hit.CachedRotation, out Vector3 direction, out float distance);
					if (hasPenetration == false)
						continue;
					hit.HasPenetration = true;
					hit.IsWithinExtent = true;
					if (distance > hit.MaxPenetration)
					{
						hit.MaxPenetration = distance;
					}
					float upDirectionDot = Vector3.Dot(direction, Vector3.up);
					if (upDirectionDot > hit.UpDirectionDot)
					{
						hit.UpDirectionDot = upDirectionDot;
						if (upDirectionDot >= minGroundDot)
						{
							hit.CollisionType = ECollisionType.Ground;
							data.IsGrounded = true;
							if (upDirectionDot >= maxGroundDot)
							{
								maxGroundDot    = upDirectionDot;
								maxGroundNormal = direction;
							}
							averageGroundNormal += direction * upDirectionDot;
						}
						else if (upDirectionDot > -minWallDot)
						{
							hit.CollisionType = ECollisionType.Slope;
						}
						else if (upDirectionDot >= minWallDot)
						{
							hit.CollisionType = ECollisionType.Wall;
						}
						else if (upDirectionDot >= minHangDot)
						{
							hit.CollisionType = ECollisionType.Hang;
						}
						else
						{
							hit.CollisionType = ECollisionType.Top;
						}
					}
					if (upDirectionDot > 0.0f && upDirectionDot < minGroundDot)
					{
						if (distance >= 0.000001f && data.DynamicVelocity.y <= 0.0f)
						{
							float movementDot = Vector3.Dot(positionDeltaXZ, direction.OnlyXZ());
							if (movementDot < 0.0f)
							{
								KCCPhysicsUtility.ProjectVerticalPenetration(ref direction, ref distance);
							}
						}
					}
					_resolver.AddCorrection(direction, distance);
				}
				if (_resolver.Count <= 0)
					break;
				int   resolverInterations = 8;
				float resolverMaxError    = 0.001f;
				if (remainingSteps == 0)
				{
					resolverInterations = 12;
					resolverMaxError    = 0.0001f;
				}
				Vector3 correction = _resolver.CalculateBest(resolverInterations, resolverMaxError);
				correction = Vector3.ClampMagnitude(correction, _settings.Radius);
				float correctionMultiplier = Mathf.Max(0.25f, 1.0f - remainingSteps * 0.25f);
				correction *= correctionMultiplier;
				data.TargetPosition += correction;
			}
			for (int i = 0; i < overlapInfo.ColliderHitCount; ++i)
			{
				KCCOverlapHit hit = overlapInfo.ColliderHits[i];
				if (hit.UpDirectionDot == float.MinValue)
				{
					hit.UpDirectionDot = default;
				}
			}
			if (data.IsGrounded == true)
			{
				data.GroundNormal = maxGroundNormal;
				averageGroundNormal.Normalize();
				float averageGroundNormalUpDot = Vector3.Dot(averageGroundNormal, Vector3.up);
				if (averageGroundNormalUpDot >= maxGroundDot)
				{
					data.GroundNormal = averageGroundNormal;
				}
				data.GroundPosition = data.TargetPosition + (Vector3.up - data.GroundNormal) * _settings.Radius;
				data.GroundDistance = default;
				data.GroundAngle    = Vector3.Angle(data.GroundNormal, Vector3.up);
			}
			if (data.IsGrounded == false && probeGrounding == true)
			{
				Vector3 closestGroundNormal   = Vector3.up;
				Vector3 closestGroundPosition = default;
				float   closestGroundDistance = 1000.0f;
				for (int i = 0; i < overlapInfo.ColliderHitCount; ++i)
				{
					KCCOverlapHit hit = overlapInfo.ColliderHits[i];
					bool isGrounded = KCCPhysicsUtility.CheckGround(_collider.Collider, data.TargetPosition, hit.Collider, hit.CachedPosition, hit.CachedRotation, _settings.Radius, _settings.Height, _settings.Extent, minGroundDot, out Vector3 checkGroundPosition, out Vector3 checkGroundNormal, out float checkGroundDistance, out bool isWithinExtent);
					if (isGrounded == true)
					{
						data.IsGrounded = true;
						if (checkGroundDistance < closestGroundDistance)
						{
							closestGroundNormal   = checkGroundNormal;
							closestGroundPosition = checkGroundPosition;
							closestGroundDistance = checkGroundDistance;
						}
						hit.IsWithinExtent = true;
						hit.CollisionType  = ECollisionType.Ground;
					}
					else if (isWithinExtent == true)
					{
						hit.IsWithinExtent = true;
						if (hit.CollisionType == ECollisionType.None)
						{
							hit.CollisionType = ECollisionType.Slope;
						}
					}
				}
				if (data.IsGrounded == true)
				{
					data.IsGrounded     = true;
					data.GroundNormal   = closestGroundNormal;
					data.GroundPosition = closestGroundPosition;
					data.GroundDistance = closestGroundDistance;
					data.GroundAngle    = Vector3.Angle(closestGroundNormal, Vector3.up);
				}
			}
		}
		private static void RecalculateGroundProperties(KCCData data)
		{
			if (data.IsGrounded == false)
				return;
			if (KCCPhysicsUtility.ProjectOnGround(data.GroundNormal, data.GroundNormal.OnlyXZ(), out Vector3 projectedGroundNormal) == true)
			{
				data.GroundTangent = projectedGroundNormal.normalized;
				return;
			}
			if (KCCPhysicsUtility.ProjectOnGround(data.GroundNormal, data.DesiredVelocity.OnlyXZ(), out Vector3 projectedDesiredVelocity) == true)
			{
				data.GroundTangent = projectedDesiredVelocity.normalized;
				return;
			}
			data.GroundTangent = data.TransformDirection;
		}
	}
}