using Sandbox;
using System;
using System.Runtime.Intrinsics.Arm;
using System.Text.RegularExpressions;

namespace VeloX;

[Group( "VeloX" )]
[Title( "VeloX - Wheel" )]
public partial class VeloXWheel : Component
{

	[Property] public float Radius { get; set; } = 15;
	[Property] public float Width { get; set; } = 6;
	[Sync] public float SideSlip { get; private set; }
	[Sync] public float ForwardSlip { get; private set; }
	[Sync, Range( 0, 1 )] public float BrakePower { get; set; }
	[Sync] public float Torque { get; set; }
	[Sync, Range( 0, 1 )] public float Brake { get; set; }
	[Property] public bool IsFront { get; protected set; }
	[Property] public float SteerMultiplier { get; set; }
	public float Spin { get; private set; }

	public float RPM { get => angularVelocity * 60f / MathF.Tau; set => angularVelocity = value / (60 / MathF.Tau); }
	internal float DistributionFactor { get; set; }

	private Vector3 StartPos { get; set; }
	private static Rotation CylinderOffset => Rotation.FromRoll( 90 );

	public SceneTraceResult Trace { get; private set; }
	public bool IsOnGround => Trace.Hit;

	private float lastSpringOffset;
	private Vector2 tractionCycle;
	private float angularVelocity;
	private float lastFraction;
	private RealTimeUntil expandSoundCD;
	private RealTimeUntil contractSoundCD;


	protected override void OnAwake()
	{
		base.OnAwake();
		if ( StartPos.IsNearZeroLength )
			StartPos = LocalPosition;
	}

	private static float TractionRamp( float slipAngle, float sideTractionMaxAng, float sideTractionMax, float sideTractionMin )
	{
		sideTractionMaxAng /= 90; // Convert max slip angle to the 0 - 1 range
		var x = (slipAngle - sideTractionMaxAng) / (1 - sideTractionMaxAng);


		return slipAngle < sideTractionMaxAng ? sideTractionMax : (sideTractionMax * (1 - x)) + (sideTractionMin * x);
	}

	private void DoSuspensionSounds( VeloXBase vehicle, float change )
	{
		if ( change > 0.1f && expandSoundCD )
		{
			expandSoundCD = 0.3f;
			var sound = Sound.Play( vehicle.SuspensionUpSound, WorldPosition );
			sound.Volume = Math.Clamp( Math.Abs( change ) * 5f, 0, 0.5f );
		}

		if ( change < -0.1f && contractSoundCD )
		{
			contractSoundCD = 0.3f;
			change = MathF.Abs( change );
			var sound = Sound.Play( change > 0.3f ? vehicle.SuspensionHeavySound : vehicle.SuspensionDownSound, WorldPosition );
			sound.Volume = Math.Clamp( change * 5f, 0, 1 );
		}
	}

	internal void Update( VeloXBase vehicle, in float dt )
	{
		UpdateVisuals( vehicle, dt );

		if ( !IsOnGround )
		{
			angularVelocity += (Torque / 20) * dt;
			angularVelocity = MathX.Approach( angularVelocity, 0, dt * 4 );
		}

	}

	private void UpdateVisuals( VeloXBase vehicle, in float dt )
	{
		//Rotate the wheel around the axle axis
		Spin = (Spin - MathX.RadianToDegree( angularVelocity ) * dt) % 360;

		WorldRotation = vehicle.WorldTransform.RotationToWorld( vehicle.SteerAngle * SteerMultiplier ).RotateAroundAxis( Vector3.Right, Spin );

	}

	public void DoPhysics( VeloXBase vehicle, ref Vector3 vehVel, ref Vector3 vehAngVel, ref Vector3 outLin, ref Vector3 outAng, in float dt )
	{

		var pos = vehicle.WorldTransform.PointToWorld( StartPos );

		var ang = vehicle.WorldTransform.RotationToWorld( vehicle.SteerAngle * SteerMultiplier );

		var fw = ang.Forward;
		var rt = ang.Right;
		var up = ang.Up;

		var maxLen = SuspensionLength;

		var endPos = pos + ang.Down * maxLen;
		Trace = Scene.Trace.IgnoreGameObjectHierarchy( vehicle.GameObject )
					.FromTo( pos, endPos )
					.Cylinder( Width, Radius )
					.Rotated( vehicle.WorldTransform.Rotation * CylinderOffset )
					.UseRenderMeshes( false )
					.UseHitPosition( false )
					.WithoutTags( vehicle.WheelIgnoredTags )
					.Run();

		var fraction = Trace.Fraction;

		var contactPos = pos - maxLen * fraction * up;

		LocalPosition = vehicle.WorldTransform.PointToLocal( contactPos );

		DoSuspensionSounds( vehicle, fraction - lastFraction );
		lastFraction = fraction;

		if ( !IsOnGround )
		{
			SideSlip = 0;
			ForwardSlip = 0;
			return;
		}
		var normal = Trace.Normal;

		var vel = vehicle.Body.GetVelocityAtPoint( pos );
		// Split that velocity among our local directions

		var velF = fw.Dot( vel );

		var velR = rt.Dot( vel );

		var velU = normal.Dot( vel );

		var absVelR = Math.Abs( velR );


		//Make forward forces be perpendicular to the surface normal

		fw = normal.Cross( rt );

		//Suspension spring force &damping

		var offset = maxLen - (fraction * maxLen);

		var springForce = (offset * SpringStrength);

		var damperForce = (lastSpringOffset - offset) * SpringDamper;
		lastSpringOffset = offset;
		var force = (springForce - damperForce) * up.Dot( normal ) * normal;

		//If the suspension spring is going to be fully compressed on the next frame...

		if ( velU < 0 && offset + Math.Abs( velU * dt ) > SuspensionLength )
		{
			var (linearVel, angularVel) = vehicle.Body.PhysicsBody.CalculateVelocityOffset( (-velU / dt) * normal, pos );
			vehVel += linearVel;
			vehAngVel += angularVel;
		}

		//Rolling resistance
		force += 0.05f * -velF * fw;

		//Brake and torque forces
		var surfaceGrip = 1;

		var maxTraction = ForwardTractionMax * surfaceGrip * 1;
		//Grip loss logic

		var brakeForce = MathX.Clamp( -velF, -Brake, Brake ) * BrakePowerMax * surfaceGrip;

		var forwardForce = Torque + brakeForce;

		var signForwardForce = forwardForce > 0 ? 1 : (forwardForce < 0 ? -1 : 0);

		// Given an amount of sideways slippage( up to the max.traction )
		// and the forward force, calculate how much grip we are losing.

		tractionCycle.x = Math.Min( absVelR, maxTraction );
		tractionCycle.y = forwardForce;

		var gripLoss = Math.Max( tractionCycle.Length - maxTraction, 0 );


		// Reduce the forward force by the amount of grip we lost,

		// but still allow some amount of brake force to apply regardless.

		forwardForce += -(gripLoss * signForwardForce) + MathX.Clamp( brakeForce * 0.5f, -maxTraction, maxTraction );

		force += fw * forwardForce;

		// Get how fast the wheel would be spinning if it had never lost grip

		var groundAngularVelocity = MathF.Tau * (velF / (Radius * MathF.Tau));

		// Add our grip loss to our spin velocity
		var _angvel = groundAngularVelocity + gripLoss * (Torque > 0 ? 1 : (Torque < 0 ? -1 : 0));

		// Smoothly match our current angular velocity to the angular velocity affected by grip loss

		angularVelocity = MathX.Approach( angularVelocity, _angvel, dt * 200 );
		ForwardSlip = groundAngularVelocity - angularVelocity;

		// Calculate side slip angle
		var slipAngle = MathF.Atan2( velR, MathF.Abs( velF ) ) / MathF.PI * 2;
		SideSlip = slipAngle * MathX.Clamp( vehicle.TotalSpeed * 0.005f, 0, 1 ) * 2;

		//Sideways traction ramp

		slipAngle = MathF.Abs( slipAngle * slipAngle );

		maxTraction = TractionRamp( slipAngle, SideTractionMaxAng, SideTractionMax, SideTractionMin );

		var sideForce = -rt.Dot( vel * SideTractionMultiplier );

		// Reduce sideways traction force as the wheel slips forward
		sideForce *= 1 - Math.Clamp( MathF.Abs( gripLoss ) * 0.1f, 0, 1 ) * 0.9f;

		// Apply sideways traction force
		force += Math.Clamp( sideForce, -maxTraction, maxTraction ) * surfaceGrip * rt;

		force += velR * SideTractionMultiplier * -0.1f * rt;
		//Apply the forces at the axle / ground contact position

		vehicle.Body.ApplyForceAt( pos, force / dt );


	}
}
