Park/Rides/BasicRide.EntranceExit.cs
using HC3.Terrain;
using System;

namespace HC3;

/// <summary>
/// A ride's entrance or exit point.
/// </summary>
public sealed class RideEntranceExit : Building, IGridObjectEvent
{
	[Property]
	public bool IsExit { get; set; }

	/// <summary>
	/// Which ride is this bound to?
	/// </summary>
	[Property]
	public BasicRide Ride { get; set; }

	public Path Path { get; set; }

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

		Cost = 0;
		GuestCost = 0;
		Need = null;

		BuildingType = BuildingType.Special;
		IsWalkable = true;
	}

	public void RotateToFaceRideEdge( TileEdge edge )
	{
		var dir = edge.GetDirection();
		WorldRotation = Rotation.LookAt( -new Vector3( dir.x, dir.y ) );
	}

	void IGridObjectEvent.NeighborsChanged( GridCell cell )
	{
		UpdatePath();

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

	/// <summary>
	/// Look for an associated path
	/// </summary>
	public void UpdatePath()
	{
		foreach ( var edge in PathMask.GetEdges() )
		{
			var neighbour = GridManager.Instance.GetCell( GridObject.GridBounds.Position + edge.RotateDegrees( -WorldRotation.Yaw() ).GetDirection() );
			if ( neighbour is null ) continue;

			if ( neighbour.GetComponents<Path>().FirstOrDefault( x => x.IsValid() && !x.GhostPreview ) is { } found )
			{
				Path = found;
				break;
			}
		}
	}
}

/// <summary>
/// Handles the exit and entrance point for a <see cref="BasicRide"/> -- we'll prompt the player to spawn a entrance and exit as part of the placement flow.
/// </summary>
partial class BasicRide : IGridObjectEvent
{
	[RequireComponent, Property]
	public QueueGuestSystem QueueSystem { get; set; } = new();

	[Sync( SyncFlags.FromHost )]
	private RideEntranceExit _entrance { get; set; }

	[Sync( SyncFlags.FromHost )]
	private RideEntranceExit _exit { get; set; }

	public bool HasEntrance => _entrance.IsValid();
	public bool HasExit => _exit.IsValid();

	public RideEntranceExit Entrance => _entrance!;
	public RideEntranceExit Exit => _exit!;

	public bool HasEntranceExit => HasEntrance && HasExit;

	/// <summary>
	/// How many guests are currently queueing for this ride.
	/// </summary>
	public int QueueingCount => QueueSystem?.GetGuestCount() ?? 0;

	public override bool IsAvailable()
	{
		return !IsBroken && HasEntranceExit && OpenState is OpenState.Open;
	}

	public override Vector3 GetEntrance()
	{
		if ( HasEntranceExit )
		{
			// Do we have queues?
			if ( QueueSystem.IsActive() )
				return QueueSystem.GetQueuePosition();

			return _entrance!.WorldPosition;
		}

		return WorldPosition;
	}

	protected override void OnStart()
	{
		if ( IsProxy )
		{
			UpdateEntranceExit();
		}

		base.OnStart();
	}

	public Vector3 GetExit()
	{
		return HasEntranceExit ? _exit!.WorldPosition : WorldPosition;
	}

	public virtual IEnumerable<(Vector3Int GridPos, TileEdge Edge)> ValidEntranceExitPositions
	{
		get
		{
			var bounds = GridObject.GridBounds;
			var height = (int)MathF.Round( WorldPosition.z / GridManager.HeightStep );

			for ( var x = bounds.Left; x < bounds.Right; x++ )
			{
				yield return (new Vector3Int( x, bounds.Top - 1, height ), TileEdge.YMin);
				yield return (new Vector3Int( x, bounds.Bottom, height ), TileEdge.YMax);
			}

			for ( var y = bounds.Top; y < bounds.Bottom; y++ )
			{
				yield return (new Vector3Int( bounds.Left - 1, y, height ), TileEdge.XMin);
				yield return (new Vector3Int( bounds.Right, y, height ), TileEdge.XMax);
			}
		}
	}

	void IGridObjectEvent.NeighborsChanged( GridCell cell )
	{
		UpdateEntranceExit();

		// Rebuild queue path
		QueueSystem.BuildQueuePath();
	}

	public bool AddToQueue( Guest guest )
	{
		if ( !QueueSystem.IsActive() )
			return false;

		// These delinquents will skip the queue! Naughty.
		// TODO: other guests may report this action.
		const float skipQueueChance = 0.8f;
		var hasQueueSkip = guest.Delinquency > 0.6f && Game.Random.Float() < skipQueueChance;
		int? queueIndex = hasQueueSkip ? 0 : null;

		return QueueSystem.Join( guest, queueIndex );
	}

	public bool RemoveFromQueue( Guest guest )
	{
		return QueueSystem.Leave( guest );
	}

	protected void UpdateEntranceExit()
	{
		// Check the entrance/exits are still in valid positions

		if ( _entrance.IsValid() )
		{
			var gridPos = GridManager.WorldToGridPosition3D( _entrance.WorldPosition );
			if ( !ValidEntranceExitPositions.Any( x => x.GridPos == gridPos ) )
			{
				_entrance.DestroyGameObject();
				_entrance = null;

				QueueSystem?.Signage?.DestroyGameObject();
			}
		}

		if ( _exit.IsValid() )
		{
			var gridPos = GridManager.WorldToGridPosition3D( _exit.WorldPosition );
			if ( !ValidEntranceExitPositions.Any( x => x.GridPos == gridPos ) )
			{
				_exit.DestroyGameObject();
				_exit = null;
			}
		}

		// Don't need to do anything
		if ( HasEntranceExit ) return;

		foreach ( var (gridPos, edge) in ValidEntranceExitPositions )
		{
			FindEntranceExit( gridPos, edge );
		}

		DirtyErrors();
	}

	private void FindEntranceExit( Vector3Int gridPos, TileEdge edge )
	{
		var gridPos2d = new Vector2Int( gridPos.x, gridPos.y );

		var entranceExit = GridManager.Instance.GetCell( gridPos2d )?
			.GetComponents<RideEntranceExit>()
			.FirstOrDefault();

		if ( !entranceExit.IsValid() ) return;
		if ( entranceExit.Ride.IsValid() ) return;

		entranceExit.RotateToFaceRideEdge( edge );

		if ( entranceExit.IsExit && !_exit.IsValid() )
		{
			_exit = entranceExit;
			entranceExit.Ride = this;
		}
		else if ( !entranceExit.IsExit && !_entrance.IsValid() )
		{
			_entrance = entranceExit;
			entranceExit.Ride = this;
		}

		foreach ( var kv in queueColors )
		{
			UpdateQueueColor( kv.Value, kv.Key );
		}

		GenerateDecorations();
	}

	public override void OnUnloaded( Guest guest )
	{
		base.OnUnloaded( guest );

		guest.SetSequenceOverride( null );

		guest.WorldPosition = GetExit();
		guest.Transform.ClearInterpolation();

		guest.ActionController.ClearAction();
	}
}