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

namespace HC3;

/// <summary>
/// A basic ride. The difference between this and a building is that rides have loadable slots and can start/end rides.
/// They can also have queues.
/// </summary>
public partial class BasicRide : Building, ITintMaskEvents
{
	[Property, Feature( "Ride" )] public int GuestLimit { get; set; } = 10;

	[Title( "Wait For" ), Inspectable] public LoadMode MinimumLoad { get; set; } = LoadMode.NotEmpty;
	[Property, Feature( "Ride" ), Inspectable( Suffix = "seconds" )] public float RideLength { get; set; } = 30.0f;
	[Property, Feature( "Ride" ), Inspectable( Suffix = "seconds" ), MinMax( 5, 120 )] public float MinimumLoadTime { get; set; } = 5.0f;
	[Property, Feature( "Ride" ), Inspectable( Suffix = "seconds" ), MinMax( 5, 120 )] public float MaximumLoadTime { get; set; } = 60.0f;

	[Sync( SyncFlags.FromHost )]
	public bool RideStarted { get; private set; }

	[Sync( SyncFlags.FromHost )]
	public float Durability { get; private set; } = 1f;

	[Sync( SyncFlags.FromHost ), Change( nameof( OnIsBrokenChanged ) )]
	public bool IsBroken { get; private set; }

	public MusicPlayer MusicPlayer { get; private set; }

	public TimeSince RideTime { get; set; } = 0.0f;
	public TimeSince LoadingTime { get; set; } = 0f;

	public override Group Group => new( "Building", BuildingType.ToString() );

	/// <summary>
	/// Tint channels for this building. These are used to tint the building's sections.
	/// </summary>
	public TintChannelData QueueRedChannel { get; set; } = new( true, Color.White );
	public TintChannelData QueueGreenChannel { get; set; } = new( false, Color.White );
	public TintChannelData QueueBlueChannel { get; set; } = new( false, Color.White );
	public TintChannelData QueueAlphaChannel { get; set; } = new( false, Color.White );

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

		MusicPlayer = GetOrAddComponent<MusicPlayer>();
	}

	protected override void OnUpdate()
	{
		if ( !Networking.IsHost )
			return;

		if ( !RideStarted )
		{
			if ( LoadingTime < MinimumLoadTime )
				return;

			int slotCount = 0;
			int occupiedCount = 0;
			foreach ( var s in GetLoadableSlots() )
			{
				slotCount++;
				if ( !s.IsFree() ) occupiedCount++;
			}

			if ( occupiedCount >= GuestLimit || (MinimumLoad.IsSatisfied( occupiedCount, slotCount ) && LoadingTime >= MaximumLoadTime) )
			{
				StartRide();
			}
		}
		else if ( RideLength > 0 && RideTime >= RideLength )
		{
			EndRide();
		}

		if ( RideStarted )
		{
			var guests = GetGuests();

			foreach ( var guest in guests )
			{
				guest.Needs.Happiness += Time.Delta * 0.015f;
			}

			// Rides start to lose durability while they are being used
			Durability -= Time.Delta * 0.003f;
			Durability = Durability.Clamp( 0f, 1f );
		}
		else
		{
			if ( Durability <= 0f && !IsBroken )
			{
				BreakDown();
			}
		}
	}

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

		if ( !Networking.IsHost )
			return;

		if ( _entrance.IsValid() )
			_entrance.DestroyGameObject();

		if ( _exit.IsValid() )
			_exit.DestroyGameObject();
	}

	public override bool CanEnter( Guest guest )
	{
		if ( RideStarted ) return false;

		return base.CanEnter( guest );
	}

	/// <summary>
	/// What sequence to run while on this ride?
	/// </summary>
	[Property] public string LoadedSequence { get; set; } = "SitPose_Default";

	public override void OnLoadedIn( Guest guest )
	{
		if ( !string.IsNullOrEmpty( LoadedSequence ) )
			guest.SetSequenceOverride( LoadedSequence );

		if ( GetOccupiedSlots() == 1 )
		{
			LoadingTime = 0f;
		}
	}
	public override string GetInUseDescription( Guest guest )
	{
		return $"Riding {Title}";
	}

	public override string GetStatus()
	{
		if ( OpenState is OpenState.Open )
		{
			if ( RideStarted )
				return "Running";
			else
				return "Waiting for guests";
		}

		return base.GetStatus();
	}

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

		LoadingTime = 0;

		if ( OpenState is not OpenState.Open )
		{
			EndRide();
			UnloadAll( false );
		}
	}

	protected virtual bool CanStart()
	{
		return HasEntranceExit && OpenState is OpenState.Open;
	}

	public void Repair()
	{
		if ( !IsBroken )
			return;

		Durability = 1f;
		IsBroken = false;
		MusicPlayer.IsBrokenDown = false;

		OnRideRepaired();

		Stats.Increment( "staff.rides_repaired" );
	}

	public void Damage( float amount )
	{
		Durability = Math.Clamp( Durability - amount, 0f, 1f );
	}

	public void BreakDown()
	{
		IsBroken = true;
		MusicPlayer.IsBrokenDown = true;
		Durability = 0f;
		OnRideBroken();
	}

	private void OnIsBrokenChanged()
	{
		DirtyErrors();
	}

	[Button( "Test Ride" )]
	protected void StartRide()
	{
		if ( RideStarted ) return;
		if ( !CanStart() ) return;

		RideStarted = true;
		RideTime = 0f;

		OnRideStarted();
	}

	protected void EndRide()
	{
		if ( !RideStarted ) return;

		RideStarted = false;
		LoadingTime = 0;

		var guestCount = GetOccupiedSlots();

		OnRideEnded();

		Uses += guestCount;
	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	protected virtual void OnRideStarted()
	{

	}

	[Rpc.Broadcast( NetFlags.HostOnly )]
	protected virtual void OnRideEnded()
	{
		// Eject all guests
		UnloadAll( true );
	}

	protected virtual void OnRideBroken()
	{

	}

	protected virtual void OnRideRepaired()
	{

	}

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

		var offset = new Vector3( -Size * GridManager.GridSize / 2.0f, 0 );

		// TODO: do flooring type from the exit? no filled pieces right now so not bothering
		/*
		for ( int x = 0; x < Size.x; x++ )
		{
			for ( int y = 0; y < Size.y; y++ )
			{
				var position = offset + (new Vector3( x, y, 0 ) * GridManager.GridSize) + GridManager.CentreOffset;

				var path = GameObject.Clone( "prefabs/paths/path_single.prefab", global::Transform.Zero, GameObject );
				path.Flags = GameObjectFlags.NotSaved;
				path.Tags.Add( "decor_generated" );
				path.WorldRotation = Rotation.Identity;
				path.LocalPosition = position;
			}
		}
		*/

		foreach ( var edge in GridManager.AllEdges )
		{
			int length = edge is TileEdge.Left or TileEdge.Right ? Size.y : Size.x;
			Vector2Int dir = edge.GetDirection();

			for ( int i = 0; i < length; i++ )
			{
				Vector2Int pos = edge switch
				{
					TileEdge.Left => new Vector2Int( 0, i ),
					TileEdge.Right => new Vector2Int( Size.x - 1, i ),
					TileEdge.Down => new Vector2Int( i, 0 ),
					TileEdge.Up => new Vector2Int( i, Size.y - 1 ),
					_ => default
				};

				Vector3 localPosition = offset + new Vector3( pos.x, pos.y ) * GridManager.GridSize + GridManager.CentreOffset;

				Vector3 worldPosition = Transform.World.PointToWorld( localPosition );
				Vector3Int gridAdjacent = GridManager.WorldToGridPosition( worldPosition ) + edge.RotateDegrees( -WorldRotation.Yaw() ).GetDirection();

				// skip entrance/exit adjacent tiles
				if ( (_entrance.IsValid() && GridManager.WorldToGridPosition( _entrance.WorldPosition ) == gridAdjacent) ||
					(_exit.IsValid() && GridManager.WorldToGridPosition( _exit.WorldPosition ) == gridAdjacent) )
				{
					continue;
				}

				var fence = GameObject.Clone( "prefabs/fences/fence_small.prefab", global::Transform.Zero, GameObject );
				fence.Tags.Add( "decor_generated" );
				fence.Flags = GameObjectFlags.NotSaved;
				fence.LocalPosition = localPosition;

				var facing = edge.GetOpposite().GetDirection();
				fence.LocalRotation = Rotation.LookAt( new Vector3( facing.x, facing.y ) );
			}
		}
	}


	/// <summary>
	/// The start of the queue (where the entrance is) if it exists
	/// </summary>
	public Queue GetQueueStart()
	{
		if ( !_entrance.IsValid() ) return null;
		return _entrance.Path as Queue;
	}

	/// <summary>
	/// The end of the queue (where the sign is) if it exists
	/// </summary>
	public Queue GetQueueEnd()
	{
		return QueueSystem._queuePath.LastOrDefault() ?? GetQueueStart();
	}

	public TintMaskComponent GetQueueTintComponent()
	{
		if ( !_entrance.IsValid() ) return null;
		var queue = GetQueueStart();
		if ( !queue.IsValid() ) return null;
		var tintComp = queue.GameObject.GetComponentsInChildren<TintMaskComponent>().FirstOrDefault();
		if ( tintComp is not null )
		{
			return tintComp;
		}
		return null;
	}

	private Dictionary<TintChannel, Color> queueColors { get; set; } = new();
	public Dictionary<TintChannel, Color> QueueColors => queueColors;

	public void UpdateQueueColor( Color color, TintChannel channel )
	{
		var end = _entrance;
		if ( !end.IsValid() ) return;

		queueColors[channel] = color;

		foreach ( var path in end.GetComponentsInChildren<TintMaskComponent>() )
		{
			if ( !path.IsValid() ) continue;

			if ( channel == TintChannel.Primary )
				path.PrimaryColor = color;
			else if ( channel == TintChannel.Secondary )
				path.SecondaryColor = color;
			else if ( channel == TintChannel.Accent )
				path.AccentColor = color;
			else if ( channel == TintChannel.Detail )
				path.DetailColor = color;
		}

		if ( !QueueSystem.IsActive() ) return;

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

			path.UpdateTint( channel, color );
		}
	}

	void ITintMaskEvents.OnColorsChanged( TintMaskComponent component )
	{
		var tintMask = GetTintComponent();

		if ( tintMask != component )
			return;

		var red = QueueRedChannel;
		red.Color = component.PrimaryColor;
		QueueRedChannel = red;

		var green = QueueGreenChannel;
		green.Color = component.SecondaryColor;
		QueueGreenChannel = green;

		var blue = QueueBlueChannel;
		blue.Color = component.AccentColor;
		QueueBlueChannel = blue;

		var alpha = QueueAlphaChannel;
		alpha.Color = component.DetailColor;
		QueueAlphaChannel = alpha;
	}
}