/* md5table - Convert a MD5 table to either PHP or C++ code
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <assert.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

void error(const char *s, ...) {
	char buf[1024];
	va_list va;

	va_start(va, s);
	vsnprintf(buf, 1024, s, va);
	va_end(va);

	fprintf(stderr, "ERROR: %s!\n", buf);

	exit(1);
}

void warning(const char *s, ...) {
	char buf[1024];
	va_list va;

	va_start(va, s);
	vsnprintf(buf, 1024, s, va);
	va_end(va);

	fprintf(stderr, "WARNING: %s!\n", buf);
}

typedef struct {
	const char *key;
	const char *value;
} StringMap;

typedef struct {
	const char *md5;
	const char *language;
	const char *platform;
	const char *variant;
	const char *extra;
	const char *size;
	const char *desc;
	const char *comments;
	const char *infoSource;
} Entry;

/* Map MD5 table platform names to ScummVM constant names.
 * Note: Currently not many constants are defined within ScummVM. However, more
 * will probably be added eventually (see also commented out constants in
 * common/util.h).
 */
static const StringMap platformMap[] = {
	{ "2gs",		"kPlatformApple2GS" },
	{ "3DO",		"kPlatform3DO" },
	{ "Amiga",		"kPlatformAmiga" },
	{ "Atari",		"kPlatformAtariST" },
	{ "C64",		"kPlatformC64" },
	{ "DOS",		"kPlatformDOS" },
	{ "FM-TOWNS",		"kPlatformFMTowns" },
	{ "iOS",		"kPlatformIOS" },
	{ "Mac",		"kPlatformMacintosh" },
	{ "NES",		"kPlatformNES" },
	{ "PC-Engine",		"kPlatformPCEngine" },
	{ "SEGA",		"kPlatformSegaCD" },
	{ "Windows",		"kPlatformWindows" },
	{ "Wii",		"kPlatformWii" },

	{ "All?",		"kPlatformUnknown" },
	{ "All",		"kPlatformUnknown" },

	{ nullptr,			"kPlatformUnknown" }
};

static const StringMap langMap[] = {
	{ "en",		"EN_ANY" },
	{ "us",		"EN_USA" },
	{ "gb",		"EN_GRB" },
	{ "de",		"DE_DEU" },
	{ "fr",		"FR_FRA" },
	{ "it",		"IT_ITA" },
	{ "br",		"PT_BRA" },
	{ "es",		"ES_ESP" },
	{ "jp",		"JA_JPN" },
	{ "zh",		"ZH_TWN" },
	{ "ko",		"KO_KOR" },
	{ "se",		"SE_SWE" },
	{ "en",		"EN_GRB" },
	{ "he",		"HE_ISR" },
	{ "ru",		"RU_RUS" },
	{ "cs",		"CS_CZE" },
	{ "nl",		"NL_NLD" },
	{ "nb",		"NB_NOR" },
	{ "pl",		"PL_POL" },

	{ "All",	"UNK_LANG" },
	{ "All?",	"UNK_LANG" },

	{ nullptr,		"UNK_LANG" }
};

static const char *php_header =
	"<!--\n"
	"  This file was generated by the md5table tool on %s"
	"  DO NOT EDIT MANUALLY!\n"
	" -->\n"
	"<?php\n"
	"\n";

static const char *c_header =
	"/*\n"
	"  This file was generated by the md5table tool on %s"
	"  DO NOT EDIT MANUALLY!\n"
	" */\n"
	"\n"
	"#ifndef SCUMM_MD5_INTERNAL_H\n"
	"#define SCUMM_MD5_INTERNAL_H\n"
	"\n"
	"#include \"common/language.h\"\n"
	"#include \"common/platform.h\"\n"
	"\n"
	"struct MD5Table {\n"
	"	const char *md5;\n"
	"	const char *gameid;\n"
	"	const char *variant;\n"
	"	const char *extra;\n"
	"	int32 filesize;\n"
	"	Common::Language language;\n"
	"	Common::Platform platform;\n"
	"};\n"
	"\n"
	"static const MD5Table md5table[] = {\n";

static const char *c_footer =
	"	{ 0, 0, 0, 0, 0, Common::UNK_LANG, Common::kPlatformUnknown }\n"
	"};\n"
	"\n"
	"#endif\n";

static void parseEntry(Entry *entry, char *line) {
	assert(entry);
	assert(line);

	/* Split at the tabs */
	entry->md5 = strtok(line, "\t\n\r");
	entry->size = strtok(nullptr, "\t\n\r");
	entry->language = strtok(nullptr, "\t\n\r");
	entry->platform = strtok(nullptr, "\t\n\r");
	entry->variant = strtok(nullptr, "\t\n\r");
	entry->extra = strtok(nullptr, "\t\n\r");
	entry->desc = strtok(nullptr, "\t\n\r");
	entry->infoSource = strtok(nullptr, "\t\n\r");
}

static int isEmptyLine(const char *line) {
	const char *whitespace = " \t\n\r";
	while (*line) {
		if (!strchr(whitespace, *line))
			return 0;
		line++;
	}
	return 1;
}

static const char *mapStr(const char *str, const StringMap *map) {
	assert(str);
	assert(map);
	while (map->key) {
		if (0 == strcmp(map->key, str))
			return map->value;
		map++;
	}
	warning("mapStr: unknown string '%s', defaulting to '%s'", str, map->value);
	return map->value;
}

void showhelp(const char *exename)
{
	printf("\nUsage: %s <params>\n", exename);
	printf("\nParams:\n");
	printf(" --c++   output C++ code for inclusion in ScummVM (default)\n");
	printf(" --php   output PHP code for the web site\n");
	printf(" --txt   output TXT file (should be identical to input file)\n");
	exit(2);
}

/* needed to call from qsort */
int strcmp_wrapper(const void *s1, const void *s2)
{
	return strcmp((const char *)s1, (const char *)s2);
}

int main(int argc, char *argv[])
{
	FILE *inFile = stdin;
	FILE *outFile = stdout;
	char buffer[1024];
	char section[256];
	char gameid[32];
	char *line;
	int err;
	int i;
	time_t theTime;
	char *generationDate;

	const int entrySize = 256;
	int numEntries = 0, maxEntries = 1;
	char *entriesBuffer = (char *)malloc(maxEntries * entrySize);

	typedef enum {
		kCPPOutput,
		kPHPOutput,
		kTXTOutput
	} OutputMode;

	OutputMode outputMode = kCPPOutput;

	if (argc != 2)
		showhelp(argv[0]);
	if (strcmp(argv[1], "--c++") == 0) {
		outputMode = kCPPOutput;
	} else if (strcmp(argv[1], "--php") == 0) {
		outputMode = kPHPOutput;
	} else if (strcmp(argv[1], "--txt") == 0) {
		outputMode = kTXTOutput;
	} else {
		showhelp(argv[0]);
	}

	time(&theTime);
	generationDate = strdup(asctime(gmtime(&theTime)));

	if (outputMode == kPHPOutput)
		fprintf(outFile, php_header, generationDate);

	section[0] = 0;
	gameid[0] = 0;
	while ((line = fgets(buffer, sizeof(buffer), inFile))) {
		/* Parse line */
		if (line[0] == '#' || isEmptyLine(line)) {
			if (outputMode == kTXTOutput)
				fprintf(outFile, "%s", line);
			continue;	/* Skip comments & empty lines */
		}
		if (line[0] == '\t') {
			Entry entry;
			assert(section[0]);
			parseEntry(&entry, line+1);
			if (outputMode == kPHPOutput) {
				fprintf(outFile, "\taddEntry(");

				// Print the description string
				fprintf(outFile, "\"");
				if (entry.extra && strcmp(entry.extra, "-")) {
					fprintf(outFile, "%s", entry.extra);
					if (entry.desc && strcmp(entry.desc, "-"))
						fprintf(outFile, " (%s)", entry.desc);
				}
				fprintf(outFile, "\", ");

				fprintf(outFile, "\"%s\", ", entry.platform);
				fprintf(outFile, "\"%s\", ", entry.language);
				fprintf(outFile, "\"%s\"", entry.md5);
				if (entry.infoSource)
					fprintf(outFile, ", \"%s\"", entry.infoSource);
				fprintf(outFile, ");\n");
			} else if (outputMode == kTXTOutput) {
				fprintf(outFile, "\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
					entry.md5,
					entry.size,
					entry.language,
					entry.platform,
					entry.variant,
					entry.extra,
					entry.desc,
					entry.infoSource ? entry.infoSource : ""
					);
			} else if (entry.md5) {
				if (numEntries >= maxEntries) {
					maxEntries *= 2;
					entriesBuffer = (char *)realloc(entriesBuffer, maxEntries * entrySize);
				}
				if (0 == strcmp(entry.variant, "-"))
					entry.variant = "";
				if (0 == strcmp(entry.extra, "-"))
					entry.extra = "";
				snprintf(entriesBuffer + numEntries * entrySize, entrySize,
					"\t{ \"%s\", \"%s\", \"%s\", \"%s\", %s, Common::%s, Common::%s },\n",
					entry.md5,
					gameid,
					entry.variant,
					entry.extra,
					entry.size,
					mapStr(entry.language, langMap),
					mapStr(entry.platform, platformMap));
				numEntries++;
			}
		} else {
			if (outputMode == kPHPOutput && gameid[0] != 0) {
				// If there is an active section, close it now
				fprintf(outFile, "endSection();\n");
			}
			// Read the gameid, followed by a tab
			for (i = 0; *line && *line != '\t'; ++i)
				gameid[i] = *line++;
			assert(i > 0);
			gameid[i] = 0;
			assert(*line != 0);
			line++;

			// Read the section header (usually the full game name)
			for (i = 0; *line && *line != '\n'; ++i)
				section[i] = *line++;
			assert(i > 0);
			section[i] = 0;

			// If in PHP or TXT mode, we write the output immediately
			if (outputMode == kPHPOutput) {
				fprintf(outFile, "beginSection(\"%s\", \"%s\");\n", section, gameid);
			} else if (outputMode == kTXTOutput) {
				fprintf(outFile, "%s\t%s\n", gameid, section);
			}
		}
	}

	err = ferror(inFile);
	if (err)
		error("Failed reading from input file, error %d", err);

	if (outputMode == kPHPOutput) {
		if (gameid[0] != 0)		// If there is an active section, close it now
			fprintf(outFile, "endSection();\n");

		fprintf(outFile, "?>\n");
	}

	if (outputMode == kCPPOutput) {
		/* Printf header */
		fprintf(outFile, c_header, generationDate);
		/* Now sort the MD5 table (this allows for binary searches) */
		qsort(entriesBuffer, numEntries, entrySize, strcmp_wrapper);
		/* Output the table and emit warnings if duplicate md5s are found */
		buffer[0] = '\0';
		for (i = 0; i < numEntries; ++i) {
			const char *currentEntry = entriesBuffer + i * entrySize;
			fprintf(outFile, "%s", currentEntry);
			if (strncmp(currentEntry + 4, buffer, 32) == 0) {
				warning("Duplicate MD5 found '%.32s'", buffer);
			} else {
				strncpy(buffer, currentEntry + 4, 32);
			}
		}
		/* Finally, print the footer */
		fprintf(outFile, "%s", c_footer);
	}

	free(entriesBuffer);
	free(generationDate);

	return 0;
}
