PixelCollision.cs
namespace Sandbox;

/// <summary>
/// Tracks a <see cref="PixelSpriteComponent"/> alongside the world-space position
/// that the game code owns. Updated each frame by the caller before collision queries.
/// </summary>
public class CollisionEntry
{
	public PixelSpriteComponent Sprite { get; set; }
	public int X { get; set; }
	public int Y { get; set; }

	public CollisionEntry( PixelSpriteComponent sprite, int x = 0, int y = 0 )
	{
		Sprite = sprite;
		X = x;
		Y = y;
	}
}

/// <summary>
/// Static scene-level collision system for <see cref="PixelSpriteComponent"/>.
///
/// Game code registers each collideable sprite via <see cref="Register"/> and
/// keeps the entry's position up to date. Collision queries scan the registry
/// using AABB overlap on the flip-aware hitboxes.
///
/// Ported from the Unity pixelshitter framework's Scene.Collide / Entity.Collide
/// pattern, flattened into a static utility because the s&amp;box port doesn't have
/// a Scene/Entity hierarchy.
/// </summary>
public static class PixelCollision
{
	private static readonly List<CollisionEntry> _entries = new();

	// ── Registration ─────────────────────────────────────────────────────

	/// <summary>
	/// Register a sprite for collision queries. Returns the <see cref="CollisionEntry"/>
	/// so the caller can update its position each frame.
	/// </summary>
	public static CollisionEntry Register( PixelSpriteComponent sprite, int x = 0, int y = 0 )
	{
		CollisionEntry entry = new( sprite, x, y );
		_entries.Add( entry );
		return entry;
	}

	/// <summary>Remove a previously registered entry.</summary>
	public static void Unregister( CollisionEntry entry )
	{
		_entries.Remove( entry );
	}

	/// <summary>Remove all entries whose sprite matches.</summary>
	public static void Unregister( PixelSpriteComponent sprite )
	{
		_entries.RemoveAll( e => e.Sprite == sprite );
	}

	/// <summary>Remove all registered entries.</summary>
	public static void Clear()
	{
		_entries.Clear();
	}

	// ── Queries ──────────────────────────────────────────────────────────

	/// <summary>
	/// Find the first registered sprite with <paramref name="tag"/> whose hitbox
	/// overlaps <paramref name="source"/> at the hypothetical position
	/// (<paramref name="x"/>, <paramref name="y"/>).
	/// </summary>
	/// <returns>The matching entry, or null if no overlap.</returns>
	public static CollisionEntry Collide( PixelSpriteComponent source, string tag, int x, int y )
	{
		PixelRect sourceBox = source.GetOffsetHitbox( x, y );

		foreach ( CollisionEntry entry in _entries )
		{
			if ( entry.Sprite == source ) continue;
			if ( !entry.Sprite.Collideable ) continue;
			if ( !entry.Sprite.Tags.Contains( tag ) ) continue;

			PixelRect otherBox = entry.Sprite.GetOffsetHitbox( entry.X, entry.Y );
			if ( sourceBox.Overlaps( otherBox ) )
				return entry;
		}

		return null;
	}

	/// <summary>
	/// Find all registered sprites with <paramref name="tag"/> whose hitboxes
	/// overlap <paramref name="source"/> at the hypothetical position.
	/// </summary>
	public static List<CollisionEntry> CollideWithAll( PixelSpriteComponent source, string tag, int x, int y )
	{
		List<CollisionEntry> results = new();
		PixelRect sourceBox = source.GetOffsetHitbox( x, y );

		foreach ( CollisionEntry entry in _entries )
		{
			if ( entry.Sprite == source ) continue;
			if ( !entry.Sprite.Collideable ) continue;
			if ( !entry.Sprite.Tags.Contains( tag ) ) continue;

			PixelRect otherBox = entry.Sprite.GetOffsetHitbox( entry.X, entry.Y );
			if ( sourceBox.Overlaps( otherBox ) )
				results.Add( entry );
		}

		return results;
	}

	/// <summary>
	/// Test whether an arbitrary <paramref name="rect"/> overlaps any registered
	/// sprite with <paramref name="tag"/> (excluding <paramref name="source"/>).
	/// </summary>
	public static CollisionEntry Collide( PixelSpriteComponent source, string tag, PixelRect rect )
	{
		foreach ( CollisionEntry entry in _entries )
		{
			if ( entry.Sprite == source ) continue;
			if ( !entry.Sprite.Collideable ) continue;
			if ( !entry.Sprite.Tags.Contains( tag ) ) continue;

			PixelRect otherBox = entry.Sprite.GetOffsetHitbox( entry.X, entry.Y );
			if ( rect.Overlaps( otherBox ) )
				return entry;
		}

		return null;
	}

	/// <summary>
	/// Check whether <paramref name="source"/>'s hitbox at the given position
	/// fits entirely within the <see cref="PixelScreen"/> pixel buffer.
	/// </summary>
	public static bool IsInBounds( PixelSpriteComponent source, int x, int y )
	{
		PixelScreen screen = PixelScreen.Instance;
		if ( screen is null )
			return false;

		PixelRect box = source.GetOffsetHitbox( x, y );

		return box.XMin >= 0
			&& box.YMin >= 0
			&& box.XMax <= screen.PixelWidth
			&& box.YMax <= screen.PixelHeight;
	}
}