Editor/Builder/MapBuilder.Entities.cs
namespace BspImport.Builder;

using BspImport.Builder.Entities;
using System.Threading;
using System.Threading.Tasks;

public partial class MapBuilder
{
	/// <summary>
	/// Register common entity class handlers
	/// </summary>
	private void SetupEntityHandlers()
	{
		Handlers.Clear();

		Handlers.Add( "prop_static", BaseEntities.HandleStaticPropEntity );
		Handlers.Add( "prop_physics", BaseEntities.HandlePhysicsPropEntity );
		Handlers.Add( "prop_dynamic", BaseEntities.HandleDynamicPropEntity );
		Handlers.Add( "info_player_start", BaseEntities.HandlePlayerStartEntity );
		Handlers.Add( "light", BaseEntities.HandleLightEntity );
		Handlers.Add( "light_spot", BaseEntities.HandleSpotLightEntity );
		Handlers.Add( "light_environment", BaseEntities.HandleLightEnvironmentEntity );
	}

	private readonly Dictionary<string, Action<GameObject, LumpEntity, GameObject, BuildSettings>> Handlers = new();

	// TODO: add filter system including target game so these rules can be split up
	private List<string> EntityClassBlacklist = new()
	{
				"worldspawn",
				"info_node",
				"info_node_air",
				"env_sun",
				"sky_camera",
				"path_track",
				"water_lod_control",
				"func_areaportal",
				"shadow_control",
				"env_skypaint",
				"lua_run",
				"path_corner",
				"info_hint",
				"info_node_air_hint",
				"info_node_climb",
				"info_node_hint",
				"filter_multi",
				"point_template",
				"filter_activator_class",
				"point_message",
				"item_item_crate",
				"game_round_win",
				"filter_activator_tfteam",
				"item_ammopack_small",
				"item_ammopack_medium",
				"item_ammopack_full",
				"item_healthkit_small",
				"item_healthkit_medium",
				"item_healthkit_full",
				"info_player_teamspawn",
				"team_control_point",
				"point_devshot_camera",
				"info_observer_point",
				"info_intermission",
				"team_round_timer",
				"team_control_point_master",
				"item_teamflag",
				"info_null",
				"game_intro_viewpoint",
				"info_player_terrorist",
				"info_player_counterterrorist",
				"func_areaportalwindow"
	};

	private bool IsAllowedEntity( LumpEntity ent )
	{
		if ( ent.ClassName is null || EntityClassBlacklist.Contains( ent.ClassName ) )
			return false;

		if ( ent.ClassName.Contains( "logic" ) )
			return false;

		var isModel = BaseEntities.IsModelEntity( ent );

		var leafIndex = TreeParse.FindLeafIndex( ent.Position );
		var leafArea = Context.Leafs![leafIndex].Area;

		var cullSkyboxModel = isModel && Context.BuildSettings.CullSkybox ? Context.SkyboxAreas.Contains( leafArea ) : false;
		var cullModel = isModel ? !Context.BuildSettings.LoadModels : false;
		return !cullModel && !cullSkyboxModel;
	}

	/// <summary>;
	/// Build entities parsed from entity lump and static props. Does not include brush entities.
	/// </summary>
	protected virtual async Task BuildEntities( GameObject _parent, IProgressSection progress, int entitiesPerFrame, CancellationToken token )
	{
		if ( Context.Entities is null )
			return;

		// for deduplicating "unhandled entity" messages
		var unhandledEntities = new HashSet<string>();

		if ( token.IsCancellationRequested )
			return;

		Log.Info( "Building Entities..." );
		progress.Title = "Building Entities...";

		var brushEntities = Context.Entities.Where( ent => ent.IsBrushEntity && IsAllowedEntity( ent ) );
		var pointEntities = Context.Entities.Where( ent => !ent.IsBrushEntity && IsAllowedEntity( ent ) );

		int total = brushEntities.Count() + pointEntities.Count();
		int count = 0;

		progress.TotalCount = total;
		progress.Current = count;

		if ( brushEntities.Any() )
		{
			var parent = new GameObject( _parent, true, "Mesh Entities" );

			progress.Subtitle = $"Building {brushEntities.Count()} Mesh Entities...";

			foreach ( var ent in brushEntities )
			{
				if ( token.IsCancellationRequested )
					return;

				if ( ent.ClassName is null )
					continue;

				// ... so we can gurantee they get their meshes
				var meshObj = CreateBrushEntity( ent, parent );

				count++;
				progress.Current = count;

				if ( !meshObj.IsValid() )
					continue;

				// try to find handlers based on classname
				if ( Handlers.TryGetValue( ent.ClassName, out var handler ) )
				{
					// apply class components via registered handler
					handler.Invoke( meshObj, ent, parent, Context.BuildSettings );
				}
				else
				{
					if ( !unhandledEntities.Contains( ent.ClassName ) )
					{
						unhandledEntities.Add( ent.ClassName );
						Log.Warning( $"unhandled entity class: {ent.ClassName}" );
					}
				}

				if ( count % entitiesPerFrame == 0 )
				{
					await GameTask.Yield();
				}
			}
		}

		if ( token.IsCancellationRequested )
			return;

		if ( pointEntities.Any() )
		{
			var parent = new GameObject( _parent, true, "Point Entities" );

			progress.Subtitle = $"Building {pointEntities.Count()} Point Entities...";

			foreach ( var ent in pointEntities )
			{
				if ( token.IsCancellationRequested )
					return;

				if ( ent.ClassName is null )
					continue;

				// try to find handlers based on classname
				if ( Handlers.TryGetValue( ent.ClassName, out var handler ) )
				{
					var newObj = CreatePointEntity( ent, parent );

					// apply class components via registered handler
					handler.Invoke( newObj, ent, parent, Context.BuildSettings );
				}
				else
				{
					if ( !unhandledEntities.Contains( ent.ClassName ) )
					{
						unhandledEntities.Add( ent.ClassName );
						Log.Warning( $"unhandled entity class: {ent.ClassName}" );
					}
				}

				// unhandled entities still count towards total
				count++;
				progress.Current = count;


				if ( count % entitiesPerFrame == 0 )
				{
					await GameTask.Yield();
				}
			}
		}
	}

	/// <summary>
	/// Create a basic point entity with Position and Rotation.
	/// </summary>
	/// <param name="ent"></param>
	/// <param name="parent"></param>
	/// <returns></returns>
	private static GameObject CreatePointEntity( LumpEntity ent, GameObject parent )
	{
		var obj = new GameObject( parent, true, ent.TargetName );
		obj.WorldPosition = ent.Position;
		obj.WorldRotation = ent.Angles.ToRotation();

		return obj;
	}

	/// <summary>
	/// Creates a GameObject based on a brush entity. If the entity doesn't have a valid model this will return null.
	/// </summary>
	/// <param name="ent"></param>
	/// <param name="parent"></param>
	/// <returns></returns>
	private GameObject? CreateBrushEntity( LumpEntity ent, GameObject parent )
	{
		if ( ent.Model is null || !Context.BuildSettings.ImportEntities )
			return null;

		var modelIndex = int.Parse( ent.Model.TrimStart( '*' ) );
		var polyMesh = Context.CachedPolygonMeshes?[modelIndex];

		if ( polyMesh is null )
			return null;

		var brushEntity = CreatePointEntity( ent, parent );

		var meshComponent = brushEntity.Components.Create<MeshComponent>();

		if ( ent.ClassName!.Contains( "trigger" ) )
		{
			meshComponent.Tags.Add( "trigger" );
			meshComponent.IsTrigger = true;
			meshComponent.HideInGame = true;

			foreach ( var face in polyMesh.FaceHandles )
			{
				polyMesh.SetFaceMaterial( face, "materials/tools/toolstrigger.vmat" );
			}
		}
		meshComponent.Mesh = polyMesh;

		CenterMeshOrigin( meshComponent );

		//var propComponent = brushEntity.Components.Create<Prop>();
		//propComponent.Model = polyMesh.Rebuild();
		//propComponent.IsStatic = true;

		return brushEntity;
	}

}