Park/Paths/Queue.cs
using HC3.Terrain;
using System;

namespace HC3;

/// <summary>
/// An inherited version of <see cref="Path"/>, which behaves differently. Queues can snake around, next to eachother, and connect to ride entrances/exits.
/// </summary>
public class Queue : Path
{
	// This queue piece can connect to 2 others at max
	// (feels weird to just rawdog Component like this but there's no need for anything else?)
	public Component[] Connections = new Component[2];

	[Property, ReadOnly]
	public QueueGuestSystem QueueSystem
	{
		get => _queueSystem;
		set
		{
			_queueSystem = value;
			UpdateTile();
		}
	}
	private QueueGuestSystem _queueSystem;

	public BasicRide Ride => QueueSystem?.Ride;

	public override int Hash => HashCode.Combine( base.Hash, Ride );

	protected IEnumerable<Component> GetDesirableConnections( PathMask mask = PathMask.All )
	{
		foreach ( TileEdge edge in mask.GetEdges() )
		{
			var pos = TilePosition + ((Vector3Int)edge.GetDirection()).WithZ( GetHeightOffset( edge ) );

			var cell = GridManager.Instance?.GetCell( new Vector2Int( pos.x, pos.y ) );
			if ( cell is null )
				continue;

			var entrance = cell.GetComponents<RideEntranceExit>( pos.z ).FirstOrDefault( x => !x.IsExit && x.Ride == Ride );
			if ( entrance is not null && entrance.CanConnectTile( TilePosition, edge.GetOpposite() ) )
			{
				yield return entrance;
			}

			var neighbour = cell.GetComponents<Path>( pos.z ).FirstOrDefault( x => !x.GhostPreview );
			if ( neighbour is null )
				continue;

			yield return neighbour;
		}
	}

	public void OnPostLoad( SaveData data )
	{
		for ( int i = 0; i < Connections.Length; i++ )
		{
			Connections[i] = null;
		}

		foreach ( var connection in GetDesirableConnections( data.Mask ) )
		{
			Connect( connection );
		}
	}

	/// <summary>
	/// Called when enabled or neighbours changed, before the tile is updated
	/// </summary>
	protected override void UpdateConnections()
	{
		if ( GhostPreview )
			return;

		if ( QueueSystem.IsValid() )
		{
			QueueSystem.BuildQueuePath();
		}

		for ( int i = 0; i < Connections.Length; i++ )
		{
			var connection = Connections[i];
			if ( !connection.IsValid() )
			{
				Connections[i] = null;
				continue;
			}

			if ( connection is Queue queue && !queue.IsConnectedTo( this ) )
			{
				Connections[i] = null;
			}
		}

		bool dirty = false;

		// Find the most desirable edges and try to connect them
		foreach ( var connection in GetDesirableConnections() )
		{
			if ( Connect( connection ) )
				dirty = true;
		}

		if ( dirty )
		{
			GetComponent<GridObject>().ForceUpdateNeighbors();
		}
	}

	public bool CanConnectTo( Component target )
	{
		if ( !target.IsValid() )
			return false;

		if ( IsConnectedTo( target ) )
			return false;

		// must be part of this queueline (or none)
		if ( target is Queue queue )
		{
			if ( QueueSystem.IsValid() && queue.QueueSystem.IsValid() && queue.QueueSystem != QueueSystem )
			{
				return false;
			}
		}

		int priority = GetPriority( target );
		if ( priority <= 0 )
			return false;

		return Connections.Any( c => c == null || GetPriority( c ) < priority );
	}

	public bool Connect( Component target )
	{
		if ( !CanConnectTo( target ) )
			return false;

		int priority = GetPriority( target );
		for ( int i = 0; i < Connections.Length; i++ )
		{
			var existing = Connections[i];

			if ( existing == null || GetPriority( existing ) < priority )
			{
				Connections[i] = target;

				if ( target is Queue queue )
				{
					if ( QueueSystem.IsValid() )
					{
						// connected - make sure it's considered on the system even if it's not really yet
						// (otherwise others might try and steal it for THEIRS and cause hell)
						queue.QueueSystem = QueueSystem;
					}

					queue.ConnectBack( this );
				}

				return true;
			}
		}

		return false;
	}

	public bool ConnectBack( Path path )
	{
		if ( !path.IsValid() || Connections.Contains( path ) )
			return false;

		int priority = GetPriority( path );

		for ( int i = 0; i < Connections.Length; i++ )
		{
			var existing = Connections[i];

			if ( existing == null || GetPriority( existing ) < priority )
			{
				Connections[i] = path;
				return true;
			}
		}

		return false;
	}

	/// <summary>
	/// For queues we already know our connections, so just check against those
	/// </summary>
	public override bool CanConnectTile( Vector3Int gridPos, TileEdge edge )
	{
		if ( GhostPreview )
			return false;

		// Only connects to connections
		var pos = gridPos + edge.GetDirection();
		var cell = GridManager.Instance?.GetCell( new Vector2Int( pos.x, pos.y ) );
		if ( cell is null )
			return false;

		int height = GetHeightOffset( edge );
		int heightSpan = height > 0 ? 0 : 1; // may be a connection from below

		return cell.GetComponents<IPathConnector>( gridPos.z, heightSpan ).Any( x => IsConnectedTo( x as Component ) );
	}

	public bool IsConnectedTo( Component c ) => Connections.Contains( c );

	int GetPriority( Component connection )
	{
		if ( !connection.IsValid() ) return -1;

		if ( connection is RideEntranceExit ) return 99;
		if ( connection is not Path ) return -1;

		int priority = 1;

		// Prioritize other queues over normal paths
		if ( connection is Queue queue )
		{
			if ( QueueSystem.IsValid() && queue.QueueSystem.IsValid() && queue.QueueSystem != QueueSystem )
				return -1;

			int connectedCount = 0;
			if ( queue.Connections[0] is { } c1 && c1 != this ) connectedCount++;
			if ( queue.Connections[1] is { } c2 && c2 != this ) connectedCount++;

			if ( connectedCount >= 2 )
				return -1; // fully connected queues aren't valid

			priority += 10;

			// Must be an end point
			if ( queue.QueueSystem != QueueSystem )
				priority -= 5;

			if ( connectedCount <= 1 )
				priority += 3;
		}

		return priority;
	}

	protected override void OnTileUpdated()
	{
		if ( Ride.IsValid() )
		{
			foreach ( var entry in Ride.QueueColors )
			{
				UpdateTint( entry.Key, entry.Value );
			}
		}
	}

	public void UpdateTint( TintChannel channel, Color color )
	{
		var tintComp = GameObject.GetComponentsInChildren<TintMaskComponent>().FirstOrDefault();
		if ( tintComp is not null )
		{
			if ( channel == TintChannel.Primary )
				tintComp.PrimaryColor = color;
			else if ( channel == TintChannel.Secondary )
				tintComp.SecondaryColor = color;
			else if ( channel == TintChannel.Accent )
				tintComp.AccentColor = color;
			else if ( channel == TintChannel.Detail )
				tintComp.DetailColor = color;
		}
	}
}