Emulator/GbaVideo.cs
namespace sGBA;

public partial class GbaVideo
{
	public Gba Gba { get; }

	public int VCount;
	public int Dot;

	public ushort DispCnt;
	public ushort DispStat;
	public ushort[] BgCnt = new ushort[4];
	public short[] BgHOfs = new short[4];
	public short[] BgVOfs = new short[4];
	public short[] BgPA = new short[2];
	public short[] BgPB = new short[2];
	public short[] BgPC = new short[2];
	public short[] BgPD = new short[2];
	public int[] BgX = new int[2];
	public int[] BgY = new int[2];
	public int[] BgRefX = new int[2];
	public int[] BgRefY = new int[2];

	public ushort BldCnt;
	public ushort BldAlpha;
	public ushort BldY;

	private ushort _win0H;
	private ushort _win0V;
	private ushort _win1H;
	private ushort _win1V;

	public ushort Win0H
	{
		get => _win0H;
		set => _win0H = CleanWindowRange( value, GbaConstants.ScreenWidth );
	}

	public ushort Win0V
	{
		get => _win0V;
		set => _win0V = CleanWindowRange( value, GbaConstants.ScreenHeight );
	}

	public ushort Win1H
	{
		get => _win1H;
		set => _win1H = CleanWindowRange( value, GbaConstants.ScreenWidth );
	}

	public ushort Win1V
	{
		get => _win1V;
		set => _win1V = CleanWindowRange( value, GbaConstants.ScreenHeight );
	}

	public ushort WinIn, WinOut;
	public ushort Mosaic;

	internal int _firstAffine = -1;
	internal int _lastDrawnY = -1;
	internal int[] _enabledAtY = [int.MaxValue, int.MaxValue, int.MaxValue, int.MaxValue];
	internal bool[] _wasFullyEnabled = new bool[4];
	internal uint[] _oldCharBase = new uint[2];
	internal int[] _oldCharBaseFirstY = new int[2];

	internal bool _oamDirty = true;
	internal int _oamBatchOffset;
	internal int _oamMax;

	private readonly GbaTimingEvent _event;
	private bool _nextIsHBlank;

	public GbaVideo( Gba gba )
	{
		Gba = gba;
		_event = new GbaTimingEvent( OnVideoEvent, 4, "video" );
	}

	private void OnVideoEvent( long late )
	{
		long when = Gba.Cpu.Cycles - late;

		if ( _nextIsHBlank )
		{
			StartHBlank( late );
			_nextIsHBlank = false;
			Gba.Timing.Schedule( _event, when + GbaConstants.VideoHBlankLength );
		}
		else
		{
			StartHDraw( late );
			_nextIsHBlank = true;
			Gba.Timing.Schedule( _event, when + GbaConstants.VideoHDrawLength );
		}
	}

	internal void ScheduleEventForLoad()
	{
		bool inHBlank = (DispStat & 0x0002) != 0;
		_nextIsHBlank = !inHBlank;
		long phaseLength = inHBlank ? GbaConstants.VideoHBlankLength : GbaConstants.VideoHDrawLength;
		Gba.Timing.Schedule( _event, Gba.Cpu.Cycles + phaseLength );
	}

	private static ushort CleanWindowRange( ushort value, int limit )
	{
		int start = value >> 8;
		int end = value & 0xFF;

		if ( start > limit && start > end )
			start = 0;

		if ( end > limit )
		{
			end = limit;
			if ( start > limit )
				start = limit;
		}

		return (ushort)(end | (start << 8));
	}

	public void Reset()
	{
		VCount = 0;
		Dot = 0;
		DispCnt = 0;
		DispStat = 0;
		Array.Clear( BgCnt );
		Array.Clear( BgHOfs );
		Array.Clear( BgVOfs );
		BgPA[0] = 0x100; BgPA[1] = 0x100;
		Array.Clear( BgPB );
		Array.Clear( BgPC );
		BgPD[0] = 0x100; BgPD[1] = 0x100;
		Array.Clear( BgX );
		Array.Clear( BgY );
		Array.Clear( BgRefX );
		Array.Clear( BgRefY );
		_win0H = 0;
		_win0V = 0;
		_win1H = 0;
		_win1V = 0;

		_firstAffine = -1;
		_lastDrawnY = -1;
		for ( int i = 0; i < 4; i++ ) _enabledAtY[i] = int.MaxValue;
		Array.Clear( _wasFullyEnabled );
		_oldCharBase[0] = 0; _oldCharBase[1] = 0;
		_oldCharBaseFirstY[0] = 0; _oldCharBaseFirstY[1] = 0;

		_oamDirty = true;
		_oamBatchOffset = 0;
		_oamMax = 0;

		_nextIsHBlank = true;
		Gba.Timing.Schedule( _event, Gba.Cpu.Cycles + GbaConstants.VideoHDrawLength );
	}

	public void WriteDispCnt( ushort value )
	{
		value &= 0xFFF7;
		ushort oldVal = DispCnt;
		DispCnt = value;

		for ( int i = 0; i < 4; i++ )
		{
			bool wasEnabled = (oldVal & (0x100 << i)) != 0;
			bool isEnabled = (value & (0x100 << i)) != 0;

			if ( !isEnabled )
			{
				if ( _enabledAtY[i] < int.MaxValue )
					_wasFullyEnabled[i] = _lastDrawnY >= 0 && _enabledAtY[i] <= _lastDrawnY;
				_enabledAtY[i] = int.MaxValue;
			}
			else if ( _enabledAtY[i] == int.MaxValue && isEnabled )
			{
				if ( _lastDrawnY < 0 )
				{
					_enabledAtY[i] = 0;
				}
				else if ( _wasFullyEnabled[i] )
				{
					_enabledAtY[i] = 0;
				}
				else
				{
					int mode = value & 7;
					_enabledAtY[i] = mode > 2 ? _lastDrawnY + 3 : _lastDrawnY + 4;
				}
				_wasFullyEnabled[i] = false;
			}
		}
	}

	public void WriteBgCnt( int bg, ushort value )
	{
		ushort oldVal = BgCnt[bg];
		BgCnt[bg] = value;

		if ( bg >= 2 )
		{
			int idx = bg - 2;
			uint oldCB = (uint)((oldVal >> 2) & 3) * 0x4000u;
			uint newCB = (uint)((value >> 2) & 3) * 0x4000u;
			if ( oldCB != newCB )
			{
				_oldCharBase[idx] = oldCB;
				_oldCharBaseFirstY[idx] = VCount;
			}
		}
	}

	public void StartHBlank( long cyclesLate )
	{
		if ( VCount < GbaConstants.VisibleLines )
		{
			int bgMode = DispCnt & 7;
			if ( bgMode != 0 )
			{
				if ( _firstAffine < 0 )
					_firstAffine = VCount;
			}
			else
			{
				_firstAffine = -1;
			}

			_lastDrawnY = VCount;
			CaptureScanline( VCount );
		}

		DispStat |= 0x0002;

		if ( VCount < GbaConstants.VisibleLines )
			Gba.Dma.OnHBlank( cyclesLate );

		if ( VCount >= 2 && VCount < GbaConstants.VisibleLines + 2 )
			Gba.Dma.OnDisplayStart( cyclesLate );

		if ( (DispStat & 0x0010) != 0 )
			Gba.Io.RaiseIrq( GbaIrq.HBlank, (int)cyclesLate - 6 );

		if ( VCount < GbaConstants.VisibleLines )
		{
			int affMode = DispCnt & 7;
			if ( affMode >= 1 )
			{
				if ( _enabledAtY[2] <= VCount )
				{
					BgX[0] += BgPB[0];
					BgY[0] += BgPD[0];
				}
				if ( _enabledAtY[3] <= VCount )
				{
					BgX[1] += BgPB[1];
					BgY[1] += BgPD[1];
				}
			}
		}
	}

	public void StartHDraw( long cyclesLate )
	{
		DispStat &= unchecked((ushort)~0x0002);
		VCount++;

		if ( VCount == GbaConstants.VisibleLines )
		{
			DispStat |= 0x0001;
			if ( (DispStat & 0x0008) != 0 )
				Gba.Io.RaiseIrq( GbaIrq.VBlank, (int)cyclesLate );
			Gba.Dma.OnVBlank( cyclesLate );
			Gba.Io.WirelessAdapter.FrameUpdate();

			SnapshotVram();
			CommitFrame();

			_oamDirty = true;
			_oamBatchOffset = 0;
			_oamMax = 0;

			BgX[0] = BgRefX[0];
			BgY[0] = BgRefY[0];
			BgX[1] = BgRefX[1];
			BgY[1] = BgRefY[1];

			_firstAffine = -1;
			_lastDrawnY = -1;
			for ( int i = 0; i < 4; i++ )
			{
				if ( _enabledAtY[i] < int.MaxValue )
					_enabledAtY[i] = 0;
			}
			_oldCharBase[0] = (uint)((BgCnt[2] >> 2) & 3) * 0x4000u;
			_oldCharBase[1] = (uint)((BgCnt[3] >> 2) & 3) * 0x4000u;
			_oldCharBaseFirstY[0] = 0;
			_oldCharBaseFirstY[1] = 0;
		}
		else if ( VCount == GbaConstants.VideoVerticalTotalPixels )
		{
			VCount = 0;
			Gba.FrameCounter++;
			Gba.TotalCycles += GbaConstants.VideoTotalLength;
		}

		if ( VCount == GbaConstants.VideoVerticalTotalPixels - 1 )
		{
			DispStat &= unchecked((ushort)~0x0001);
		}

		int lyc = (DispStat >> 8) & 0xFF;
		if ( VCount == lyc )
		{
			DispStat |= 0x0004;
			if ( (DispStat & 0x0020) != 0 )
				Gba.Io.RaiseIrq( GbaIrq.VCounter, (int)cyclesLate );
		}
		else
		{
			DispStat &= unchecked((ushort)~0x0004);
		}
	}
}