using Sandbox;
using Sandbox.Audio;
using System;
using System.Collections.Generic;
using System.Linq;
using VeloX.Utils;
using static Sandbox.VertexLayout;
using static VeloX.EngineStream;

namespace VeloX;

public class Engine : PowertrainComponent, IScenePhysicsEvents
{
	protected override void OnAwake()
	{
		base.OnAwake();
		Name ??= "Engine";
		UpdatePeakPowerAndTorque();

	}

	[Hide] public new bool Input { get; set; }

	public delegate float CalculateTorque( float angularVelocity, float dt );
	/// <summary>
	///     Delegate for a function that modifies engine power.
	/// </summary>
	public delegate float PowerModifier();

	public enum EngineType
	{
		ICE,
		Electric,
	}
	/// <summary>
	///     If true starter will be ran for [starterRunTime] seconds if engine receives any throttle input.
	/// </summary>
	[Property] public bool AutoStartOnThrottle { get; set; } = true;

	/// <summary>
	///     Assign your own delegate to use different type of torque calculation.
	/// </summary>
	public CalculateTorque CalculateTorqueDelegate;


	/// <summary>
	///     Engine type. ICE (Internal Combustion Engine) supports features such as starter, stalling, etc.
	///     Electric engine (motor) can run in reverse, can not be stalled and does not use starter.
	/// </summary>
	[Property] public EngineType Type { get; set; } = EngineType.ICE;

	/// <summary>
	///     Power generated by the engine in kW
	/// </summary>
	public float generatedPower;


	/// <summary>
	///     RPM at which idler circuit will try to keep RPMs when there is no input.
	/// </summary>
	[Property] public float IdleRPM { get; set; } = 900;

	/// <summary>
	///     Maximum engine power in [kW].
	/// </summary>
	[Property, Group( "Power" )] public float MaxPower { get; set; } = 120;

	/// <summary>
	/// Loss power (pumping, friction losses) is calculated as the percentage of maxPower.
	/// Should be between 0 and 1 (100%).
	/// </summary>
	[Range( 0, 1 ), Property] public float EngineLossPercent { get; set; } = 0.8f;


	/// <summary>
	///     If true the engine will be started immediately, without running the starter, when the vehicle is enabled.
	///     Sets engine angular velocity to idle angular velocity.
	/// </summary>
	[Property] public bool FlyingStartEnabled { get; set; }

	[Property] public bool Ignition { get; set; }

	/// <summary>
	///     Power curve with RPM range [0,1] on the X axis and power coefficient [0,1] on Y axis.
	///     Both values are represented as percentages and should be in 0 to 1 range.
	///     Power coefficient is multiplied by maxPower to get the final power at given RPM.
	/// </summary>
	[Property, Group( "Power" )] public Curve PowerCurve { get; set; } = new( new List<Curve.Frame>() { new( 0, 0.4f ), new( 0.16f, 0.65f ), new( 0.32f, 0.85f ), new( 0.64f, 1f ), new( 0.8f, 0.9f ), new( 0.9f, 0.6f ), new( 1f, 0.2f ) } );

	/// <summary>
	///     Is the engine currently hitting the rev limiter?
	/// </summary>
	public bool RevLimiterActive;

	/// <summary>
	///     If engine RPM rises above revLimiterRPM, how long should fuel cutoff last?
	///     Higher values make hitting rev limiter more rough and choppy.
	/// </summary>
	[Property] public float RevLimiterCutoffDuration { get; set; } = 0.12f;

	/// <summary>
	///     Engine RPM at which rev limiter activates.
	/// </summary>
	[Property] public float RevLimiterRPM { get; set; } = 6700;

	/// <summary>
	///     Is the starter currently active?
	/// </summary>
	[Property, ReadOnly, Group( "Info" )] public bool StarterActive = false;

	/// <summary>
	///     Torque starter motor can put out. Make sure that this torque is more than loss torque
	///     at the starter RPM limit. If too low the engine will fail to start.
	/// </summary>
	[Property] public float StartDuration = 0.5f;


	/// <summary>
	/// Peak power as calculated from the power curve. If the power curve peaks at Y=1 peak power will equal max power field value.
	/// After changing power, power curve or RPM range call UpdatePeakPowerAndTorque() to get update the value.
	/// </summary>
	[Property, ReadOnly, Group( "Info" )]
	public float EstimatedPeakPower => _peakPower;
	private float _peakPower;



	/// <summary>
	/// RPM at which the peak power is achieved.
	/// After changing power, power curve or RPM range call UpdatePeakPowerAndTorque() to get update the value.
	/// </summary>
	[Property, ReadOnly, Group( "Info" )]
	public float EstimatedPeakPowerRPM => _peakPowerRpm;

	private float _peakPowerRpm;

	/// <summary>
	/// Peak torque value as calculated from the power curve.
	/// After changing power, power curve or RPM range call UpdatePeakPowerAndTorque() to get update the value.
	/// </summary>
	[Property, ReadOnly, Group( "Info" )]
	public float EstimatedPeakTorque => _peakTorque;

	private float _peakTorque;

	/// <summary>
	/// RPM at which the engine achieves the peak torque, calculated from the power curve.
	/// After changing power, power curve or RPM range call UpdatePeakPowerAndTorque() to get update the value.
	/// </summary>
	[Property, ReadOnly, Group( "Info" )]
	public float EstimatedPeakTorqueRPM => _peakTorqueRpm;

	private float _peakTorqueRpm;

	/// <summary>
	///     RPM as a percentage of maximum RPM.
	/// </summary>
	[Property, ReadOnly, Group( "Info" )]
	public float RPMPercent => _rpmPercent;

	[Sync] private float _rpmPercent { get; set; }
	/// <summary>
	///     Engine throttle position. 0 for no throttle and 1 for full throttle.
	/// </summary>
	[Property, ReadOnly, Group( "Info" )]
	public float ThrottlePosition { get; private set; }

	/// <summary>
	/// Is the engine currently running?
	/// Requires ignition to be enabled.
	/// </summary>
	[Property, ReadOnly, Group( "Info" )]
	public bool IsRunning { get; private set; }

	/// <summary>
	/// Is the engine currently running?
	/// Requires ignition to be enabled.
	/// </summary>
	[Property, ReadOnly, Group( "Info" )]
	public bool IsActive { get; private set; }

	private float _idleAngularVelocity;



	/// <summary>
	/// Current load of the engine, based on the power produced.
	/// </summary>
	public float Load => _load;
	public event Action OnEngineStart;
	public event Action OnEngineStop;
	public event Action OnRevLimiter;

	private float _load;


	protected override void OnStart()
	{
		base.OnStart();

		if ( Type == EngineType.ICE )
		{
			CalculateTorqueDelegate = CalculateTorqueICE;
		}
		else if ( Type == EngineType.Electric )
		{
			IdleRPM = 0f;
			FlyingStartEnabled = true;
			CalculateTorqueDelegate = CalculateTorqueElectric;
			StarterActive = false;
			StartDuration = 0.001f;
			RevLimiterCutoffDuration = 0f;
		}
	}

	public void StartEngine()
	{
		if ( IsRunning ) return;

		Ignition = true;
		OnEngineStart?.Invoke();

		if ( Type != EngineType.Electric )
		{
			if ( FlyingStartEnabled )
			{
				FlyingStart();
			}
			else if ( !StarterActive && Controller != null )
			{
				StarterCoroutine();
			}
		}
	}
	private async void StarterCoroutine()
	{
		if ( Type == EngineType.Electric || StarterActive )
			return;

		try
		{
			float startTimer = 0f;
			StarterActive = true;

			// Ensure safe start duration
			StartDuration = Math.Max( 0.1f, StartDuration );

			_starterTorque = ((_idleAngularVelocity - OutputAngularVelocity) * Inertia) / StartDuration;

			while ( startTimer <= StartDuration && StarterActive )
			{
				startTimer += 0.1f;
				await Task.DelaySeconds( 0.1f );
			}
		}
		finally
		{
			_starterTorque = 0;
			StarterActive = false;
			IsActive = true;
		}
	}


	private void FlyingStart()
	{
		Ignition = true;
		StarterActive = false;
		OutputAngularVelocity = IdleRPM.RPMToAngularVelocity();
	}

	public void StopEngine()
	{
		Ignition = false;
		IsActive = true;
		OnEngineStop?.Invoke();
	}


	/// <summary>
	/// Toggles engine state.
	/// </summary>
	public void StartStopEngine()
	{
		if ( IsRunning )
			StopEngine();
		else
			StartEngine();
	}

	public void UpdatePeakPowerAndTorque()
	{
		GetPeakPower( out _peakPower, out _peakPowerRpm );
		GetPeakTorque( out _peakTorque, out _peakTorqueRpm );
	}

	public void UpdateEngine( in float dt )
	{
		StreamEngineUpdate( dt, WorldPosition );
		if ( IsProxy )
			return;

		// Cache values
		_userThrottleInput = _userThrottleInput.LerpTo( Controller.SwappedThrottle, 0.05f );

		ThrottlePosition = _userThrottleInput;
		_idleAngularVelocity = IdleRPM.RPMToAngularVelocity();
		_revLimiterAngularVelocity = RevLimiterRPM.RPMToAngularVelocity();
		if ( _revLimiterAngularVelocity == 0f )
			return;
		// Check for start on throttle

		if ( !IsRunning && !StarterActive && AutoStartOnThrottle && ThrottlePosition > 0.2f )
			StartEngine();


		bool wasRunning = IsRunning;
		IsRunning = Ignition;
		if ( wasRunning && !IsRunning )
			StopEngine();

		// Physics update
		if ( OutputNameHash == 0 )
			return;

		float drivetrainInertia = _output.QueryInertia();
		float inertiaSum = Inertia + drivetrainInertia;
		if ( inertiaSum == 0 )
			return;

		float drivetrainAngularVelocity = QueryAngularVelocity( OutputAngularVelocity, dt );
		float targetAngularVelocity = Inertia / inertiaSum * OutputAngularVelocity + drivetrainInertia / inertiaSum * drivetrainAngularVelocity;

		// Calculate generated torque and power
		float generatedTorque = CalculateTorqueICE( OutputAngularVelocity, dt );
		generatedPower = TorqueToPowerInKW( in OutputAngularVelocity, in generatedTorque );

		// Calculate reaction torque
		float reactionTorque = (targetAngularVelocity - OutputAngularVelocity) * Inertia / dt;

		// Calculate/get torque returned from wheels

		OutputTorque = generatedTorque - reactionTorque;
		float returnTorque = ForwardStep( OutputTorque, 0, dt );

		float totalTorque = generatedTorque + returnTorque + reactionTorque;
		OutputAngularVelocity += totalTorque / inertiaSum * dt;

		// Clamp the angular velocity to prevent any powertrain instabilities over the limits
		OutputAngularVelocity = Math.Clamp( OutputAngularVelocity, 0, _revLimiterAngularVelocity * 1.05f );

		// Calculate cached values
		_rpmPercent = Math.Clamp( OutputAngularVelocity / _revLimiterAngularVelocity, 0, 1 );
		_load = Math.Clamp( generatedPower / MaxPower, 0, 1 );


	}

	private float _starterTorque;
	private float _revLimiterAngularVelocity;
	private float _userThrottleInput;

	private async void RevLimiter()
	{
		if ( RevLimiterActive || Type == EngineType.Electric || RevLimiterCutoffDuration == 0 )
			return;

		RevLimiterActive = true;
		OnRevLimiter?.Invoke();
		await Task.DelayRealtimeSeconds( RevLimiterCutoffDuration );
		RevLimiterActive = false;
	}


	/// <summary>
	///     Calculates torque for electric engine type.
	/// </summary>
	public float CalculateTorqueElectric( float angularVelocity, float dt )
	{
		float absAngVel = Math.Abs( angularVelocity );

		// Avoid angular velocity spikes while shifting
		if ( Controller.Transmission.IsShifting )
			ThrottlePosition = 0;

		float maxLossPower = MaxPower * 0.3f;
		float lossPower = maxLossPower * (1f - ThrottlePosition) * RPMPercent;
		float genPower = MaxPower * ThrottlePosition;
		float totalPower = genPower - lossPower;
		totalPower = MathX.Lerp( totalPower * 0.1f, totalPower, RPMPercent * 10f );
		float clampedAngVel = absAngVel < 10f ? 10f : absAngVel;
		return PowerInKWToTorque( clampedAngVel, totalPower );
	}


	/// <summary>
	/// Calculates torque for ICE (Internal Combustion Engine).
	/// </summary>
	public float CalculateTorqueICE( float angularVelocity, float dt )
	{
		// Set the throttle to 0 when shifting, but avoid doing so around idle RPM to prevent stalls.
		if ( Controller.Transmission.IsShifting && angularVelocity > _idleAngularVelocity )
			ThrottlePosition = 0f;

		// Set throttle to 0 when starter active.
		if ( StarterActive )
			ThrottlePosition = 0f;
		// Apply idle throttle correction to keep the engine running
		else
			ApplyICEIdleCorrection();
		
		// Trigger rev limiter if needed
		if ( angularVelocity >= _revLimiterAngularVelocity && !RevLimiterActive )
			RevLimiter();

		// Calculate torque
		float generatedTorque;

		// Do not generate any torque while starter is active to prevent RPM spike during startup
		// or while stalled to prevent accidental starts.
		if ( StarterActive )
			generatedTorque = 0f;
		else
			generatedTorque = CalculateICEGeneratedTorqueFromPowerCurve();

		float lossTorque = (StarterActive || ThrottlePosition > 0.2f) ? 0f : CalculateICELossTorqueFromPowerCurve();

		// Reduce the loss torque at rev limiter, but allow it to be >0 to prevent vehicle getting
		// stuck at rev limiter.
		if ( RevLimiterActive )
			lossTorque *= 0.25f;
		generatedTorque += _starterTorque + lossTorque;
		return generatedTorque;
	}

	private float CalculateICELossTorqueFromPowerCurve()
	{
		// Avoid issues with large torque spike around 0 angular velocity.
		if ( OutputAngularVelocity < 10f )
			return -OutputAngularVelocity * MaxPower * 0.03f;

		float angVelPercent = OutputAngularVelocity < 10f ? 0.1f : Math.Clamp( OutputAngularVelocity, 0, _revLimiterAngularVelocity ) / _revLimiterAngularVelocity;

		float lossPower = angVelPercent * 3f * -MaxPower * Math.Clamp( _userThrottleInput + 0.5f, 0, 1 ) * EngineLossPercent;

		return PowerInKWToTorque( OutputAngularVelocity, lossPower );
	}

	private void ApplyICEIdleCorrection()
	{
		if ( Ignition && OutputAngularVelocity < _idleAngularVelocity * 1.1f )
		{
			// Apply a small correction to account for the error since the throttle is applied only
			// if the idle RPM is below the target RPM.
			float idleCorrection = _idleAngularVelocity * 1.08f - OutputAngularVelocity;
			idleCorrection = idleCorrection < 0f ? 0f : idleCorrection;
			float idleThrottlePosition = Math.Clamp( idleCorrection * 0.01f, 0, 1 );
			ThrottlePosition = Math.Max( _userThrottleInput, idleThrottlePosition );
		}
	}

	private float CalculateICEGeneratedTorqueFromPowerCurve()
	{
		generatedPower = 0;
		float torque = 0;

		if ( !Ignition && !StarterActive )
			return 0;
		if ( RevLimiterActive )
			ThrottlePosition = 0.2f;
		else
		{
			// Add maximum losses to the maximum power when calculating the generated power since the maxPower is net value (after losses).
			generatedPower = PowerCurve.Evaluate( _rpmPercent ) * (MaxPower * (1f + EngineLossPercent)) * ThrottlePosition;
			torque = PowerInKWToTorque( OutputAngularVelocity, generatedPower );
		}
		return torque;
	}


	public void GetPeakTorque( out float peakTorque, out float peakTorqueRpm )
	{
		peakTorque = 0;
		peakTorqueRpm = 0;

		for ( float i = 0.05f; i < 1f; i += 0.05f )
		{
			float rpm = i * RevLimiterRPM;
			float P = PowerCurve.Evaluate( i ) * MaxPower;
			if ( rpm < IdleRPM )
			{
				continue;
			}

			float W = rpm.RPMToAngularVelocity();
			float T = (P * 1000f) / W;

			if ( T > peakTorque )
			{
				peakTorque = T;
				peakTorqueRpm = rpm;
			}
		}
	}

	public void GetPeakPower( out float peakPower, out float peakPowerRpm )
	{
		float maxY = 0f;
		float maxX = 1f;
		for ( float i = 0f; i < 1f; i += 0.05f )
		{
			float y = PowerCurve.Evaluate( i );
			if ( y > maxY )
			{
				maxY = y;
				maxX = i;
			}
		}

		peakPower = maxY * MaxPower;
		peakPowerRpm = maxX * RevLimiterRPM;
	}



	[Property, Group( "Parsing" )] string Clipboard { get; set; }
	[Button, Group( "Parsing" )]
	public void FromLUT()
	{
		if ( Clipboard == null )
			return;

		using var undo = Scene.Editor?.UndoScope( "From LUT" ).WithComponentChanges( this ).Push();

		var data = new List<(float RPM, float PowerHP)>();

		var lines = Clipboard.Split( ['\n', '\r'], StringSplitOptions.RemoveEmptyEntries );

		foreach ( var line in lines )
		{
			var parts = line.Split( '|' );
			if ( parts.Length == 2 &&
				float.TryParse( parts[0], out float rpm ) &&
				float.TryParse( parts[1], out float powerHP ) )
			{
				data.Add( (rpm, powerHP) );
			}
		}

		RevLimiterRPM = data.Max( x => x.RPM );
		MaxPower = data.Max( x => x.PowerHP ) * 0.746f;

		var frames = data.Select( d =>
			new Curve.Frame(
				d.RPM / RevLimiterRPM,
				(d.PowerHP * 0.746f) / MaxPower
			)
		).ToList();
		PowerCurve = new( frames );
		Clipboard = null;
	}




	[Property] public EngineStream Stream { get; set; }


	public bool EngineSoundPaused => !IsRunning;
	private float _wobbleTime;

	private static readonly Mixer EngineMixer = Mixer.FindMixerByName( "Car Engine" );
	private readonly Dictionary<Layer, SoundHandle> EngineSounds = [];

	protected override void OnDestroy()
	{
		base.OnDestroy();

		foreach ( var item in EngineSounds.Values )
			item.Dispose();
	}
	public void StreamEngineUpdate( float deltaTime, Vector3 position, bool isLocal = false )
	{
		var globalPitch = 1.0f;

		// Gear wobble effect
		if ( _wobbleTime > 0 )
		{
			_wobbleTime -= deltaTime * (0.1f + ThrottlePosition);
			globalPitch += MathF.Cos( _wobbleTime * Stream.Parameters.WobbleFrequency ) * _wobbleTime * (1 - _wobbleTime) * Stream.Parameters.WobbleStrength;
		}

		globalPitch *= Stream.Parameters.Pitch;

		// Redline effect
		var redlineVolume = 1.0f;
		if ( RPMPercent >= 0.95f )
		{
			redlineVolume = 1 - Stream.Parameters.RedlineStrength +
						   MathF.Cos( Time.Now * Stream.Parameters.RedlineFrequency ) *
						   Stream.Parameters.RedlineStrength;
		}
		// Process layers
		foreach ( var (id, layer) in Stream.Layers )
		{
			EngineSounds.TryGetValue( layer, out var channel );

			if ( !channel.IsValid() )
			{
				channel = Sound.PlayFile( layer.AudioPath );
				EngineSounds[layer] = channel;
			}

			if ( channel.Paused && EngineSoundPaused )
				continue;

			// Reset controller outputs
			float layerVolume = 1.0f;
			float layerPitch = 1.0f;

			// Apply all controllers
			foreach ( var controller in layer.Controllers )
			{
				var inputValue = controller.InputParameter switch
				{
					Audio.Controller.InputTypes.Throttle => _userThrottleInput,
					Audio.Controller.InputTypes.RpmFraction => RPMPercent,
					_ => 0.0f
				};

				var normalized = Math.Clamp( inputValue, controller.InputRange.Min, controller.InputRange.Max );
				var outputValue = controller.InputRange.Remap(
					normalized,
					controller.OutputRange.Min,
					controller.OutputRange.Max
				);

				// Apply to correct parameter
				switch ( controller.OutputParameter )
				{
					case Audio.Controller.OutputTypes.Volume:
						layerVolume *= outputValue;
						break;
					case Audio.Controller.OutputTypes.Pitch:
						layerPitch *= outputValue;
						break;
				}
			}

			// Apply redline effect if needed
			layerVolume *= layer.UseRedline ? redlineVolume : 1.0f;
			layerPitch *= globalPitch;
			// Update audio channel
			channel.Pitch = layerPitch;
			channel.Volume = layerVolume * Stream.Parameters.Volume;
			channel.Position = position;
			channel.ListenLocal = isLocal;
			channel.Paused = EngineSoundPaused;
			channel.TargetMixer = EngineMixer;

		}
	}
}
