#include <windows.h>
#include <string.h>
#include <time.h>

#include "Daodan.h"
#include "Daodan_Config.h"
#include "Daodan_Patch.h"
#include "Patches/Utility.h"

#include "Oni/Oni.h"
#include "_Version.h"

#include "Inifile_Reader.h"

static const char* iniName = "daodan.ini";
static const char* helpFile = "daodan_help.txt";

static const char* defaultSection = "options";

static char invalidCurParamaters[2000] = "";
static char invalidTotalParamaters[4000] = "";

void DDrConfig_PrintHelp();


ConfigSection_t config[] = {
	{ "", "Command line only", {
		{ "help",
			"Generates this help file.",
			C_CMD,
			{.intBoolVal = 0},
			{.callback = DDrConfig_PrintHelp} },
		{ 0, 0, 0, {0}, {0} }
	} },
	{ "devmode", "Developer Mode", {
		{ "highres_console",
			"Fixes bug where console line becomes invisible at higher resolutions.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "showtriggervolumes",
			"Allows BSL variable \"show_triggervolumes\" and Ctrl+Shift+X (in devmode) to work.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ 0, 0, 0, {0}, {0} }
	} },
	{ "gameplay", "Gameplay", {
		{ "bindablecheats",
			"Allows cheats to be bound to keys. Requires 'customactions' and 'cheattable' to be true.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "characterawareness",
			"Makes AI remember the player.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "cheatsenabled",
			"Enables cheats without having to beat the game first.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "cheattable",
			"Replaces Oni's cheat table with table that includes new cheats including devmode.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "cooldowntimer",
			"Disables weapon cooldown exploit.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "customactions",
			"Allows more actions to be bound through Daodan.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "kickguns",
			"EXPERIMENTAL! Unfinished, do not use.",
			C_BOOL,
			{.intBoolVal = false},
			{.intBoolVal = false} },
		{ "pathfinding",
			"Size of pathfinding grid cache increased by eight times in order to prevent crashes in large levels.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "projaware",
			"Allows AI to dodge incoming gunfire properly.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "throwtest",
			"EXPERIMENTAL! Experiment with allowing enemies to be thrown over railings.",
			C_BOOL,
			{.intBoolVal = false},
			{.intBoolVal = false} },
		{ "wpfadetime",
			"Adds working function for existing BSL command wp_fadetime, sets fade time to 4800.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ 0, 0, 0, {0}, {0} }
	} },
	{ "graphics", "Graphics", {
		{ "binkplay",
			"Fix binkplay calls to use GDI and outro same mode as intro.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "daodangl",
			"Provides an improved windowed mode (-noswitch).",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "displayenum",
			"Offers a more accurate list of available display modes in the Options menu.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "gamma",
			"Enable gamma slider in fullscreen, disable in windowed mode.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "newweap",
			"Standing above a weapon displays a message containing the weapon name and amount of ammo.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "optionsvisible",
			"Always show options button in main menu, even when pausing from a game.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "showalllasersights",
			"Show all (also enemies') weapon lasersights.",
			C_BOOL,
			{.intBoolVal = false},
			{.intBoolVal = false} },
		{ "widescreenportraits",
			"Prevents fly-in portraits from being stretched when playing in widescreen resolutions.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ 0, 0, 0, {0}, {0} }
	} },
	{ "language", "Language", {
		{ "chinese",
			"Allow for chinese fonts to be shown if the required DLL is available.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "fonttexturecache",
			"Doubles size of font texture cache.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "language",
			"Localization for hardcoded strings (e.g. \"Savepoints\").",
			C_STRING,
			{.stringVal = "en"},
			{.stringVal = "en"} },
		{ "nomultibyte",
			"Enables languages which use multibyte coding (such as Chinese).",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ 0, 0, 0, {0}, {0} }
	} },
	{ "modding", "Modding", {
		{ "argb8888",
			"Allows using textures with ARGB8888.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "d_regen",
			"Enables script command d_regen (query/set regeneration for any character).",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "daodanbsl",
			"Adds new BSL commands.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "hdscreens_lowres",
			"Allow HD intro/ending screens on game resolutions smaller than 1024x768.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "largetextures",
			"Textures up to 512x512 can be used.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "levelplugins",
			"Allows level files to be loaded which do not end in \"_Final\".",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ 0, 0, 0, {0}, {0} }
	} },
	{ "oni", "Original Oni Options", {
		{ "debug",
			"Not useful, probably does nothing.",
			EXT_BOOL,
			{.intBoolVal = false },
			{.extBoolVal = &AKgDebug_DebugMaps } },
		{ "debugfiles",
			"Logs called BSL functions to script_debug.txt.",
			EXT_BOOL,
			{.intBoolVal = false },
			{.extBoolVal = &BFgDebugFileEnable } },
		{ "findsounds",
			"Not useful, extends output of sound_list_broken_links.",
			EXT_BOOL,
			{.intBoolVal = false },
			{.extBoolVal = &SSgSearchOnDisk } },
		{ "ignore_private_data",
			"Not useful, probably does nothing.",
			EXT_BOOL,
			{.intBoolVal = false },
			{.extBoolVal = &opt_ignore_private_data } },
		{ "sound",
			"Enable sound.",
			EXT_BOOL,
			{.intBoolVal = true },
			{.extBoolVal = &opt_sound } },
		{ "switch",
			"Switch to fullscreen instead of staying in a window.",
			EXT_BOOL,
			{.intBoolVal = true},
			{.extBoolVal = &M3gResolutionSwitch} },
		{ 0, 0, 0, {0}, {0} }
	} },
	{ "windows", "Windows", {
		{ "alttab",
			"Allows to Alt-Tab out of Oni and use Windows key. May enable the screensaver as well.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "border",
			"Add a border if in windowed mode and \"usedaodangl\" patch is active.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "clipcursor",
			"Limit cursor to Oni's window.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "daodaninput",
			"New input system that reports input on every frame, supports Raw Input for mice.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "directinput",
			"Enforces the usage of DirectInput on every system. Should be off for Linux/Wine.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "mousesensitivity",
			"Multiplier for Oni's mouse sensitivity. 1.0 is Oni's default.",
			C_FLOAT,
			{.floatVal = 1.0f},
			{.floatVal = 1.0f} },
		{ "disablecmdline",
			"Disables Oni's existing command line parser as Daodan has its own.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "killvtune",
			"Prevent loading of vtuneapi.dll.",
			C_BOOL,
			{.intBoolVal = false},
			{.intBoolVal = false} },
		{ "safeprintf",
			"Replaces Oni's function that prints to startup.txt with a safer one.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ "topmost",
			"Keep game window on top in windowed mode, even when switching applications.",
			C_BOOL,
			{.intBoolVal = false},
			{.intBoolVal = false} },
		{ "usegettickcount",
			"Replaces Oni's timing functions with more accurate ones.",
			C_BOOL,
			{.intBoolVal = true},
			{.intBoolVal = true} },
		{ 0, 0, 0, {0}, {0} }
	} }
};


void DDrConfig_Print()
{
	for (unsigned int s = 0; s < ARRAY_SIZE(config); s++) {
		for (ConfigOption_t* co = config[s].options; co->name != 0; co++) {
			switch (co->type) {
				case C_STRING:
					STARTUPMESSAGE("Option %s.%s = %s (def %s)", config[s].name, co->name, co->value.stringVal, co->defaultValue.stringVal);
					break;
				case EXT_BOOL:
					STARTUPMESSAGE("Option %s.%s = %d (def %d)", config[s].name, co->name, *co->value.extBoolVal, co->defaultValue.intBoolVal);
					break;
				case C_CMD:
					break;
				case C_FLOAT:
					STARTUPMESSAGE("Option %s.%s = %f (def %f)", config[s].name, co->name, co->value.floatVal, co->defaultValue.floatVal);
					break;
				default:
					STARTUPMESSAGE("Option %s.%s = %d (def %d)", config[s].name, co->name, co->value.intBoolVal, co->defaultValue.intBoolVal);
			}
		}
	}
}

void DDrConfig_PrintHelp()
{
	STARTUPMESSAGE("Writing Daodan help file (%s)", helpFile);

	FILE* fp;
	remove(helpFile);
	fp = fopen(helpFile, "w");
	if (fp)
	{
		time_t rawtime;
		struct tm* timeinfo;
		char buffer[80];
		time(&rawtime);
		timeinfo = localtime(&rawtime);
		strftime(buffer, 80, "%Y-%m-%d %H:%M:%S", timeinfo);
		
		fprintf(fp, "Daodan help - generated on %s for Daodan v."DAODAN_VERSION_STRING"\n\n", buffer);
		fprintf(fp, "List of Daodan configuration parameters:\n");
		for (unsigned int s = 0; s < ARRAY_SIZE(config); s++) {
			fprintf(fp, "    %s - %s:\n", config[s].name, config[s].description);
			for (ConfigOption_t* co = config[s].options; co->name != 0; co++) {
				char* name = co->name;
				char* desc = co->description;
				const char* tName = DDrConfig_GetOptionTypeName(co->type);
				const char* val = DDrConfig_GetOptionValueString(co, 1);
				if (!val)
					val = "";
				fprintf(fp, "        %-22s %6s=%-5s %s\n", name, tName, val, desc);
			}
			fprintf(fp, "\n");
		}
		fprintf(fp, "\nConfiguration parameters can be either set in daodan.ini or passed on command line.\n\n");
		fprintf(fp, "In daodan.ini each section of parameters has its own section in the ini file started by [section name]. Parameters are given within that section by their name only, followed by an equals sign and the desired value. Example:\n");
		fprintf(fp, "    [sectionX]\n    parameterName = false\n");
		fprintf(fp, "\nTo pass the parameter on the command line:\n");
		fprintf(fp, "    Oni.exe -parameterName false\n");
		fprintf(fp, "For bool parameters the value can be ommitted so it is regarded as \"true\":\n");
		fprintf(fp, "    Oni.exe -parameterName\n");
		fprintf(fp, "To disable a bool parameter you can prefix \"no\" to the parameter name like this:\n");
		fprintf(fp, "    Oni.exe -noparameterName\n");

		fclose(fp);
	}
	else
	{
		STARTUPMESSAGE("Writing Daodan help file failed", 0);
	}
}

const char* DDrConfig_GetOptionTypeName(OptionType_t type)
{
	switch (type) {
		case C_INT:
			return "Int";
		case C_FLOAT:
			return "Float";
		case C_BOOL:
			return "Bool";
		case C_STRING:
			return "String";
		case C_CMD:
			return "Cmd";
		case EXT_BOOL:
			return "pBool";
		default:
			return "unknown";
	}
}

const char* DDrConfig_GetOptionValueString(ConfigOption_t* opt, char printdefault)
{
	OptionValue_t* optVal = (printdefault ? &opt->defaultValue : &opt->value);
	int boolV = optVal->intBoolVal;
	char* val = 0;
	switch (opt->type) {
		case C_STRING:
			return optVal->stringVal;
		case EXT_BOOL:
			if (printdefault)
				return (boolV ? "true" : "false");
			else
				return (*optVal->extBoolVal ? "true" : "false");
		case C_BOOL:
			return (boolV ? "true" : "false");
		case C_CMD:
			return 0;
		case C_FLOAT:
			val = malloc(50);
			sprintf(val, "%f", optVal->floatVal);
			return val;
		default:
			val = malloc(20);
			sprintf(val, "%d", boolV);
			return val;
	}
}

char DDrConfig_NonDefaultOptionValue(ConfigOption_t* opt)
{
	switch (opt->type) {
		case C_STRING:
			return _stricmp(opt->defaultValue.stringVal, opt->value.stringVal);
		case EXT_BOOL:
			return !opt->defaultValue.intBoolVal != !*opt->value.extBoolVal;
		case C_BOOL:
			return !opt->defaultValue.intBoolVal != !opt->value.intBoolVal;
		case C_CMD:
			return 0;
		case C_INT:
			return opt->defaultValue.intBoolVal != opt->value.intBoolVal;
		case C_FLOAT:
			return opt->defaultValue.floatVal != opt->value.floatVal;
	}
	return 0;
}

static ConfigOption_t* DDrConfig_GetOption(const char* fullOptName)
{
	char section[50];
	strcpy(section, fullOptName);

	char* option = strchr(section, '.');
	if (option == 0) {
		STARTUPMESSAGE("Could not find option separator in \"%s\"", fullOptName);
		return 0;
	}
	*option++ = 0;

	char isWildcardSection = !_stricmp(section, "*");

	for (unsigned int s = 0; s < ARRAY_SIZE(config); s++) {
		if (isWildcardSection || !_stricmp(config[s].name, section)) {
			for (ConfigOption_t* co = config[s].options; co->name != 0; co++) {
				if (!_stricmp(co->name, option)) {
					return co;
				}
			}
			if (!isWildcardSection) {
				STARTUPMESSAGE("Could not find option \"%s\" in section \"%s\"", option, section);
				return 0;
			}
		}
	}
	if (!isWildcardSection)
		STARTUPMESSAGE("Could not find section \"%s\" for option \"%s\"", section, option);
	else
		STARTUPMESSAGE("Could not find option \"%s\"", option);
	return 0;
}



ConfigOption_t* DDrConfig_GetOptOfType(const char* fullOptName, OptionType_t type)
{
	ConfigOption_t* co = DDrConfig_GetOption(fullOptName);
	if (co == 0)
		return 0;

	if (co->type != type) {
		STARTUPMESSAGE("Option \"%s\" is not of type %s", fullOptName, DDrConfig_GetOptionTypeName(type));
		return 0;
	}
	return co;
}



void DDrConfig_InitExtBools()
{
	for (unsigned int s = 0; s < ARRAY_SIZE(config); s++) {
		for (ConfigOption_t* co = config[s].options; co->name != 0; co++) {
			if (co->type == EXT_BOOL) {
				*co->value.extBoolVal = co->defaultValue.intBoolVal;
			}
		}
	}
}



void DDrConfig_WriteIni()
{
	FILE* fp;
	STARTUPMESSAGE("%s doesn't exist, creating", iniName);
	fp = fopen(iniName, "w");
	if (fp)
	{
		for (unsigned int s = 0; s < ARRAY_SIZE(config); s++) {
			if (strlen(config[s].name)) {
				fprintf(fp, "[%s]\n", config[s].name);
				for (ConfigOption_t* co = config[s].options; co->name != 0; co++) {
					char* name = co->name;
					const char* val = DDrConfig_GetOptionValueString(co, 0);
					if (val && DDrConfig_NonDefaultOptionValue(co))
						fprintf(fp, "%s = %s\n", name, val);
				}
				fprintf(fp, "\n");
			}
		}
		fclose(fp);
	}
	else
	{
		STARTUPMESSAGE("Writing %s template file failed", iniName);
	}
}


void DDrIniCallback(const char* section, const char* name, const char* value)
{
	char fullOptName[50];

	if (!_stricmp(section, "patch"))
		section = "patches";

	strcpy(fullOptName, section);
	fullOptName[strlen(section)] = '.';
	strcpy(fullOptName+strlen(section)+1, name);

	ConfigOption_t* co = DDrConfig_GetOption(fullOptName);

	if (co)
	{
		char* buf = 0;
		switch (co->type) {
			case C_INT:
				co->value.intBoolVal = strtol(value, NULL, 0);
				break;
			case C_FLOAT:
				co->value.floatVal = strtof(value, NULL);
				break;
			case C_BOOL:
				co->value.intBoolVal = !_stricmp(value, "true");
				break;
			case C_STRING:
				buf = malloc(strlen(value)+1);
				strcpy(buf, value);
				co->value.stringVal = buf;
				break;
			case C_CMD:
				co->value.callback();
				break;
			case EXT_BOOL:
				*(co->value.extBoolVal) = !_stricmp(value, "true");
				break;
			default:
				STARTUPMESSAGE("Config value type unknown: %d", co->type);
		}
	} else {
		char buf[100];
		if (!_stricmp(section, "*"))
			sprintf(buf, "  %s\n", name);
		else
			sprintf(buf, "  %s.%s\n", section, name);
		if (strlen(buf) + strlen(invalidCurParamaters) < sizeof(invalidCurParamaters) - 1) {
			strcpy(invalidCurParamaters + strlen(invalidCurParamaters), buf);
		}
	}
}


bool DDrConfig_ParseCommandLine(int argc, char* argv[])
{
	for (int i = 1; i < argc; i ++)
	{
		if (argv[i][0] == '-')
		{
			char* option;
			bool invertedOption;

			option = argv[i]+1;

			invertedOption = (option[0] == 'n' || option[0] == 'N') && (option[1] == 'o' || option[1] == 'O');
			if (invertedOption)
				option += 2;

			if (i < (argc - 1) && argv[i+1][0] != '-')
			// Has value in next field
			{
				DDrIniCallback("*", option, argv[++i]);
			}
			else
			// Implicit value
			{
				DDrIniCallback("*", option, (invertedOption ? "false" : "true"));
			}
		}
		else
		{
			STARTUPMESSAGE("Parse error \"%s\"", argv[i]);
			return false;
		}
	}
	return true;
}

void DDrConfig(int argc, char* argv[])
{
	STARTUPMESSAGE("Initializing standard booleans", 0);
	DDrConfig_InitExtBools();

	if (GetFileAttributes(iniName) == INVALID_FILE_ATTRIBUTES)
		DDrConfig_WriteIni();
	
	STARTUPMESSAGE("Parsing daodan.ini...", 0);
	if (!Inifile_Read(iniName, DDrIniCallback))
		STARTUPMESSAGE("Error reading daodan.ini, check your syntax!", 0);
	STARTUPMESSAGE("Finished parsing", 0);

	if (strlen(invalidCurParamaters) > 0)
	{
		sprintf(invalidTotalParamaters, "In %s:\n%s\n", iniName, invalidCurParamaters);
		invalidCurParamaters[0] = 0;
	}

	STARTUPMESSAGE("Parsing command line...", 0);
	DDrConfig_ParseCommandLine(argc, argv);
	STARTUPMESSAGE("Finished parsing", 0);

	if (strlen(invalidCurParamaters) > 0)
	{
		sprintf(invalidTotalParamaters, "%sOn command line:\n%s\n", invalidTotalParamaters, invalidCurParamaters);
	}

	if (strlen(invalidTotalParamaters) > 0)
	{
		char msg[4096];
		sprintf(msg, "Invalid parameters given:\n%sContinue launching Oni?", invalidTotalParamaters);
		int res = MessageBox(NULL, msg, "Parameters invalid", MB_ICONWARNING | MB_YESNO | MB_DEFBUTTON1);
		if (res == IDNO) {
			exit(0);
		}
	}

	DDrConfig_WriteIni();

//	DDrConfig_Print();
}

