/* 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 <mgba/core/cheats.h>

#include <mgba/core/core.h>
#include <mgba-util/string.h>
#include <mgba-util/vfs.h>

#define MAX_LINE_LENGTH 512
#define MAX_CHEATS 1000

const uint32_t M_CHEAT_DEVICE_ID = 0xABADC0DE;

mLOG_DEFINE_CATEGORY(CHEATS, "Cheats", "core.cheats");

DEFINE_VECTOR(mCheatList, struct mCheat);
DEFINE_VECTOR(mCheatSets, struct mCheatSet*);

static int32_t _readMem(struct mCore* core, uint32_t address, int width) {
	switch (width) {
	case 1:
		return core->busRead8(core, address);
	case 2:
		return core->busRead16(core, address);
	case 4:
		return core->busRead32(core, address);
	}
	return 0;
}

static void _writeMem(struct mCore* core, uint32_t address, int width, int32_t value) {
	switch (width) {
	case 1:
		core->busWrite8(core, address, value);
		break;
	case 2:
		core->busWrite16(core, address, value);
		break;
	case 4:
		core->busWrite32(core, address, value);
		break;
	}
}

static void mCheatDeviceInit(void*, struct mCPUComponent*);
static void mCheatDeviceDeinit(struct mCPUComponent*);

void mCheatDeviceCreate(struct mCheatDevice* device) {
	device->d.id = M_CHEAT_DEVICE_ID;
	device->d.init = mCheatDeviceInit;
	device->d.deinit = mCheatDeviceDeinit;
	device->autosave = false;
	device->buttonDown = false;
	mCheatSetsInit(&device->cheats, 4);
}

void mCheatDeviceDestroy(struct mCheatDevice* device) {
	mCheatDeviceClear(device);
	mCheatSetsDeinit(&device->cheats);
}

void mCheatDeviceClear(struct mCheatDevice* device) {
	size_t i;
	for (i = 0; i < mCheatSetsSize(&device->cheats); ++i) {
		struct mCheatSet* set = *mCheatSetsGetPointer(&device->cheats, i);
		mCheatSetDeinit(set);
	}
	mCheatSetsClear(&device->cheats);
}

void mCheatSetInit(struct mCheatSet* set, const char* name) {
	mCheatListInit(&set->list, 4);
	StringListInit(&set->lines, 4);
	if (name) {
		set->name = strdup(name);
	} else {
		set->name = 0;
	}
	set->enabled = true;
}

void mCheatSetDeinit(struct mCheatSet* set) {
	size_t i;
	for (i = 0; i < StringListSize(&set->lines); ++i) {
		free(*StringListGetPointer(&set->lines, i));
	}
	mCheatListDeinit(&set->list);
	if (set->name) {
		free(set->name);
	}
	StringListDeinit(&set->lines);
	set->deinit(set);
	free(set);
}

void mCheatSetRename(struct mCheatSet* set, const char* name) {
	if (set->name) {
		free(set->name);
		set->name = NULL;
	}
	if (name) {
		set->name = strdup(name);
	}
}

bool mCheatAddLine(struct mCheatSet* set, const char* line, int type) {
	if (!set->addLine(set, line, type)) {
		return false;
	}
	*StringListAppend(&set->lines) = strdup(line);
	return true;
}

void mCheatAddSet(struct mCheatDevice* device, struct mCheatSet* cheats) {
	*mCheatSetsAppend(&device->cheats) = cheats;
	cheats->add(cheats, device);
}

void mCheatRemoveSet(struct mCheatDevice* device, struct mCheatSet* cheats) {
	size_t i;
	for (i = 0; i < mCheatSetsSize(&device->cheats); ++i) {
		if (*mCheatSetsGetPointer(&device->cheats, i) == cheats) {
			break;
		}
	}
	if (i == mCheatSetsSize(&device->cheats)) {
		return;
	}
	mCheatSetsShift(&device->cheats, i, 1);
	cheats->remove(cheats, device);
}

bool mCheatParseFile(struct mCheatDevice* device, struct VFile* vf) {
	char cheat[MAX_LINE_LENGTH];
	struct mCheatSet* set = NULL;
	struct mCheatSet* newSet;
	bool nextDisabled = false;
	struct StringList directives;
	StringListInit(&directives, 4);

	while (true) {
		size_t i = 0;
		ssize_t bytesRead = vf->readline(vf, cheat, sizeof(cheat));
		rtrim(cheat);
		if (bytesRead == 0) {
			break;
		}
		if (bytesRead < 0) {
			StringListDeinit(&directives);
			return false;
		}
		while (isspace((int) cheat[i])) {
			++i;
		}
		switch (cheat[i]) {
		case '#':
			do {
				++i;
			} while (isspace((int) cheat[i]));
			newSet = device->createSet(device, &cheat[i]);
			newSet->enabled = !nextDisabled;
			nextDisabled = false;
			if (set) {
				mCheatAddSet(device, set);
			}
			if (set) {
				newSet->copyProperties(newSet, set);
			}
			newSet->parseDirectives(newSet, &directives);
			set = newSet;
			break;
		case '!':
			do {
				++i;
			} while (isspace((int) cheat[i]));
			if (strcasecmp(&cheat[i], "disabled") == 0) {
				nextDisabled = true;
				break;
			}
			if (strcasecmp(&cheat[i], "reset") == 0) {
				size_t d;
				for (d = 0; d < StringListSize(&directives); ++d) {
					free(*StringListGetPointer(&directives, d));
				}
				StringListClear(&directives);
				break;
			}
			*StringListAppend(&directives) = strdup(&cheat[i]);
			break;
		default:
			if (!set) {
				if (strncmp(cheat, "cheats = ", 9) == 0) {
					// This is in libretro format, switch over to that parser
					vf->seek(vf, 0, SEEK_SET);
					StringListDeinit(&directives);
					return mCheatParseLibretroFile(device, vf);
				}
				if (cheat[0] == '[') {
					// This is in EZ Flash CHT format, switch over to that parser
					vf->seek(vf, 0, SEEK_SET);
					StringListDeinit(&directives);
					return mCheatParseEZFChtFile(device, vf);
				}
				set = device->createSet(device, NULL);
				set->enabled = !nextDisabled;
				nextDisabled = false;
			}
			mCheatAddLine(set, cheat, 0);
			break;
		}
	}
	if (set) {
		mCheatAddSet(device, set);
	}
	size_t d;
	for (d = 0; d < StringListSize(&directives); ++d) {
		free(*StringListGetPointer(&directives, d));
	}
	StringListClear(&directives);
	StringListDeinit(&directives);
	return true;
}

bool mCheatParseLibretroFile(struct mCheatDevice* device, struct VFile* vf) {
	char cheat[MAX_LINE_LENGTH];
	char parsed[MAX_LINE_LENGTH];
	struct mCheatSet* set = NULL;
	unsigned long i = 0;
	bool startFound = false;

	while (true) {
		ssize_t bytesRead = vf->readline(vf, cheat, sizeof(cheat));
		if (bytesRead == 0) {
			break;
		}
		if (bytesRead < 0) {
			return false;
		}
		if (cheat[0] == '\n') {
			continue;
		}
		if (strncmp(cheat, "cheat", 5) != 0) {
			return false;
		}
		char* underscore = strchr(&cheat[5], '_');
		if (!underscore) {
			if (!startFound && cheat[5] == 's') {
				startFound = true;
				char* eq = strchr(&cheat[6], '=');
				if (!eq) {
					return false;
				}
				++eq;
				while (isspace((int) eq[0])) {
					if (eq[0] == '\0') {
						return false;
					}
					++eq;
				}

				char* end;
				unsigned long nCheats = strtoul(eq, &end, 10);
				if (end[0] != '\0' && !isspace(end[0])) {
					return false;
				}

				if (nCheats > MAX_CHEATS) {
					return false;
				}

				while (nCheats > mCheatSetsSize(&device->cheats)) {
					struct mCheatSet* newSet = device->createSet(device, NULL);
					if (!newSet) {
						return false;
					}
					mCheatAddSet(device, newSet);
				}
				continue;
			}
			return false;
		}
		char* underscore2;
		i = strtoul(&cheat[5], &underscore2, 10);
		if (underscore2 != underscore) {
			return false;
		}
		++underscore;
		char* eq = strchr(underscore, '=');
		if (!eq) {
			return false;
		}
		++eq;
		while (isspace((int) eq[0])) {
			if (eq[0] == '\0') {
				return false;
			}
			++eq;
		}

		if (i >= mCheatSetsSize(&device->cheats)) {
			return false;
		}
		set = *mCheatSetsGetPointer(&device->cheats, i);

		if (strncmp(underscore, "desc", 4) == 0) {
			parseQuotedString(eq, strlen(eq), parsed, sizeof(parsed));
			mCheatSetRename(set, parsed);
		} else if (strncmp(underscore, "enable", 6) == 0) {
			set->enabled = strncmp(eq, "true\n", 5) == 0;
		} else if (strncmp(underscore, "code", 4) == 0) {
			parseQuotedString(eq, strlen(eq), parsed, sizeof(parsed));
			char* cur = parsed;
			char* next;
			while ((next = strchr(cur, '+'))) {
				next[0] = '\0';
				mCheatAddLine(set, cur, 0);
				cur = &next[1];
			}
			mCheatAddLine(set, cur, 0);

			for (++i; i < mCheatSetsSize(&device->cheats); ++i) {
				struct mCheatSet* newSet = *mCheatSetsGetPointer(&device->cheats, i);
				newSet->copyProperties(newSet, set);
			}
		}
	}
	return true;
}

bool mCheatParseEZFChtFile(struct mCheatDevice* device, struct VFile* vf) {
	char cheat[MAX_LINE_LENGTH];
	char cheatName[MAX_LINE_LENGTH];
	char miniline[32];
	size_t cheatNameLength = 0;
	struct mCheatSet* set = NULL;

	cheatName[MAX_LINE_LENGTH - 1] = '\0';
	while (true) {
		ssize_t bytesRead = vf->readline(vf, cheat, sizeof(cheat));
		if (bytesRead == 0) {
			break;
		}
		if (bytesRead < 0) {
			return false;
		}
		if (cheat[0] == '\n' || (bytesRead >= 2 && cheat[0] == '\r' && cheat[1] == '\n')) {
			continue;
		}

		if (cheat[0] == '[') {
			if (strncmp(cheat, "[GameInfo]", 10) == 0) {
				break;
			}
			char* end = strchr(cheat, ']');
			if (!end) {
				return false;
			}
			char* name = gbkToUtf8(&cheat[1], end - cheat - 1);
			strncpy(cheatName, name, sizeof(cheatName) - 1);
			free(name);
			cheatNameLength = strlen(cheatName);
			continue;
		}

		char* eq = strchr(cheat, '=');
		if (!eq) {
			continue;
		}
		if (strncmp(cheat, "ON", eq - cheat) != 0) {
			char* subname = gbkToUtf8(cheat, eq - cheat);
			snprintf(&cheatName[cheatNameLength], sizeof(cheatName) - cheatNameLength - 1, ": %s", subname);
		}
		set = device->createSet(device, cheatName);
		set->enabled = false;
		mCheatAddSet(device, set);
		cheatName[cheatNameLength] = '\0';
		++eq;

		uint32_t gameptr = 0;
		uint32_t hexval = 0;
		int digit;
		while (eq[0] != '\r' && eq[1] != '\n') {
			if (cheat + bytesRead == eq || eq[0] == '\0') {
				bytesRead = vf->readline(vf, cheat, sizeof(cheat));
				eq = cheat;
				if (bytesRead == 0) {
					break;
				}
				if (bytesRead < 0) {
					return false;
				}
			}
			switch (eq[0]) {
			case ',':
				if (!gameptr) {
					gameptr = hexval;
					if (hexval < 0x40000) {
						gameptr += 0x02000000;
					} else {
						gameptr += 0x03000000 - 0x40000;
					}
				} else {
					if (hexval > 0xFF) {
						return false;
					}
					snprintf(miniline, sizeof(miniline) - 1, "%08X:%02X", gameptr, hexval);
					mCheatAddLine(set, miniline, 0);
					++gameptr;
				}
				hexval = 0;
				break;
			case ';':
				if (hexval > 0xFF) {
					return false;
				}
				snprintf(miniline, sizeof(miniline) - 1, "%08X:%02X", gameptr, hexval);
				mCheatAddLine(set, miniline, 0);
				hexval = 0;
				gameptr = 0;
				break;
			default:
				digit = hexDigit(eq[0]);
				if (digit < 0) {
					return false;
				}
				hexval <<= 4;
				hexval |= digit;
				break;
			}
			++eq;
		}
		if (gameptr) {
			if (hexval > 0xFF) {
				return false;
			}
			snprintf(miniline, sizeof(miniline) - 1, "%08X:%02X", gameptr, hexval);
			mCheatAddLine(set, miniline, 0);
		}
	}
	return true;
}

bool mCheatSaveFile(struct mCheatDevice* device, struct VFile* vf) {
	static const char lineStart[3] = "# ";
	static const char lineEnd = '\n';
	struct StringList directives;
	StringListInit(&directives, 4);

	size_t i;
	for (i = 0; i < mCheatSetsSize(&device->cheats); ++i) {
		struct mCheatSet* set = *mCheatSetsGetPointer(&device->cheats, i);
		set->dumpDirectives(set, &directives);
		if (!set->enabled) {
			static const char* disabledDirective = "!disabled\n";
			vf->write(vf, disabledDirective, strlen(disabledDirective));
		}
		size_t d;
		for (d = 0; d < StringListSize(&directives); ++d) {
			char directive[64];
			ssize_t len = snprintf(directive, sizeof(directive) - 1, "!%s\n", *StringListGetPointer(&directives, d));
			if (len > 1) {
				vf->write(vf, directive, (size_t) len > sizeof(directive) ? sizeof(directive) : (size_t) len);
			}
		}

		vf->write(vf, lineStart, 2);
		if (set->name) {
			vf->write(vf, set->name, strlen(set->name));
		}
		vf->write(vf, &lineEnd, 1);
		size_t c;
		for (c = 0; c < StringListSize(&set->lines); ++c) {
			const char* line = *StringListGetPointer(&set->lines, c);
			vf->write(vf, line, strlen(line));
			vf->write(vf, &lineEnd, 1);
		}
	}
	size_t d;
	for (d = 0; d < StringListSize(&directives); ++d) {
		free(*StringListGetPointer(&directives, d));
	}
	StringListClear(&directives);
	StringListDeinit(&directives);
	return true;
}

#if !defined(MINIMAL_CORE) || MINIMAL_CORE < 2
void mCheatAutosave(struct mCheatDevice* device) {
	if (!device->autosave) {
		return;
	}
	struct VFile* vf = mDirectorySetOpenSuffix(&device->p->dirs, device->p->dirs.cheats, ".cheats", O_WRONLY | O_CREAT | O_TRUNC);
	if (!vf) {
		return;
	}
	mCheatSaveFile(device, vf);
	vf->close(vf);
}
#endif

void mCheatRefresh(struct mCheatDevice* device, struct mCheatSet* cheats) {
	cheats->refresh(cheats, device);
	if (!cheats->enabled) {
		return;
	}

	size_t elseLoc = 0;
	size_t endLoc = 0;
	size_t nCodes = mCheatListSize(&cheats->list);
	size_t i;
	for (i = 0; i < nCodes; ++i) {
		struct mCheat* cheat = mCheatListGetPointer(&cheats->list, i);
		int32_t value = 0;
		int32_t operand = cheat->operand;
		uint32_t operationsRemaining = cheat->repeat;
		uint32_t address = cheat->address;
		bool performAssignment = false;
		bool condition = true;
		int conditionRemaining = 0;
		int negativeConditionRemaining = 0;

		for (; operationsRemaining; --operationsRemaining) {
			switch (cheat->type) {
			case CHEAT_ASSIGN:
				value = operand;
				performAssignment = true;
				break;
			case CHEAT_ASSIGN_INDIRECT:
				value = operand;
				address = _readMem(device->p, address + cheat->addressOffset, 4);
				performAssignment = true;
				break;
			case CHEAT_AND:
				value = _readMem(device->p, address, cheat->width) & operand;
				performAssignment = true;
				break;
			case CHEAT_ADD:
				value = _readMem(device->p, address, cheat->width) + operand;
				performAssignment = true;
				break;
			case CHEAT_OR:
				value = _readMem(device->p, address, cheat->width) | operand;
				performAssignment = true;
				break;
			case CHEAT_IF_EQ:
				condition = _readMem(device->p, address, cheat->width) == operand;
				conditionRemaining = cheat->repeat;
				negativeConditionRemaining = cheat->negativeRepeat;
				operationsRemaining = 1;
				break;
			case CHEAT_IF_NE:
				condition = _readMem(device->p, address, cheat->width) != operand;
				conditionRemaining = cheat->repeat;
				negativeConditionRemaining = cheat->negativeRepeat;
				operationsRemaining = 1;
				break;
			case CHEAT_IF_LT:
				condition = _readMem(device->p, address, cheat->width) < operand;
				conditionRemaining = cheat->repeat;
				negativeConditionRemaining = cheat->negativeRepeat;
				operationsRemaining = 1;
				break;
			case CHEAT_IF_GT:
				condition = _readMem(device->p, address, cheat->width) > operand;
				conditionRemaining = cheat->repeat;
				negativeConditionRemaining = cheat->negativeRepeat;
				operationsRemaining = 1;
				break;
			case CHEAT_IF_ULT:
				condition = (uint32_t) _readMem(device->p, address, cheat->width) < (uint32_t) operand;
				conditionRemaining = cheat->repeat;
				negativeConditionRemaining = cheat->negativeRepeat;
				operationsRemaining = 1;
				break;
			case CHEAT_IF_UGT:
				condition = (uint32_t) _readMem(device->p, address, cheat->width) > (uint32_t) operand;
				conditionRemaining = cheat->repeat;
				negativeConditionRemaining = cheat->negativeRepeat;
				operationsRemaining = 1;
				break;
			case CHEAT_IF_AND:
				condition = _readMem(device->p, address, cheat->width) & operand;
				conditionRemaining = cheat->repeat;
				negativeConditionRemaining = cheat->negativeRepeat;
				operationsRemaining = 1;
				break;
			case CHEAT_IF_LAND:
				condition = _readMem(device->p, address, cheat->width) && operand;
				conditionRemaining = cheat->repeat;
				negativeConditionRemaining = cheat->negativeRepeat;
				operationsRemaining = 1;
				break;
			case CHEAT_IF_NAND:
				condition = !(_readMem(device->p, address, cheat->width) & operand);
				conditionRemaining = cheat->repeat;
				negativeConditionRemaining = cheat->negativeRepeat;
				operationsRemaining = 1;
				break;
			case CHEAT_IF_BUTTON:
				condition = device->buttonDown;
				conditionRemaining = cheat->repeat;
				negativeConditionRemaining = cheat->negativeRepeat;
				operationsRemaining = 1;
				break;
			}

			if (performAssignment) {
				_writeMem(device->p, address, cheat->width, value);
			}

			address += cheat->addressOffset;
			operand += cheat->operandOffset;
		}


		if (elseLoc && i == elseLoc) {
			i = endLoc;
			endLoc = 0;
		}
		if (conditionRemaining > 0 && !condition) {
			i += conditionRemaining;
		} else if (negativeConditionRemaining > 0) {
			elseLoc = i + conditionRemaining;
			endLoc = elseLoc + negativeConditionRemaining;
		}
	}
}

void mCheatPressButton(struct mCheatDevice* device, bool down) {
	device->buttonDown = down;
}

void mCheatDeviceInit(void* cpu, struct mCPUComponent* component) {
	UNUSED(cpu);
	struct mCheatDevice* device = (struct mCheatDevice*) component;
	size_t i;
	for (i = 0; i < mCheatSetsSize(&device->cheats); ++i) {
		struct mCheatSet* cheats = *mCheatSetsGetPointer(&device->cheats, i);
		cheats->add(cheats, device);
	}
}

void mCheatDeviceDeinit(struct mCPUComponent* component) {
	struct mCheatDevice* device = (struct mCheatDevice*) component;
	size_t i;
	for (i = mCheatSetsSize(&device->cheats); i--;) {
		struct mCheatSet* cheats = *mCheatSetsGetPointer(&device->cheats, i);
		cheats->remove(cheats, device);
	}
}