#include "services/cfg.hpp"
#include "services/dsp.hpp"
#include "system_models.hpp"
#include "ipc.hpp"

#include <array>
#include <bit>
#include <string>
#include <unordered_map>

namespace CFGCommands {
	enum : u32 {
		GetConfigInfoBlk2 = 0x00010082,
		SecureInfoGetRegion = 0x00020000,
		GenHashConsoleUnique = 0x00030040,
		GetRegionCanadaUSA = 0x00040000,
		GetSystemModel = 0x00050000,
		GetCountryCodeID = 0x000A0040,
	};
}

void CFGService::reset() {}

void CFGService::handleSyncRequest(u32 messagePointer) {
	const u32 command = mem.read32(messagePointer);
	switch (command) {
		case CFGCommands::GetConfigInfoBlk2: [[likely]] getConfigInfoBlk2(messagePointer); break;
		case CFGCommands::GetCountryCodeID: getCountryCodeID(messagePointer); break;
		case CFGCommands::GetRegionCanadaUSA: getRegionCanadaUSA(messagePointer); break;
		case CFGCommands::GetSystemModel: getSystemModel(messagePointer); break;
		case CFGCommands::GenHashConsoleUnique: genUniqueConsoleHash(messagePointer); break;
		case CFGCommands::SecureInfoGetRegion: secureInfoGetRegion(messagePointer); break;
		default: Helpers::panic("CFG service requested. Command: %08X\n", command);
	}
}

void CFGService::getSystemModel(u32 messagePointer) {
	log("CFG::GetSystemModel\n");

	mem.write32(messagePointer, IPC::responseHeader(0x05, 2, 0));
	mem.write32(messagePointer + 4, Result::Success);
	mem.write8(messagePointer + 8, SystemModel::Nintendo3DS); // TODO: Make this adjustable via GUI
}

// Write a UTF16 string to 3DS memory starting at "pointer". Appends a null terminator.
void CFGService::writeStringU16(u32 pointer, const std::u16string& string) {
	for (auto c : string) {
		mem.write16(pointer, static_cast<u16>(c));
		pointer += 2;
	}

	mem.write16(pointer, static_cast<u16>(u'\0')); // Null terminator
}

void CFGService::getConfigInfoBlk2(u32 messagePointer) {
	u32 size = mem.read32(messagePointer + 4);
	u32 blockID = mem.read32(messagePointer + 8);
	u32 output = mem.read32(messagePointer + 16); // Pointer to write the output data to
	log("CFG::GetConfigInfoBlk2 (size = %X, block ID = %X, output pointer = %08X\n", size, blockID, output);

	// TODO: Make this not bad
	if (size == 1 && blockID == 0x70001) { // Sound output mode
		mem.write8(output, static_cast<u8>(DSPService::SoundOutputMode::Stereo));
	} else if (size == 1 && blockID == 0xA0002){ // System language
		mem.write8(output, static_cast<u8>(LanguageCodes::English));
	} else if (size == 4 && blockID == 0xB0000) { // Country info
		mem.write8(output, 0); // Unknown
		mem.write8(output + 1, 0); // Unknown
		mem.write8(output + 2, 2); // Province (Temporarily stubbed to Washington DC like Citra)
		mem.write8(output + 3, static_cast<u8>(country)); // Country code
	} else if (size == 0x20 && blockID == 0x50005) {
		// "Stereo Camera settings"
		// Implementing this properly fixes NaN uniforms in some games. Values taken from 3dmoo & Citra
		static constexpr std::array<float, 8> STEREO_CAMERA_SETTINGS = {
			62.0f, 289.0f, 76.80000305175781f, 46.08000183105469f,
			10.0f, 5.0f,   55.58000183105469f, 21.56999969482422f
		};

		for (int i = 0; i < 8; i++) {
			mem.write32(output + i * 4, Helpers::bit_cast<u32, float>(STEREO_CAMERA_SETTINGS[i]));
		}
	} else if (size == 0x1C && blockID == 0xA0000) { // Username
		writeStringU16(output, u"Pander");
	} else if (size == 0xC0 && blockID == 0xC0000) { // Parental restrictions info
		for (int i = 0; i < 0xC0; i++)
			mem.write8(output + i, 0);
	} else if (size == 4 && blockID == 0xD0000) { // Agreed EULA version (first 2 bytes) and latest EULA version (next 2 bytes)
		log("Read EULA info\n");
		mem.write16(output, 0x0202); // Agreed EULA version = 2.2 (Random number. TODO: Check)
		mem.write16(output + 2, 0x0202); // Latest EULA version = 2.2
	} else if (size == 0x800 && blockID == 0xB0001) { // UTF-16 name for our country in every language at 0x80 byte intervals
		constexpr size_t languageCount = 16;
		constexpr size_t nameSize = 0x80; // Max size of each name in bytes
		std::u16string name = u"PandaLand (Home of PandaSemi LLC) (aka Pandistan)"; // Note: This + the null terminator needs to fit in 0x80 bytes

		for (int i = 0; i < languageCount; i++) {
			u32 pointer = output + i * nameSize;
			writeStringU16(pointer, name);
		}
	} else if (size == 0x800 && blockID == 0xB0002) { // UTF-16 name for our state in every language at 0x80 byte intervals
		constexpr size_t languageCount = 16;
		constexpr size_t nameSize = 0x80; // Max size of each name in bytes
		std::u16string name = u"Pandington"; // Note: This + the null terminator needs to fit in 0x80 bytes

		for (int i = 0; i < languageCount; i++) {
			u32 pointer = output + i * nameSize;
			writeStringU16(pointer, name);
		}
	} else if (size == 4 && blockID == 0xB0003) { // Coordinates (latidude and longtitude) as s16
		mem.write16(output, 0); // Latitude
		mem.write16(output + 2, 0); // Longtitude
	} else if (size == 2 && blockID == 0xA0001) { // Birthday
		mem.write8(output, 5); // Month (May)
		mem.write8(output + 1, 5); // Day (Fifth)
	} else if (size == 8 && blockID == 0x30001) { // User time offset
		printf("Read from user time offset field in NAND. TODO: What is this\n");
		mem.write64(output, 0);
	} else if (size == 20 && blockID == 0xC0001) { // COPPACS restriction data, used by games when they detect a USA/Canada region for market restriction stuff
		for (u32 i = 0; i < size; i += 4) {
			mem.write32(output + i, 0);
		}
	} else if (size == 4 && blockID == 0x170000) {  // Miiverse access key
		mem.write32(output, 0);
	} else {
		Helpers::panic("Unhandled GetConfigInfoBlk2 configuration. Size = %d, block = %X", size, blockID);
	}

	mem.write32(messagePointer, IPC::responseHeader(0x1, 1, 2));
	mem.write32(messagePointer + 4, Result::Success);
}

void CFGService::secureInfoGetRegion(u32 messagePointer) {
	log("CFG::SecureInfoGetRegion\n");

	mem.write32(messagePointer, IPC::responseHeader(0x2, 2, 0));
	mem.write32(messagePointer + 4, Result::Success);
	mem.write32(messagePointer + 8, static_cast<u32>(mem.getConsoleRegion()));
}

void CFGService::genUniqueConsoleHash(u32 messagePointer) {
	log("CFG::GenUniqueConsoleHash (semi-stubbed)\n");
	const u32 salt = mem.read32(messagePointer + 4) & 0x000FFFFF;

	mem.write32(messagePointer, IPC::responseHeader(0x3, 3, 0));
	mem.write32(messagePointer + 4, Result::Success);
	// We need to implement hash generation & the SHA-256 digest properly later on. We have cryptopp so the hashing isn't too hard to do
	// Let's stub it for now
	mem.write32(messagePointer + 8, 0x33646D6F ^ salt); // Lower word of hash
	mem.write32(messagePointer + 12, 0xA3534841 ^ salt);  // Upper word of hash
}

// Returns 1 if the console region is either Canada or USA, otherwise returns 0
// Used for market restriction-related stuff
void CFGService::getRegionCanadaUSA(u32 messagePointer) {
	log("CFG::GetRegionCanadaUSA\n");
	bool regionUSA = mem.getConsoleRegion() == Regions::USA;
	u8 ret;

	// First, this function checks that the console region is 1 (USA). If not then it instantly returns 0
	// Then it checks whether the country is US or Canda. If yes it returns 1, else it returns 0.
	if (!regionUSA) {
		ret = 0;
	} else {
		ret = (country == CountryCodes::US || country == CountryCodes::CA) ? 1 : 0;
	}

	mem.write32(messagePointer, IPC::responseHeader(0x4, 2, 0));
	mem.write32(messagePointer + 4, Result::Success);
	mem.write8(messagePointer + 8, ret);
}

constexpr u16 C(const char name[3]) { return name[0] | (name[1] << 8); }
static std::unordered_map<u16, u16> countryCodeToTableIDMap = {
	{C("JP"), 1},   {C("AI"), 8},   {C("AG"), 9},   {C("AR"), 10},  {C("AW"), 11},  {C("BS"), 12},  {C("BB"), 13},  {C("BZ"), 14},  {C("BO"), 15},
	{C("BR"), 16},  {C("VG"), 17},  {C("CA"), 18},  {C("KY"), 19},  {C("CL"), 20},  {C("CO"), 21},  {C("CR"), 22},  {C("DM"), 23},  {C("DO"), 24},
	{C("EC"), 25},  {C("SV"), 26},  {C("GF"), 27},  {C("GD"), 28},  {C("GP"), 29},  {C("GT"), 30},  {C("GY"), 31},  {C("HT"), 32},  {C("HN"), 33},
	{C("JM"), 34},  {C("MQ"), 35},  {C("MX"), 36},  {C("MS"), 37},  {C("AN"), 38},  {C("NI"), 39},  {C("PA"), 40},  {C("PY"), 41},  {C("PE"), 42},
	{C("KN"), 43},  {C("LC"), 44},  {C("VC"), 45},  {C("SR"), 46},  {C("TT"), 47},  {C("TC"), 48},  {C("US"), 49},  {C("UY"), 50},  {C("VI"), 51},
	{C("VE"), 52},  {C("AL"), 64},  {C("AU"), 65},  {C("AT"), 66},  {C("BE"), 67},  {C("BA"), 68},  {C("BW"), 69},  {C("BG"), 70},  {C("HR"), 71},
	{C("CY"), 72},  {C("CZ"), 73},  {C("DK"), 74},  {C("EE"), 75},  {C("FI"), 76},  {C("FR"), 77},  {C("DE"), 78},  {C("GR"), 79},  {C("HU"), 80},
	{C("IS"), 81},  {C("IE"), 82},  {C("IT"), 83},  {C("LV"), 84},  {C("LS"), 85},  {C("LI"), 86},  {C("LT"), 87},  {C("LU"), 88},  {C("MK"), 89},
	{C("MT"), 90},  {C("ME"), 91},  {C("MZ"), 92},  {C("NA"), 93},  {C("NL"), 94},  {C("NZ"), 95},  {C("NO"), 96},  {C("PL"), 97},  {C("PT"), 98},
	{C("RO"), 99},  {C("RU"), 100}, {C("RS"), 101}, {C("SK"), 102}, {C("SI"), 103}, {C("ZA"), 104}, {C("ES"), 105}, {C("SZ"), 106}, {C("SE"), 107},
	{C("CH"), 108}, {C("TR"), 109}, {C("GB"), 110}, {C("ZM"), 111}, {C("ZW"), 112}, {C("AZ"), 113}, {C("MR"), 114}, {C("ML"), 115}, {C("NE"), 116},
	{C("TD"), 117}, {C("SD"), 118}, {C("ER"), 119}, {C("DJ"), 120}, {C("SO"), 121}, {C("AD"), 122}, {C("GI"), 123}, {C("GG"), 124}, {C("IM"), 125},
	{C("JE"), 126}, {C("MC"), 127}, {C("TW"), 128}, {C("KR"), 136}, {C("HK"), 144}, {C("MO"), 145}, {C("ID"), 152}, {C("SG"), 153}, {C("TH"), 154},
	{C("PH"), 155}, {C("MY"), 156}, {C("CN"), 160}, {C("AE"), 168}, {C("IN"), 169}, {C("EG"), 170}, {C("OM"), 171}, {C("QA"), 172}, {C("KW"), 173},
	{C("SA"), 174}, {C("SY"), 175}, {C("BH"), 176}, {C("JO"), 177}, {C("SM"), 184}, {C("VA"), 185}, {C("BM"), 186},
};

void CFGService::getCountryCodeID(u32 messagePointer) {
	// Read the character code as a u16 instead of as ASCII, and use it to index the unordered_map above and get the result
	const u16 characterCode = mem.read16(messagePointer + 4);
	log("CFG::GetCountryCodeID (code = %04X)\n", characterCode);

	mem.write32(messagePointer, IPC::responseHeader(0x0A, 2, 0));
	
	// If the character code is valid, return its table ID and a success code
	if (auto search = countryCodeToTableIDMap.find(characterCode); search != countryCodeToTableIDMap.end()) {
		mem.write32(messagePointer + 4, Result::Success);
		mem.write16(messagePointer + 8, search->second);
	}
	
	else {
		Helpers::warn("CFG::GetCountryCodeID: Invalid country code %X", characterCode);
		mem.write32(messagePointer + 4, Result::CFG::NotFound);
		mem.write16(messagePointer + 8, 0xFF);
	}
}