Park/Rides/Queues/QueueGuestSystem.cs
using HC3.Terrain;

namespace HC3;

/// <summary>
/// A queue system set up on a ride that allocates guests, and positions them.
/// </summary>
public sealed class QueueGuestSystem : Component
{
	[ConVar( "hc3.debug.queue" )]
	public static bool DebugDraw { get; set; } = false;

	[RequireComponent] public BasicRide Ride { get; set; }
	[Property] public int GuestsPerTile { get; set; } = 4;
	[Property] public float GuestSpacing { get; set; } = 16f;

	public QueueSign Signage { get; private set; }

	private List<Guest> _guests = new();
	public List<Queue> _queuePath = new();

	public bool IsActive() => _queuePath.Count > 0;

	/// <summary>
	/// Maximum number of guests this queue can currently hold.
	/// </summary>
	public int MaxGuests => GuestsPerTile * _queuePath.Count;

	public bool IsFull()
	{
		return _guests.Count >= MaxGuests;
	}

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

		if ( Signage.IsValid() )
		{
			Signage.DestroyGameObject();
		}

		foreach ( var path in _queuePath )
		{
			if ( !path.IsValid() ) continue;
			path.QueueSystem = null;
		}
	}

	public void BuildQueuePath()
	{
		List<Queue> removed = new List<Queue>( _queuePath );
		_queuePath.Clear();

		var start = Ride.GetQueueStart();
		if ( start is not null )
		{
			var visited = new HashSet<Queue>();
			var current = start;
			Queue previous = null;

			while ( current != null )
			{
				removed.Remove( current );
				_queuePath.Add( current );

				var next = current.Connections
					.OfType<Queue>()
					.FirstOrDefault( x => x.IsValid() && x != previous && !visited.Contains( x ) );

				if ( next.IsValid() )
				{
					if ( next.QueueSystem.IsValid() && next.QueueSystem != this )
						break;
				}

				previous = current;
				current = next;
			}
		}

		foreach ( var path in removed )
		{
			if ( !path.IsValid() ) continue;
			path.QueueSystem = null;
		}

		foreach ( var path in _queuePath )
		{
			if ( !path.IsValid() ) continue;
			path.QueueSystem = this;
		}

		UpdatePositions();
		UpdateSignage();
	}

	private void UpdateSignage()
	{
		if ( !Signage.IsValid() )
		{
			var obj = GameObject.Clone( "prefabs/queues/path_queue_sign.prefab" );
			Signage = obj.GetComponent<QueueSign>();
			Signage.SetRide( Ride );
		}

		Signage.UpdateText();

		var path = _queuePath.LastOrDefault();
		if ( path.IsValid() )
		{
			Signage.WorldPosition = path.WorldPosition;

			// work out which way it should face

			foreach ( var connection in path.Connections )
			{
				if ( connection.IsValid() && !_queuePath.Contains( connection ) && connection != Ride.Entrance )
				{
					var next = connection.WorldPosition;
					Signage.WorldRotation = Rotation.LookAt( (path.WorldPosition - next).WithZ( 0 ) );

					var edge = Signage.WorldRotation.ToTileEdge();
					Signage.WorldPosition += Vector3.Up * path.GetHeightOffset( edge.GetOpposite() ) * GridManager.HeightStep;
					return;
				}
			}

			{
				// not conneted to any other path piece, just use the last one (or entrance)
				var prev = _queuePath.Count > 1 ? _queuePath.ElementAt( _queuePath.Count - 2 ).WorldPosition : Ride.Entrance.WorldPosition;
				Signage.WorldRotation = Rotation.LookAt( (prev - path.WorldPosition).WithZ( 0 ) );

				var edge = Signage.WorldRotation.ToTileEdge();
				Signage.WorldPosition += Vector3.Up * path.GetHeightOffset( edge.GetOpposite() ) * GridManager.HeightStep;
			}
		}
		else
		{
			if ( Ride.Entrance.IsValid() )
			{
				Signage.WorldPosition = Ride.Entrance.WorldPosition;
				Signage.WorldRotation = Ride.Entrance.WorldRotation;
			}
		}
	}

	public bool Join( Guest guest, int? index = null )
	{
		if ( !_guests.Contains( guest ) )
		{
			if ( index.HasValue && index.Value < _guests.Count )
				_guests.Insert( index.Value, guest );
			else
				_guests.Add( guest );

			UpdatePositions();
			return true;
		}
		return false;
	}

	public bool Leave( Guest guest )
	{
		if ( _guests.Remove( guest ) )
		{
			UpdatePositions();
			return true;
		}
		return false;
	}

	public void Clear()
	{
		_guests.Clear();
		UpdatePositions();
	}

	public bool Contains( Guest guest ) => _guests.Contains( guest );

	public int GetPosition( Guest guest ) => _guests.IndexOf( guest );

	public int GetGuestCount() => _guests.Count;

	private void UpdatePositions()
	{
		if ( _queuePath.Count == 0 )
			return;

		// Could've been deleted at some point
		_guests = _guests.Where( x => x.IsValid() ).ToList();

		for ( int i = 0; i < _guests.Count; i++ )
		{
			var guest = _guests[i];
			var spot = GetQueueSpot( i );

			guest.WorldPosition = spot.Position;
			guest.WorldRotation = spot.Rotation;
		}
	}

	private Transform GetQueueSpot( int i )
	{
		float distance = i * GuestSpacing;

		int segIndex = (int)(distance / GridManager.GridSize);
		float t = (distance % GridManager.GridSize) / GridManager.GridSize;

		segIndex = segIndex.Clamp( 0, _queuePath.Count - 1 );

		var start = segIndex > 0 && _queuePath[segIndex - 1].IsValid() ? _queuePath[segIndex - 1].WorldPosition : Ride.Entrance.WorldPosition;
		var end = _queuePath[segIndex].IsValid() ? _queuePath[segIndex].WorldPosition : start;

		var position = Vector3.Lerp( start, end, t );

		float z = start.z;
		if ( segIndex > 0 )
		{
			// make sure we're giving the right Z pos at the point we've worked out above
			var current = (t < 0.5f ? _queuePath[segIndex - 1] : _queuePath[segIndex]);
			if ( current.IsValid() )
			{
				z = current.GetHeightAt( position );
			}
		}

		var dir = (start - end).Normal;
		return new Transform( position.WithZ( z ), Rotation.LookAt( dir.WithZ( 0 ) ) );
	}

	/// <summary>
	/// Gets a position along the queue path based on how many guests are currently in line.
	/// This is where the next guest should walk to.
	/// </summary>
	public Vector3 GetQueuePosition()
	{
		if ( _queuePath.Count == 0 )
			return Vector3.Zero;

		return GetQueueSpot( _guests.Count ).Position;
	}

	protected override void OnUpdate()
	{
		if ( !DebugDraw || !Ride.HasEntrance ) return;

		var start = Ride.Entrance.WorldPosition;
		var offset = new Vector3( 0, 0, 4 );

		for ( int i = 0; i < _queuePath.Count; i++ )
		{
			var path = _queuePath[i];
			if ( !path.IsValid() )
				return;

			Gizmo.Draw.Color = Color.Green;
			Gizmo.Draw.LineThickness = 5;
			Gizmo.Draw.Arrow( path.WorldPosition + offset, start + offset );

			Gizmo.Draw.Color = Color.White;
			Gizmo.Draw.Text( i.ToString(), new Transform( path.WorldPosition + offset ), size: 16 );

			start = path.WorldPosition;
		}

		Gizmo.Draw.Color = Color.Orange;
		Gizmo.Draw.SolidSphere( GetQueuePosition(), 5 );
	}
}