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

public sealed partial class NDS
{
	private const int FirmwareLength = 0x20000;
	private const uint FirmwareMask = FirmwareLength - 1;
	private const uint UserDataAddr = 0x1FE00;

	private byte[] _firmware;
	private readonly byte[] _userData = new byte[0x100];

	public byte[] GetFirmware() => _firmware;

	private static readonly byte[] BBINIT =
	[
		0x03, 0x17, 0x40, 0x00, 0x1B, 0x6C, 0x48, 0x80, 0x38, 0x00, 0x35, 0x07, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC7, 0xBB, 0x01, 0x24, 0x7F,
		0x5A, 0x01, 0x3F, 0x01, 0x3F, 0x36, 0x1D, 0x00, 0x78, 0x35, 0x55, 0x12, 0x34, 0x1C, 0x00, 0x01,
		0x0E, 0x38, 0x03, 0x70, 0xC5, 0x2A, 0x0A, 0x08, 0x04, 0x01, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFE,
		0xFE, 0xFE, 0xFE, 0xFC, 0xFC, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xF8, 0xF8, 0xF6, 0x00, 0x12, 0x14,
		0x12, 0x41, 0x23, 0x03, 0x04, 0x70, 0x35, 0x0E, 0x2C, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x12, 0x28, 0x1C
	];

	private static readonly byte[] RFINIT =
	[
		0x31, 0x4C, 0x4F, 0x21, 0x00, 0x10, 0xB0, 0x08, 0xFA, 0x15, 0x26, 0xE6, 0xC1, 0x01, 0x0E, 0x50,
		0x05, 0x00, 0x6D, 0x12, 0x00, 0x00, 0x01, 0xFF, 0x0E, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x06,
		0x06, 0x00, 0x00, 0x00, 0x18, 0x00, 0x02, 0x00, 0x00
	];

	private static readonly byte[] CHANDATA =
	[
		0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x16,
		0x26, 0x1C, 0x1C, 0x1C, 0x1D, 0x1D, 0x1D, 0x1E, 0x1E, 0x1E, 0x1E, 0x1F, 0x1E, 0x1F, 0x18,
		0x01, 0x4B, 0x4B, 0x4B, 0x4B, 0x4C, 0x4C, 0x4C, 0x4C, 0x4C, 0x4C, 0x4C, 0x4D, 0x4D, 0x4D,
		0x02, 0x6C, 0x71, 0x76, 0x5B, 0x40, 0x45, 0x4A, 0x2F, 0x34, 0x39, 0x3E, 0x03, 0x08, 0x14
	];

	private static readonly byte[] DEFAULT_UNUSED3 = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00];

	public ushort SpiCnt;

	private byte _fwCmd;
	private bool _fwHold;
	private uint _fwAddr;
	private uint _fwDataPos;
	private byte _fwStatus;
	private byte _fwData;

	private byte _pmIndex;
	private bool _pmHold;
	private uint _pmDataPos;
	private byte _pmData;
	private readonly byte[] _pmRegisters = new byte[8];
	private static readonly byte[] PmRegMasks = [0x7F, 0x00, 0x01, 0x03, 0x0F, 0x00, 0x00, 0x00];

	private byte _tscData;
	private byte _tscControlByte;
	private int _tscDataPos;
	private ushort _tscConvResult;
	private ushort TouchX, TouchY = 0xFFF;

	private static ushort CRC16( byte[] data, int offset, int len, ushort start )
	{
		ushort[] blarg = [0xC0C1, 0xC181, 0xC301, 0xC601, 0xCC01, 0xD801, 0xF001, 0xA001];
		uint s = start;

		for ( int i = 0; i < len; i++ )
		{
			s ^= data[offset + i];
			for ( int j = 0; j < 8; j++ )
			{
				if ( (s & 0x1) != 0 )
				{
					s >>= 1;
					s ^= (uint)(blarg[j] << (7 - j));
				}
				else
				{
					s >>= 1;
				}
			}
		}

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

	private void BuildUserData()
	{
		Array.Clear( _userData );

		Write16( _userData, 0x00, 5 );
		_userData[0x03] = 1;
		_userData[0x04] = 1;

		ReadOnlySpan<char> nick = "Emulotl";
		for ( int i = 0; i < nick.Length; i++ )
			Write16( _userData, (uint)(0x06 + i * 2), nick[i] );
		Write16( _userData, 0x1A, (ushort)nick.Length );

		Write16( _userData, 0x64, 0x0031 );

		Write16( _userData, 0x58, 0 );
		Write16( _userData, 0x5A, 0 );
		_userData[0x5C] = 0;
		_userData[0x5D] = 0;
		Write16( _userData, 0x5E, 255 << 4 );
		Write16( _userData, 0x60, 191 << 4 );
		_userData[0x62] = 255;
		_userData[0x63] = 191;

		ushort crc = CRC16( _userData, 0, 0x70, 0xFFFF );
		Write16( _userData, 0x72, crc );
	}

	private void ResetSpi()
	{
		BuildUserData();

		_firmware ??= new byte[FirmwareLength];
		Array.Fill( _firmware, (byte)0xFF );

		Array.Clear( _firmware, 0, 0x200 );

		_firmware[0x08] = (byte)'M';
		_firmware[0x09] = (byte)'E';
		_firmware[0x0A] = (byte)'L';
		_firmware[0x0B] = (byte)'N';

		_firmware[0x1D] = 0x20;
		Write16( _firmware, 0x20, (ushort)(UserDataAddr >> 3) );

		Write16( _firmware, 0x2C, 0x0138 );
		_firmware[0x2E] = 0x00;
		_firmware[0x2F] = 0x06;
		Array.Copy( DEFAULT_UNUSED3, 0, _firmware, 0x30, DEFAULT_UNUSED3.Length );
		_firmware[0x36] = 0x00; _firmware[0x37] = 0x09; _firmware[0x38] = 0xBF;
		_firmware[0x39] = 0x11; _firmware[0x3A] = 0x22; _firmware[0x3B] = 0x33;
		Write16( _firmware, 0x3C, 0x3FFE );
		_firmware[0x3E] = 0xFF; _firmware[0x3F] = 0xFF;
		_firmware[0x40] = 0x03;
		_firmware[0x41] = 0x94;
		_firmware[0x42] = 0x29;
		_firmware[0x43] = 0x02;

		Write16( _firmware, 0x44, 0x0002 );
		Write16( _firmware, 0x46, 0x0017 );
		Write16( _firmware, 0x48, 0x0026 );
		Write16( _firmware, 0x4A, 0x1818 );
		Write16( _firmware, 0x4C, 0x0048 );
		Write16( _firmware, 0x4E, 0x4840 );
		Write16( _firmware, 0x50, 0x0058 );
		Write16( _firmware, 0x52, 0x0042 );
		Write16( _firmware, 0x54, 0x0146 );
		Write16( _firmware, 0x56, 0x8064 );
		Write16( _firmware, 0x58, 0xE6E6 );
		Write16( _firmware, 0x5A, 0x2443 );
		Write16( _firmware, 0x5C, 0x000E );
		Write16( _firmware, 0x5E, 0x0001 );
		Write16( _firmware, 0x60, 0x0001 );
		Write16( _firmware, 0x62, 0x0402 );

		Array.Copy( BBINIT, 0, _firmware, 0x64, BBINIT.Length );
		_firmware[0xCD] = 0x00;
		Array.Copy( RFINIT, 0, _firmware, 0xCE, RFINIT.Length );
		_firmware[0xF7] = 0x02;
		Array.Copy( CHANDATA, 0, _firmware, 0xF8, CHANDATA.Length );
		Array.Fill( _firmware, (byte)0xFF, 0x134, 46 );

		Write16( _firmware, 0x2A, CRC16( _firmware, 0x2C, 0x138, 0x0000 ) );

		_firmware[0x2FF] = 0x80;

		Array.Copy( _userData, 0, _firmware, UserDataAddr, 0x100 );
		Array.Copy( _userData, 0, _firmware, UserDataAddr + 0x100, 0x100 );

		for ( uint ap = 0; ap < 3; ap++ )
		{
			uint b = UserDataAddr - 0x400 + ap * 0x100;
			Array.Clear( _firmware, (int)b, 0x100 );

			if ( ap == 0 )
			{
				ReadOnlySpan<char> ssid = "EmulotlAP";
				for ( int i = 0; i < ssid.Length; i++ )
					_firmware[b + 0x40 + i] = (byte)ssid[i];
				_firmware[b + 0xE7] = 0x00;
			}
			else
			{
				_firmware[b + 0xE7] = 0xFF;
			}

			_firmware[b + 0xEF] = 0x01;
			Write16( _firmware, b + 0xFE, CRC16( _firmware, (int)b, 0xFE, 0x0000 ) );
		}

		SpiCnt = 0;

		_fwCmd = 0;
		_fwHold = false;
		_fwAddr = 0;
		_fwDataPos = 0;
		_fwStatus = 0;
		_fwData = 0;

		_pmIndex = 0;
		_pmHold = false;
		_pmDataPos = 0;
		_pmData = 0;
		Array.Clear( _pmRegisters );
		_pmRegisters[4] = 0x40;

		_tscData = 0;
		_tscControlByte = 0;
		_tscDataPos = 0;
		_tscConvResult = 0;
		TouchX = 0;
		TouchY = 0xFFF;
	}

	public void SetupFirmwareDirectBoot()
	{
		ARM9Write32( 0x027FF864, 0 );
		ARM9Write32( 0x027FF868, UserDataAddr );

		ARM9Write16( 0x027FF874, 0 );
		ARM9Write16( 0x027FF876, 0 );

		for ( uint i = 0; i < 0x70; i += 4 )
			ARM9Write32( 0x027FFC80 + i, Read32( _userData, i ) );
	}

	public void WriteSpiCnt( ushort val )
	{
		if ( (SpiCnt & (1 << 15)) != 0 && (val & (1 << 15)) == 0 )
			SpiRelease( (SpiCnt >> 8) & 0x3 );

		SpiCnt = (ushort)((SpiCnt & 0x0080) | (val & 0xCF03));
	}

	public byte ReadSpiData()
	{
		if ( (SpiCnt & (1 << 15)) == 0 ) return 0;
		if ( (SpiCnt & (1 << 7)) != 0 ) return 0;

		return ((SpiCnt >> 8) & 0x3) switch
		{
			0 => _pmData,
			1 => _fwData,
			2 => _tscData,
			_ => 0,
		};
	}

	public void WriteSpiData( byte val )
	{
		if ( (SpiCnt & (1 << 15)) == 0 ) return;
		if ( (SpiCnt & (1 << 7)) != 0 ) return;

		SpiCnt |= 1 << 7;

		int dev = (SpiCnt >> 8) & 0x3;
		switch ( dev )
		{
			case 0: PmWrite( val ); break;
			case 1: FwWrite( val ); break;
			case 2: TscWrite( val ); break;
		}

		if ( (SpiCnt & (1 << 11)) == 0 )
			SpiRelease( dev );

		uint delay = (uint)(8 * (8 << (SpiCnt & 0x3)));
		ScheduleEvent( SysEvent.SpiTransfer, false, delay, OnSpiTransferDone );
	}

	private void OnSpiTransferDone( uint param )
	{
		SpiCnt = (ushort)(SpiCnt & ~(1 << 7));
		if ( (SpiCnt & (1 << 14)) != 0 )
			SetIRQ( 1, IRQ.SPI );
	}

	private void SpiRelease( int dev )
	{
		switch ( dev )
		{
			case 0: _pmHold = false; break;
			case 1: _fwHold = false; _fwCmd = 0; break;
			case 2: break;
		}
	}

	private void FwWrite( byte val )
	{
		if ( !_fwHold )
		{
			_fwCmd = val;
			_fwHold = true;
			_fwData = 0;
			_fwDataPos = 1;
			_fwAddr = 0;

			switch ( _fwCmd )
			{
				case 0x04: _fwStatus &= 0xFD; break;
				case 0x06: _fwStatus |= 0x02; break;
			}
			return;
		}

		switch ( _fwCmd )
		{
			case 0x03:
				if ( _fwDataPos < 4 )
				{
					_fwAddr = (_fwAddr << 8) | val;
					_fwData = 0;
				}
				else
				{
					_fwData = _firmware[_fwAddr & FirmwareMask];
					_fwAddr++;
				}
				_fwDataPos++;
				break;

			case 0x05:
				_fwData = _fwStatus;
				break;

			case 0x0A:
				if ( _fwDataPos < 4 )
				{
					_fwAddr = (_fwAddr << 8) | val;
					_fwData = 0;
				}
				else
				{
					_firmware[_fwAddr & FirmwareMask] = val;
					_fwData = val;
					_fwAddr++;
				}
				_fwDataPos++;
				break;

			case 0x9F:
				_fwData = _fwDataPos switch { 1 => 0x20, 2 => 0x40, 3 => 0x12, _ => 0 };
				_fwDataPos++;
				break;

			default:
				_fwData = 0xFF;
				break;
		}
	}

	private void PmWrite( byte val )
	{
		if ( !_pmHold )
		{
			_pmIndex = val;
			_pmHold = true;
			_pmData = 0;
			_pmDataPos = 1;
			return;
		}

		if ( _pmDataPos == 1 )
		{
			int regid = _pmIndex & 0x07;
			if ( (_pmIndex & 0x80) != 0 )
			{
				_pmData = _pmRegisters[regid];
			}
			else
			{
				_pmRegisters[regid] = (byte)((_pmRegisters[regid] & ~PmRegMasks[regid]) | (val & PmRegMasks[regid]));
			}
		}

		_pmDataPos++;
	}

	private void TscWrite( byte val )
	{
		if ( _tscDataPos == 1 )
			_tscData = (byte)((_tscConvResult >> 5) & 0x7F);
		else if ( _tscDataPos == 2 )
			_tscData = (byte)((_tscConvResult << 3) & 0xFF);
		else
			_tscData = 0;

		if ( (val & 0x80) != 0 )
		{
			_tscControlByte = val;
			_tscDataPos = 1;

			switch ( _tscControlByte & 0x70 )
			{
				case 0x10: _tscConvResult = TouchY; break;
				case 0x50: _tscConvResult = TouchX; break;
				default: _tscConvResult = 0xFFF; break;
			}

			if ( (_tscControlByte & 0x08) != 0 )
				_tscConvResult &= 0x0FF0;
		}
		else
		{
			_tscDataPos++;
		}
	}

	public void SetTouchCoords( ushort x, ushort y )
	{
		TouchX = x;
		TouchY = y;

		if ( y == 0xFFF )
		{
			ExtKeyIn = (ushort)(ExtKeyIn | 0x40);
			return;
		}

		TouchX <<= 4;
		TouchY <<= 4;
		ExtKeyIn = (ushort)(ExtKeyIn & ~0x40);
	}

	public void TouchScreen( int x, int y ) => SetTouchCoords( (ushort)x, (ushort)y );
	public void ReleaseScreen() => SetTouchCoords( 0, 0xFFF );
}