Park/Areas/AreaTile.Helpers.cs
using HC3.Terrain;
using System;

namespace HC3;

public partial class AreaTile
{
	/// <summary>
	/// Cached boundary tile data: the tile and which edge faces outside the enclosure.
	/// </summary>
	public readonly record struct BoundaryInfo( AreaTile Tile, TileEdge OutwardEdge );

	private static readonly Dictionary<int, List<AreaTile>> _regionTileCache = new();
	private static readonly Dictionary<int, List<BoundaryInfo>> _regionBoundaryCache = new();
	private static uint _cacheNavVersion;

	/// <summary>
	/// Gets all non-ghost enclosure AreaTiles in the given region. Cached until the grid changes.
	/// </summary>
	public static IReadOnlyList<AreaTile> GetTilesInRegion( int regionId )
	{
		EnsureCache();

		if ( _regionTileCache.TryGetValue( regionId, out var list ) )
			return list;

		return Array.Empty<AreaTile>();
	}

	/// <summary>
	/// Gets boundary tiles (tiles with at least one fence edge) in the given region.
	/// Each entry includes which edge faces outside. Cached until the grid changes.
	/// </summary>
	public static IReadOnlyList<BoundaryInfo> GetBoundaryTiles( int regionId )
	{
		EnsureCache();

		if ( _regionBoundaryCache.TryGetValue( regionId, out var list ) )
			return list;

		return Array.Empty<BoundaryInfo>();
	}

	/// <summary>
	/// Finds the nearest boundary tile to a world position within a region.
	/// Returns null if no boundary tiles exist.
	/// </summary>
	public static BoundaryInfo? FindNearestBoundary( int regionId, Vector3 worldPosition )
	{
		var boundaries = GetBoundaryTiles( regionId );
		if ( boundaries.Count == 0 )
			return null;

		BoundaryInfo? best = null;
		float bestDist = float.MaxValue;
		bool hasStaleEntries = false;

		foreach ( var info in boundaries )
		{
			if ( !info.Tile.IsValid() )
			{
				hasStaleEntries = true;
				continue;
			}

			float dist = info.Tile.WorldPosition.DistanceSquared( worldPosition );
			if ( dist < bestDist )
			{
				bestDist = dist;
				best = info;
			}
		}

		// If we found stale entries but no valid result, force a cache rebuild and retry
		if ( best == null && hasStaleEntries )
		{
			InvalidateCache();
			boundaries = GetBoundaryTiles( regionId );

			foreach ( var info in boundaries )
			{
				if ( !info.Tile.IsValid() )
					continue;

				float dist = info.Tile.WorldPosition.DistanceSquared( worldPosition );
				if ( dist < bestDist )
				{
					bestDist = dist;
					best = info;
				}
			}
		}

		return best;
	}

	/// <summary>
	/// Forces the cache to rebuild on next access.
	/// </summary>
	public static void InvalidateCache()
	{
		_cacheNavVersion = 0;
		_regionTileCache.Clear();
		_regionBoundaryCache.Clear();
	}

	private static void EnsureCache()
	{
		var navVersion = GridManager.Instance?.NavigationVersion ?? 0;

		if ( _cacheNavVersion == navVersion && _regionTileCache.Count > 0 )
			return;

		RebuildCache( navVersion );
	}

	private static void RebuildCache( uint navVersion )
	{
		_cacheNavVersion = navVersion;
		_regionTileCache.Clear();
		_regionBoundaryCache.Clear();

		var scene = Game.ActiveScene;
		if ( scene == null )
			return;

		foreach ( var area in scene.GetAllComponents<AreaTile>() )
		{
			if ( area.GhostPreview || area.AreaType != AreaType.Enclosure )
				continue;

			var gridPos = GridManager.WorldToGridPosition3D( area.WorldPosition );
			int regionId = GridManager.GetRegion( gridPos );
			if ( regionId < 0 )
				continue;

			// Add to tile cache
			if ( !_regionTileCache.TryGetValue( regionId, out var tileList ) )
			{
				tileList = new List<AreaTile>();
				_regionTileCache[regionId] = tileList;
			}
			tileList.Add( area );

			// If boundary tile, add to boundary cache
			if ( area.Mask == PathMask.All )
				continue;

			if ( !_regionBoundaryCache.TryGetValue( regionId, out var boundaryList ) )
			{
				boundaryList = new List<BoundaryInfo>();
				_regionBoundaryCache[regionId] = boundaryList;
			}

			foreach ( var edge in GridManager.AllEdges )
			{
				if ( !area.Mask.CanConnectTile( edge ) )
				{
					boundaryList.Add( new BoundaryInfo( area, edge ) );
				}
			}
		}
	}
}