Editor/Builder/BspTreeParser.cs
using Editor.MovieMaker;
using System.IO.Compression;

namespace BspImport.Builder;

public class BspTreeParser
{
	public class TreeParseResult
	{
		public List<ushort> FaceIndices = new();
	}

	private ImportContext Context { get; set; }
	private List<int>[]? FaceLeavesLookup { get; set; }

	public BspTreeParser( ImportContext context )
	{
		Context = context;
	}

	/// <summary>
	/// Traverse the bsp tree to find the leaf index for a given point in world space.
	/// </summary>
	/// <param name="point"></param>
	/// <returns>-1 if not found, otherwise the leaf index.</returns>
	public int FindLeafIndex( Vector3 point )
	{
		int nodeIndex = 0; // Start at headnode (model 0)  
		while ( nodeIndex >= 0 )
		{
			var node = Context.Nodes![nodeIndex];
			var plane = Context.Planes![node.PlaneIndex];

			float distance = plane.Normal.x * point.x +
							plane.Normal.y * point.y +
							plane.Normal.z * point.z - plane.Distance;

			nodeIndex = distance >= 0 ? node.Children[0] : node.Children[1];
		}

		return -1 - nodeIndex; // Convert negative leaf index to positive  
	}

	/// <summary>
	/// Get all unique Face indices from the BSP tree. Results represent render meshes, not brushes. Never brushes.
	/// </summary>
	/// <returns></returns>
	public TreeParseResult GetUniqueWorldspawnFaces()
	{
		var result = new TreeParseResult();

		if ( !Context.HasCompleteGeometry( out var geo ) )
			return result;

		var faces = new HashSet<ushort>();
		ParseNodeFacesRecursively( 0, ref faces );

		result.FaceIndices = faces.ToList();

		return result;
	}

	private void ParseNodeFacesRecursively( int index, ref HashSet<ushort> faceIndices )
	{
		if ( Context.Nodes is null )
			return;

		MapNode node = Context.Nodes[index];

		if ( Context.BuildSettings.CullSkybox && Context.SkyboxAreas.Contains( node.Area ) )
			return;

		// contribute to faces collection
		for ( ushort i = 0; i < node.FaceCount; i++ )
		{
			ushort faceIndex = node.FirstFaceIndex;
			faceIndex += i;

			TryAddFace( faceIndex, ref faceIndices );
		}

		// gather faces from children
		for ( int i = 0; i < 2; i++ )
		{
			var child = node.Children[i];

			// 0 = no child
			if ( child == 0 ) continue;

			// <0 = leaf, not node
			if ( child < 0 )
			{
				AddLeafFaces( -1 - child, ref faceIndices );
				continue;
			}

			// parse child node recursively
			ParseNodeFacesRecursively( child, ref faceIndices );
		}
	}

	private void AddLeafFaces( int index, ref HashSet<ushort> faceIndices )
	{
		if ( Context.Leafs is null )
			return;

		if ( index >= Context.Leafs.Length )
			return;

		var leaf = Context.Leafs[index];

		if ( leaf.WaterDataIndex != -1 )
			return;


		bool isWater = (leaf.Contents & ContentsFlags.Water) == ContentsFlags.Water;
		if ( isWater )
			return;

		//var isWaterLeaf = leaf.WaterDataIndex != -1;
		//var isSkyboxLeaf = (leaf.Flags & 0x01) != 0;

		if ( Context.BuildSettings.CullSkybox && Context.SkyboxAreas.Contains( leaf.Area ) )
			return;

		// contribute to faces collection
		for ( ushort i = 0; i < leaf.FaceCount; i++ )
		{
			ushort leafFaceIndex = leaf.FirstFaceIndex;
			leafFaceIndex += i;

			Context.Geometry.TryGetLeafFaceIndex( leafFaceIndex, out var faceIndex );

			TryAddFace( faceIndex, ref faceIndices );
		}
	}

	private bool TryAddFace( ushort faceIndex, ref HashSet<ushort> faceIndices )
	{
		if ( !Context.Geometry.TryGetFace( faceIndex, out var face ) )
			return false;

		if ( !faceIndices.Add( faceIndex ) )
			return false;

		return true;
	}
}