/* Copyright (c) 2013-2016 Jeffrey Pfau
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "gb/mbc/mbc-private.h"

#include <mgba/internal/gb/gb.h>
#include <mgba/internal/sm83/sm83.h>
#include <mgba-util/crc32.h>
#include <mgba-util/vfs.h>

const uint32_t GB_LOGO_HASH = 0x46195417;

mLOG_DEFINE_CATEGORY(GB_MBC, "GB MBC", "gb.mbc");

static void _GBMBCNone(struct GB* gb, uint16_t address, uint8_t value) {
	UNUSED(address);
	UNUSED(value);

	if (!gb->yankedRomSize) {
		mLOG(GB_MBC, GAME_ERROR, "Wrote to invalid MBC");
	}
}

void GBMBCSwitchBank(struct GB* gb, int bank) {
	size_t bankStart = bank * GB_SIZE_CART_BANK0;
	if (bankStart + GB_SIZE_CART_BANK0 > gb->memory.romSize) {
		mLOG(GB_MBC, GAME_ERROR, "Attempting to switch to an invalid ROM bank: %0X", bank);
		bankStart &= (gb->memory.romSize - 1);
		bank = bankStart / GB_SIZE_CART_BANK0;
	}
	gb->memory.romBank = &gb->memory.rom[bankStart];
	gb->memory.currentBank = bank;
	if (gb->cpu->pc < GB_BASE_VRAM) {
		gb->cpu->memory.setActiveRegion(gb->cpu, gb->cpu->pc);
	}
}

void GBMBCSwitchBank0(struct GB* gb, int bank) {
	size_t bankStart = bank * GB_SIZE_CART_BANK0;
	if (bankStart + GB_SIZE_CART_BANK0 > gb->memory.romSize) {
		mLOG(GB_MBC, GAME_ERROR, "Attempting to switch to an invalid ROM bank: %0X", bank);
		bankStart &= (gb->memory.romSize - 1);
	}
	gb->memory.romBase = &gb->memory.rom[bankStart];
	gb->memory.currentBank0 = bank;
	if (gb->cpu->pc < GB_SIZE_CART_BANK0) {
		gb->cpu->memory.setActiveRegion(gb->cpu, gb->cpu->pc);
	}
}

void GBMBCSwitchHalfBank(struct GB* gb, int half, int bank) {
	size_t bankStart = bank * GB_SIZE_CART_HALFBANK;
	bool isFlash = false;
	if (gb->memory.mbcType == GB_MBC6) {
		isFlash = half ? gb->memory.mbcState.mbc6.flashBank1 : gb->memory.mbcState.mbc6.flashBank0;
	}
	if (isFlash) {
		if (bankStart + GB_SIZE_CART_HALFBANK > GB_SIZE_MBC6_FLASH) {
			mLOG(GB_MBC, GAME_ERROR, "Attempting to switch to an invalid Flash bank: %0X", bank);
			bankStart &= GB_SIZE_MBC6_FLASH - 1;
			bank = bankStart / GB_SIZE_CART_HALFBANK;
		}
		bankStart += gb->sramSize - GB_SIZE_MBC6_FLASH;
	} else {
		if (bankStart + GB_SIZE_CART_HALFBANK > gb->memory.romSize) {
			mLOG(GB_MBC, GAME_ERROR, "Attempting to switch to an invalid ROM bank: %0X", bank);
			bankStart &= gb->memory.romSize - 1;
			bank = bankStart / GB_SIZE_CART_HALFBANK;
			if (!bank) {
				++bank;
			}
		}
	}
	if (!half) {
		if (isFlash) {
			gb->memory.romBank = &gb->memory.sram[bankStart];
		} else {
			gb->memory.romBank = &gb->memory.rom[bankStart];
		}
		gb->memory.currentBank = bank;
	} else {
		if (isFlash) {
			gb->memory.romBank1 = &gb->memory.sram[bankStart];
		} else {
			gb->memory.romBank1 = &gb->memory.rom[bankStart];
		}
		gb->memory.currentBank1 = bank;
	}
	if (gb->cpu->pc < GB_BASE_VRAM) {
		gb->cpu->memory.setActiveRegion(gb->cpu, gb->cpu->pc);
	}
}

static struct {
	const char* fourcc;
	enum GBMemoryBankControllerType mbc;
} _gbxToMbc[] = {
	{"ROM", GB_MBC_NONE},
	{"MBC1", GB_MBC1},
	{"MBC2", GB_MBC2},
	{"MBC3", GB_MBC3},
	{"MBC5", GB_MBC5},
	{"MBC6", GB_MBC6},
	{"MBC7", GB_MBC7},
	{"MB1M", GB_MBC1},
	{"MMM1", GB_MMM01},
	{"CAMR", GB_POCKETCAM},
	{"HUC1", GB_HuC1},
	{"HUC3", GB_HuC3},
	{"TAM5", GB_TAMA5},
	{"M161", GB_M161},
	{"BBD", GB_UNL_BBD},
	{"HITK", GB_UNL_HITEK},
	{"SNTX", GB_UNL_SINTAX},
	{"NTO1", GB_UNL_NT_OLD_1},
	{"NTO2", GB_UNL_NT_OLD_2},
	{"NTN", GB_UNL_NT_NEW},
	{"LICH", GB_UNL_LI_CHENG},
	{"LBMC", GB_MBC_AUTODETECT}, // TODO
	{"LIBA", GB_MBC_AUTODETECT}, // TODO
	{"PKJD", GB_UNL_PKJD},
	{"WISD", GB_UNL_WISDOM_TREE},
	{"SAM1", GB_UNL_SACHEN_MMC1},
	{"SAM2", GB_UNL_SACHEN_MMC2},
	{"ROCK", GB_MBC_AUTODETECT}, // TODO
	{"NGHK", GB_MBC_AUTODETECT}, // TODO
	{"GB81", GB_UNL_GGB81},
	{"TPP1", GB_MBC_AUTODETECT}, // TODO
	{"VF01", GB_MBC_AUTODETECT}, // TODO
	{"SKL8", GB_MBC_AUTODETECT}, // TODO

	{NULL, GB_MBC_AUTODETECT},
};

enum GBMemoryBankControllerType GBMBCFromGBX(const void* fourcc) {
	size_t i;
	for (i = 0; _gbxToMbc[i].fourcc; ++i) {
		if (memcmp(fourcc, _gbxToMbc[i].fourcc, 4) == 0) {
			break;
		}
	}
	return _gbxToMbc[i].mbc;
}

static bool _isMulticart(const uint8_t* mem) {
	bool success;
	struct VFile* vf;

	vf = VFileFromConstMemory(&mem[GB_SIZE_CART_BANK0 * 0x10], 1024);
	success = GBIsROM(vf);
	vf->close(vf);

	if (!success) {
		return false;
	}

	vf = VFileFromConstMemory(&mem[GB_SIZE_CART_BANK0 * 0x20], 1024);
	success = GBIsROM(vf);
	vf->close(vf);

	if (!success) {
		vf = VFileFromConstMemory(&mem[GB_SIZE_CART_BANK0 * 0x30], 1024);
		success = GBIsROM(vf);
		vf->close(vf);
	}

	return success;
}

static bool _isWisdomTree(const uint8_t* mem, size_t size) {
	size_t i;
	for (i = 0x134; i < 0x14C; i += 4) {
		if (*(uint32_t*) &mem[i] != 0) {
			return false;
		}
	}
	for (i = 0xF0; i < 0x100; i += 4) {
		if (*(uint32_t*) &mem[i] != 0) {
			return false;
		}
	}
	if (mem[0x14D] != 0xE7) {
		return false;
	}
	for (i = 0x300; i < size - 11; ++i) {
		if (memcmp(&mem[i], "WISDOM", 6) == 0 && memcmp(&mem[i + 7], "TREE", 4) == 0) {
			return true;
		}
	}
	return false;
}

static enum GBMemoryBankControllerType _detectUnlMBC(const uint8_t* mem, size_t size) {
	const struct GBCartridge* cart = (const struct GBCartridge*) &mem[0x100];

	switch (cart->type) {
		case 0:
		if (_isWisdomTree(mem, size)) {
			return GB_UNL_WISDOM_TREE;
		}
		break;
	}

	uint32_t secondaryLogo = doCrc32(&mem[0x184], 0x30);
	switch (secondaryLogo) {
	case 0x4fdab691:
		return GB_UNL_HITEK;
	case 0xc7d8c1df:
	case 0x6d1ea662: // Garou
		if (mem[0x7FFF] != 0x01) { // Make sure we're not using a "fixed" version
			return GB_UNL_BBD;
		}
		break;
	case 0x79f34594: // DATA.
	case 0x7e8c539b: // TD-SOFT
		return GB_UNL_GGB81;
	case 0x20d092e2:
	case 0xd2b57657:
		if (cart->type == 0x01) { // Make sure we're not using a "fixed" version
			return GB_UNL_LI_CHENG;
		}
		if ((0x8000U << cart->romSize) != size) {
			return GB_UNL_LI_CHENG;
		}
		break;
	case 0x6c1dcf2d:
	case 0x99e3449d:
		if (mem[0x7FFF] != 0x01) { // Make sure we're not using a "fixed" version
			return GB_UNL_SINTAX;
		}
		break;
	}

	if (mem[0x104] == 0xCE && mem[0x144] == 0xED && mem[0x114] == 0x66) {
		return GB_UNL_SACHEN_MMC1;
	}

	if (mem[0x184] == 0xCE && mem[0x1C4] == 0xED && mem[0x194] == 0x66) {
		return GB_UNL_SACHEN_MMC2;
	}

	return GB_MBC_AUTODETECT;
}

void GBMBCSwitchSramBank(struct GB* gb, int bank) {
	size_t bankStart = bank * GB_SIZE_EXTERNAL_RAM;
	if (bankStart + GB_SIZE_EXTERNAL_RAM > gb->sramSize) {
		mLOG(GB_MBC, GAME_ERROR, "Attempting to switch to an invalid RAM bank: %0X", bank);
		bankStart &= (gb->sramSize - 1);
		bank = bankStart / GB_SIZE_EXTERNAL_RAM;
	}
	gb->memory.sramBank = &gb->memory.sram[bankStart];
	gb->memory.sramCurrentBank = bank;
}

void GBMBCSwitchSramHalfBank(struct GB* gb, int half, int bank) {
	size_t bankStart = bank * GB_SIZE_EXTERNAL_RAM_HALFBANK;
	size_t sramSize = gb->sramSize - GB_SIZE_MBC6_FLASH;
	if (bankStart + GB_SIZE_EXTERNAL_RAM_HALFBANK > sramSize) {
		mLOG(GB_MBC, GAME_ERROR, "Attempting to switch to an invalid RAM bank: %0X", bank);
		bankStart &= (sramSize - 1);
		bank = bankStart / GB_SIZE_EXTERNAL_RAM_HALFBANK;
	}
	if (!half) {
		gb->memory.sramBank = &gb->memory.sram[bankStart];
		gb->memory.sramCurrentBank = bank;
	} else {
		gb->memory.sramBank1 = &gb->memory.sram[bankStart];
		gb->memory.currentSramBank1 = bank;
	}
}

void GBMBCInit(struct GB* gb) {
	const struct GBCartridge* cart = (const struct GBCartridge*) &gb->memory.rom[0x100];
	size_t sramSize = 0;
	if (gb->memory.rom && gb->memory.romSize) {
		if (gb->memory.romSize >= 0x8000) {
			const struct GBCartridge* cartFooter = (const struct GBCartridge*) &gb->memory.rom[gb->memory.romSize - 0x7F00];
			if (doCrc32(cartFooter->logo, sizeof(cartFooter->logo)) == GB_LOGO_HASH && cartFooter->type >= 0x0B && cartFooter->type <= 0x0D) {
				cart = cartFooter;
			}
		}
		if (gb->gbx.romSize) {
			sramSize = gb->gbx.ramSize;
			gb->memory.mbcType = gb->gbx.mbc;
		} else {
			switch (cart->ramSize) {
			case 0:
				sramSize = 0;
				break;
			default:
			case 2:
				sramSize = 0x2000;
				break;
			case 3:
				sramSize = 0x8000;
				break;
			case 4:
				sramSize = 0x20000;
				break;
			case 5:
				sramSize = 0x10000;
				break;
			}
		}
		if (gb->memory.mbcType == GB_MBC_AUTODETECT) {
			gb->memory.mbcType = _detectUnlMBC(gb->memory.rom, gb->memory.romSize);
		}

		if (gb->memory.mbcType == GB_MBC_AUTODETECT) {
			switch (cart->type) {
			case 0:
			case 8:
			case 9:
				gb->memory.mbcType = GB_MBC_NONE;
				break;
			case 1:
			case 2:
			case 3:
				gb->memory.mbcType = GB_MBC1;
				break;
			case 5:
			case 6:
				gb->memory.mbcType = GB_MBC2;
				break;
			case 0x0B:
			case 0x0C:
			case 0x0D:
				gb->memory.mbcType = GB_MMM01;
				break;
			case 0x0F:
			case 0x10:
				gb->memory.mbcType = GB_MBC3_RTC;
				break;
			case 0x11:
			case 0x12:
			case 0x13:
				gb->memory.mbcType = GB_MBC3;
				break;
			default:
				mLOG(GB_MBC, WARN, "Unknown MBC type: %02X", cart->type);
				// Fall through
			case 0x19:
			case 0x1A:
			case 0x1B:
				gb->memory.mbcType = GB_MBC5;
				break;
			case 0x1C:
			case 0x1D:
			case 0x1E:
				gb->memory.mbcType = GB_MBC5_RUMBLE;
				break;
			case 0x20:
				gb->memory.mbcType = GB_MBC6;
				break;
			case 0x22:
				gb->memory.mbcType = GB_MBC7;
				break;
			case 0xFC:
				gb->memory.mbcType = GB_POCKETCAM;
				break;
			case 0xFD:
				gb->memory.mbcType = GB_TAMA5;
				break;
			case 0xFE:
				gb->memory.mbcType = GB_HuC3;
				break;
			case 0xFF:
				gb->memory.mbcType = GB_HuC1;
				break;
			}
		}
	} else {
		gb->memory.mbcType = GB_MBC_NONE;
	}
	gb->memory.mbcRead = NULL;
	gb->memory.directSramAccess = true;
	gb->memory.mbcReadBank0 = false;
	gb->memory.mbcReadBank1 = false;
	gb->memory.mbcReadHigh = false;
	gb->memory.mbcWriteHigh = false;
	gb->memory.cartBusDecay = 4;
	switch (gb->memory.mbcType) {
	case GB_MBC_NONE:
		gb->memory.mbcWrite = _GBMBCNone;
		break;
	case GB_MBC1:
		gb->memory.mbcWrite = _GBMBC1;
		if (gb->gbx.mapperVars.u8[0]) {
			gb->memory.mbcState.mbc1.multicartStride = gb->gbx.mapperVars.u8[0];
		} else if (gb->memory.romSize >= GB_SIZE_CART_BANK0 * 0x31 && _isMulticart(gb->memory.rom)) {
			gb->memory.mbcState.mbc1.multicartStride = 4;
		} else {
			gb->memory.mbcState.mbc1.multicartStride = 5;
		}
		break;
	case GB_MBC2:
		gb->memory.mbcWrite = _GBMBC2;
		gb->memory.mbcRead = _GBMBC2Read;
		gb->memory.directSramAccess = false;
		sramSize = 0x100;
		break;
	case GB_MBC3:
		gb->memory.mbcWrite = _GBMBC3;
		break;
	default:
		mLOG(GB_MBC, WARN, "Unknown MBC type: %02X", cart->type);
		// Fall through
	case GB_MBC5:
		gb->memory.mbcWrite = _GBMBC5;
		break;
	case GB_MBC6:
		gb->memory.mbcWrite = _GBMBC6;
		gb->memory.mbcRead = _GBMBC6Read;
		gb->memory.directSramAccess = false;
		if (!sramSize) {
			sramSize = GB_SIZE_EXTERNAL_RAM; // Force minimum size for convenience
		}
		sramSize += GB_SIZE_MBC6_FLASH; // Flash is concatenated at the end
		break;
	case GB_MBC7:
		gb->memory.mbcWrite = _GBMBC7;
		gb->memory.mbcRead = _GBMBC7Read;
		sramSize = 0x100;
		break;
	case GB_MMM01:
		gb->memory.mbcWrite = _GBMMM01;
		break;
	case GB_HuC1:
		gb->memory.mbcWrite = _GBHuC1;
		break;
	case GB_HuC3:
		gb->memory.mbcWrite = _GBHuC3;
		gb->memory.mbcRead = _GBHuC3Read;
		break;
	case GB_TAMA5:
		gb->memory.mbcWrite = _GBTAMA5;
		gb->memory.mbcRead = _GBTAMA5Read;
		gb->memory.mbcState.tama5.rtcAlarmPage[GBTAMA6_RTC_PAGE] = 1;
		gb->memory.mbcState.tama5.rtcFreePage0[GBTAMA6_RTC_PAGE] = 2;
		gb->memory.mbcState.tama5.rtcFreePage1[GBTAMA6_RTC_PAGE] = 3;
		sramSize = 0x20;
		break;
	case GB_MBC3_RTC:
		memset(gb->memory.rtcRegs, 0, sizeof(gb->memory.rtcRegs));
		gb->memory.mbcWrite = _GBMBC3;
		break;
	case GB_MBC5_RUMBLE:
		gb->memory.mbcWrite = _GBMBC5;
		break;
	case GB_POCKETCAM:
		gb->memory.mbcWrite = _GBPocketCam;
		gb->memory.mbcRead = _GBPocketCamRead;
		if (!sramSize) {
			sramSize = GB_SIZE_EXTERNAL_RAM; // Force minimum size for convenience
		}
		if (gb->memory.cam && gb->memory.cam->startRequestImage) {
			gb->memory.cam->startRequestImage(gb->memory.cam, GBCAM_WIDTH, GBCAM_HEIGHT, mCOLOR_ANY);
		}
		break;
	case GB_M161:
		gb->memory.mbcWrite = _GBM161;
		break;
	case GB_UNL_WISDOM_TREE:
		gb->memory.mbcWrite = _GBWisdomTree;
		break;
	case GB_UNL_NT_OLD_1:
		gb->memory.mbcWrite = _GBNTOld1;
		break;
	case GB_UNL_NT_OLD_2:
		gb->memory.mbcWrite = _GBNTOld2;
		break;
	case GB_UNL_NT_NEW:
		gb->memory.mbcWrite = _GBNTNew;
		break;
	case GB_UNL_PKJD:
		gb->memory.mbcWrite = _GBPKJD;
		gb->memory.mbcRead = _GBPKJDRead;
		break;
	case GB_UNL_BBD:
		gb->memory.mbcWrite = _GBBBD;
		gb->memory.mbcRead = _GBBBDRead;
		gb->memory.mbcReadBank1 = true;
		break;
	case GB_UNL_HITEK:
		gb->memory.mbcWrite = _GBHitek;
		gb->memory.mbcRead = _GBHitekRead;
		gb->memory.mbcState.bbd.dataSwapMode = 7;
		gb->memory.mbcState.bbd.bankSwapMode = 7;
		gb->memory.mbcReadBank1 = true;
		break;
	case GB_UNL_LI_CHENG:
		gb->memory.mbcWrite = _GBLiCheng;
		break;
	case GB_UNL_GGB81:
		gb->memory.mbcWrite = _GBGGB81;
		gb->memory.mbcRead = _GBGGB81Read;
		gb->memory.mbcReadBank1 = true;
		break;
	case GB_UNL_SACHEN_MMC1:
		gb->memory.mbcWrite = _GBSachen;
		gb->memory.mbcRead = _GBSachenMMC1Read;
		gb->memory.mbcReadBank0 = true;
		gb->memory.mbcReadBank1 = true;
		break;
	case GB_UNL_SACHEN_MMC2:
		gb->memory.mbcWrite = _GBSachen;
		gb->memory.mbcRead = _GBSachenMMC2Read;
		gb->memory.mbcReadBank0 = true;
		gb->memory.mbcReadBank1 = true;
		gb->memory.mbcReadHigh = true;
		gb->memory.mbcWriteHigh = true;
		if (sramSize) {
			gb->memory.sramAccess = true;
		}
		break;
	case GB_UNL_SINTAX:
		gb->memory.mbcWrite = _GBSintax;
		gb->memory.mbcRead = _GBSintaxRead;
		gb->memory.mbcReadBank1 = true;
		if (sramSize) {
			gb->memory.sramAccess = true;
		}
		break;
	}

	gb->memory.currentBank = 1;
	gb->memory.sramCurrentBank = 0;
	gb->memory.sramAccess = false;
	gb->memory.rtcAccess = false;
	gb->memory.activeRtcReg = 0;
	gb->memory.rtcLatched = false;
	gb->memory.rtcLastLatch = 0;
	if (gb->memory.rtc) {
		if (gb->memory.rtc->sample) {
			gb->memory.rtc->sample(gb->memory.rtc);
		}
		gb->memory.rtcLastLatch = gb->memory.rtc->unixTime(gb->memory.rtc);
	} else {
		gb->memory.rtcLastLatch = time(0);
	}
	memset(&gb->memory.rtcRegs, 0, sizeof(gb->memory.rtcRegs));

	GBResizeSram(gb, sramSize);

	if (gb->memory.mbcType == GB_MBC3_RTC) {
		GBMBCRTCRead(gb);
	} else if (gb->memory.mbcType == GB_HuC3) {
		GBMBCHuC3Read(gb);
	} else if (gb->memory.mbcType == GB_TAMA5) {
		GBMBCTAMA5Read(gb);
	}
}

void GBMBCReset(struct GB* gb) {
	gb->memory.currentBank0 = 0;
	gb->memory.romBank = &gb->memory.rom[GB_SIZE_CART_BANK0];
	gb->memory.cartBus = 0xFF;
	gb->memory.cartBusPc = 0;
	gb->memory.cartBusDecay = 1;
	gb->memory.lastRumble = 0;

	memset(&gb->memory.mbcState, 0, sizeof(gb->memory.mbcState));
	GBMBCInit(gb);
	switch (gb->memory.mbcType) {
	case GB_MBC1:
		gb->memory.mbcState.mbc1.mode = 0;
		gb->memory.mbcState.mbc1.bankLo = 1;
		break;
	case GB_MBC6:
		GBMBCSwitchHalfBank(gb, 0, 2);
		GBMBCSwitchHalfBank(gb, 1, 3);
		GBMBCSwitchSramHalfBank(gb, 0, 0);
		GBMBCSwitchSramHalfBank(gb, 0, 1);
		break;
	case GB_MMM01:
		GBMBCSwitchBank0(gb, gb->memory.romSize / GB_SIZE_CART_BANK0 - 2);
		GBMBCSwitchBank(gb, gb->memory.romSize / GB_SIZE_CART_BANK0 - 1);
		break;
	case GB_UNL_SINTAX:
		gb->memory.mbcState.sintax.mode = 0xF;
		break;
	default:
		break;
	}
	gb->memory.sramBank = gb->memory.sram;
}

void _GBMBCAppendSaveSuffix(struct GB* gb, const void* buffer, size_t size) {
	struct VFile* vf = gb->sramVf;
	if ((size_t) vf->size(vf) < gb->sramSize + size) {
		// Writing past the end of the file can invalidate the file mapping
		vf->unmap(vf, gb->memory.sram, gb->sramSize);
		gb->memory.sram = NULL;
	}
	vf->seek(vf, gb->sramSize, SEEK_SET);
	vf->write(vf, buffer, size);
	if (!gb->memory.sram) {
		gb->memory.sram = vf->map(vf, gb->sramSize, MAP_WRITE);
		GBMBCSwitchSramBank(gb, gb->memory.sramCurrentBank);
	}
}

void GBMBCRTCRead(struct GB* gb) {
	struct GBMBCRTCSaveBuffer rtcBuffer;
	struct VFile* vf = gb->sramVf;
	if (!vf) {
		return;
	}
	vf->seek(vf, gb->sramSize, SEEK_SET);
	if (vf->read(vf, &rtcBuffer, sizeof(rtcBuffer)) < (ssize_t) sizeof(rtcBuffer) - 4) {
		return;
	}

	LOAD_32LE(gb->memory.rtcRegs[0], 0, &rtcBuffer.latchedSec);
	LOAD_32LE(gb->memory.rtcRegs[1], 0, &rtcBuffer.latchedMin);
	LOAD_32LE(gb->memory.rtcRegs[2], 0, &rtcBuffer.latchedHour);
	LOAD_32LE(gb->memory.rtcRegs[3], 0, &rtcBuffer.latchedDays);
	LOAD_32LE(gb->memory.rtcRegs[4], 0, &rtcBuffer.latchedDaysHi);
	LOAD_64LE(gb->memory.rtcLastLatch, 0, &rtcBuffer.unixTime);
}

void GBMBCRTCWrite(struct GB* gb) {
	struct VFile* vf = gb->sramVf;
	if (!vf) {
		return;
	}

	uint8_t rtcRegs[5];
	memcpy(rtcRegs, gb->memory.rtcRegs, sizeof(rtcRegs));
	time_t rtcLastLatch = gb->memory.rtcLastLatch;
	_GBMBCLatchRTC(gb->memory.rtc, rtcRegs, &rtcLastLatch);

	struct GBMBCRTCSaveBuffer rtcBuffer;
	STORE_32LE(rtcRegs[0], 0, &rtcBuffer.sec);
	STORE_32LE(rtcRegs[1], 0, &rtcBuffer.min);
	STORE_32LE(rtcRegs[2], 0, &rtcBuffer.hour);
	STORE_32LE(rtcRegs[3], 0, &rtcBuffer.days);
	STORE_32LE(rtcRegs[4], 0, &rtcBuffer.daysHi);
	STORE_32LE(gb->memory.rtcRegs[0], 0, &rtcBuffer.latchedSec);
	STORE_32LE(gb->memory.rtcRegs[1], 0, &rtcBuffer.latchedMin);
	STORE_32LE(gb->memory.rtcRegs[2], 0, &rtcBuffer.latchedHour);
	STORE_32LE(gb->memory.rtcRegs[3], 0, &rtcBuffer.latchedDays);
	STORE_32LE(gb->memory.rtcRegs[4], 0, &rtcBuffer.latchedDaysHi);
	STORE_64LE(gb->memory.rtcLastLatch, 0, &rtcBuffer.unixTime);

	_GBMBCAppendSaveSuffix(gb, &rtcBuffer, sizeof(rtcBuffer));
}
