/*  PCSX2 - PS2 Emulator for PCs
 *  Copyright (C) 2002-2022  PCSX2 Dev Team
 *
 *  PCSX2 is free software: you can redistribute it and/or modify it under the terms
 *  of the GNU Lesser General Public License as published by the Free Software Found-
 *  ation, either version 3 of the License, or (at your option) any later version.
 *
 *  PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 *  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 *  PURPOSE.  See the GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along with PCSX2.
 *  If not, see <http://www.gnu.org/licenses/>.
 */

#include "PrecompiledHeader.h"
#include "R3000A.h"
#include "IopHw.h"
#include "IopDma.h"

#include "Common.h"
#include "Sio.h"
#include "MemoryCardProtocol.h"
#include "MultitapProtocol.h"
#include "Config.h"
#include "Host.h"
#include "PAD/Host/PAD.h"

#include "common/Timer.h"
#include "Recording/InputRecording.h"
#include "IconsFontAwesome5.h"

#define SIO0LOG_ENABLE 0
#define SIO2LOG_ENABLE 0

#define Sio0Log if (SIO0LOG_ENABLE) DevCon
#define Sio2Log if (SIO2LOG_ENABLE) DevCon

std::deque<u8> fifoIn;
std::deque<u8> fifoOut;

Sio0 sio0;
Sio2 sio2;

_mcd mcds[2][4];
_mcd *mcd;

// ============================================================================
// SIO0
// ============================================================================

void Sio0::ClearStatAcknowledge()
{
	stat &= ~(SIO0_STAT::ACK);
}

Sio0::Sio0()
{
	this->FullReset();
}

Sio0::~Sio0() = default;

void Sio0::SoftReset()
{
	padStarted = false;
	sioMode = SioMode::NOT_SET;
	sioCommand = 0;
	sioStage = SioStage::IDLE;
}

void Sio0::FullReset()
{
	SoftReset();

	port = 0;
	slot = 0;

	for (int i = 0; i < 2; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			mcds[i][j].term = 0x55;
			mcds[i][j].port = i;
			mcds[i][j].slot = j;
			mcds[i][j].FLAG = 0x08;
			mcds[i][j].autoEjectTicks = 0;
		}
	}

	mcd = &mcds[0][0];
}

// Simulates the ACK line on the bus. Peripherals are expected to send an ACK signal
// over this line to tell the PS1 "keep sending me things I'm not done yet". The PS1
// then uses this after it receives the peripheral's response to decide what to do.
void Sio0::Acknowledge()
{
	stat |= SIO0_STAT::ACK;
}

void Sio0::Interrupt(Sio0Interrupt sio0Interrupt)
{
	switch (sio0Interrupt)
	{
		case Sio0Interrupt::TEST_EVENT:
			iopIntcIrq(7);
			break;
		case Sio0Interrupt::STAT_READ:
			ClearStatAcknowledge();
			break;
		case Sio0Interrupt::TX_DATA_WRITE:
			break;
		default:
			Console.Error("%s(%d) Invalid parameter", __FUNCTION__, sio0Interrupt);
			assert(false);
			break;
	}
	
	if (!(psxRegs.interrupt & (1 << IopEvt_SIO)))
	{
		PSX_INT(IopEvt_SIO, PSXCLK / 250000); // PSXCLK/250000);
	}
}

u8 Sio0::GetTxData()
{
	Sio0Log.WriteLn("%s() SIO0 TX_DATA Read (%02X)", __FUNCTION__, txData);
	return txData;
}

u8 Sio0::GetRxData()
{
	Sio0Log.WriteLn("%s() SIO0 RX_DATA Read (%02X)", __FUNCTION__, rxData);
	stat |= (SIO0_STAT::TX_READY | SIO0_STAT::TX_EMPTY);
	stat &= ~(SIO0_STAT::RX_FIFO_NOT_EMPTY);
	return rxData;
}

u32 Sio0::GetStat()
{
	Sio0Log.WriteLn("%s() SIO0 STAT Read (%08X)", __FUNCTION__, stat);
	const u32 ret = stat;
	Interrupt(Sio0Interrupt::STAT_READ);
	return ret;
}

u16 Sio0::GetMode()
{
	Sio0Log.WriteLn("%s() SIO0 MODE Read (%08X)", __FUNCTION__, mode);
	return mode;
}

u16 Sio0::GetCtrl()
{
	Sio0Log.WriteLn("%s() SIO0 CTRL Read (%08X)", __FUNCTION__, ctrl);
	return ctrl;
}

u16 Sio0::GetBaud()
{
	Sio0Log.WriteLn("%s() SIO0 BAUD Read (%08X)", __FUNCTION__, baud);
	return baud;
}

void Sio0::SetTxData(u8 value)
{
	Sio0Log.WriteLn("%s(%02X) SIO0 TX_DATA Write", __FUNCTION__, value);

	stat |= SIO0_STAT::TX_READY | SIO0_STAT::TX_EMPTY;
	stat |= (SIO0_STAT::RX_FIFO_NOT_EMPTY);

	if (!(ctrl & SIO0_CTRL::TX_ENABLE))
	{
		Console.Warning("%s(%02X) CTRL in illegal state, exiting instantly", __FUNCTION__, value);
		return;
	}

	txData = value;
	u8 res = 0;

	switch (sioStage)
	{
		case SioStage::IDLE:
			sioMode = value;
			stat |= SIO0_STAT::TX_READY;

			switch (sioMode)
			{
				case SioMode::PAD:
					res = PADstartPoll(port, slot);

					if (res)
					{
						Acknowledge();
					}

					break;
				case SioMode::MEMCARD:
					mcd = &mcds[port][slot];

					// Check if auto ejection is active. If so, set RECV1 to DISCONNECTED,
					// and zero out the fifo to simulate dead air over the wire.
					if (mcd->autoEjectTicks)
					{
						SetRxData(0x00);
						mcd->autoEjectTicks--;

						if (mcd->autoEjectTicks == 0)
						{
							Host::AddKeyedFormattedOSDMessage(
								StringUtil::StdStringFromFormat("AutoEjectSlotClear%u%u", port, slot),
								10.0f,
								"Memory card in port %d / slot %d reinserted",
								port + 1,
								slot + 1);
						}

						return;
					}
					
					// If memcard is missing, not PS1, or auto ejected, do not let SIO0 stage advance,
					// reply with dead air and no ACK.
					if (!mcd->IsPresent() || !mcd->IsPSX())
					{
						SetRxData(0x00);
						return;
					}

					Acknowledge();
					break;
			}

			SetRxData(res);
			sioStage = SioStage::WAITING_COMMAND;
			break;
		case SioStage::WAITING_COMMAND:
			stat &= ~(SIO0_STAT::TX_READY);

			if (IsPadCommand(value))
			{
				res = PADpoll(value);
				SetRxData(res);

				if (!PADcomplete())
				{
					Acknowledge();
				}

				sioStage = SioStage::WORKING;
			}
			else if (IsMemcardCommand(value))
			{
				SetRxData(flag);
				Acknowledge();
				sioCommand = value;
				sioStage = SioStage::WORKING;
			}
			else if (IsPocketstationCommand(value))
			{
				// Set the line low, no acknowledge.
				SetRxData(0x00);
				sioStage = SioStage::IDLE;
			}
			else
			{
				Console.Error("%s(%02X) Bad SIO command", __FUNCTION__, value);
				SetRxData(0xff);
				SoftReset();
			}

			break;
		case SioStage::WORKING:
			switch (sioMode)
			{
				case SioMode::PAD:
					res = PADpoll(value);
					SetRxData(res);

					if (!PADcomplete())
					{
						Acknowledge();
					}

					break;
				case SioMode::MEMCARD:
					SetRxData(Memcard(value));
					break;
				default:
					Console.Error("%s(%02X) Unhandled SioMode: %02X", __FUNCTION__, value, sioMode);
					SetRxData(0xff);
					SoftReset();
					break;
			}

			break;
		default:
			Console.Error("%s(%02X) Unhandled SioStage: %02X", __FUNCTION__, value, static_cast<u8>(sioStage));
			SetRxData(0xff);
			SoftReset();
			break;
	}

	Interrupt(Sio0Interrupt::TX_DATA_WRITE);
}

void Sio0::SetRxData(u8 value)
{
	Sio0Log.WriteLn("%s(%02X) SIO0 RX_DATA Write", __FUNCTION__, value);
	rxData = value;
}

void Sio0::SetStat(u32 value)
{
	Sio0Log.Error("%s(%08X) SIO0 STAT Write", __FUNCTION__, value);
}

void Sio0::SetMode(u16 value)
{
	Sio0Log.WriteLn("%s(%04X) SIO0 MODE Write", __FUNCTION__, value);
	mode = value;
}

void Sio0::SetCtrl(u16 value)
{
	Sio0Log.WriteLn("%s(%04X) SIO0 CTRL Write", __FUNCTION__, value);
	ctrl = value;
	port = (ctrl & SIO0_CTRL::PORT) > 0;

	// CTRL appears to be set to 0 between every "transaction".
	// Not documented anywhere, but we'll use this to "reset"
	// the SIO0 state, particularly during the annoying probes
	// to memcards that occur when a game boots.
	if (ctrl == 0)
	{
		g_MemoryCardProtocol.ResetPS1State();
		SoftReset();
	}

	// If CTRL acknowledge, reset STAT bits 3 and 9
	if (ctrl & SIO0_CTRL::ACK)
	{
		stat &= ~(SIO0_STAT::IRQ | SIO0_STAT::RX_PARITY_ERROR);
	}

	if (ctrl & SIO0_CTRL::RESET)
	{
		stat = 0;
		ctrl = 0;
		mode = 0;
		SoftReset();
	}
}

void Sio0::SetBaud(u16 value)
{
	Sio0Log.WriteLn("%s(%04X) SIO0 BAUD Write", __FUNCTION__, value);
	baud = value;
}

bool Sio0::IsPadCommand(u8 command)
{
	return command >= PadCommand::UNK_0 && command <= PadCommand::ANALOG;
}

bool Sio0::IsMemcardCommand(u8 command)
{
	return command == MemcardCommand::PS1_READ || command == MemcardCommand::PS1_STATE || command == MemcardCommand::PS1_WRITE;
}

bool Sio0::IsPocketstationCommand(u8 command)
{
	return command == MemcardCommand::PS1_POCKETSTATION;
}

u8 Sio0::Pad(u8 value)
{
	if (PADcomplete())
	{
		padStarted = false;
	}
	else if (!padStarted)
	{
		padStarted = true;
		PADstartPoll(port, slot);
		Acknowledge();
	}

	return PADpoll(value);
}

u8 Sio0::Memcard(u8 value)
{
	switch (sioCommand)
	{
		case MemcardCommand::PS1_READ:
			return g_MemoryCardProtocol.PS1Read(value);
		case MemcardCommand::PS1_STATE:
			return g_MemoryCardProtocol.PS1State(value);
		case MemcardCommand::PS1_WRITE:
			return g_MemoryCardProtocol.PS1Write(value);
		case MemcardCommand::PS1_POCKETSTATION:
			return g_MemoryCardProtocol.PS1Pocketstation(value);
		default:
			Console.Error("%s(%02X) Unhandled memcard command (%02X)", __FUNCTION__, value, sioCommand);
			SoftReset();
			break;
	}

	return 0xff;
}

// ============================================================================
// SIO2
// ============================================================================


Sio2::Sio2()
{
	this->FullReset();
}

Sio2::~Sio2() = default;

void Sio2::SoftReset()
{
	send3Read = false;
	send3Position = 0;
	commandLength = 0;
	processedLength = 0;
	// Clear dmaBlockSize, in case the next SIO2 command is not sent over DMA11.
	dmaBlockSize = 0;
	send3Complete = false;

	// Anything in fifoIn which was not necessary to consume should be cleared out prior to the next SIO2 cycle.
	while (!fifoIn.empty())
	{
		fifoIn.pop_front();
	}
}

void Sio2::FullReset()
{
	this->SoftReset();

	for (size_t i = 0; i < send3.size(); i++)
	{
		send3.at(i) = 0;
	}

	for (size_t i = 0; i < send1.size(); i++)
	{
		send1.at(i) = 0;
		send2.at(i) = 0;
	}

	dataIn = 0;
	dataOut = 0;
	SetCtrl(Sio2Ctrl::SIO2MAN_RESET);
	SetRecv1(Recv1::DISCONNECTED);
	recv2 = Recv2::DEFAULT;
	recv3 = Recv3::DEFAULT;
	unknown1 = 0;
	unknown2 = 0;
	iStat = 0;

	port = 0;
	slot = 0;

	while (!fifoOut.empty())
	{
		fifoOut.pop_front();
	}

	for (int i = 0; i < 2; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			mcds[i][j].term = 0x55;
			mcds[i][j].port = i;
			mcds[i][j].slot = j;
			mcds[i][j].FLAG = 0x08;
			mcds[i][j].autoEjectTicks = 0;
		}
	}

	mcd = &mcds[0][0];
}


void Sio2::Interrupt()
{
	iopIntcIrq(17);
}

void Sio2::SetCtrl(u32 value)
{
	this->ctrl = value;

	if (this->ctrl & Sio2Ctrl::START_TRANSFER)
	{
		Interrupt();
	}
}

void Sio2::SetSend3(size_t position, u32 value)
{
	this->send3.at(position) = value;

	if (position == 0)
	{
		SoftReset();
	}
}

void Sio2::SetRecv1(u32 value)
{
	this->recv1 = value;
}

void Sio2::Pad()
{
	// Send PAD our current port, and get back whatever it says the first response byte should be.
	const u8 firstResponseByte = PADstartPoll(port, slot);
	fifoOut.push_back(firstResponseByte);
	// Some games will refuse to read ALL pads, if RECV1 is not set to the CONNECTED value when ANY pad is polled,
	// REGARDLESS of whether that pad is truly connected or not.
	SetRecv1(Recv1::CONNECTED);

	// Then for every byte in fifoIn, pass to PAD and see what it kicks back to us.
	while (!fifoIn.empty())
	{
		const u8 commandByte = fifoIn.front();
		fifoIn.pop_front();
		const u8 responseByte = PADpoll(commandByte);
		fifoOut.push_back(responseByte);
	}
}

void Sio2::Multitap()
{
	fifoOut.push_back(0x00);

	const bool multitapEnabled = (port == 0 && EmuConfig.MultitapPort0_Enabled) || (port == 1 && EmuConfig.MultitapPort1_Enabled);
	SetRecv1(multitapEnabled ? Recv1::CONNECTED : Recv1::DISCONNECTED);

	if (multitapEnabled)
	{
		g_MultitapProtocol.SendToMultitap();
	}
	else 
	{
		while (fifoOut.size() < commandLength)
		{
			fifoOut.push_back(0x00);
		}
	}
}

void Sio2::Infrared()
{
	SetRecv1(Recv1::DISCONNECTED);

	fifoIn.pop_front();
	const u8 responseByte = 0xff;
	
	while (fifoOut.size() < commandLength)
	{
		fifoOut.push_back(responseByte);
	}
}

void Sio2::Memcard()
{
	mcd = &mcds[port][slot];

	// Check if auto ejection is active. If so, set RECV1 to DISCONNECTED,
	// and zero out the fifo to simulate dead air over the wire.
	if (mcd->autoEjectTicks)
	{
		SetRecv1(Recv1::DISCONNECTED);
		fifoOut.push_back(0x00); // Because Sio2::Write pops the first fifoIn member

		while (!fifoIn.empty())
		{
			fifoIn.pop_front();
			fifoOut.push_back(0x00);
		}

		mcd->autoEjectTicks--;

		if (mcd->autoEjectTicks == 0)
		{
			Host::AddKeyedFormattedOSDMessage(
				StringUtil::StdStringFromFormat("AutoEjectSlotClear%u%u", port, slot),
				10.0f,
				"Memory card in port %d / slot %d reinserted",
				port + 1,
				slot + 1);
		}

		return;
	}

	SetRecv1(mcd->IsPresent() ? Recv1::CONNECTED : Recv1::DISCONNECTED);
	
	const u8 commandByte = fifoIn.front();
	fifoIn.pop_front();
	const u8 responseByte = mcd->IsPresent() ? 0x00 : 0xff;
	fifoOut.push_back(responseByte);
	// Technically, the FLAG byte is only for PS1 memcards. However,
	// since this response byte is still a dud on PS2 memcards, we can
	// basically just cheat and always make this our second response byte for memcards.
	fifoOut.push_back(mcd->FLAG);
	u8 ps1Input = 0;
	u8 ps1Output = 0;

	switch (commandByte)
	{
		case MemcardCommand::PROBE:
			g_MemoryCardProtocol.Probe();
			break;
		case MemcardCommand::UNKNOWN_WRITE_DELETE_END:
			g_MemoryCardProtocol.UnknownWriteDeleteEnd();
			break;
		case MemcardCommand::SET_ERASE_SECTOR:
		case MemcardCommand::SET_WRITE_SECTOR:
		case MemcardCommand::SET_READ_SECTOR:
			g_MemoryCardProtocol.SetSector();
			break;
		case MemcardCommand::GET_SPECS:
			g_MemoryCardProtocol.GetSpecs();
			break;
		case MemcardCommand::SET_TERMINATOR:
			g_MemoryCardProtocol.SetTerminator();
			break;
		case MemcardCommand::GET_TERMINATOR:
			g_MemoryCardProtocol.GetTerminator();
			break;
		case MemcardCommand::WRITE_DATA:
			g_MemoryCardProtocol.WriteData();
			break;
		case MemcardCommand::READ_DATA:
			g_MemoryCardProtocol.ReadData();
			break;
		case MemcardCommand::PS1_READ:
			g_MemoryCardProtocol.ResetPS1State();

			while (!fifoIn.empty())
			{
				ps1Input = fifoIn.front();
				ps1Output = g_MemoryCardProtocol.PS1Read(ps1Input);
				fifoIn.pop_front();
				fifoOut.push_back(ps1Output);
			}

			break;
		case MemcardCommand::PS1_STATE:
			g_MemoryCardProtocol.ResetPS1State();

			while (!fifoIn.empty())
			{
				ps1Input = fifoIn.front();
				ps1Output = g_MemoryCardProtocol.PS1State(ps1Input);
				fifoIn.pop_front();
				fifoOut.push_back(ps1Output);
			}

			break;
		case MemcardCommand::PS1_WRITE:
			g_MemoryCardProtocol.ResetPS1State();

			while (!fifoIn.empty())
			{
				ps1Input = fifoIn.front();
				ps1Output = g_MemoryCardProtocol.PS1Write(ps1Input);
				fifoIn.pop_front();
				fifoOut.push_back(ps1Output);
			}

			break;
		case MemcardCommand::PS1_POCKETSTATION:
			g_MemoryCardProtocol.ResetPS1State();

			while (!fifoIn.empty())
			{
				ps1Input = fifoIn.front();
				ps1Output = g_MemoryCardProtocol.PS1Pocketstation(ps1Input);
				fifoIn.pop_front();
				fifoOut.push_back(ps1Output);
			}

			break;
		case MemcardCommand::READ_WRITE_END:
			g_MemoryCardProtocol.ReadWriteEnd();
			break;
		case MemcardCommand::ERASE_BLOCK:
			g_MemoryCardProtocol.EraseBlock();
			break;
		case MemcardCommand::UNKNOWN_BOOT:
			g_MemoryCardProtocol.UnknownBoot();
			break;
		case MemcardCommand::AUTH_XOR:
			g_MemoryCardProtocol.AuthXor();
			break;
		case MemcardCommand::AUTH_F3:
			g_MemoryCardProtocol.AuthF3();
			break;
		case MemcardCommand::AUTH_F7:
			g_MemoryCardProtocol.AuthF7();
			break;
		default:
			Console.Warning("%s() Unhandled memcard command %02X, things are about to break!", __FUNCTION__, commandByte);
			break;
	}
}

void Sio2::Write(u8 data)
{
	Sio2Log.WriteLn("%s(%02X) SIO2 DATA Write", __FUNCTION__, data);

	if (!send3Read)
	{
		// No more SEND3 positions to access, but the game is still sending us SIO2 writes. Lets ignore them.
		if (send3Position > send3.size())
		{
			Console.Warning("%s(%02X) Received data after exhausting all SEND3 values!", __FUNCTION__, data);
			return;
		}

		const u32 currentSend3 = send3.at(send3Position);
		port = currentSend3 & Send3::PORT;
		commandLength = (currentSend3 >> 8) & Send3::COMMAND_LENGTH_MASK;
		send3Read = true;

		// The freshly read SEND3 position had a length of 0, so we are done handling SIO2 commands until
		// the next SEND3 writes.
		if (commandLength == 0)
		{
			send3Complete = true;
		}

		// If the prior command did not need to fully pop fifoIn, do so now,
		// so that the next command isn't trying to read the last command's leftovers.
		while (!fifoIn.empty())
		{
			fifoIn.pop_front();
		}
	}

	if (send3Complete)
	{
		return;
	}
	
	fifoIn.push_back(data);

	// We have received as many command bytes as we expect, and...
	//
	// ... These were from direct writes into IOP memory (DMA block size is zero when direct writes occur)
	// ... These were from SIO2 DMA (DMA block size is non-zero when SIO2 DMA occurs)
	if ((fifoIn.size() == sio2.commandLength && sio2.dmaBlockSize == 0) || fifoIn.size() == sio2.dmaBlockSize)
	{
		// Go ahead and prep so the next write triggers a load of the new SEND3 value.
		sio2.send3Read = false;
		sio2.send3Position++;

		// Check the SIO mode
		const u8 sioMode = fifoIn.front();
		fifoIn.pop_front();

		switch (sioMode)
		{
			case SioMode::PAD:
				this->Pad();
				break;
			case SioMode::MULTITAP:
				this->Multitap();
				break;
			case SioMode::INFRARED:
				this->Infrared();
				break;
			case SioMode::MEMCARD:
				this->Memcard();
				break;
			default:
				Console.Error("%s(%02X) Unhandled SIO mode %02X", __FUNCTION__, data, sioMode);
				fifoOut.push_back(0x00);
				SetRecv1(Recv1::DISCONNECTED);
				break;
		}

		// If command was sent over SIO2 DMA, align fifoOut to the block size
		if (sio2.dmaBlockSize > 0)
		{
			const size_t dmaDiff = fifoOut.size() % sio2.dmaBlockSize;

			if (dmaDiff > 0)
			{
				const size_t padding = sio2.dmaBlockSize - dmaDiff;

				for (size_t i = 0; i < padding; i++)
				{
					fifoOut.push_back(0x00);
				}
			}
		}
	}
}

u8 Sio2::Read()
{
	u8 ret = 0x00;
	
	if (!fifoOut.empty())
	{
		ret = fifoOut.front();
		fifoOut.pop_front();
	}
	else
	{
		Console.Warning("%s() fifoOut underflow! Returning 0x00.", __FUNCTION__);
	}
	
	Sio2Log.WriteLn("%s() SIO2 DATA Read (%02X)", __FUNCTION__, ret);
	return ret;
}

void sioNextFrame() {
	for ( uint port = 0; port < 2; ++port ) {
		for ( uint slot = 0; slot < 4; ++slot ) {
			mcds[port][slot].NextFrame();
		}
	}
}

void sioSetGameSerial( const std::string& serial ) {
	for ( uint port = 0; port < 2; ++port ) {
		for ( uint slot = 0; slot < 4; ++slot ) {
			if ( mcds[port][slot].ReIndex( serial ) ) {
				AutoEject::Set( port, slot );
			}
		}
	}
}

void SaveStateBase::sio2Freeze()
{
	FreezeTag("sio2");

	Freeze(sio2);
	FreezeDeque(fifoIn);
	FreezeDeque(fifoOut);

	// CRCs for memory cards.
	// If the memory card hasn't changed when loading state, we can safely skip ejecting it.
	u64 mcdCrcs[SIO::PORTS][SIO::SLOTS];
	if (IsSaving())
	{
		for (u32 port = 0; port < SIO::PORTS; port++)
		{
			for (u32 slot = 0; slot < SIO::SLOTS; slot++)
				mcdCrcs[port][slot] = mcds[port][slot].GetChecksum();
		}
	}
	Freeze(mcdCrcs);

	if (IsLoading())
	{
		bool ejected = false;
		for (u32 port = 0; port < SIO::PORTS && !ejected; port++)
		{
			for (u32 slot = 0; slot < SIO::SLOTS; slot++)
			{
				if (mcdCrcs[port][slot] != mcds[port][slot].GetChecksum())
				{
					AutoEject::SetAll();
					ejected = true;
					break;
				}
			}
		}
	}
}

void SaveStateBase::sioFreeze()
{
	FreezeTag("sio0");
	Freeze(sio0);
}

std::tuple<u32, u32> sioConvertPadToPortAndSlot(u32 index)
{
	if (index > 4) // [5,6,7]
		return std::make_tuple(1, index - 4); // 2B,2C,2D
	else if (index > 1) // [2,3,4]
		return std::make_tuple(0, index - 1); // 1B,1C,1D
	else // [0,1]
		return std::make_tuple(index, 0); // 1A,2A
}

u32 sioConvertPortAndSlotToPad(u32 port, u32 slot)
{
	if (slot == 0)
		return port;
	else if (port == 0) // slot=[0,1]
		return slot + 1; // 2,3,4
	else
		return slot + 4; // 5,6,7
}

bool sioPadIsMultitapSlot(u32 index)
{
	return (index >= 2);
}

bool sioPortAndSlotIsMultitap(u32 port, u32 slot)
{
	return (slot != 0);
}

void AutoEject::Set(size_t port, size_t slot)
{
	if (EmuConfig.McdEnableEjection)
	{
		mcds[port][slot].autoEjectTicks = 60;
	}
}

void AutoEject::Clear(size_t port, size_t slot)
{
	mcds[port][slot].autoEjectTicks = 0;
}

void AutoEject::SetAll()
{
	Host::AddIconOSDMessage("AutoEjectAllSet", ICON_FA_SD_CARD, "Force ejecting all memory cards.", Host::OSD_INFO_DURATION);

	for (size_t port = 0; port < SIO::PORTS; port++)
	{
		for (size_t slot = 0; slot < SIO::SLOTS; slot++)
		{
			AutoEject::Set(port, slot);
		}
	}
}

void AutoEject::ClearAll()
{
	for (size_t port = 0; port < SIO::PORTS; port++)
	{
		for (size_t slot = 0; slot < SIO::SLOTS; slot++)
		{
			AutoEject::Clear(port, slot);
		}
	}
}