Vehicles/BaseVehicle.cs
using System;
using Clover.Components;
using Clover.Interactable;
using Clover.Player;
using Light = Sandbox.Light;

namespace Clover.Vehicles;

[Category( "Clover/Vehicles" )]
public class BaseVehicle : Component, IInteract
{
	[RequireComponent] public CharacterController CharacterController { get; set; }

	[Property] public GameObject Model { get; set; }

	[Property] public List<Light> Headlights { get; set; }

	[Property] public List<SittableNode> Seats { get; set; }

	[Property] public float Steering { get; set; } = 0.5f;

	[Property] public SoundEvent StartEngineSound { get; set; }
	[Property] public SoundEvent StopEngineSound { get; set; }
	[Property] public SoundEvent IdleSound { get; set; }
	[Property] public SoundEvent EngineSound { get; set; }
	[Property] public SoundEvent HornSound { get; set; }

	[Property] public GameObject ExhaustParticles { get; set; }

	[Sync] private bool IsOn { get; set; }

	public bool LocalPlayerInside => Occupants.Values.Contains( PlayerCharacter.Local?.GameObject );

	public bool LocalPlayerIsDriver =>
		Occupants.Keys.Contains( Seats[0] ) && Occupants[Seats[0]] == PlayerCharacter.Local?.GameObject;

	[Property, Sync] public NetDictionary<SittableNode, GameObject> Occupants { get; set; } = new();

	public bool HasDriver => Occupants.Keys.Contains( Seats[0] );

	private SoundHandle _idleSoundHandle;
	private SoundHandle _engineSoundHandle;
	[Sync] private TimeSince _startEngineTime { get; set; }

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

		foreach ( var light in Headlights )
		{
			light.Enabled = IsOn;
		}

		ExhaustParticles.Enabled = IsOn;

		if ( IsOn )
		{
			_idleSoundHandle = GameObject.PlaySound( IdleSound );
			_idleSoundHandle.Volume = 0;

			_engineSoundHandle = GameObject.PlaySound( EngineSound );
			_engineSoundHandle.Volume = 0;
		}
	}

	public void StartInteract( PlayerCharacter player )
	{
		EnterVehicle();
	}

	public void FinishInteract( PlayerCharacter player )
	{
	}

	public string GetInteractName()
	{
		return "Enter vehicle";
	}

	[Rpc.Broadcast]
	private void StartEngine()
	{
		IsOn = true;

		foreach ( var light in Headlights )
		{
			light.Enabled = true;
		}

		GameObject.PlaySound( StartEngineSound );

		_idleSoundHandle = GameObject.PlaySound( IdleSound );
		if ( _idleSoundHandle.IsValid() ) _idleSoundHandle.Volume = 0;

		_engineSoundHandle = GameObject.PlaySound( EngineSound );
		if ( _engineSoundHandle.IsValid() ) _engineSoundHandle.Volume = 0;

		_startEngineTime = 0;

		if ( ExhaustParticles != null )
		{
			ExhaustParticles.Enabled = true;
		}
	}

	[Rpc.Broadcast]
	private void StopEngine()
	{
		IsOn = false;

		foreach ( var light in Headlights )
		{
			light.Enabled = false;
		}

		GameObject?.PlaySound( StopEngineSound );

		_idleSoundHandle?.Stop();
		_engineSoundHandle?.Stop();

		if ( ExhaustParticles != null )
		{
			ExhaustParticles.Enabled = false;
		}
	}


	[Rpc.Owner]
	private void EnterVehicle()
	{
		var caller = Rpc.Caller;
		var player = PlayerCharacter.Get( caller );

		if ( !player.IsValid() )
		{
			Log.Error( "Player not found" );
			return;
		}

		// check if player is already inside
		if ( Occupants.Values.Contains( player.GameObject ) )
		{
			Log.Error( "Player is already inside" );
			return;
		}

		if ( !HasDriver )
		{
			var seat = Seats[0];

			Log.Info( "Player is entering as driver" );

			Occupants[seat] = player.GameObject;

			seat.Occupant = player.GameObject;

			using ( Rpc.FilterInclude( caller ) )
			{
				SitDown( seat );
			}

			// player.VehicleRider.OnEnterVehicle( this, Seats[0], 0 );

			StartEngine();
		}
		else
		{
			Log.Info( "Player is entering as passenger" );

			var seat = Seats.FirstOrDefault( x => !Occupants.ContainsKey( x ) );

			if ( seat == null )
			{
				Log.Error( "No available seats" );
				return;
			}

			Occupants[seat] = player.GameObject;

			seat.Occupant = player.GameObject;

			using ( Rpc.FilterInclude( caller ) )
			{
				SitDown( seat );
			}

			// player.VehicleRider.OnEnterVehicle( this, seat, Seats.IndexOf( seat ) );
		}
	}

	[Rpc.Broadcast]
	private void SitDown( SittableNode seat )
	{
		var player = PlayerCharacter.Local;

		if ( player.IsValid() )
		{
			player.GameObject.SetParent( GameObject );
			// player.Seat = seat;
			player.TeleportTo( seat.WorldPosition, seat.WorldRotation );
			player.PlayerController.Yaw = Model.WorldRotation.Yaw();
			player.SetCollisionEnabled( false );
			player.SetCarriableVisibility( false );

			if ( player.Components.TryGet<VehicleRider>( out var rider ) )
			{
				rider.OnEnterVehicle( this, seat, Seats.IndexOf( seat ) );
			}
		}
	}

	/*public void AddOccupant( GameObject occupant )
	{
		var seat = Seats.FirstOrDefault( x => !Occupants.ContainsKey( x ) );

		if ( seat == null ) throw new InvalidOperationException( "No available seats" );

		AddOccupant( Seats.IndexOf( seat ), occupant );
	}

	public void AddOccupant( int seatIndex, GameObject occupant )
	{
		var seat = Seats[seatIndex];

		if ( Occupants.ContainsKey( seat ) ) throw new InvalidOperationException( "Seat is already occupied" );

		Occupants[seat] = occupant;
		Log.Info( $"Added occupant to seat {seatIndex}" );

		// occupant.WorldPosition = seat.WorldPosition;
		occupant.SetParent( GameObject );
		Log.Info( $"Occupant {occupant.Name} position: {seat.WorldPosition}, vehicle position: {WorldPosition}" );
		if ( occupant.Components.TryGet<PlayerCharacter>( out var player ) )
		{
			// player.Seat = seat;
			player.TeleportTo( seat.WorldPosition, seat.WorldRotation );
			player.PlayerController.Yaw = Model.WorldRotation.Yaw();
			player.SetCollisionEnabled( false );
			player.SetCarriableVisibility( false );
		}

		if ( occupant.Components.TryGet<VehicleRider>( out var rider ) )
		{
			rider.OnEnterVehicle( this, seat, seatIndex );
		}

		if ( seatIndex == 0 )
		{
			StartEngine();
		}
	}

	public void RemoveOccupant( GameObject occupant )
	{
		var seat = Occupants.FirstOrDefault( x => x.Value == occupant ).Key;

		if ( seat == null ) throw new InvalidOperationException( "Occupant not found" );

		Occupants.Remove( seat );
		Log.Info( $"Removed occupant from seat {Seats.IndexOf( seat )}" );

		occupant.SetParent( null );

		if ( occupant.Components.TryGet<PlayerCharacter>( out var player ) )
		{
			player.Seat = null;
			player.SetCollisionEnabled( true );
			player.SetCarriableVisibility( true );
		}

		if ( occupant.Components.TryGet<VehicleRider>( out var rider ) )
		{
			rider.OnExitVehicle();
		}

		if ( seat == Seats[0] )
		{
			StopEngine();
		}
	}*/

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

		foreach ( var occupant in Occupants.Values )
		{
			if ( occupant.Components.TryGet<PlayerCharacter>( out var player ) )
			{
				player.Seat = null;
				player.SetCollisionEnabled( true );
				player.SetCarriableVisibility( true );
			}

			if ( occupant.Components.TryGet<VehicleRider>( out var rider ) )
			{
				rider.OnExitVehicle();
			}
		}

		StopEngine();
	}

	public void TryToEjectOccupant( GameObject occupant )
	{
		var seat = Occupants.FirstOrDefault( x => x.Value == occupant ).Key;

		if ( seat == null ) throw new InvalidOperationException( "Occupant not found" );

		var exitPosition = FindExitPosition( occupant );

		// RemoveOccupant( occupant );

		Occupants.Remove( seat );
		seat.Occupant = null;

		if ( occupant.Components.TryGet<PlayerCharacter>( out var player ) )
		{
			player.TeleportTo( exitPosition, Model.WorldRotation );
		}
		else
		{
			occupant.WorldPosition = exitPosition;
		}

		occupant.SetParent( null );
	}

	private Vector3 FindExitPosition( GameObject occupant )
	{
		return WorldPosition + Model.WorldRotation.Backward * 100;
	}

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

		// HandleOccupants();
		HandleSounds();
		Handling();

		if ( LocalPlayerInside && Input.Pressed( "Use" ) )
		{
			// TryToEjectOccupant( Occupants[Seats[0]] );
			ExitVehicle();
			Input.Clear( "Use" );
		}
	}

	[Rpc.Owner]
	private void ExitVehicle()
	{
		var caller = Rpc.Caller;
		var player = PlayerCharacter.Get( caller );

		if ( !player.IsValid() )
		{
			Log.Error( "Player not found" );
			return;
		}

		if ( !Occupants.Values.Contains( player.GameObject ) )
		{
			Log.Error( "Player is not inside" );
			return;
		}

		var seat = Occupants.FirstOrDefault( x => x.Value == player.GameObject ).Key;

		if ( seat == null )
		{
			Log.Error( "Seat not found" );
			return;
		}

		Occupants.Remove( seat );
		seat.Occupant = null;

		using ( Rpc.FilterInclude( caller ) )
		{
			ExitVehicleRpc();
		}

		// player.VehicleRider.OnExitVehicle();

		if ( seat == Seats[0] )
		{
			StopEngine();
		}
	}

	[Rpc.Broadcast]
	private void ExitVehicleRpc()
	{
		var player = PlayerCharacter.Local;

		if ( player.IsValid() )
		{
			player.GameObject.SetParent( null );
			player.TeleportTo( FindExitPosition( player.GameObject ), Model.WorldRotation );
			// player.PlayerController.Yaw = Model.WorldRotation.Yaw();
			player.SetCollisionEnabled( true );
			player.SetCarriableVisibility( true );

			if ( player.Components.TryGet<VehicleRider>( out var rider ) )
			{
				rider.OnExitVehicle();
			}
		}
	}

	private void HandleSounds()
	{
		if ( !IsOn ) return;

		if ( _startEngineTime < 1f ) return;

		var velocity = CharacterController.Velocity.Length;

		if ( velocity < 2f )
		{
			if ( _idleSoundHandle.IsValid() )
				_idleSoundHandle.Volume =
					_idleSoundHandle.Volume.LerpTo( IdleSound.Volume.FixedValue, Time.Delta * 2.0f );

			if ( _engineSoundHandle.IsValid() )
				_engineSoundHandle.Volume = _engineSoundHandle.Volume.LerpTo( 0, Time.Delta * 2.0f );
		}
		else
		{
			if ( _idleSoundHandle.IsValid() )
				_idleSoundHandle.Volume = _idleSoundHandle.Volume.LerpTo( 0, Time.Delta * 2.0f );

			if ( _engineSoundHandle.IsValid() )
				_engineSoundHandle.Volume =
					_engineSoundHandle.Volume.LerpTo( EngineSound.Volume.FixedValue, Time.Delta * 2.0f );
		}

		if ( _engineSoundHandle.IsValid() )
			_engineSoundHandle.Pitch = MathX.Lerp( 0.5f, 1.5f, MathF.Abs( velocity / 500 ) );
	}

	public Vector3 WishVelocity { get; private set; }

	[Property] public Vector3 Gravity { get; set; } = new Vector3( 0, 0, 800 );

	private void Handling()
	{
		BuildInput();

		if ( IsProxy )
			return;

		BuildWishVelocity();

		if ( CharacterController.IsOnGround )
		{
			CharacterController.Velocity = CharacterController.Velocity.WithZ( 0 );
			CharacterController.Accelerate( WishVelocity );
			CharacterController.ApplyFriction( 1.0f );
		}
		else
		{
			CharacterController.Velocity -= Gravity * Time.Delta * 0.5f;
			CharacterController.Accelerate( WishVelocity.ClampLength( 50 ) );
			CharacterController.ApplyFriction( 0.1f );
		}

		CharacterController.Move();

		if ( !CharacterController.IsOnGround )
		{
			CharacterController.Velocity -= Gravity * Time.Delta * 0.5f;
		}
		else
		{
			CharacterController.Velocity = CharacterController.Velocity.WithZ( 0 );
		}
	}

	public Vector3 ProxyInput;

	private void BuildInput()
	{
		if ( !LocalPlayerIsDriver ) return;

		if ( !IsProxy )
		{
			ProxyInput = Input.AnalogMove;
		}
		else
		{
			InputRpc( Input.AnalogMove );
		}
	}

	[Rpc.Owner]
	private void InputRpc( Vector3 input )
	{
		ProxyInput = input;
	}

	public void BuildWishVelocity()
	{
		if ( !HasDriver ) return;

		var input = ProxyInput;

		if ( input.Length > 0 /*&& !Player.ShouldDisableMovement()*/ )
		{
			// Player.Model.WorldRotation = Rotation.Lerp( Player.Model.WorldRotation, Rotation.LookAt( input, Vector3.Up ),
			// 	Time.Delta * 10.0f );

			if ( !Model.IsValid() )
			{
				Log.Error( "Model is not valid" );
				return;
			}

			// var inputYaw = MathF.Atan2( input.y, input.x ).RadianToDegree();
			// Player.Model.WorldRotation = Rotation.Lerp( Player.Model.WorldRotation, Rotation.From( 0, yaw + 180, 0 ), Time.Delta * 10.0f );
			// Yaw = yaw + 180;
			// Yaw = Yaw.LerpDegreesTo( inputYaw + 180, Time.Delta * 10.0f );

			Model.WorldRotation *= Rotation.From( 0, Steering * input.y, 0 );
			// Model.WorldRotation = Rotation.Lerp( Model.WorldRotation, Model.WorldRotation * Rotation.From( 0, Steering * input.y, 0 ), Time.Delta * 10.0f );

			var forward = Model.WorldRotation.Forward;
			WishVelocity = forward * (input.x * 300f);
			WishVelocity = WishVelocity.ClampLength( 200f );

			Gizmo.Draw.ScreenText( WishVelocity.Length.ToString(), new Vector2( 20, 20 ) );
		}
		else
		{
			WishVelocity = Vector3.Zero;
		}

		// WishVelocity *= 200f;

		/*if ( Input.Down( "Run" ) )
		{
			WishVelocity *= 180.0f;
		}
		else
		{
			WishVelocity *= 110.0f;
		}*/
	}
}