#include "Options.h"
#include "String.h"
#include "ExtMath.h"
#include "Platform.h"
#include "Stream.h"
#include "Errors.h"
#include "Utils.h"
#include "Logger.h"
#include "PackedCol.h"

CC_BIG_VAR struct StringsBuffer Options;
static CC_BIG_VAR struct StringsBuffer changedOpts;
cc_result Options_LoadResult;
static cc_bool savingPaused;

#if defined CC_BUILD_WEB || defined CC_BUILD_MOBILE || defined CC_BUILD_CONSOLE
	#define OPTIONS_SAVE_IMMEDIATELY
#endif

void Options_Free(void) {
	StringsBuffer_Clear(&Options);
	StringsBuffer_Clear(&changedOpts);
}

static cc_bool HasChanged(const cc_string* key) {
	cc_string entry;
	int i;

	for (i = 0; i < changedOpts.count; i++) {
		entry = StringsBuffer_UNSAFE_Get(&changedOpts, i);
		if (String_CaselessEquals(&entry, key)) return true;
	}
	return false;
}

static cc_bool Options_LoadFilter(const cc_string* entry) {
	cc_string key, value;
	String_UNSAFE_Separate(entry, '=', &key, &value);
	return !HasChanged(&key);
}

void Options_Load(void) {
	/* Increase from max 512 to 2048 per entry */
	StringsBuffer_SetLengthBits(&Options, 11);
	Options_LoadResult = EntryList_Load(&Options, "options-default.txt", '=', NULL);
	Options_LoadResult = EntryList_Load(&Options, "options.txt",         '=', NULL);
}

void Options_Reload(void) {
	cc_string entry, key, value;
	int i;

	/* Reset all the unchanged options */
	for (i = Options.count - 1; i >= 0; i--) {
		entry = StringsBuffer_UNSAFE_Get(&Options, i);
		String_UNSAFE_Separate(&entry, '=', &key, &value);

		if (HasChanged(&key)) continue;
		StringsBuffer_Remove(&Options, i);
	}
	/* Load only options which have not changed */
	Options_LoadResult = EntryList_Load(&Options, "options.txt", '=', Options_LoadFilter);
}

static void SaveOptions(void) {
	EntryList_Save(&Options, "options.txt");
	StringsBuffer_Clear(&changedOpts);
}

void Options_SaveIfChanged(void) {
	if (!changedOpts.count) return;
	
	Options_Reload();
	SaveOptions();
}

void Options_PauseSaving(void) { savingPaused = true; }

void Options_ResumeSaving(void) { 
	savingPaused = false;
#if defined OPTIONS_SAVE_IMMEDIATELY
	SaveOptions();
#endif
}


cc_bool Options_UNSAFE_Get(const char* keyRaw, cc_string* value) {
	int idx;
	cc_string key = String_FromReadonly(keyRaw);

	*value = EntryList_UNSAFE_Get(&Options, &key, '=');
	if (value->length) return true; 

	/* Fallback to without '-' (e.g. "hacks-fly" to "fly") */
	/* Needed for some very old options.txt files */
	idx = String_IndexOf(&key, '-');
	if (idx == -1) return false;
	key = String_UNSAFE_SubstringAt(&key, idx + 1);

	*value = EntryList_UNSAFE_Get(&Options, &key, '=');
	return value->length > 0;
}

void Options_Get(const char* key, cc_string* value, const char* defValue) {
	cc_string str;
	Options_UNSAFE_Get(key, &str);
	value->length = 0;

	if (str.length) {
		String_AppendString(value, &str);
	} else {
		String_AppendConst(value, defValue);
	}
}

int Options_GetInt(const char* key, int min, int max, int defValue) {
	cc_string str;
	int value;
	if (!Options_UNSAFE_Get(key, &str))  return defValue;
	if (!Convert_ParseInt(&str, &value)) return defValue;

	Math_Clamp(value, min, max);
	return value;
}

cc_bool Options_GetBool(const char* key, cc_bool defValue) {
	cc_string str;
	cc_bool value;
	if (!Options_UNSAFE_Get(key, &str))   return defValue;
	if (!Convert_ParseBool(&str, &value)) return defValue;

	return value;
}

float Options_GetFloat(const char* key, float min, float max, float defValue) {
	cc_string str;
	float value;
	if (!Options_UNSAFE_Get(key, &str))    return defValue;
	if (!Convert_ParseFloat(&str, &value)) return defValue;

	Math_Clamp(value, min, max);
	return value;
}

int Options_GetEnum(const char* key, int defValue, const char* const* names, int namesCount) {
	cc_string str;
	if (!Options_UNSAFE_Get(key, &str)) return defValue;
	return Utils_ParseEnum(&str, defValue, names, namesCount);
}

cc_bool Options_GetColor(const char* key, cc_uint8* rgb) {
	cc_string value, parts[3];
	if (!Options_UNSAFE_Get(key, &value))   return false;
	if (PackedCol_TryParseHex(&value, rgb)) return true;

	/* Try parsing as R,G,B instead */
	return String_UNSAFE_Split(&value, ',', parts, 3)
		&& Convert_ParseUInt8(&parts[0], &rgb[0])
		&& Convert_ParseUInt8(&parts[1], &rgb[1])
		&& Convert_ParseUInt8(&parts[2], &rgb[2]);
}


void Options_SetBool(const char* keyRaw, cc_bool value) {
	static const cc_string str_true  = String_FromConst("True");
	static const cc_string str_false = String_FromConst("False");
	Options_Set(keyRaw, value ? &str_true : &str_false);
}

void Options_SetInt(const char* keyRaw, int value) {
	cc_string str; char strBuffer[STRING_INT_CHARS];
	String_InitArray(str, strBuffer);
	String_AppendInt(&str, value);
	Options_Set(keyRaw, &str);
}

void Options_Set(const char* keyRaw, const cc_string* value) {
	cc_string key = String_FromReadonly(keyRaw);
	Options_SetString(&key, value);
}

void Options_SetString(const cc_string* key, const cc_string* value) {
	if (!value || !value->length) {
		if (!EntryList_Remove(&Options, key, '=')) return;
	} else {
		EntryList_Set(&Options, key, value, '=');
	}

#if defined OPTIONS_SAVE_IMMEDIATELY
	if (!savingPaused) SaveOptions();
#endif

	if (HasChanged(key)) return;
	StringsBuffer_Add(&changedOpts, key);
}

void Options_SetSecure(const char* opt, const cc_string* src) {
	char data[2000], encData[1500+1];
	cc_string tmp, enc;
	cc_result res;
	if (!src->length) return;

	String_InitArray(enc, encData);
	res = Platform_Encrypt(src->buffer, src->length, &enc);
	if (res) { Platform_Log2("Error %e encrypting option %c", &res, opt); return; }

	/* base64 encode the data, as user might edit options.txt with a text editor */
	if (enc.length > 1500) Process_Abort("too large to base64");
	tmp.buffer   = data;
	tmp.length   = Convert_ToBase64(enc.buffer, enc.length, data);
	tmp.capacity = tmp.length;
	Options_Set(opt, &tmp);
}

void Options_GetSecure(const char* opt, cc_string* dst) {
	cc_uint8 data[1500];
	int dataLen;
	cc_string raw;
	cc_result res;

	Options_UNSAFE_Get(opt, &raw);
	if (!raw.length) return;
	if (raw.length > 2000) Process_Abort("too large to base64");

	dataLen = Convert_FromBase64(raw.buffer, raw.length, data);
	res = Platform_Decrypt(data, dataLen, dst);
	if (res) Platform_Log2("Error %e decrypting option %c", &res, opt);
}
