// SPDX-License-Identifier: MIT

#include "fix/mbc.hpp"

#include <optional>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unordered_map>
#include <utility>

#include "helpers.hpp"  // unreachable_
#include "platform.hpp" // strcasecmp
#include "util.hpp"

#include "fix/warning.hpp"

// Associate every MBC type with its name and whether it has RAM
static std::unordered_map<MbcType, std::pair<char const *, bool>> mbcData{
    {ROM,                                   {"ROM", false}                           },
    {ROM_RAM,                               {"ROM+RAM", true}                        },
    {ROM_RAM_BATTERY,                       {"ROM+RAM+BATTERY", true}                },
    {MBC1,                                  {"MBC1", false}                          },
    {MBC1_RAM,                              {"MBC1+RAM", true}                       },
    {MBC1_RAM_BATTERY,                      {"MBC1+RAM+BATTERY", true}               },
    // MBC2 technically has RAM, but is not marked as such
    {MBC2,                                  {"MBC2", false}                          },
    {MBC2_BATTERY,                          {"MBC2+BATTERY", false}                  },
    {MMM01,                                 {"MMM01", false}                         },
    {MMM01_RAM,                             {"MMM01+RAM", true}                      },
    {MMM01_RAM_BATTERY,                     {"MMM01+RAM+BATTERY", true}              },
    {MBC3,                                  {"MBC3", false}                          },
    {MBC3_TIMER_BATTERY,                    {"MBC3+TIMER+BATTERY", false}            },
    {MBC3_TIMER_RAM_BATTERY,                {"MBC3+TIMER+RAM+BATTERY", true}         },
    {MBC3_RAM,                              {"MBC3+RAM", true}                       },
    {MBC3_RAM_BATTERY,                      {"MBC3+RAM+BATTERY", true}               },
    {MBC5,                                  {"MBC5", false}                          },
    {MBC5_RAM,                              {"MBC5+RAM", true}                       },
    {MBC5_RAM_BATTERY,                      {"MBC5+RAM+BATTERY", true}               },
    {MBC5_RUMBLE,                           {"MBC5+RUMBLE", false}                   },
    {MBC5_RUMBLE_RAM,                       {"MBC5+RUMBLE+RAM", true}                },
    {MBC5_RUMBLE_RAM_BATTERY,               {"MBC5+RUMBLE+RAM+BATTERY", true}        },
    // MBC6 "Net de Get - Minigame @ 100" has RAM size 3 (32 KiB)
    {MBC6,                                  {"MBC6", true}                           },
    {MBC7_SENSOR_RUMBLE_RAM_BATTERY,        {"MBC7+SENSOR+RUMBLE+RAM+BATTERY", true} },
    {POCKET_CAMERA,                         {"POCKET CAMERA", true}                  },
    // Bandai TAMA5 "Game de Hakken!! Tamagotchi - Osutchi to Mesutchi" has RAM size 0
    {BANDAI_TAMA5,                          {"BANDAI TAMA5", false}                  },
    {HUC3,                                  {"HUC3", true}                           },
    {HUC1_RAM_BATTERY,                      {"HUC1+RAM+BATTERY", true}               },
    // TPP1 may or may not have RAM, don't use these flags for it
    {TPP1,                                  {"TPP1", false}                          },
    {TPP1_RUMBLE,                           {"TPP1+RUMBLE", false}                   },
    {TPP1_MULTIRUMBLE_RUMBLE,               {"TPP1+MULTIRUMBLE", false}              },
    {TPP1_TIMER,                            {"TPP1+TIMER", false}                    },
    {TPP1_TIMER_RUMBLE,                     {"TPP1+TIMER+RUMBLE", false}             },
    {TPP1_TIMER_MULTIRUMBLE_RUMBLE,         {"TPP1+TIMER+MULTIRUMBLE", false}        },
    {TPP1_BATTERY,                          {"TPP1+BATTERY", false}                  },
    {TPP1_BATTERY_RUMBLE,                   {"TPP1+BATTERY+RUMBLE", false}           },
    {TPP1_BATTERY_MULTIRUMBLE_RUMBLE,       {"TPP1+BATTERY+MULTIRUMBLE", false}      },
    {TPP1_BATTERY_TIMER,                    {"TPP1+BATTERY+TIMER", false}            },
    {TPP1_BATTERY_TIMER_RUMBLE,             {"TPP1+BATTERY+TIMER+RUMBLE", false}     },
    {TPP1_BATTERY_TIMER_MULTIRUMBLE_RUMBLE, {"TPP1+BATTERY+TIMER+MULTIRUMBLE", false}},
};

static char const *acceptedMBCNames =
    "Accepted MBC names:\n"
    "\tROM ($00) [aka ROM_ONLY]\n"
    "\tMBC1 ($01), MBC1+RAM ($02), MBC1+RAM+BATTERY ($03)\n"
    "\tMBC2 ($05), MBC2+BATTERY ($06)\n"
    "\tROM+RAM ($08) [deprecated], ROM+RAM+BATTERY ($09) [deprecated]\n"
    "\tMMM01 ($0B), MMM01+RAM ($0C), MMM01+RAM+BATTERY ($0D)\n"
    "\tMBC3+TIMER+BATTERY ($0F), MBC3+TIMER+RAM+BATTERY ($10)\n"
    "\tMBC3 ($11), MBC3+RAM ($12), MBC3+RAM+BATTERY ($13)\n"
    "\tMBC5 ($19), MBC5+RAM ($1A), MBC5+RAM+BATTERY ($1B)\n"
    "\tMBC5+RUMBLE ($1C), MBC5+RUMBLE+RAM ($1D), MBC5+RUMBLE+RAM+BATTERY ($1E)\n"
    "\tMBC6 ($20)\n"
    "\tMBC7+SENSOR+RUMBLE+RAM+BATTERY ($22)\n"
    "\tPOCKET_CAMERA ($FC)\n"
    "\tBANDAI_TAMA5 ($FD) [aka TAMA5]\n"
    "\tHUC3 ($FE)\n"
    "\tHUC1+RAM+BATTERY ($FF)\n"
    "\n"
    "\tTPP1_1.0, TPP1_1.0+RUMBLE, TPP1_1.0+MULTIRUMBLE, TPP1_1.0+TIMER,\n"
    "\tTPP1_1.0+TIMER+RUMBLE, TPP1_1.0+TIMER+MULTIRUMBLE, TPP1_1.0+BATTERY,\n"
    "\tTPP1_1.0+BATTERY+RUMBLE, TPP1_1.0+BATTERY+MULTIRUMBLE,\n"
    "\tTPP1_1.0+BATTERY+TIMER, TPP1_1.0+BATTERY+TIMER+RUMBLE,\n"
    "\tTPP1_1.0+BATTERY+TIMER+MULTIRUMBLE"; // No trailing newline

char const *mbc_Name(MbcType type) {
	auto search = mbcData.find(type);
	return search != mbcData.end() ? search->second.first : "(unknown)";
}

bool mbc_HasRAM(MbcType type) {
	auto search = mbcData.find(type);
	return search != mbcData.end() && search->second.second;
}

static void skipMBCSpace(char const *&ptr) {
	ptr += strspn(ptr, " \t_");
}

static char normalizeMBCChar(char c) {
	if (c == '_') {
		return ' '; // Treat underscores as spaces
	}
	return toUpper(c); // Uppercase for comparison with `mbc_Name`s
}

[[noreturn]]
static void fatalUnknownMBC(char const *name) {
	fatal("Unknown MBC \"%s\"\n%s", name, acceptedMBCNames);
}

[[noreturn]]
static void fatalWrongMBCFeatures(char const *name) {
	fatal("Features incompatible with MBC (\"%s\")\n%s", name, acceptedMBCNames);
}

MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor) {
	char const *ptr = name + strspn(name, " \t"); // Skip leading blank space

	if (!strcasecmp(ptr, "help") || !strcasecmp(ptr, "list")) {
		puts(acceptedMBCNames); // Outputs to stdout and appends a newline
		exit(0);
	}

	// Parse numeric MBC and return it as-is (unless it's too large)
	if (char c = *ptr; isDigit<10>(c) || c == '$' || c == '&' || c == '%') {
		if (std::optional<uint64_t> mbc = parseWholeNumber(ptr); !mbc) {
			fatalUnknownMBC(name);
		} else if (*mbc > 0xFF) {
			fatal("Specified MBC ID out of range 0-255: \"%s\"", name);
		} else {
			return static_cast<MbcType>(*mbc);
		}
	}

	// Begin by reading the MBC type:
	uint16_t mbc = UINT16_MAX;

	auto tryReadSlice = [&ptr, &name](char const *expected) {
		while (*expected) {
			// If `name` is too short, the character will be '\0' and this will return `false`
			if (normalizeMBCChar(*ptr++) != *expected++) {
				fatalUnknownMBC(name);
			}
		}
	};

	switch (*ptr++) {
	case 'R': // ROM / ROM_ONLY
	case 'r':
		tryReadSlice("OM");
		// Handle optional " ONLY"
		skipMBCSpace(ptr);
		if (*ptr == 'O' || *ptr == 'o') {
			++ptr;
			tryReadSlice("NLY");
		}
		mbc = ROM;
		break;

	case 'M': // MBC{1, 2, 3, 5, 6, 7} / MMM01
	case 'm':
		switch (*ptr++) {
		case 'B':
		case 'b':
			tryReadSlice("C");
			switch (*ptr++) {
			case '1':
				mbc = MBC1;
				break;
			case '2':
				mbc = MBC2;
				break;
			case '3':
				mbc = MBC3;
				break;
			case '5':
				mbc = MBC5;
				break;
			case '6':
				mbc = MBC6;
				break;
			case '7':
				mbc = MBC7_SENSOR_RUMBLE_RAM_BATTERY;
				break;
			}
			break;
		case 'M':
		case 'm':
			tryReadSlice("M01");
			mbc = MMM01;
			break;
		}
		break;

	case 'P': // POCKET_CAMERA
	case 'p':
		tryReadSlice("OCKET CAMERA");
		mbc = POCKET_CAMERA;
		break;

	case 'B': // BANDAI_TAMA5
	case 'b':
		tryReadSlice("ANDAI TAMA5");
		mbc = BANDAI_TAMA5;
		break;

	case 'T': // TAMA5 / TPP1
	case 't':
		switch (*ptr++) {
		case 'A':
			tryReadSlice("MA5");
			mbc = BANDAI_TAMA5;
			break;
		case 'P':
			tryReadSlice("P1");
			// Parse version
			skipMBCSpace(ptr);
			// Major
			if (std::optional<uint64_t> major = parseNumber(ptr, BASE_10); !major) {
				fatal("Failed to parse TPP1 major revision number");
			} else if (*major != 1) {
				fatal("RGBFIX only supports TPP1 version 1.0");
			} else {
				tpp1Major = *major;
			}
			tryReadSlice(".");
			// Minor
			if (std::optional<uint64_t> minor = parseNumber(ptr, BASE_10); !minor) {
				fatal("Failed to parse TPP1 minor revision number");
			} else if (*minor > 0xFF) {
				fatal("TPP1 minor revision number must be 8-bit");
			} else {
				tpp1Minor = *minor;
			}
			mbc = TPP1;
			break;
		}
		break;

	case 'H': // HuC{1, 3}
	case 'h':
		tryReadSlice("UC");
		switch (*ptr++) {
		case '1':
			mbc = HUC1_RAM_BATTERY;
			break;
		case '3':
			mbc = HUC3;
			break;
		}
		break;
	}

	if (mbc == UINT16_MAX) {
		fatalUnknownMBC(name);
	}

	// Read "additional features"
	uint8_t features = 0;
	// clang-format off: vertically align values
	static constexpr uint8_t RAM         = 1 << 7;
	static constexpr uint8_t BATTERY     = 1 << 6;
	static constexpr uint8_t TIMER       = 1 << 5;
	static constexpr uint8_t RUMBLE      = 1 << 4;
	static constexpr uint8_t SENSOR      = 1 << 3;
	static constexpr uint8_t MULTIRUMBLE = 1 << 2;
	// clang-format on

	while (*ptr) {
		// We expect a '+' at this point
		skipMBCSpace(ptr);
		tryReadSlice("+");
		skipMBCSpace(ptr);

		switch (*ptr++) {
		case 'B': // BATTERY
		case 'b':
			tryReadSlice("ATTERY");
			features |= BATTERY;
			break;

		case 'M':
		case 'm':
			tryReadSlice("ULTIRUMBLE");
			features |= MULTIRUMBLE;
			break;

		case 'R': // RAM or RUMBLE
		case 'r':
			switch (*ptr++) {
			case 'U':
			case 'u':
				tryReadSlice("MBLE");
				features |= RUMBLE;
				break;
			case 'A':
			case 'a':
				tryReadSlice("M");
				features |= RAM;
				break;
			}
			break;

		case 'S': // SENSOR
		case 's':
			tryReadSlice("ENSOR");
			features |= SENSOR;
			break;

		case 'T': // TIMER
		case 't':
			tryReadSlice("IMER");
			features |= TIMER;
			break;
		}
	}

	switch (mbc) {
	case ROM:
		if (!features) {
			break;
		}
		mbc = ROM_RAM - 1;
		static_assert(ROM_RAM + 1 == ROM_RAM_BATTERY, "Enum sanity check failed!");
		static_assert(MBC1 + 1 == MBC1_RAM, "Enum sanity check failed!");
		static_assert(MBC1 + 2 == MBC1_RAM_BATTERY, "Enum sanity check failed!");
		static_assert(MMM01 + 1 == MMM01_RAM, "Enum sanity check failed!");
		static_assert(MMM01 + 2 == MMM01_RAM_BATTERY, "Enum sanity check failed!");
		[[fallthrough]];
	case MBC1:
	case MMM01:
		if (features == RAM) {
			++mbc;
		} else if (features == (RAM | BATTERY)) {
			mbc += 2;
		} else if (features) {
			fatalWrongMBCFeatures(name);
		}
		break;

	case MBC2:
		if (features == BATTERY) {
			mbc = MBC2_BATTERY;
		} else if (features) {
			fatalWrongMBCFeatures(name);
		}
		break;

	case MBC3:
		// Handle timer, which also requires battery
		if (features & TIMER) {
			if (!(features & BATTERY)) {
				warning(WARNING_MBC, "\"MBC3+TIMER\" implies \"BATTERY\"");
			}
			features &= ~(TIMER | BATTERY); // Reset those bits
			mbc = MBC3_TIMER_BATTERY;
			// RAM is handled below
		}
		static_assert(MBC3 + 1 == MBC3_RAM, "Enum sanity check failed!");
		static_assert(MBC3 + 2 == MBC3_RAM_BATTERY, "Enum sanity check failed!");
		static_assert(
		    MBC3_TIMER_BATTERY + 1 == MBC3_TIMER_RAM_BATTERY, "Enum sanity check failed!"
		);
		if (features == RAM) {
			++mbc;
		} else if (features == (RAM | BATTERY)) {
			mbc += 2;
		} else if (features) {
			fatalWrongMBCFeatures(name);
		}
		break;

	case MBC5:
		if (features & RUMBLE) {
			features &= ~RUMBLE;
			mbc = MBC5_RUMBLE;
		}
		static_assert(MBC5 + 1 == MBC5_RAM, "Enum sanity check failed!");
		static_assert(MBC5 + 2 == MBC5_RAM_BATTERY, "Enum sanity check failed!");
		static_assert(MBC5_RUMBLE + 1 == MBC5_RUMBLE_RAM, "Enum sanity check failed!");
		static_assert(MBC5_RUMBLE + 2 == MBC5_RUMBLE_RAM_BATTERY, "Enum sanity check failed!");
		if (features == RAM) {
			++mbc;
		} else if (features == (RAM | BATTERY)) {
			mbc += 2;
		} else if (features) {
			fatalWrongMBCFeatures(name);
		}
		break;

	case MBC6:
	case POCKET_CAMERA:
	case BANDAI_TAMA5:
	case HUC3:
		// No extra features accepted
		if (features) {
			fatalWrongMBCFeatures(name);
		}
		break;

	case MBC7_SENSOR_RUMBLE_RAM_BATTERY:
		if (features != (SENSOR | RUMBLE | RAM | BATTERY)) {
			fatalWrongMBCFeatures(name);
		}
		break;

	case HUC1_RAM_BATTERY:
		if (features != (RAM | BATTERY)) { // HuC1 expects RAM+BATTERY
			fatalWrongMBCFeatures(name);
		}
		break;

	case TPP1: {
		// clang-format off: vertically align values
		static constexpr uint8_t BATTERY_TPP1     = 1 << 3;
		static constexpr uint8_t TIMER_TPP1       = 1 << 2;
		static constexpr uint8_t MULTIRUMBLE_TPP1 = 1 << 1;
		static constexpr uint8_t RUMBLE_TPP1      = 1 << 0;
		// clang-format on

		if (features & RAM) {
			warning(WARNING_MBC, "TPP1 requests RAM implicitly if given a non-zero RAM size");
		}
		if (features & BATTERY) {
			mbc |= BATTERY_TPP1;
		}
		if (features & TIMER) {
			mbc |= TIMER_TPP1;
		}
		if (features & RUMBLE) {
			mbc |= RUMBLE_TPP1;
		}
		if (features & SENSOR) {
			fatalWrongMBCFeatures(name);
		}
		if (features & MULTIRUMBLE) {
			mbc |= MULTIRUMBLE_TPP1 | RUMBLE_TPP1; // Multiple rumble speeds imply rumble
		}
		break;
	}
	}

	return static_cast<MbcType>(mbc);
}
