Transposer/Entity.cs
namespace Sandbox.Transposer;

/// <summary>
/// Callback invoked when a PlayOnce animation reaches its last frame.
/// </summary>
public delegate void AnimationCompleteDelegate();

/// <summary>
/// Callback for scheduled (delayed) actions.
/// </summary>
public delegate void ScheduledDelegate();

/// <summary>
/// Callback invoked when a specific animation frame is entered.
/// </summary>
public delegate void SetFrameDelegate();

/// <summary>
/// A delayed callback that fires after <see cref="Delay"/> seconds.
/// </summary>
public struct ScheduledDelegateData
{
	public ScheduledDelegate Callback;
	public float Delay;

	public ScheduledDelegateData( ScheduledDelegate callback, float delay )
	{
		Callback = callback;
		Delay = delay;
	}
}

/// <summary>
/// Base class for all Transposer game entities.
///
/// Direct port of the original Unity Entity class as a plain C# object
/// (not an engine Component). Entities are managed by a <see cref="TransposerScene"/>
/// which handles update ordering, draw sorting, and deferred add/remove.
///
/// Uses <see cref="SpriteManager"/> and the framework's <see cref="PixelSpriteComponent"/>
/// internally for animation playback and rendering, while keeping the original
/// Entity API surface intact.
/// </summary>
public class Entity
{
	// ── Position ─────────────────────────────────────────────────────────
	// Sub-pixel float position — rounded to integer for drawing.
	protected Vector2 _position;

	public Vector2 ExactPos
	{
		get => _position;
		set => _position = value;
	}

	public float ExactX
	{
		get => _position.x;
		set => _position = new Vector2( value, _position.y );
	}

	public float ExactY
	{
		get => _position.y;
		set => _position = new Vector2( _position.x, value );
	}

	/// <summary>
	/// Integer pixel position, using rounding (same as original).
	/// </summary>
	public PixelPoint PixelPosition
	{
		get => new( (int)MathF.Round( _position.x ), (int)MathF.Round( _position.y ) );
		set => _position = new Vector2( value.X, value.Y );
	}

	public int PixelX
	{
		get => (int)MathF.Round( _position.x );
		set => _position = new Vector2( value, _position.y );
	}

	public int PixelY
	{
		get => (int)MathF.Round( _position.y );
		set => _position = new Vector2( _position.x, value );
	}

	/// <summary>
	/// Center of the entity in pixel coordinates (offset by half anim size).
	/// </summary>
	public PixelPoint CenterPixelPos
	{
		get
		{
			if ( _currentAnimation is not null )
				return new PixelPoint(
					PixelX + (int)MathF.Round( (_currentAnimation.AnimSize.X - 1) / 2f ),
					PixelY + (int)MathF.Round( (_currentAnimation.AnimSize.Y - 1) / 2f ) );
			return PixelPosition;
		}
	}

	public Vector2 CenterExactPos
	{
		get
		{
			if ( _currentAnimation is not null )
				return new Vector2(
					ExactX + _currentAnimation.AnimSize.X * 0.5f,
					ExactY + _currentAnimation.AnimSize.Y * 0.5f );
			return ExactPos;
		}
	}

	// ── Sprite type ──────────────────────────────────────────────────────
	protected string _type;
	public string Type => _type;

	// ── Depth/layer for draw sorting ─────────────────────────────────────
	protected int _depth;
	public int Depth { get => _depth; set => _depth = value; }

	protected int _layer;
	public int Layer { get => _layer; set => _layer = value; }

	// ── Hitbox / collision ───────────────────────────────────────────────
	protected PixelRect _hitbox;
	public PixelRect Hitbox => _hitbox;

	/// <summary>Hitbox offset by current pixel position.</summary>
	public PixelRect OffsetHitbox => _hitbox + PixelPosition;

	public PixelRect GetOffsetHitbox( PixelPoint position ) => _hitbox + position;
	public PixelRect GetOffsetHitbox( int x, int y ) => _hitbox + new PixelPoint( x, y );

	protected bool _collideable;
	public bool Collideable => _collideable;

	// ── Scene reference ──────────────────────────────────────────────────
	protected TransposerScene _scene;
	public TransposerScene ContainingScene { get => _scene; set => _scene = value; }

	// ── Animation state ──────────────────────────────────────────────────
	protected AnimationData _currentAnimation;
	protected int _currentFrameNumber;
	protected float _currentFrameTime;
	protected FrameData _currentFrameData;
	protected bool _pingPongForward;
	protected float _animationTimeScale = 1.0f;
	public float AnimTimeScale { get => _animationTimeScale; set => _animationTimeScale = value; }

	private bool _overrideLoopMode;
	private LoopMode _overriddenLoopMode;

	private AnimationCompleteDelegate _animCompleteCallback;
	public void SetAnimCompleteCallback( AnimationCompleteDelegate callback ) => _animCompleteCallback = callback;

	private bool _playOnceAnimFinished;

	private Dictionary<int, SetFrameDelegate> _setFrameDelegates = new();
	public void AddSetFrameCallback( int frameNumber, SetFrameDelegate callback ) =>
		_setFrameDelegates[frameNumber] = callback;
	public void ClearSetFrameCallbacks() => _setFrameDelegates.Clear();

	// ── Flip ─────────────────────────────────────────────────────────────
	protected bool _flipX;
	public bool FlipX { get => _flipX; set => _flipX = value; }
	protected bool _flipY;
	public bool FlipY { get => _flipY; set => _flipY = value; }

	// ── Color override ───────────────────────────────────────────────────
	private bool _overrideColor;
	private bool _overrideAlpha;
	private Color _overriddenColor;

	public void SetOverriddenColor( Color color )
	{
		_overrideColor = true;
		_overriddenColor = color;
	}
	public void RemoveOverriddenColor() => _overrideColor = false;
	public void SetOverrideAlpha( bool overrideAlpha ) => _overrideAlpha = overrideAlpha;

	// ── Scheduled callbacks ──────────────────────────────────────────────
	private List<ScheduledDelegateData> _scheduledCallbacks = new();
	public void AddScheduledCallback( float delay, ScheduledDelegate callback ) =>
		_scheduledCallbacks.Add( new ScheduledDelegateData( callback, delay ) );

	// ── Screen reference ─────────────────────────────────────────────────
	protected PixelScreen Screen => PixelScreen.Instance;

	// ── Update ───────────────────────────────────────────────────────────

	public virtual void UpdateEntity( float deltaTime )
	{
		if ( _currentAnimation is not null )
			HandleAnimation( deltaTime );

		if ( _scheduledCallbacks.Count > 0 )
			HandleScheduledCallbacks( deltaTime );
	}

	private void HandleScheduledCallbacks( float deltaTime )
	{
		// Iterate backwards so RemoveAt(i) doesn't shift unvisited entries.
		// Expired callbacks are fired immediately; remaining ones are re-added
		// at the end of the list with the updated time. The re-add is safe here
		// because we're iterating from the back toward the front.
		for ( int i = _scheduledCallbacks.Count - 1; i >= 0; i-- )
		{
			ScheduledDelegateData data = _scheduledCallbacks[i];
			float newTime = data.Delay - deltaTime;
			_scheduledCallbacks.RemoveAt( i );

			if ( newTime <= 0f )
				data.Callback();
			else
				_scheduledCallbacks.Add( new ScheduledDelegateData( data.Callback, newTime ) );
		}
	}

	private void HandleAnimation( float deltaTime )
	{
		LoopMode loopMode = _overrideLoopMode ? _overriddenLoopMode : _currentAnimation.LoopMode;
		int numFrames = _currentAnimation.Frames.Count;

		_currentFrameTime += deltaTime * _animationTimeScale;

		var animTime = _currentAnimation.Frames[_currentFrameNumber].AnimTime;
		if ( _currentFrameTime <= animTime )
			return;

		switch ( loopMode )
		{
			case LoopMode.Loops when numFrames > 1:
				SetAnimationFrame( _currentFrameNumber == numFrames - 1 ? 0 : _currentFrameNumber + 1 );
				break;

			case LoopMode.PlayOnce when !_playOnceAnimFinished:
				// Guard prevents re-firing the callback if PlayAnimation is called again
				// on the same animation while still on the last frame.
				if ( _currentFrameNumber < numFrames - 1 )
				{
					SetAnimationFrame( _currentFrameNumber + 1 );
				}
				else
				{
					_playOnceAnimFinished = true;
					AnimationCompleteDelegate cb = _animCompleteCallback;
					_animCompleteCallback = null;
					cb?.Invoke();
				}
				break;

			case LoopMode.PingPong when numFrames > 1:
				if ( _pingPongForward )
				{
					if ( _currentFrameNumber < numFrames - 1 )
						SetAnimationFrame( _currentFrameNumber + 1 );
					else
					{
						SetAnimationFrame( _currentFrameNumber - 1 );
						_pingPongForward = false;
					}
				}
				else
				{
					if ( _currentFrameNumber > 0 )
						SetAnimationFrame( _currentFrameNumber - 1 );
					else
					{
						SetAnimationFrame( _currentFrameNumber + 1 );
						_pingPongForward = true;
					}
				}
				break;

			case LoopMode.RandomFrame when numFrames > 1:
				int newFrame = Game.Random.Next( 0, numFrames );
				while ( newFrame == _currentFrameNumber )
					newFrame = Game.Random.Next( 0, numFrames );
				SetAnimationFrame( newFrame );
				break;
		}

		//_currentFrameTime = 0f;
		_currentFrameTime -= animTime;
	}

	private void SetAnimationFrame( int frameNumber )
	{
		if ( _setFrameDelegates.TryGetValue( frameNumber, out SetFrameDelegate cb ) )
			cb?.Invoke();

		_currentFrameNumber = frameNumber;
		_currentFrameData = _currentAnimation.Frames[_currentFrameNumber];
	}

	// ── Animation playback ───────────────────────────────────────────────

	public virtual void PlayAnimation( string animName )
	{
		List<AnimationData> anims = SpriteManager.GetAnimations( _type );
		if ( anims is null )
		{
			Log.Error( $"Entity: no entity type called '{_type}'" );
			return;
		}

		foreach ( AnimationData anim in anims )
		{
			if ( anim.Name != animName )
				continue;

			_currentAnimation = anim;
			_hitbox = anim.Hitbox;
			SetAnimationFrame( 0 );
			_currentFrameTime = 0f;
			_overrideLoopMode = false;
			_playOnceAnimFinished = false;
			_pingPongForward = true; // Always start ping-pong animations going forward.
			return;
		}

		Log.Error( $"Entity: no animation '{animName}' for '{_type}'" );
	}

	public virtual void PlayAnimation( string animName, LoopMode overriddenLoopMode )
	{
		PlayAnimation( animName );
		_overrideLoopMode = true;
		_overriddenLoopMode = overriddenLoopMode;
	}

	// ── Drawing ──────────────────────────────────────────────────────────

	public virtual void Draw()
	{
		if ( _currentAnimation is null )
			return;

		if ( !IsInScreenBounds() && !Screen.DrawInGUISpace )
			return;

		List<PixelData> pixels = GetCurrentPixels();

		foreach ( PixelData pixel in pixels )
		{
			Color32 color = ApplyColorOverride( pixel.Color );

			// Fully opaque → SetPixel (no blend math). Semi-transparent → AddPixel
			// (Porter-Duff over). Fully transparent (a == 0) → skip entirely.
			if ( color.a == 255 )
				Screen.SetPixel( PixelX + pixel.Position.X, PixelY + pixel.Position.Y, color );
			else if ( color.a > 0 )
				Screen.AddPixel( PixelX + pixel.Position.X, PixelY + pixel.Position.Y, color );
		}
	}

	/// <summary>
	/// Draw arbitrary pixel data at a position with optional scale and random colour.
	/// Used for text rendering and particle effects.
	/// </summary>
	public virtual void DrawPixels( List<PixelData> pixelDataList, int x, int y, int scale = 1, bool randomColor = false )
	{
		foreach ( PixelData pixel in pixelDataList )
		{
			Color32 color;
			if ( randomColor )
			{
				color = new Color32(
					(byte)Game.Random.Next( 0, 100 ),
					(byte)Game.Random.Next( 0, 100 ),
					(byte)Game.Random.Next( 0, 100 ),
					(byte)Game.Random.Next( 0, 256 ) );
			}
			else
			{
				color = ApplyColorOverride( pixel.Color );
			}

			for ( int xOff = 0; xOff < scale; xOff++ )
			{
				for ( int yOff = 0; yOff < scale; yOff++ )
				{
					int px = x + pixel.Position.X * scale + xOff;
					int py = y + pixel.Position.Y * scale + yOff;

					if ( color.a == 255 )
						Screen.SetPixel( px, py, color );
					else if ( color.a > 0 )
						Screen.AddPixel( px, py, color );
				}
			}
		}
	}

	// ── Pixel data access ────────────────────────────────────────────────

	/// <summary>
	/// Get the current frame's pixel data, respecting flip state.
	/// </summary>
	public List<PixelData> GetPixelDataList()
	{
		if ( _currentAnimation is null )
			return null;

		return GetFlippedPixels( _currentAnimation.Frames[_currentFrameNumber], _currentAnimation.AnimSize );
	}

	/// <summary>
	/// Get pixel data for a specific type/anim/frame, optionally flipped.
	/// Used by the pixel-colour collision system to check against a known frame.
	/// </summary>
	public List<PixelData> GetPixelDataList( string type, string animName, int frameNum = 0, bool flippedX = false, bool flippedY = false )
	{
		AnimationData anim = SpriteManager.GetAnimation( type, animName );
		if ( anim is null || frameNum >= anim.Frames.Count )
			return null;

		FrameData frame = anim.Frames[frameNum];
		int w = anim.AnimSize.X;
		int h = anim.AnimSize.Y;

		if ( flippedX && flippedY ) return frame.GetPixelsFlippedXY( w, h );
		if ( flippedX ) return frame.GetPixelsFlippedX( w );
		if ( flippedY ) return frame.GetPixelsFlippedY( h );

		return new List<PixelData>( frame.Pixels );
	}

	/// <summary>Get animation size for a specific type/anim.</summary>
	public PixelPoint GetAnimSize( string type, string animName )
	{
		AnimationData anim = SpriteManager.GetAnimation( type, animName );
		return anim?.AnimSize ?? PixelPoint.Zero;
	}

	/// <summary>Get animation hitbox for a specific type/anim.</summary>
	public PixelRect GetAnimHitbox( string type, string animName )
	{
		AnimationData anim = SpriteManager.GetAnimation( type, animName );
		return anim?.Hitbox ?? new PixelRect( 0, 0, 0, 0 );
	}

	/// <summary>Get the AnimationData for a specific type/anim.</summary>
	public AnimationData GetAnimationData( string type, string animName )
	{
		return SpriteManager.GetAnimation( type, animName );
	}

	// ── Collision (delegates to scene) ───────────────────────────────────

	protected Entity Collide( string type, int xPixel, int yPixel )
	{
		return _scene?.Collide( this, type, xPixel, yPixel );
	}

	protected Entity Collide( string type )
	{
		return _scene?.Collide( this, type, PixelX, PixelY );
	}

	protected List<Entity> CollideWithAll( string type, int xPixel, int yPixel )
	{
		return _scene?.CollideWithAll( this, type, xPixel, yPixel );
	}

	protected List<Entity> CollideWithAll( string type )
	{
		return _scene?.CollideWithAll( this, type, PixelX, PixelY );
	}

	protected bool IsInBounds( int xPixel, int yPixel, int padding = 0 )
	{
		return _scene?.IsInBounds( this, xPixel, yPixel, padding ) ?? false;
	}

	protected void Unpenetrate( Entity other )
	{
		if ( !OffsetHitbox.Overlaps( other.OffsetHitbox ) )
			return;

		PixelRect myHitbox = OffsetHitbox;
		PixelRect otherHitbox = other.OffsetHitbox;

		int leftPen = myHitbox.XMax - otherHitbox.XMin + 1;
		int rightPen = otherHitbox.XMax - myHitbox.XMin + 1;
		int upPen = otherHitbox.YMax - myHitbox.YMin + 1;
		int downPen = myHitbox.YMax - otherHitbox.YMin + 1;

		Direction dir = Direction.Up;
		int amount = upPen;
		if ( downPen < amount ) { dir = Direction.Down; amount = downPen; }
		if ( leftPen < amount ) { dir = Direction.Left; amount = leftPen; }
		if ( rightPen < amount ) { dir = Direction.Right; amount = rightPen; }

		switch ( dir )
		{
			case Direction.Up: PixelY += amount; break;
			case Direction.Down: PixelY -= amount; break;
			case Direction.Left: PixelX -= amount; break;
			case Direction.Right: PixelX += amount; break;
		}
	}

	// ── Bounds check ─────────────────────────────────────────────────────

	public bool IsInScreenBounds()
	{
		if ( _currentAnimation is null )
			return false;

		// Conservative AABB cull: allows one full sprite width/height of slack
		// so sprites whose origin is just off-screen but whose pixels are still
		// visible don't get incorrectly culled.
		int w = _currentAnimation.AnimSize.X;
		int h = _currentAnimation.AnimSize.Y;

		if ( PixelX < Screen.CameraPos.X - w ) return false;
		if ( PixelX > Screen.PixelWidth + Screen.CameraPos.X ) return false;
		if ( PixelY < Screen.CameraPos.Y - h ) return false;
		if ( PixelY > Screen.PixelHeight + Screen.CameraPos.Y ) return false;

		return true;
	}

	// ── Helpers ──────────────────────────────────────────────────────────

	private List<PixelData> GetCurrentPixels()
	{
		return GetFlippedPixels( _currentFrameData, _currentAnimation.AnimSize );
	}

	private List<PixelData> GetFlippedPixels( FrameData frame, PixelPoint animSize )
	{
		if ( _flipX && _flipY ) return frame.GetPixelsFlippedXY( animSize.X, animSize.Y );
		if ( _flipX ) return frame.GetPixelsFlippedX( animSize.X );
		if ( _flipY ) return frame.GetPixelsFlippedY( animSize.Y );
		return frame.Pixels; // No flip — return the original list directly (no copy needed; Draw only reads it).
	}

	/// <summary>
	/// Apply colour/alpha override to a pixel, matching the original Entity.Draw logic.
	/// </summary>
	private Color32 ApplyColorOverride( Color32 color )
	{
		if ( !_overrideColor )
			return color;

		if ( _overrideAlpha )
		{
			float colorAlpha = color.a / 255f;
			if ( color.a == 0 ) return color;
			return new Color32(
				(byte)(_overriddenColor.r * 255f),
				(byte)(_overriddenColor.g * 255f),
				(byte)(_overriddenColor.b * 255f),
				(byte)(_overriddenColor.a * colorAlpha * 255f) );
		}

		return new Color32(
			(byte)(_overriddenColor.r * 255f),
			(byte)(_overriddenColor.g * 255f),
			(byte)(_overriddenColor.b * 255f),
			color.a );
	}
}