Editor/Builder/MapBuilder.Geometry.cs
using SkiaSharp;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace BspImport.Builder;

public partial class MapBuilder
{
	/// <summary>
	/// Builds cached PolygonMeshes for bsp models, skips index 0 (worldspawn).
	/// </summary>
	/// <param name="progress">Current progress section</param>
	/// <param name="token">Progress cancellation token</param>
	public async Task BuildModelMeshes( IProgressSection progress, CancellationToken token )
	{
		var modelCount = Context.Models?.Length ?? 0;

		if ( modelCount <= 0 )
		{
			Log.Error( $"Unable to build bsp models, Context has no Models!" );
			return;
		}

		Log.Info( $"Constructing {modelCount} Entity Models..." );
		progress.Title = $"Constructing {modelCount} Entity Models...";
		progress.TotalCount = modelCount;
		progress.Current = 0;

		var polyMeshes = new PolygonMesh[modelCount];

		// i = 1 to skip 0 (worldspawn)
		for ( int i = 1; i < modelCount; i++ )
		{
			if ( token.IsCancellationRequested )
				return;

			var polyMesh = ConstructModel( i );
			progress.Current = i;

			if ( polyMesh is null )
				continue;

			polyMeshes[i] = polyMesh;

			await GameTask.Yield();
		}

		Context.CachedPolygonMeshes = polyMeshes;
	}

	/// <summary>
	/// Find all areas with a sky_camera entity in them.
	/// </summary>
	private List<short> FindSkyboxAreas()
	{
		var result = new List<short>();

		ArgumentNullException.ThrowIfNull( Context.Entities );

		foreach ( var ent in Context.Entities )
		{
			if ( ent.ClassName != "sky_camera" )
				continue;

			var origin = ent.Position;

			var leafIndex = TreeParse.FindLeafIndex( origin );
			if ( leafIndex == -1 )
				continue;

			var leaf = Context.Leafs![leafIndex];
			result.Add( leaf.Area );
		}

		return result;
	}

	/// <summary>
	/// Constructs quad corners for a plane.
	/// </summary>
	/// <param name="p"></param>
	private List<Vector3> BuildBasePolygon( Plane p )
	{
		var normal = p.Normal;

		// pick a tangent
		Vector3 up = Math.Abs( normal.z ) > 0.99f ? Vector3.Forward : Vector3.Up;
		Vector3 right = Vector3.Cross( up, normal ).Normal;
		up = Vector3.Cross( normal, right );

		float size = 16384f; // big enough for BSP scale

		Vector3 center = normal * p.Distance;
		return new List<Vector3>
		{
			center + (-right - up) * size,
			center + ( right - up) * size,
			center + ( right + up) * size,
			center + (-right + up) * size,
		};
	}

	List<Vector3> ClipPolygon( List<Vector3> input, Plane plane )
	{
		var output = new List<Vector3>();

		for ( int i = 0; i < input.Count; i++ )
		{
			var a = input[i];
			var b = input[(i + 1) % input.Count];

			float da = Vector3.Dot( plane.Normal, a ) - plane.Distance;
			float db = Vector3.Dot( plane.Normal, b ) - plane.Distance;

			const float EPS = 0.001f;

			bool ina = da <= EPS;
			bool inb = db <= EPS;

			if ( ina && inb )
			{
				output.Add( b );
			}
			else if ( ina && !inb )
			{
				float t = da / (da - db);
				output.Add( a + (b - a) * t );
			}
			else if ( !ina && inb )
			{
				float t = da / (da - db);
				output.Add( a + (b - a) * t );
				output.Add( b );
			}
		}

		return output;
	}

	private void BuildClipBrushes( GameObject _parent )
	{
		if ( Context.Brushes is not null && Context.BrushSides is not null && Context.Planes is not null )
		{
			var clipBrushes = Context.Brushes.Where( b => b.IsClipBrush ).ToList();
			if ( clipBrushes.Count == 0 )
				return;

			int count = 0;
			var parent = new GameObject( _parent, true, "Clip Brushes" );
			foreach ( var brush in clipBrushes )
			{
				if ( !brush.IsClipBrush )
					continue;

				var clipObject = new GameObject( parent, true, $"Clip Brush {count}" );
				var clipMesh = new PolygonMesh();

				// get brush sides
				var firstSide = brush.FirstSide;
				var numSides = brush.NumSides;

				for ( int i = 0; i < numSides; i++ )
				{
					int sideIndex = firstSide + i;

					var brushSide = Context.BrushSides[sideIndex];
					var planeIndex = brushSide.PlaneNum;

					var plane = Context.Planes[planeIndex];

					// build quad
					var poly = BuildBasePolygon( plane );
					for ( int j = 0; j < numSides; j++ )
					{
						int otherSideIndex = firstSide + j;

						if ( sideIndex == otherSideIndex )
							continue;

						var other = Context.Planes[Context.BrushSides[otherSideIndex].PlaneNum];

						poly = ClipPolygon( poly, other );

						if ( poly.Count == 0 )
						{
							break;
						}
					}

					if ( poly.Count >= 3 )
					{
						var hVerts = clipMesh.AddVertices( poly.ToArray() );
						var hFace = clipMesh.AddFace( hVerts );
						clipMesh.SetFaceMaterial( hFace, "materials/tools/toolsclip.vmat" );
						clipMesh.TextureAlignToGrid( Transform.Zero, hFace );
					}
				}

				var meshComp = clipObject.Components.Create<MeshComponent>();
				meshComp.Mesh = clipMesh;
				meshComp.HideInGame = true;
				meshComp.RenderType = ModelRenderer.ShadowRenderType.Off;

				CenterMeshOrigin( meshComp );

				count++;
			}
		}
	}

	/// <summary>
	/// Builds the map world geometry of the current context. Brush entities require pre-built PolygonMeshes. See <see cref="BuildModelMeshes"/>.
	/// </summary>
	protected virtual async Task BuildWorldGeometry( GameObject parent, IProgressSection progress, int meshesPerFrame, CancellationToken token )
	{
		var displacementMeshes = await ConstructDisplacementMeshesAsync( token, progress, meshesPerFrame );

		if ( token.IsCancellationRequested )
			return;

		var worldspawnMeshes = await ConstructWorldspawnMeshes( token, progress );

		if ( token.IsCancellationRequested )
			return;

		Log.Info( "Building World..." );
		progress.Title = "Building World...";
		progress.TotalCount = displacementMeshes.Count + worldspawnMeshes.Count;

		if ( displacementMeshes.Count >= 0 )
		{
			var displacementParent = new GameObject( parent, true, "Displacements" );
			int count = 0;

			progress.Subtitle = $"Building {displacementMeshes.Count} Displacement Meshes";

			foreach ( var displacement in displacementMeshes )
			{
				try
				{
					if ( token.IsCancellationRequested )
					{
						return;
					}

					progress.Current = count;

					ConstructMesh( displacementParent, $"Displacement {count}", displacement );

					count++;

					if ( count % meshesPerFrame == 0 )
					{
						await GameTask.Yield();
					}
				}
				catch ( Exception )
				{
					Log.Error( "Failed building displacement!" );
					continue;
				}
			}
		}

		if ( worldspawnMeshes.Count >= 0 )
		{
			var meshParent = new GameObject( parent, true, "Meshes" );
			int count = 0;

			progress.Subtitle = $"Building {worldspawnMeshes.Count} World Meshes";

			foreach ( var meshResult in worldspawnMeshes )
			{
				if ( token.IsCancellationRequested )
				{
					return;
				}
				var mesh = meshResult.Mesh;
				if ( mesh is null )
					continue;


				var meshName = $"Mesh {count}";

				if ( meshResult.IsWater )
				{
					meshName = $"Water Mesh";
				}

				var meshComp = ConstructMesh( meshParent, meshName, mesh );
				meshComp.Collision = meshResult.IsWater ? MeshComponent.CollisionType.None : MeshComponent.CollisionType.Mesh;

				progress.Current = count + displacementMeshes.Count;
				count++;

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

		BuildClipBrushes( parent );
	}

	private MeshComponent ConstructMesh( GameObject parent, string name, PolygonMesh mesh )
	{
		using var scope = parent.Scene.Push();
		var meshObj = new GameObject( parent, true, name );
		var meshComp = meshObj.Components.Create<MeshComponent>();
		meshComp.Mesh = mesh;
		CenterMeshOrigin( meshComp );

		return meshComp;
	}

	static void CenterMeshOrigin( MeshComponent meshComponent )
	{
		if ( !meshComponent.IsValid() ) return;

		var mesh = meshComponent.Mesh;
		if ( mesh is null ) return;

		var children = meshComponent.GameObject.Children
			.Select( x => (GameObject: x, Transform: x.WorldTransform) )
			.ToArray();

		var world = meshComponent.WorldTransform;
		var bounds = mesh.CalculateBounds( world );
		var center = bounds.Center;
		var localCenter = world.PointToLocal( center );
		meshComponent.WorldPosition = center;
		meshComponent.Mesh.ApplyTransform( new Transform( -localCenter ) );
		meshComponent.RebuildMesh();

		foreach ( var child in children )
		{
			child.GameObject.WorldTransform = child.Transform;
		}
	}

	private async Task<List<PolygonMesh>> ConstructDisplacementMeshesAsync( CancellationToken token, IProgressSection progress, int meshesPerFrame = 16 )
	{
		// gather unique displacement face indices
		HashSet<ushort> dispIndices = new();

		for ( short i = 0; i < Context.Geometry.DisplacementInfoCount; i++ )
		{
			Context.Geometry.TryGetDisplacementInfo( i, out var dispInfo );

			dispIndices.Add( dispInfo.MapFace );
		}

		var displacements = new List<PolygonMesh>();
		if ( dispIndices.Count == 0 )
			return displacements;

		Log.Info( "Constructing Displacement Meshes..." );
		progress.Title = "Constructing Displacement Meshes...";
		progress.TotalCount = dispIndices.Count;

		int count = 0;
		foreach ( ushort dispFaceIndex in dispIndices )
		{
			if ( token.IsCancellationRequested )
				return displacements;

			var dispOrigin = DisplacementHelper.GetDisplacementOrigin( Context, dispFaceIndex );
			var dispLeafIndex = TreeParse.FindLeafIndex( dispOrigin!.Value );
			var dispLeaf = Context.Leafs![dispLeafIndex];

			if ( Context.BuildSettings.CullSkybox && Context.SkyboxAreas.Contains( dispLeaf.Area ) )
				continue;

			// create one mesh per displacement
			var dispMesh = DisplacementHelper.CreateDisplacementMesh( Context, dispFaceIndex );
			if ( dispMesh is null )
				continue;

			if ( dispMesh.FaceHandles.Any() )
			{
				displacements.Add( dispMesh );
			}

			progress.Current = count;

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

		return displacements;
	}

	public static Vector2 GetTexCoords( ImportContext context, int texInfoIndex, Vector3 position, int width = 1024, int height = 1024 )
	{
		// validate texinfo availability and index
		if ( context.TexInfo is null || texInfoIndex < 0 || texInfoIndex >= context.TexInfo.Length )
			return default;

		var ti = context.TexInfo[texInfoIndex];

		if ( context.TexData is not null && ti.TexData >= 0 && ti.TexData < context.TexData.Length )
		{
			var texData = context.TexData[ti.TexData];
			width = texData.Width;
			height = texData.Height;
		}

		return ti.GetUvs( position, width, height );
	}

	private bool IsWaterSurface( ushort faceIndex )
	{
		if ( !Context.HasCompleteGeometry( out var geo ) )
			return false;

		if ( !geo.TryGetFace( faceIndex, out var face ) )
			return false;

		var surfaceFlags = face.GetSurfaceFlags( Context );
		return (surfaceFlags & SurfaceFlags.Warp) != 0;
	}

	public class WorldspawnMesh
	{
		public PolygonMesh? Mesh { get; set; }
		public bool IsTranslucent { get; set; }
		public bool IsWater { get; set; }
	}

	/// <summary>
	/// Construct PolygonMeshes from the bsp-tree, chunked into individual Meshes based on Settings.ChunkSize and surface properties such as Translucent or Water.
	/// </summary>
	/// <returns></returns>
	private async Task<List<WorldspawnMesh>> ConstructWorldspawnMeshes( CancellationToken token, IProgressSection progress )
	{
		var geo = Context.Geometry;

		var meshes = new List<WorldspawnMesh>();

		if ( !Context.HasCompleteGeometry( out geo ) )
		{
			Log.Error( $"Failed constructing worldspawn geometry! No valid geometry in Context!" );
			return meshes;
		}

		// construct world mesh faces from bsp tree
		var result = TreeParse.GetUniqueWorldspawnFaces();

		var faceIndices = result.FaceIndices;
		var waterFaces = faceIndices.Where( fi => IsWaterSurface( fi ) ).ToList();
		var solidFaces = faceIndices.Where( fi => !IsWaterSurface( fi ) ).ToList();

		if ( solidFaces.Count == 0 )
		{
			Log.Error( $"Failed constructing worldspawn geometry! No faces in tree!" );
			return meshes;
		}

		// spawn solid geometry first
		var chunks = solidFaces.Chunk( Context.BuildSettings.ChunkSize );

		if ( token.IsCancellationRequested )
			return meshes;

		Log.Info( "Constructing World Meshes..." );
		progress.Title = "Constructing World Meshes...";
		progress.TotalCount = chunks.Count();
		progress.Current = 0;

		// chunk tree faces into batches for MeshComponent
		foreach ( var chunk in chunks )
		{
			if ( token.IsCancellationRequested )
				return meshes;

			var mesh = new PolygonMesh();

			foreach ( var face in chunk )
			{
				if ( token.IsCancellationRequested )
					return meshes;

				if ( !geo.TryGetFace( face, out var _ ) )
					continue;

				mesh.AddMeshFace( Context, face );
			}

			progress.Current++;

			if ( mesh.FaceHandles.Any() )
			{
				var meshResult = new WorldspawnMesh()
				{
					Mesh = mesh,
					IsTranslucent = false,
					IsWater = false
				};
				meshes.Add( meshResult );
			}

			await GameTask.Yield();
		}

		// add water surfaces as a mesh
		var waterMesh = new PolygonMesh();
		foreach ( var face in waterFaces )
		{
			waterMesh.AddMeshFace( Context, face );
		}
		if ( waterMesh.FaceHandles.Any() )
		{
			var meshResult = new WorldspawnMesh()
			{
				Mesh = waterMesh,
				IsTranslucent = true,
				IsWater = true
			};
			meshes.Add( meshResult );
		}

		return meshes;
	}

	/// <summary>
	/// Construct a PolygonMesh from a bsp model index.
	/// </summary>
	/// <param name="modelIndex"></param>
	/// <returns></returns>
	/// <exception cref="Exception"></exception>
	private PolygonMesh? ConstructModel( int modelIndex )
	{
		// return already cached mesh
		if ( Context.CachedPolygonMeshes?[modelIndex] is not null )
		{
			return Context.CachedPolygonMeshes[modelIndex];
		}

		var geo = Context.Geometry;

		if ( !Context.HasCompleteGeometry( out geo ) )
		{
			throw new Exception( "No valid map geometry to construct!" );
		}

		if ( Context.Models is null )
		{
			throw new Exception( "No valid models to construct!" );
		}

		if ( modelIndex < 0 || modelIndex >= Context.Models.Length )
		{
			throw new Exception( $"Tried to construct map model with index: {modelIndex}. Exceeds available Models!" );
		}

		var model = Context.Models[modelIndex];

		return ConstructPolygonMesh( model.FirstFace, model.FaceCount );
	}

	/// <summary>
	/// Construct a PolygonMesh from a firstFace index and face count.
	/// </summary>
	/// <param name="firstFaceIndex"></param>
	/// <param name="faceCount"></param>
	/// <returns></returns>
	/// <exception cref="Exception"></exception>
	private PolygonMesh? ConstructPolygonMesh( int firstFaceIndex, int faceCount )
	{
		if ( faceCount <= 0 )
			return null;

		//Log.Info( $"construct poly mesh: [{firstFaceIndex}, {faceCount}]" );

		var geo = Context.Geometry;
		if ( !geo.IsValid() )
		{
			throw new Exception( "No valid map geometry to construct!" );
		}

		// models support int firstFace and faceCount for some reason, but faces are limited to ushort
		var faces = GetFaceIndices( (ushort)firstFaceIndex, (ushort)faceCount );

		// invalid world mesh
		if ( faces.Length <= 0 )
			return null;

		// build all split faces
		var polyMesh = new PolygonMesh();
		foreach ( ushort faceIndex in faces )
		{
			polyMesh.AddMeshFace( Context, faceIndex );
		}

		return polyMesh;
	}

	/// <summary>
	/// Gather all unique face indices from a firstFace index and a face count. Skips displacement faces.
	/// </summary>
	/// <param name="firstFaceIndex"></param>
	/// <param name="faceCount"></param>
	/// <returns></returns>
	/// <exception cref="Exception"></exception>
	private ushort[] GetFaceIndices( ushort firstFaceIndex, ushort faceCount )
	{
		var geo = Context.Geometry;
		if ( !geo.IsValid() )
		{
			throw new Exception( "No valid map geometry to construct!" );
		}

		var faces = new HashSet<ushort>();

		for ( ushort i = 0; i < faceCount; i++ )
		{
			var faceIndex = firstFaceIndex;
			faceIndex += i;

			geo.TryGetFace( faceIndex, out var face );

			// skip faces with invalid area
			if ( face.Area <= 0 || face.Area.AlmostEqual( 0 ) )
			{
				//Log.Info( $"skipping face with invalid area: {faceIndex}" );
				continue;
			}

			// skip displacement faces, is this needed anymore?
			if ( face.DisplacementInfo >= 0 )
			{
				continue;
			}

			faces.Add( faceIndex );
		}

		return faces.ToArray();
	}
}