Emulator/GbaTimer.cs
namespace sGBA;

public class GbaTimerController
{
	public Gba Gba { get; }
	public GbaTimer[] Channels = new GbaTimer[4];
	public long NextGlobalEvent = long.MaxValue;

	private static readonly int[] PrescaleBits = [0, 6, 8, 10];

	private readonly GbaTimingEvent _event;

	public GbaTimerController( Gba gba )
	{
		Gba = gba;
		for ( int i = 0; i < 4; i++ )
			Channels[i] = new GbaTimer( i );
		_event = new GbaTimingEvent( late => Tick( Gba.Cpu.Cycles - late ), 1, "timer" );
	}

	public void Reset()
	{
		for ( int i = 0; i < 4; i++ )
			Channels[i].Reset();
		NextGlobalEvent = long.MaxValue;
	}

	public void WriteControl( int idx, ushort value )
	{
		value &= 0x00C7;
		var c = Channels[idx];
		bool wasEnabled = c.Enabled;
		bool wasCountUp = c.CountUp;
		int oldPrescaleBits = c.PrescaleBits;

		if ( wasEnabled && !wasCountUp )
		{
			SyncCounter( idx );
		}

		c.Control = value;
		c.Enabled = (value & 0x80) != 0;
		c.CountUp = idx > 0 && (value & 0x04) != 0;
		c.DoIrq = (value & 0x40) != 0;
		c.PrescaleBits = value & 3;

		bool reschedule = false;

		if ( wasEnabled != c.Enabled )
		{
			reschedule = true;
			if ( c.Enabled )
			{
				c.Counter = c.Reload;
			}
		}
		else if ( wasCountUp != c.CountUp )
		{
			reschedule = true;
		}
		else if ( c.PrescaleBits != oldPrescaleBits )
		{
			reschedule = true;
		}

		if ( reschedule )
		{
			c.NextOverflowCycle = long.MaxValue;
			if ( c.Enabled && !c.CountUp )
			{
				int bits = PrescaleBits[c.PrescaleBits];
				long tickMask = (1L << bits) - 1;
				c.LastEvent = Gba.Cpu.InstructionStartCycles & ~tickMask;
				ScheduleOverflow( c );
			}
			RecalcGlobalEvent();
		}
	}

	public void RecalcGlobalEvent()
	{
		long min = long.MaxValue;
		for ( int i = 0; i < 4; i++ )
		{
			var ch = Channels[i];
			if ( ch.Enabled && !ch.CountUp && ch.NextOverflowCycle < min )
				min = ch.NextOverflowCycle;
		}
		NextGlobalEvent = min;

		if ( min == long.MaxValue )
			Gba.Timing.Deschedule( _event );
		else
			Gba.Timing.Schedule( _event, min );
	}

	private void ScheduleOverflow( GbaTimer c )
	{
		int bits = PrescaleBits[c.PrescaleBits];
		long tickMask = (1L << bits) - 1;
		long ticksToOverflow = (long)(0x10000 - c.Counter) << bits;
		c.NextOverflowCycle = (c.LastEvent & ~tickMask) + ticksToOverflow;
	}

	private void SyncCounter( int idx )
	{
		var c = Channels[idx];
		if ( !c.Enabled || c.CountUp ) return;

		int bits = PrescaleBits[c.PrescaleBits];
		long tickMask = (1L << bits) - 1;
		long currentCycle = Gba.Cpu.InstructionStartCycles & ~tickMask;
		long ticks = (currentCycle - c.LastEvent) >> bits;
		c.LastEvent = currentCycle;

		long total = c.Counter + ticks;
		int reload = c.Reload;
		int range = 0x10000 - reload;

		while ( total >= 0x10000 )
		{
			if ( range <= 0 ) { total = reload; break; }
			total -= range;
		}

		c.Counter = (ushort)total;
	}

	public ushort GetCounter( int idx )
	{
		var c = Channels[idx];
		if ( !c.Enabled || c.CountUp ) return c.Counter;

		int bits = PrescaleBits[c.PrescaleBits];
		long tickMask = (1L << bits) - 1;
		long adjustedCycle = (Gba.Cpu.InstructionStartCycles - 2) & ~tickMask;
		long ticks = (adjustedCycle - c.LastEvent) >> bits;

		int result = c.Counter + (int)ticks;

		int reload = c.Reload;
		int range = 0x10000 - reload;
		while ( result >= 0x10000 )
		{
			if ( range <= 0 ) { result = reload; break; }
			result -= range;
		}

		return (ushort)(result & 0xFFFF);
	}

	public void Tick( long eventWhen )
	{
		long now = Gba.Cpu.Cycles;

		for ( int i = 0; i < 4; i++ )
		{
			var c = Channels[i];
			if ( !c.Enabled || c.CountUp ) continue;

			while ( eventWhen >= c.NextOverflowCycle )
			{
				long overflowCycle = c.NextOverflowCycle;
				c.Counter = c.Reload;
				c.LastEvent = overflowCycle;

				int bits = PrescaleBits[c.PrescaleBits];
				long ticksToOverflow = (long)(0x10000 - c.Counter) << bits;
				if ( ticksToOverflow <= 0 ) ticksToOverflow = 1;
				c.NextOverflowCycle = overflowCycle + ticksToOverflow;

				if ( c.DoIrq )
				{
					int late = (int)(now - overflowCycle);
					Gba.Io.RaiseIrq( (GbaIrq)(1 << (3 + i)), late );
				}

				if ( i <= 1 )
				{
					Gba.Audio.OnTimerOverflow( i );
				}

				if ( i < 3 )
				{
					var next = Channels[i + 1];
					if ( next.Enabled && next.CountUp )
					{
						int cascadeLate = (int)(now - overflowCycle);
						IncrementCascade( i + 1, cascadeLate );
					}
				}
			}
		}

		RecalcGlobalEvent();
	}

	private void IncrementCascade( int idx, int late )
	{
		var c = Channels[idx];
		c.Counter++;
		if ( c.Counter == 0 )
		{
			c.Counter = c.Reload;

			if ( c.DoIrq )
			{
				Gba.Io.RaiseIrq( (GbaIrq)(1 << (3 + idx)), late );
			}

			if ( idx <= 1 )
			{
				Gba.Audio.OnTimerOverflow( idx );
			}

			if ( idx < 3 )
			{
				var next = Channels[idx + 1];
				if ( next.Enabled && next.CountUp )
				{
					IncrementCascade( idx + 1, late );
				}
			}
		}
	}
}

public class GbaTimer
{
	public int Index;
	public ushort Reload;
	public ushort Counter;
	public ushort Control;

	public bool Enabled;
	public bool CountUp;
	public bool DoIrq;
	public int PrescaleBits;

	public long LastEvent;
	public long NextOverflowCycle = long.MaxValue;

	public GbaTimer( int index )
	{
		Index = index;
	}

	public void Reset()
	{
		Reload = Counter = Control = 0;
		Enabled = CountUp = DoIrq = false;
		PrescaleBits = 0;
		LastEvent = 0;
		NextOverflowCycle = long.MaxValue;
	}
}