Cores/Nds/NDS.CartXfer.cs
namespace Emulotl.Nds;

public sealed partial class NDS
{
	public uint ROMCnt;
	public ushort[] AuxSpiCnt = new ushort[2];
	public byte[] AuxSpiData = new byte[2];

	public byte[] ROMCommand = new byte[8];
	private readonly byte[] ROMCmd = new byte[8];

	private uint ROMTransferPos, ROMTransferLen;

	private readonly uint[] ROMData = new uint[2];
	private uint ROMDataPosCPU, ROMDataPosCart, ROMDataCount;
	private bool ROMDataLate;

	private uint ROMAddr;
	private uint ChipID;
	private uint ROMMask;
	private uint CmdEncMode;
	private bool CartInserted;
	private uint _cartCpu;

	private void ResetCart()
	{
		ROMCnt = 0;
		AuxSpiCnt[0] = AuxSpiCnt[1] = 0;
		AuxSpiData[0] = AuxSpiData[1] = 0;
		Array.Clear( ROMCommand );
		Array.Clear( ROMCmd );
		ROMTransferPos = 0;
		ROMTransferLen = 0;
		ROMAddr = 0;
		CmdEncMode = 0;
		_cartCpu = 0;

		CartInserted = _rom != null && _rom.Length >= 0x200;

		uint cartromsize = 0x200;
		if ( _rom != null )
			while ( cartromsize < _rom.Length ) cartromsize <<= 1;

		ROMMask = cartromsize - 1;

		ChipID = 0x000000C2;
		if ( cartromsize >= 1024 * 1024 && cartromsize <= 128 * 1024 * 1024 )
			ChipID |= ((cartromsize >> 20) - 1) << 8;
		else
			ChipID |= (0x100 - (cartromsize >> 28)) << 8;
	}

	private byte RomByte( uint addr ) => addr < (uint)_rom.Length ? _rom[addr] : (byte)0xFF;

	private uint ROMRead32()
	{
		uint addrhi = ROMAddr & ROMMask & ~0xFFFu;
		uint addrlo = ROMAddr & 0xFFF;

		uint ret = RomByte( addrhi | addrlo );
		addrlo = (addrlo + 1) & 0xFFF;
		ret |= (uint)RomByte( addrhi | addrlo ) << 8;
		addrlo = (addrlo + 1) & 0xFFF;
		ret |= (uint)RomByte( addrhi | addrlo ) << 16;
		addrlo = (addrlo + 1) & 0xFFF;
		ret |= (uint)RomByte( addrhi | addrlo ) << 24;
		addrlo = (addrlo + 1) & 0xFFF;

		ROMAddr = addrhi | addrlo;
		return ret;
	}

	private void ROMCommandStart()
	{
		if ( CmdEncMode == 0 )
		{
			Array.Copy( ROMCommand, ROMCmd, 8 );

			switch ( ROMCmd[0] )
			{
				case 0x00:
					ROMAddr = (uint)((ROMCmd[1] << 24) | (ROMCmd[2] << 16) | (ROMCmd[3] << 8) | ROMCmd[4]);
					ROMAddr &= 0xFFF;
					return;
				case 0x3C:
					CmdEncMode = 1;
					Key1_InitKeycode( RomU32( 0x0C ), 2, 2 );
					return;
				default:
					return;
			}
		}
		else if ( CmdEncMode == 1 )
		{
			uint[] cmddec =
			[
				ByteSwap( ReadCmdU32( 4 ) ),
				ByteSwap( ReadCmdU32( 0 ) ),
			];
			Key1_Decrypt( cmddec, 0 );
			uint tmp = ByteSwap( cmddec[1] );
			cmddec[1] = ByteSwap( cmddec[0] );
			cmddec[0] = tmp;

			ROMCmd[0] = (byte)cmddec[0];
			ROMCmd[1] = (byte)(cmddec[0] >> 8);
			ROMCmd[2] = (byte)(cmddec[0] >> 16);
			ROMCmd[3] = (byte)(cmddec[0] >> 24);
			ROMCmd[4] = (byte)cmddec[1];
			ROMCmd[5] = (byte)(cmddec[1] >> 8);
			ROMCmd[6] = (byte)(cmddec[1] >> 16);
			ROMCmd[7] = (byte)(cmddec[1] >> 24);

			switch ( ROMCmd[0] & 0xF0 )
			{
				case 0x40:
					return;
				case 0x20:
					ROMAddr = (uint)((ROMCmd[2] & 0xF0) << 8);
					return;
				case 0xA0:
					CmdEncMode = 2;
					return;
				default:
					return;
			}
		}
		else if ( CmdEncMode == 2 )
		{
			Array.Copy( ROMCommand, ROMCmd, 8 );

			if ( ROMCmd[0] == 0xB7 )
			{
				ROMAddr = (uint)((ROMCmd[1] << 24) | (ROMCmd[2] << 16) | (ROMCmd[3] << 8) | ROMCmd[4]) & ROMMask;
				if ( ROMAddr < 0x8000 )
					ROMAddr = 0x8000 + (ROMAddr & 0x1FF);
			}
		}
	}

	private uint ReadCmdU32( int off )
		=> (uint)(ROMCommand[off] | (ROMCommand[off + 1] << 8) | (ROMCommand[off + 2] << 16) | (ROMCommand[off + 3] << 24));

	private uint ROMCommandReceive()
	{
		if ( CmdEncMode == 0 )
		{
			switch ( ROMCmd[0] )
			{
				case 0x9F: return 0xFFFFFFFF;
				case 0x00: return ROMRead32();
				case 0x90: return ChipID;
			}
		}
		else if ( CmdEncMode == 1 )
		{
			switch ( ROMCmd[0] & 0xF0 )
			{
				case 0x10: return ChipID;
				case 0x20: return ROMRead32();
			}
		}
		else if ( CmdEncMode == 2 )
		{
			switch ( ROMCmd[0] )
			{
				case 0xB7: return ROMRead32();
				case 0xB8: return ChipID;
			}
		}

		return 0;
	}

	public void WriteROMCnt( uint cpu, uint val, uint mask )
	{
		_cartCpu = cpu;
		val &= mask;
		uint xferstart = val & ~ROMCnt & (1u << 31);
		ROMCnt = (ROMCnt & (~mask | 0x20800000)) | (val & 0xFF7F7FFF);

		if ( (AuxSpiCnt[cpu] & (1 << 15)) == 0 ) return;
		if ( (AuxSpiCnt[cpu] & (1 << 13)) != 0 ) return;
		if ( xferstart == 0 ) return;

		uint datasize = (ROMCnt >> 24) & 0x7;
		if ( datasize == 7 )
			datasize = 4;
		else if ( datasize > 0 )
			datasize = 0x100u << (int)datasize;

		ROMTransferPos = 0;
		ROMTransferLen = datasize;

		if ( CartInserted )
			ROMCommandStart();

		ROMDataPosCPU = 0;
		ROMDataPosCart = 0;
		ROMDataCount = 0;
		ROMDataLate = false;

		uint xfercycle = (ROMCnt & (1 << 27)) != 0 ? 8u : 5u;
		uint cmddelay = 8 + (ROMCnt & 0x1FFF);
		if ( datasize != 0 )
			cmddelay += (ROMCnt >> 16) & 0x3F;

		if ( datasize == 0 )
			ScheduleEvent( SysEvent.RomTransfer, false, xfercycle * cmddelay, OnRomEnd );
		else
			ScheduleEvent( SysEvent.RomTransfer, false, xfercycle * (cmddelay + 4), OnRomReceiveData );
	}

	private void OnRomEnd( uint param ) => ROMEndTransfer();
	private void OnRomReceiveData( uint param ) => ROMReceiveData();

	private void ROMReceiveData()
	{
		uint data = CartInserted ? ROMCommandReceive() : 0;

		ROMData[ROMDataPosCart] = data;
		ROMDataPosCart ^= 1;
		ROMDataCount++;
		ROMTransferPos += 4;

		RaiseDRQ();

		if ( ROMDataCount < 2 )
			ROMAdvanceReceive();
		else
			ROMDataLate = true;
	}

	private void ROMAdvanceReceive()
	{
		if ( ROMTransferPos >= ROMTransferLen )
			return;

		uint xfercycle = (ROMCnt & (1 << 27)) != 0 ? 8u : 5u;
		uint delay = 4;
		if ( (ROMTransferPos & 0x1FF) == 0 )
			delay += (ROMCnt >> 16) & 0x3F;

		ScheduleEvent( SysEvent.RomTransfer, false, xfercycle * delay, OnRomReceiveData );
	}

	private void ROMEndTransfer()
	{
		ROMCnt &= ~(1u << 31);

		ROMTransferPos = 0;
		ROMTransferLen = 0;

		if ( (AuxSpiCnt[_cartCpu] & (1 << 14)) != 0 )
			SetIRQ( _cartCpu, IRQ.CartXferDone );
	}

	private void RaiseDRQ()
	{
		ROMCnt |= 1u << 23;
		CheckCartDMA();
	}

	public void CartCheckDMA() => CheckCartDMA();

	private void CheckCartDMA()
	{
		if ( (ROMCnt & (1 << 23)) == 0 )
			return;

		if ( _cartCpu != 0 )
			CheckDMAs( 1, 0x12 );
		else
			CheckDMAs( 0, 0x05 );
	}

	public uint ReadROMData()
	{
		uint ret = ROMData[ROMDataPosCPU];
		if ( (ROMCnt & (1 << 30)) != 0 )
			return ret;

		ROMDataPosCPU ^= 1;
		if ( ROMDataCount > 0 )
			ROMDataCount--;

		ROMCnt &= ~(1u << 23);

		if ( ROMTransferPos < ROMTransferLen )
		{
			if ( ROMDataLate )
			{
				ROMDataLate = false;
				ROMAdvanceReceive();
			}
		}
		else
		{
			if ( ROMDataCount == 0 )
				ROMEndTransfer();
			else
				RaiseDRQ();
		}

		return ret;
	}

	public void WriteROMData( uint val, uint mask )
	{
		if ( (ROMCnt & (1 << 30)) == 0 )
			return;

		ROMTransferPos += 4;
		if ( ROMTransferPos >= ROMTransferLen )
			ROMEndTransfer();
	}

	public void WriteAuxSpiCnt( uint cpu, ushort val, ushort mask )
	{
		val &= mask;
		AuxSpiCnt[cpu] = (ushort)((AuxSpiCnt[cpu] & (~mask | 0x0080)) | (val & 0xE043));
	}

	public void WriteROMCommand( int pos, byte val ) => ROMCommand[pos] = val;
}