#include <SDL_guid.h>
#include "IZ_config.h"

const char* IZ_ConfigGetCommandlineOption(u8 argc, const char* argv[], const char* val) {
	size_t n = strlen(val);
	int c = argc;

	while (--c > 0) {
		if (!strncmp(argv[c], val, n)) {
			if (!*(argv[c] + n) && c < argc - 1) {
				/* coverity treats unchecked argv as "tainted" */
				if (!argv[c + 1] || strlen(argv[c + 1]) > 1024)
					return NULL;
				return argv[c + 1];
			}

			if (argv[c][n] == '=')
				return &argv[c][n + 1];
			return argv[c] + n;
		}
	}

	return NULL;
}

typedef enum {
	IZ_CONFIG_SAVE_ITEM_ERROR = -1,
	IZ_CONFIG_SAVE_ITEM_OK,
} IZ_ConfigSaveItemResult;

#define IZ_CONFIG_REGISTER_INT_TYPE(ID, T) \
typedef bool IZ_ConfigValidate##ID(T); \
 \
typedef T IZ_ConfigDeserialize##ID(const char*); \
 \
typedef void IZ_ConfigSerialize##ID(T, const char[128]); \
 \
void IZ_ConfigEnsureValid##ID(IZ_ConfigItem* item, T raw_value, T default_value) { \
	T* dest = item->dest; \
	if (item->validator) { \
		IZ_ConfigValidate##ID* validate = item->validator; \
		if (validate(raw_value)) { \
			*dest = raw_value; \
			return; \
		} \
		*dest = default_value; \
		return; \
	} \
	*dest = raw_value; \
} \
 \
void IZ_ConfigLoad##ID(IZ_ConfigItem* item, const char* config_path) { \
	static T raw_value; \
	static T default_value; \
	default_value = *((T*) item->default_value); \
	if (item->transformer.deserialize && item->transformer.serialize) { \
		IZ_ConfigDeserialize##ID* deserialize = item->transformer.deserialize; \
		IZ_ConfigSerialize##ID* serialize = item->transformer.serialize; \
		const char serialized_default_value[128]; \
		if (default_value) { \
			serialize(default_value, serialized_default_value); \
		} \
		char buffer[128]; \
		ini_gets(item->section, item->key, serialized_default_value, buffer, 128, config_path); \
		raw_value = deserialize(buffer); \
	} else { \
		raw_value = ini_getl(item->section, item->key, default_value, config_path); \
	} \
	IZ_ConfigEnsureValid##ID(item, raw_value, default_value); \
} \
 \
IZ_ConfigSaveItemResult IZ_ConfigSave##ID(IZ_ConfigItem* item, const char* config_path) { \
	T dest = *((T*) item->dest); \
	if (item->validator) { \
		IZ_ConfigValidate##ID* validate = item->validator; \
		if (!validate(dest)) { \
			dest = *((const T*) item->default_value); \
		} \
	} \
 \
	if (item->transformer.deserialize && item->transformer.serialize) { \
		IZ_ConfigSerialize##ID* serialize = item->transformer.serialize; \
		const char serialized_value[128]; \
		serialize(dest, serialized_value); \
		if (!ini_puts(item->section, item->key, serialized_value, config_path)) { \
			return -1; \
		} \
		return 0; \
	} \
 \
	if (!ini_putl(item->section, item->key, dest, config_path)) { \
		return -1; \
	} \
 \
	return 0; \
} \
 \
void IZ_ConfigOverride##ID(IZ_ConfigItem* item, u8 argc, const char* argv[]) { \
	if (!item->cmdline_option) { \
		return; \
	} \
	const char* cmdline_buffer; \
	char* rest_of_string; \
	static T dest; \
	static T config_value; \
	config_value = *((T*) item->dest); \
	if ((cmdline_buffer = IZ_ConfigGetCommandlineOption(argc, argv, item->cmdline_option))) { \
		dest = strtol(cmdline_buffer, &rest_of_string, 10); \
		if (strcmp(cmdline_buffer, rest_of_string) != 0) { \
			IZ_ConfigEnsureValid##ID(item, dest, config_value); \
			return; \
		} \
	} \
}

IZ_CONFIG_REGISTER_INT_TYPE(U8, u8);
IZ_CONFIG_REGISTER_INT_TYPE(U16, u16);
IZ_CONFIG_REGISTER_INT_TYPE(I32, i32);

typedef bool IZ_ConfigLoadParamsStringValidator(const char*);

void IZ_ConfigEnsureValidString(IZ_ConfigItem* item, const char* buffer) {
	if (item->validator) {
		IZ_ConfigLoadParamsStringValidator* validator = item->validator;
		if (validator(buffer)) {
			memcpy(item->dest, buffer, item->dest_size);
			return;
		}
		memcpy(item->dest, item->default_value, item->dest_size);
		return;
	}
	memcpy(item->dest, buffer, item->dest_size);
}

void IZ_ConfigLoadString(IZ_ConfigItem* item, const char* config_path) {
	char buffer[item->dest_size];
	ini_gets(item->section, item->key, item->default_value, buffer, (i32) item->dest_size, config_path);
	IZ_ConfigEnsureValidString(item, buffer);
}

IZ_ConfigSaveItemResult IZ_ConfigSaveString(IZ_ConfigItem* item, const char* config_path) {
	const char* dest = (const char*) item->dest;
	if (item->validator) {
		IZ_ConfigLoadParamsStringValidator* validator = item->validator;
		if (!validator(dest)) {
			dest = (const char*) item->default_value;
		}
	}

	if (!ini_puts(item->section, item->key, dest, config_path)) {
		return IZ_CONFIG_SAVE_ITEM_ERROR;
	}

	return IZ_CONFIG_SAVE_ITEM_OK;
}

void IZ_ConfigOverrideString(IZ_ConfigItem* item, u8 argc, const char* argv[]) {
	if (!item->cmdline_option) {
		return;
	}
	const char* cmdline_buffer;
	if ((cmdline_buffer = IZ_ConfigGetCommandlineOption(argc, argv, item->cmdline_option))) {
		IZ_ConfigEnsureValidString(item, cmdline_buffer);
	}
}

typedef bool IZ_ConfigLoadParamsGuidValidator(SDL_GUID);

void IZ_ConfigEnsureValidGuid(IZ_ConfigItem* item, SDL_GUID raw_value, SDL_GUID default_value) {
	SDL_GUID* dest = item->dest;
	if (item->validator) {
		IZ_ConfigLoadParamsGuidValidator* validate = item->validator;
		if (validate(raw_value)) {
			// within valid values
			*dest = raw_value;
			return;
		}
		// outside valid values, we use default value
		//
		// TODO: what if the default value is also invalid?
		*dest = default_value;
		return;
	}

	// no validator, get whatever is the deserialized value
	*dest = raw_value;
}

typedef SDL_GUID IZ_ConfigDeserializeGUID(const char*);

typedef void IZ_ConfigSerializeGUID(SDL_GUID, const char[128]);

void IZ_ConfigLoadGuid(IZ_ConfigItem* item, const char* config_path) {
	static SDL_GUID raw_value;
	static SDL_GUID default_value = {
		.data = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
	};

	if (item->default_value) {
		default_value = *((SDL_GUID *) item->default_value);
	}

	char buffer[128];
	if (item->transformer.deserialize && item->transformer.serialize) {
		IZ_ConfigDeserializeGUID* deserialize = item->transformer.deserialize;
		IZ_ConfigSerializeGUID* serialize = item->transformer.serialize;
		const char serialized_default_value[128];
		serialize(default_value, serialized_default_value);
		ini_gets(item->section, item->key, serialized_default_value, buffer, 128, config_path);
		raw_value = deserialize(buffer);
	} else {
		ini_gets(item->section, item->key, "00000000000000000000000000000000", buffer, 128, config_path);
		raw_value = SDL_GUIDFromString(buffer);
	}
	IZ_ConfigEnsureValidGuid(item, raw_value, default_value);
}

void IZ_ConfigLoad(IZ_ConfigItem item[], const char* config_path) {
	u8 i;
	for (i = 0; item[i].type != IZ_CONFIG_TYPE_VOID; i += 1) {
		switch (item[i].type) {
			case IZ_CONFIG_TYPE_STRING:
				IZ_ConfigLoadString(&item[i], config_path);
				break;
			case IZ_CONFIG_TYPE_U8:
				IZ_ConfigLoadU8(&item[i], config_path);
				break;
			case IZ_CONFIG_TYPE_U16:
				IZ_ConfigLoadU16(&item[i], config_path);
				break;
			case IZ_CONFIG_TYPE_I32:
				IZ_ConfigLoadI32(&item[i], config_path);
				break;
			case IZ_CONFIG_TYPE_GUID:
				IZ_ConfigLoadGuid(&item[i], config_path);
				break;
			default:
				break;
		}
	}
}

IZ_ConfigSaveItemResult IZ_ConfigSaveGuid(IZ_ConfigItem* item, const char* config_path) {
	SDL_GUID dest = *((SDL_GUID*) item->dest);
	if (item->validator) {
		IZ_ConfigLoadParamsGuidValidator* validate = item->validator;
		if (!validate(dest)) {
			dest = *((const SDL_GUID*) item->default_value);
		}
	}

	if (item->transformer.deserialize && item->transformer.serialize) {
		IZ_ConfigSerializeGUID* serialize = item->transformer.serialize;
		const char serialized_value[128];
		serialize(dest, serialized_value);
		if (!ini_puts(item->section, item->key, serialized_value, config_path)) {
			return -1;
		}
		return 0;
	}

	char guid_str[33];
	memset(guid_str, 0, 33);
	SDL_GUIDToString(dest, guid_str, 33);
	if (!ini_puts(item->section, item->key, guid_str, config_path)) {
		return -1;
	}

	return 0;
}

void IZ_ConfigOverrideGuid(IZ_ConfigItem* item, u8 argc, const char* argv[]) {
	if (!item->cmdline_option) {
		return;
	}
	const char* cmdline_buffer;
	static SDL_GUID dest;
	static SDL_GUID config_value;
	config_value = *((SDL_GUID*) item->dest);
	if((cmdline_buffer = IZ_ConfigGetCommandlineOption(argc, argv, item->cmdline_option))) {
		dest = SDL_GUIDFromString(cmdline_buffer);
		IZ_ConfigEnsureValidGuid(item, dest, config_value);
	}
}

IZ_ConfigSaveResult IZ_ConfigSave(IZ_ConfigItem item[], const char* config_path) {
	u8 i;
	i64 problems = 0;
	for (i = 0; item[i].type != IZ_CONFIG_TYPE_VOID; i += 1) {
		i8 result = 0;
		switch (item[i].type) {
			case IZ_CONFIG_TYPE_STRING:
				result = IZ_ConfigSaveString(&item[i], config_path);
				break;
			case IZ_CONFIG_TYPE_GUID:
				result = IZ_ConfigSaveGuid(&item[i], config_path);
				break;
			case IZ_CONFIG_TYPE_U8:
				result = IZ_ConfigSaveU8(&item[i], config_path);
				break;
			case IZ_CONFIG_TYPE_U16:
				result = IZ_ConfigSaveU16(&item[i], config_path);
				break;
			case IZ_CONFIG_TYPE_I32:
				result = IZ_ConfigSaveI32(&item[i], config_path);
			default:
				break;
		}

		if (result < 0) {
			problems |= (1u << (i64) i);
		}
	}

	return -problems;
}

void IZ_ConfigOverride(IZ_ConfigItem item[], u8 argc, const char* argv[]) {
	u8 i;
	for (i = 0; item[i].type != IZ_CONFIG_TYPE_VOID; i += 1) {
		switch (item[i].type) {
			case IZ_CONFIG_TYPE_STRING:
				IZ_ConfigOverrideString(&item[i], argc, argv);
				break;
			case IZ_CONFIG_TYPE_U8:
				IZ_ConfigOverrideU8(&item[i], argc, argv);
				break;
			case IZ_CONFIG_TYPE_U16:
				IZ_ConfigOverrideU16(&item[i], argc, argv);
				break;
			case IZ_CONFIG_TYPE_I32:
				IZ_ConfigOverrideI32(&item[i], argc, argv);
				break;
			case IZ_CONFIG_TYPE_GUID:
				IZ_ConfigOverrideGuid(&item[i], argc, argv);
				break;
			default:
				break;
		}
	}
}

IZ_ConfigInitializeResult IZ_ConfigInitialize(IZ_ConfigItem item[], const char* config_path, u8 argc, const char* argv[]) {
	IZ_ConfigLoad(item, config_path);
	IZ_ConfigSaveResult save_result = IZ_ConfigSave(item, config_path);
	if (save_result < 0) {
		return IZ_CONFIG_INITIALIZE_RESULT_ERROR;
	}
	IZ_ConfigOverride(item, argc, argv);
	if (save_result > 0) {
		return IZ_CONFIG_INITIALIZE_RESULT_WARNING;
	}
	return IZ_CONFIG_INITIALIZE_RESULT_OK;
}
