//__BIGPEMU_SCRIPT_MODULE__
//__BIGPEMU_META_DESC__		"Cheat code module."
//__BIGPEMU_META_AUTHOR__	"Rich Whitehouse"

#include "bigpcrt.h"

static int32_t sOnLoadEvent = -1;
static int32_t sOnEmuFrameEvent = -1;

#if !defined(_BIGP_PLATFORM_NEW_)
	#define CHEATPLAT_JAG
#elif defined(_BIGP_MACH_KI_)
	#define CHEATPLAT_KI
#elif defined(_BIGP_MACH_PSX_)
	#define CHEATPLAT_PSX
#else
	#error "Unimplemented cheat platform"
#endif

#if defined(CHEATPLAT_JAG)
	#include "jagregs.h"
#endif

static TSharedResPtr sJson = 0;
static TBigPJsonNode sMainNode = 0;
static char *spJsonStr = NULL;

static int32_t sSetCodesSettingHandle = -1;

//#define LOG_CHEAT_HITS

#define MAX_CHEAT_CODE_DESC_SIZE 128
#define MAX_CHEAT_CODE_MENU_ITEMS 128
#define MAX_ACTIVE_CHEAT_CODES 512
#define MAX_CODE_POOL_SIZE 16384
typedef struct
{
	char mName[MAX_CHEAT_CODE_DESC_SIZE];
	TBigPJsonNode mCodeNode;
	uint32_t mCodeArraySize;
} TCheatCodeInfo;
static char sMenuLabelText[BIGP_CRT_MAX_LOCAL_BUFFER_SIZE];
static TBigPEmuUserMenuItem sMenuItems[MAX_CHEAT_CODE_MENU_ITEMS];
static TCheatCodeInfo sMenuCodes[MAX_CHEAT_CODE_MENU_ITEMS];
static uint32_t sMenuCodeStartIndex = 0;
static uint32_t sMenuCodeEndIndexPlusOne = 0;
static uint32_t sMenuCodeItemCount = 0;
static uint64_t sLastLoadedHash = 0;

static char sCurrentSWHashStr[128];

static uint8_t sCodePool[MAX_CODE_POOL_SIZE];
static uint32_t sCodePoolOfs = 0;
static void *code_pool_alloc(const uint32_t sz)
{
	if ((sCodePoolOfs + sz) > MAX_CODE_POOL_SIZE)
	{
		return NULL;
	}
	void *p = &sCodePool[sCodePoolOfs];
	sCodePoolOfs += sz;
	return p;
}

static void code_pool_reset()
{
	sCodePoolOfs = 0;
}

static TDickGenieCode sActiveCodes[MAX_ACTIVE_CHEAT_CODES];
uint32_t sActiveCountCount = 0;

static void cheat_check_codes_at_address(const uint64_t addr);

static void cheat_set_paused(const uint32_t isPaused)
{
#if defined(CHEATPLAT_JAG)
	bigpemu_jag_set_paused(isPaused);
#else
	bigpemu_emu_set_paused(isPaused);
#endif
}

#if defined(CHEATPLAT_JAG)
static void cheatplat_code_write(const uint64_t addr, const uint64_t data, const uint32_t dataSize)
{
	if (addr >= 0x800000 && addr < 0xDFFF00)
	{
		//force rom writes
		for (uint32_t i = 0; i < dataSize; ++i)
		{
			const uint32_t shiftCount = ((dataSize - i - 1) << 3);
			const uint8_t d = (uint8_t)((data >> shiftCount) & 0xFF);
			bigpemu_jag_sysmemwrite(&d, addr + i, 1);
		}
	}
	else
	{
		switch (dataSize)
		{
		case 1:
			bigpemu_jag_write8((uint32_t)addr, (uint8_t)data);
			break;
		case 2:
			bigpemu_jag_write16((uint32_t)addr, (uint16_t)data);
			break;
		case 4:
			bigpemu_jag_write32((uint32_t)addr, (uint32_t)data);
			break;
		case 8:
			bigpemu_jag_write64((uint32_t)addr, (uint64_t)data);
			break;
		default:
			assert(0);
			break;
		}
	}
}

static uint64_t cheatplat_code_read(const uint64_t addr, const uint32_t dataSize)
{
	switch (dataSize)
	{
	case 1:
		return (uint64_t)bigpemu_jag_read8((uint32_t)addr);
	case 2:
		return (uint64_t)bigpemu_jag_read16((uint32_t)addr);
	case 4:
		return (uint64_t)bigpemu_jag_read32((uint32_t)addr);
	case 8:
		return (uint64_t)bigpemu_jag_read64((uint32_t)addr);
	default:
		assert(0);
		break;
	}
	return 0;
}

static void cheatplat_code_set_pc(const uint64_t pcAddr)
{
	bigpemu_jag_m68k_set_pc((uint32_t)pcAddr);
}

static void jag_bp_callback(const uint32_t addr)
{
	cheat_check_codes_at_address(addr);
}
#endif //CHEATPLAT_JAG

static void cheat_check_codes_at_address(const uint64_t addr)
{
	//could map this to speed things up a bit, or add user data to breakpoints, but for now we'll just be terrible
	for (uint32_t codeIndex = 0; codeIndex < sActiveCountCount; ++codeIndex)
	{
		const TDickGenieCode *pCode = &sActiveCodes[codeIndex];
		if (pCode->mPCAddr == addr)
		{
			switch (pCode->mType)
			{
			case kDGCT_PCLocWriteMem:
#if defined(LOG_CHEAT_HITS)
				printf("Hit kDGCT_PCLocWriteMem: %08x -> [%08x]\n", (uint32_t)pCode->mData, (uint32_t)pCode->mMemAddr);
#endif
				cheatplat_code_write(pCode->mMemAddr, pCode->mData, pCode->mDataSize);
				break;
			case kDGCT_PCLocCompareAndWriteMem:
				{
					const uint64_t val = cheatplat_code_read(pCode->mMemAddr, pCode->mDataSize);
					if (val == pCode->mCompare)
					{
#if defined(LOG_CHEAT_HITS)
						printf("Hit kDGCT_PCLocCompareAndWriteMem: %08x -> [%08x]\n", (uint32_t)pCode->mData, (uint32_t)pCode->mMemAddr);
#endif
						cheatplat_code_write(pCode->mMemAddr, pCode->mData, pCode->mDataSize);
					}
				}
				break;
			case kDGCT_PCLocReloc:
#if defined(LOG_CHEAT_HITS)
				printf("Hit kDGCT_PCLocWriteMem: %08x -> PC\n", (uint32_t)pCode->mData);
#endif
				cheatplat_code_set_pc(pCode->mData);
				break;
			case kDGCT_PCLocCompareAndReloc:
				{
					const uint64_t val = cheatplat_code_read(pCode->mMemAddr, pCode->mDataSize);
					if (val == pCode->mCompare)
					{
#if defined(LOG_CHEAT_HITS)
						printf("Hit kDGCT_PCLocCompareAndReloc: %08x -> PC\n", (uint32_t)pCode->mData);
#endif
						cheatplat_code_set_pc(pCode->mData);
					}
				}
				break;
			}
		}
	}
}

static void cheat_cfg_blob_name(char *pBlobName, const TCheatCodeInfo *pCodeInfo)
{
	sprintf(pBlobName, "CheatSetting_%s_%s", sCurrentSWHashStr, pCodeInfo->mName);
	bigpemu_sanitize_cfg_blob_name(pBlobName);
}

static void cheat_setting_from_cfg(TBigPEmuUserMenuItem *pMenuItem, const TCheatCodeInfo *pCodeInfo)
{
	char blobName[BIGP_CRT_MAX_LOCAL_BUFFER_SIZE];
	cheat_cfg_blob_name(blobName, pCodeInfo);
	uint32_t val = 0;
	if (bigpemu_get_cfg_data_blob(&val, NULL, sizeof(val), blobName))
	{
		pMenuItem->mValue = val;
	}
}

static void cheat_setting_to_cfg(const TBigPEmuUserMenuItem *pMenuItem, const TCheatCodeInfo *pCodeInfo)
{
	char blobName[BIGP_CRT_MAX_LOCAL_BUFFER_SIZE];
	cheat_cfg_blob_name(blobName, pCodeInfo);
	bigpemu_set_cfg_data_blob(&pMenuItem->mValue, sizeof(pMenuItem->mValue), blobName);
}

static uint32_t sDidCheatPause = 0;

static void cheat_menu_done(void *pUserData)
{
	sActiveCountCount = 0;
	
	//this call will be happening on the main thread, so we have to hard sync with the emulator thread to make any breakpoint calls safe
	bigpemu_emu_lock_system_mutex();
	
	for (uint32_t codeIndex = sMenuCodeStartIndex; codeIndex < sMenuCodeEndIndexPlusOne; ++codeIndex)
	{
		TCheatCodeInfo *pCodeInfo = &sMenuCodes[codeIndex];
		TBigPEmuUserMenuItem *pItem = &sMenuItems[codeIndex];
		cheat_setting_to_cfg(pItem, pCodeInfo);
		if (pItem->mValue)
		{
			char codeText[DICKGENIE_MAX_CODE_STRING_SIZE];
			TBigPJsonNode codeNode = pCodeInfo->mCodeNode;
			if (pCodeInfo->mCodeArraySize > 0)
			{
				const uint32_t arrayIndex = BIGPEMU_MIN(pItem->mValue - 1, pCodeInfo->mCodeArraySize - 1);
				codeNode = bigpemu_json_get_array_elem(codeNode, arrayIndex);
			}
			bigpemu_json_get_value_string(codeText, DICKGENIE_MAX_CODE_STRING_SIZE, codeNode);

			const uint32_t subCodeCount = bigpemu_dickgenie_decode_multiple(&sActiveCodes[sActiveCountCount], MAX_ACTIVE_CHEAT_CODES - sActiveCountCount, codeText);
			//printf("Code enabled: %s (%i) - %s\n", pCodeInfo->mName, subCodeCount, codeText);
			if (subCodeCount > 0)
			{
				for (uint32_t subIndex = 0; subIndex < subCodeCount; ++subIndex)
				{
					const TDickGenieCode *pCode = &sActiveCodes[sActiveCountCount + subIndex];
					//printf("MemAddr: %08x\n", (uint32_t)sActiveCodes[sActiveCountCount + subIndex].mMemAddr);
#if defined(CHEATPLAT_JAG)
					bigpemu_jag_m68k_bp_add((uint32_t)pCode->mPCAddr, jag_bp_callback);
#else
	#error "Unimplemented cheat platform"
#endif
				}
				sActiveCountCount += subCodeCount;
			}
			else
			{
				printf("Invalid DickGenie code: %s\n", codeText);
			}
		}
	}
	
	bigpemu_emu_unlock_system_mutex();
	
	if (sDidCheatPause)
	{
		sDidCheatPause = 0;
		cheat_set_paused(0);
	}
}

static void cheat_menu_on_accept(const TBigPEmuUserMenuItem *pItem)
{
	bigpemu_exit_user_menu();
}

static void cheat_menu_on_reset_user_accepted(void *pUserData)
{
	for (uint32_t codeIndex = sMenuCodeStartIndex; codeIndex < sMenuCodeEndIndexPlusOne; ++codeIndex)
	{
		TBigPEmuUserMenuItem *pItem = &sMenuItems[codeIndex];
		pItem->mValue = 0;
	}	
}

static void cheat_menu_on_reset(const TBigPEmuUserMenuItem *pItem)
{
	bigpemu_menu_do_modal_accept_cancel("!STR_CHEATS_RESET", cheat_menu_on_reset_user_accepted, NULL, NULL);
}

static void check_cheats_for_current_sw(const uint32_t menuTrigger)
{
	if (sMainNode)
	{
		const uint64_t hash = bigpemu_get_loaded_fnv1a64();
		sprintf(sCurrentSWHashStr, "%08x%08x", (uint32_t)(hash >> 32), (uint32_t)(hash & 0xFFFFFFFF));

		code_pool_reset();
		
		if (bigpemu_json_has_member(sMainNode, sCurrentSWHashStr))
		{
			const TBigPJsonNode swNode = bigpemu_json_get_member(sMainNode, sCurrentSWHashStr);
			const TBigPJsonNode nameNode = bigpemu_json_get_member(swNode, "Name");
			const TBigPJsonNode codesNode = bigpemu_json_get_member(swNode, "Codes");
			if (nameNode && codesNode)
			{
				char gameName[BIGP_CRT_MAX_LOCAL_BUFFER_SIZE];
				if (!bigpemu_json_get_value_string(gameName, BIGP_CRT_MAX_LOCAL_BUFFER_SIZE, nameNode))
				{
					gameName[0] = 0;
				}
				const uint32_t codeCount = bigpemu_json_get_array_size(codesNode);
				if (codeCount > 0)
				{
					//lazy way of keeping the cheat code settings if we're loading the same thing again.
					//could potentially save this to preferences.
					if (sLastLoadedHash != hash)
					{
						sLastLoadedHash = hash;
						memset(sMenuItems, 0, sizeof(sMenuItems));
					}
					strcpy(sMenuLabelText, "STR_CHEATS_LABEL");
					const int32_t labelOfs = bigpemu_get_localized_string(sMenuLabelText);
					if (labelOfs >= 0)
					{
						sprintf(&sMenuLabelText[labelOfs], " %s", gameName);
					}
					uint32_t menuItemCount = 0;

					bigpemu_usermenu_default_sep(&sMenuItems[menuItemCount], sMenuLabelText)->mAddY += 6.0f;
					++menuItemCount;
					
					sMenuCodeStartIndex = menuItemCount;
					
					for (uint32_t codeIndex = 0; codeIndex < codeCount && menuItemCount < (MAX_CHEAT_CODE_MENU_ITEMS - 3); ++codeIndex)
					{
						const TBigPJsonNode codeNode = bigpemu_json_get_array_elem(codesNode, codeIndex);
						if (codeNode && bigpemu_json_has_member(codeNode, "Name") && bigpemu_json_has_member(codeNode, "Code"))
						{
							TCheatCodeInfo *pCodeInfo = &sMenuCodes[menuItemCount];
							bigpemu_json_get_value_string(pCodeInfo->mName, MAX_CHEAT_CODE_DESC_SIZE, bigpemu_json_get_member(codeNode, "Name"));
							pCodeInfo->mCodeNode = bigpemu_json_get_member(codeNode, "Code");
							pCodeInfo->mCodeArraySize = bigpemu_json_get_array_size(pCodeInfo->mCodeNode);
							TBigPEmuUserMenuItem *pMenuItem = &sMenuItems[menuItemCount];
							cheat_setting_from_cfg(pMenuItem, pCodeInfo);
							if (pCodeInfo->mCodeArraySize > 0)
							{
								bigpemu_usermenu_default_setting_uint(pMenuItem, pCodeInfo->mName, pMenuItem->mValue, 0, pCodeInfo->mCodeArraySize, 1);
							}
							else
							{
								bigpemu_usermenu_default_setting_bool(pMenuItem, pCodeInfo->mName, pMenuItem->mValue);
							}
							if (bigpemu_json_has_member(codeNode, "CodeLabels"))
							{
								const TBigPJsonNode labelNode = bigpemu_json_get_member(codeNode, "CodeLabels");
								const uint32_t labelCount = bigpemu_json_get_array_size(labelNode);
								char **ppLabels = (char **)code_pool_alloc(sizeof(char *) * labelCount);
								if (ppLabels)
								{
									char strBuffer[BIGP_CRT_MAX_LOCAL_BUFFER_SIZE];
									for (uint32_t labelIndex = 0; labelIndex < labelCount; ++labelIndex)
									{
										const TBigPJsonNode strNode = bigpemu_json_get_array_elem(labelNode, labelIndex);
										if (!bigpemu_json_get_value_string(strBuffer, BIGP_CRT_MAX_LOCAL_BUFFER_SIZE, strNode))
										{
											strBuffer[0] = 0;
										}
										const uint32_t l = (uint32_t)strlen(strBuffer);
										char *pStrMem = (char *)code_pool_alloc(l + 1);
										if (!pStrMem)
										{
											ppLabels = NULL;
											break;
										}
										memcpy(pStrMem, strBuffer, l + 1);
										ppLabels[labelIndex] = pStrMem;
									}
								}
								if (ppLabels)
								{
									pMenuItem->mppValLabels = (vmuptr_t)ppLabels;
									pMenuItem->mValLabelCount = labelCount;
								}
								else
								{
									printf("Warning: Ran out of pool memory when allocating labels.\n");
								}
							}
							++menuItemCount;
						}
					}
					
					sMenuCodeEndIndexPlusOne = menuItemCount;
					
					bigpemu_usermenu_default_sep(&sMenuItems[menuItemCount], NULL)->mAddY += 6.0f;
					++menuItemCount;					

					bigpemu_usermenu_default_trigger(&sMenuItems[menuItemCount], "!STR_RESETALL", cheat_menu_on_reset)->mAddY += 6.0f;
					++menuItemCount;					

					bigpemu_usermenu_default_trigger(&sMenuItems[menuItemCount], "!STR_MENU_INPUT_ACCEPT", cheat_menu_on_accept);
					++menuItemCount;					
					
					sMenuCodeItemCount = menuItemCount;
					
					if (bigpemu_show_user_menu(sMenuItems, menuItemCount, cheat_menu_done, NULL))
					{
						if (!menuTrigger)
						{
							sDidCheatPause = 1;
							cheat_set_paused(1);
						}
					}
				}
			}
			else
			{
				printf("Error: Cheat code for entry '%s' missing Name and/or Codes.\n", sCurrentSWHashStr);
			}
		}
		else if (menuTrigger)
		{
			if (hash)
			{
				strcpy(sMenuLabelText, "STR_CHEATS_NONEFORSW");
				const int32_t labelOfs = bigpemu_get_localized_string(sMenuLabelText);
				if (labelOfs >= 0)
				{
					sprintf(&sMenuLabelText[labelOfs], " %s", sCurrentSWHashStr);
				}
				printf_notify("%s", sMenuLabelText);
			}
			else
			{
				strcpy(sMenuLabelText, "STR_CHEATS_NOSW");
				bigpemu_get_localized_string(sMenuLabelText);
				printf_notify("%s", sMenuLabelText);
			}
		}
	}
}

static uint32_t on_sw_loaded(const int eventHandle, void *pEventData)
{
	check_cheats_for_current_sw(0);
	return 0;
}

static void set_codes_setting_callback(void *pUserData)
{
	check_cheats_for_current_sw(1);
}

static uint32_t on_emu_frame(const int eventHandle, void *pEventData)
{
	//consider pc address 0 breakpoints to be per-frame
	cheat_check_codes_at_address(0);
	return 0;
}

void bigp_init()
{
	void *pMod = bigpemu_get_module_handle();

	bigpemu_set_module_usage_flags(pMod, BIGPEMU_MODUSAGE_DETERMINISMWARNING);

	sOnLoadEvent = bigpemu_register_event_sw_loaded(pMod, on_sw_loaded);
	sOnEmuFrameEvent = bigpemu_register_event_video_thread_frame(pMod, on_emu_frame);

	uint64_t f = fs_open("cheat_codes.json", 0);
	if (f)
	{
		const uint64_t fSize = fs_get_size(f);
		spJsonStr = (char *)bigpemu_vm_alloc(fSize + 1);
		fs_read(spJsonStr, fSize, f);
		spJsonStr[fSize] = 0;
		fs_close(f);
		
		sJson = bigpemu_json_parse(pMod, spJsonStr);
		if (sJson)
		{
			sMainNode = bigpemu_json_get_member(bigpemu_json_doc_root(sJson), "DickGenie");
		}
	}
	
	if (!sMainNode)
	{
		printf("Error: Failed to parse cheat code JSON.\n");
	}
	else
	{
		const int catHandle = bigpemu_register_setting_category(pMod, "DickGenie");
		sSetCodesSettingHandle = bigpemu_register_setting_trigger(catHandle, "Set Codes", set_codes_setting_callback, NULL);		
	}
}

void bigp_shutdown()
{
	void *pMod = bigpemu_get_module_handle();
	
	sMainNode = 0;
	if (sJson)
	{
		bigpemu_json_free(pMod, sJson);
		sJson = 0;
	}
	if (spJsonStr)
	{
		bigpemu_vm_free(spJsonStr);
		spJsonStr = NULL;
	}
	
	bigpemu_unregister_event(pMod, sOnLoadEvent);
	sOnLoadEvent = -1;
	bigpemu_unregister_event(pMod, sOnEmuFrameEvent);
	sOnEmuFrameEvent = -1;
	sSetCodesSettingHandle = -1;
}
