Park/Buildings/Building.cs
using HC3.Terrain;
using HC3.UI;
using System;
using System.Text.Json.Nodes;

namespace HC3;

/// <summary>
/// Some events for building types.
/// </summary>
public interface IBuildingEvents : ISceneEvent<IBuildingEvents>
{
	/// <summary>
	/// Called on the host when a guest enters the building.
	/// </summary>
	public void OnGuestEnter( Guest guest ) { }

	/// <summary>
	/// Called on the host when a guest leaves the building.
	/// </summary>
	public void OnGuestLeave( Guest guest ) { }

	/// <summary>
	/// Called on the host when the building state changes.
	/// </summary>
	public void OnStateChanged( OpenState state ) { }
}

public partial class Building : Component, IPathConnector, IInspectable, IPlacementObject
{
	/// <summary>
	/// The building's <see cref="HC3.GridObject"/>
	/// </summary>
	[RequireComponent]
	public GridObject GridObject { get; private set; }

	/// <summary>
	/// The size of this building in tiles. Defaults to 1x1.
	/// </summary>
	[Property, Feature( "Building" )]
	public Vector2Int Size { get; set; } = 1;

	/// <summary>
	/// Should this building satisfy a need?
	/// </summary>
	[Property, Feature( "Needs" )]
	public bool SatisfyNeed { get; set; } = true;

	/// <summary>
	/// Which need is satisfied by using this building?
	/// </summary>
	[Property, Feature( "Needs" ), ShowIf( nameof( SatisfyNeed ), true )]
	public NeedDefinition Need { get; set; }

	/// <summary>
	/// How much of this need should be decreased when a guest uses the building.
	/// </summary>
	[Property, Feature( "Needs" ), ShowIf( nameof( SatisfyNeed ), true )]
	public float NeedDecrease { get; set; } = 1f;

	/// <summary>
	/// How steep of a slope can this be built on? The shallowest slope has gradient <c>1</c>.
	/// </summary>
	[Property, Feature( "Building" )]
	public int MaxGradient { get; set; } = 0;

	/// <summary>
	/// How much money this building costs for a guest to use it.
	/// </summary>
	[Property, Feature( "Balance" )]
	public int GuestCost { get; set; } = 50;

	/// <summary>
	/// Cost to place this in the world.
	/// </summary>
	[Property, Feature( "Balance" )]
	public int Cost { get; set; } = 15;

	/// <summary>
	/// How many times has this building been used?
	/// </summary>
	[Sync( SyncFlags.FromHost )]
	public int Uses { get; set; }

	/// <summary>
	/// Usage string, used with <see cref="GetUseString()"/> by default.
	/// </summary>
	[Property]
	protected string UseString { get; set; } = "Has been used {0} time(s)";

	/// <summary>
	/// The inspector locale string for usage of this building.
	/// </summary>
	/// <returns></returns>
	public virtual string GetUseString() => string.Format( UseString, Uses );

	/// <summary>
	/// The building's name
	/// </summary>
	[Property, Feature( "Building" ), Group( "Display" )]
	public string Title { get; set; } = "Building";

	/// <summary>
	/// The thumbnail for this building
	/// </summary>
	[Property, Feature( "Building" ), Group( "Display" ), ImageAssetPath]
	public string Thumbnail { get; set; } = "textures/thumbnails/missing_thumbnail.png";

	/// <summary>
	/// The type of building -- used for categorization in the build menu.
	/// </summary>
	[Property, Feature( "Building" ), Group( "Display" )]
	public BuildingType BuildingType { get; set; } = BuildingType.Shop;


	/// <summary>
	/// The material to use for this building. This decides what sound to play when placing the building. (Why not just a SoundEvent?)
	/// </summary>
	[Property, Feature( "Building" ), Group( "Display" )]
	public ObjectMaterial ObjectMaterial { get; set; } = ObjectMaterial.Default;

	/// <summary>
	/// The grouping for this building -- used for the build menu.
	/// </summary>
	public virtual Group Group => new( "Building", BuildingType.ToString() );

	// TODO: Get rid of this please

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

	/// <summary>
	/// Is this a walkable building? 
	/// </summary>
	[Property, Feature( "Navigation" )]
	public bool IsWalkable { get; set; }

	/// <summary>
	/// For shops and entrances / exits, which tile edges can it connect to paths with?
	/// </summary>
	[Property, Feature( "Navigation" )]
	[ShowIf( nameof( BuildingType ), BuildingType.Shop ), ShowIf( nameof( BuildingType ), BuildingType.Special )]
	public PathMask PathMask { get; set; } = PathMask.Down;

	/// <summary>
	/// How many extra guests does this give us for the park?
	/// </summary>
	[Property, Feature( "Balance" )]
	public int AddedGuests { get; set; } = 0;

	[Sync( SyncFlags.FromHost )]
	private OpenState _openState { get; set; } = OpenState.Open;

	/// <summary>
	/// The current open state of the building. <see cref="OpenState.Testing"/> runs the ride without guests.
	/// </summary>
	public OpenState OpenState
	{
		get => _openState;
		set => ModifyOpenState( value );
	}

	[Rpc.Host]
	private void ModifyOpenState( OpenState value )
	{
		_openState = value;
		OpenStateChanged();
	}

	[Sync( SyncFlags.FromHost )]
	public string CustomTitle { get; set; }

	/// <summary>
	/// Set the building title. This will be networked to other players.
	/// </summary>
	[Rpc.Broadcast]
	public void SetBuildingTitle( string title )
	{
		CustomTitle = title;
		OnTitleChanged?.Invoke( title );

		Stats.Increment( "building.renamed" );
	}

	/// <summary>
	/// Check whether or not this building has been placed or is currently being placed.
	/// </summary>
	public bool IsPlaced
	{
		get => _isPlaced;
		set
		{
			_isPlaced = value;

			GridObject.Enabled = value;

			if ( value )
			{
				ActiveBuildings.Add( this );
				DirtyErrors();
			}
			else
			{
				ActiveBuildings.Remove( this );
			}
		}
	}

	/// <summary>
	/// Does this building snap to the grid?
	/// </summary>
	[Property]
	public bool SnapToGrid { get; set; } = true;

	/// <summary>
	/// Is this a placed Building -- not being currently placed?
	/// </summary>
	private bool _isPlaced = false;

	/// <summary>
	/// Called when the building title is changed
	/// </summary>
	public Action<string> OnTitleChanged;

	/// <summary>
	/// Called when the open state is changed
	/// </summary>
	public Action<OpenState> OnOpenStateChanged;

	/// <summary>
	/// Right now just a collection of all enabled buildings, we can later what active means, have separate sets for open/closed buildings, etc.
	/// Conna: should this be synchronized? Do remote clients need to know which buildings are active?
	/// tony: Why does this exist
	/// </summary>
	public static HashSet<Building> ActiveBuildings = new();

	protected override void OnValidate()
	{
		GridObject ??= GetOrAddComponent<GridObject>();
		GridObject.LocalBounds = new RectInt( -Size / 2, Size );
		GridObject.GridFlags = GridObjectFlags.BlocksConstruction | (IsWalkable ? GridObjectFlags.IsWalkable : 0);

		SetupErrors();
	}

	/// <summary>
	/// Can this guest enter the building? If not, they'll stay queueing
	/// </summary>
	public virtual bool CanEnter( Guest guest )
	{
		return IsAvailable() && HasFreeSlot();
	}

	/// <summary>
	/// Calculates how interested a guest is in using this building based on a bunch of information
	/// TODO: Expose an interface so systems can hook in
	/// </summary>
	/// <param name="guest"></param>
	/// <returns></returns>
	public virtual float GetInterestWeight( Guest guest )
	{
		var weight = 100.0f;

		// Less likely to go on the rides that are more expensive (in relation to the guest's money)
		weight -= (GuestCost / guest.Money) * 40f;

		// Less likely to go to anything with a longer queue
		if ( Components.TryGet<QueueGuestSystem>( out var queueGuestSystem ) )
		{
			weight -= MathF.Min( queueGuestSystem.GetGuestCount() / 10f, 20 );
		}

		// Less likely to go on rides they've already been on (only rides since we likely want to use the same toilet/shop multiple times)
		if ( this is BasicRide )
		{
			if ( guest.BuildingsVisited.Contains( this ) )
			{
				weight -= 20f;
			}
		}

		// Less likely to go to rides that are far away
		var distance = (guest.WorldPosition - WorldPosition).Length;
		weight -= MathF.Min( distance / 10f, 20f );

		// Ever so slightly more likely to go on larger (more impressive) rides
		weight += Size.Length * 0.5f;

		return weight;
	}

	/// <summary>
	/// Is this building currently open and operational
	/// </summary>
	public virtual bool IsAvailable()
	{
		return HasFreeSlot() && OpenState is OpenState.Open;
	}

	/// <summary>
	/// Gets a description for a guest when they're using this building
	/// </summary>
	/// <param name="guest"></param>
	/// <returns></returns>
	public virtual string GetInUseDescription( Guest guest )
	{
		return $"Using {Title}";
	}

	/// <summary>
	/// Status description of what this building is doing
	/// </summary>
	public virtual string GetStatus()
	{
		return OpenState.ToString();
	}

	/// <summary>
	/// Called when the OpenState is changed
	/// </summary>
	protected virtual void OpenStateChanged()
	{
		OnOpenStateChanged?.Invoke( OpenState );
		IBuildingEvents.PostToGameObject( GameObject, x => x.OnStateChanged( OpenState ) );
	}

	/// <summary>
	/// Called when used by a Guest
	/// </summary>
	/// <param name="guest"></param>
	public virtual bool OnUse( Guest guest )
	{
		var need = guest.Needs.GetNeed( Need );

		if ( need != null )
			need.Level -= NeedDecrease;

		Stats.Increment( "building.used" );
		Stats.Increment( $"building.used.{Title.ToIdentifier()}" );

		guest.BuildingsVisited.Add( this );

		return true;
	}

	protected override void OnEnabled()
	{
		if ( IsPlaced )
		{
			ActiveBuildings.Add( this );
			DirtyErrors();
		}

		GenerateDecorations();
	}

	protected override void OnStart()
	{
		UpdateColors();
	}

	protected override void OnDisabled()
	{
		if ( IsPlaced ) ActiveBuildings.Remove( this );
	}

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

		using ( Gizmo.Scope( "building" ) )
		{
			var gridSize = GridManager.GridSize;
			Gizmo.Draw.Color = Color.Green;
			Gizmo.Draw.LineThickness = 2;
			Gizmo.Draw.LineBBox( new BBox( -Size * gridSize / 2f, Size * gridSize / 2f ) );

			if ( BuildingType is BuildingType.Shop or BuildingType.Special )
			{
				foreach ( var edge in GridManager.AllEdges )
				{
					if ( (PathMask & edge.ToPathMask()) != 0 )
					{
						Vector3 dir = (Vector2)edge.GetDirection();
						Gizmo.Draw.Arrow( dir * 96f + Vector3.Up * 16f, dir * 32f + Vector3.Up * 16f );
					}
				}
			}
		}
	}

	/// <summary>
	/// Returns the tile range occupied by this building, taking into account rotation.
	/// </summary>
	/// <param name="rot"></param>
	/// <returns></returns>
	public RectInt GetTileRange( float? rot = null )
	{
		var min = -Size / 2;
		var max = min + Vector2Int.Max( 0, Size );

		rot ??= WorldRotation.Yaw();
		if ( rot == 90 || rot == 270 )
		{
			min = new Vector2Int( min.y, min.x );
			max = new Vector2Int( max.y, max.x );
		}

		var worldToGrid = GridManager.WorldToGridPosition( WorldPosition );
		min += worldToGrid;
		max += worldToGrid;

		return new RectInt( min, max - min );
	}

	public virtual Vector3 GetEntrance()
	{
		if ( BuildingType is BuildingType.Shop )
		{
			var gridPos = GridManager.WorldToGridPosition( WorldPosition );
			var height = (int)MathF.Round( WorldPosition.z / GridManager.HeightStep );

			foreach ( var edge in Enum.GetValues<TileEdge>() )
			{
				var rotation = (int)MathF.Round( WorldRotation.Yaw() / 90f );

				if ( (PathMask & edge.Rotate( rotation ).ToPathMask()) != 0 )
				{
					var cell = GridManager.Instance.GetCell( gridPos + edge.GetDirection() );
					if ( cell is null ) continue;

					if ( cell.GetConnection( new Vector3Int( cell.Position.x, cell.Position.y, height ), edge.GetOpposite() ) is not null )
					{
						return (GridManager.GridToWorldPosition( cell.Position ) + GridManager.CentreOffset).WithZ( WorldPosition.z );
					}
				}
			}
		}

		return WorldPosition;
	}

	/// <summary>
	/// Can we connect this building to this tile?
	/// </summary>
	/// <param name="gridPos"></param>
	/// <param name="edge"></param>
	/// <returns></returns>
	public virtual bool CanConnectTile( Vector3Int gridPos, TileEdge edge )
	{
		// TODO: non-shops

		if ( BuildingType is BuildingType.Shop or BuildingType.Special )
		{
			return PathMask.CanConnectTile( edge.RotateDegrees( WorldRotation.Yaw() ) );
		}

		return false;
	}

	/// <summary>
	/// Called when we're inspecting this building (clicking it to open UI)
	/// </summary>
	/// <returns></returns>
	Window IInspectable.Select()
	{
		var building = this;

		if ( building is RideEntranceExit entrance )
		{
			if ( entrance.Ride.IsValid() ) building = entrance.Ride;
			else return null;
		}

		var inspector = new BuildingInspector();
		inspector.Building = building;

		return inspector;
	}

	/// <summary>
	/// Accessor to get the TintMaskComponent on this building, if it has one.
	/// </summary>
	/// <returns></returns>
	public TintMaskComponent GetTintComponent()
	{
		return GetComponentsInChildren<TintMaskComponent>().FirstOrDefault();
	}

	/// <summary>
	/// Called when the colour has changed from the TintMaskCopmonent -- we need to update our channel data.
	/// </summary>
	/// <param name="component"></param>
	void ITintMaskEvents.OnColorsChanged( TintMaskComponent component )
	{
		var tintMask = GetTintComponent();

		if ( tintMask != component )
			return;

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

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

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

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

	/// <summary>
	/// Updates the TintMaskComponent with our current channel colors.
	/// </summary>
	void UpdateColors()
	{
		if ( !Networking.IsHost )
			return;

		var tintMask = GetTintComponent();
		if ( tintMask is null ) return;

		tintMask.PrimaryColor = RedChannel.Color;
		tintMask.SecondaryColor = GreenChannel.Color;
		tintMask.AccentColor = BlueChannel.Color;
		tintMask.DetailColor = AlphaChannel.Color;
	}

	/// <summary>
	/// Some buildings might want to generate decorations by default when placed.
	/// </summary>
	protected virtual void GenerateDecorations()
	{
		foreach ( var existing in GameObject.Children.Where( x => x.Tags.Has( "decor_generated" ) ).ToArray() )
		{
			existing.Destroy();
		}
	}

	/// <summary>
	/// Lets us add some extra state for a GUEST when saving them.. This... Kinda fucking sucks.
	/// </summary>
	/// <param name="guest"></param>
	/// <returns></returns>
	internal JsonNode GetStateJson( Guest guest )
	{
		return new JsonObject()
		{
			// Store grid of the building so we can recall it when we've loaded everything and slot guests back in
			["Position"] = GridManager.Instance.Terrain.WorldToGrid( WorldPosition ).ToString()
		};
	}
}

/// <summary>
/// A type of building, used for categorization in the build menu.
/// </summary>
public enum BuildingType
{
	/// <summary>
	/// A ride
	/// </summary>
	[Icon( "world" )]
	Ride,
	/// <summary>
	/// A shop
	/// </summary>
	[Icon( "money_add" )]
	Shop,
	/// <summary>
	/// Sideshows and other entertainment buildings
	/// </summary>
	[Icon( "bell" )]
	Sideshows,

	/// <summary>
	/// Reserved for stuff that shouldn't appear in the build menu.
	/// </summary>
	Special
}