Source: gui/options/options.js

var g_HasCallback = false;
var g_Controls;

function init(data)
{
	if (data && data.callback)
		g_HasCallback = true;
	g_Controls = {};

	var options = Engine.ReadJSONFile("gui/options/options.json");
	for (let category of Object.keys(options))
	{
		let lastSize;
		for (let i = 0; i < options[category].length; ++i)
		{
			let option = options[category][i];
			if (!option.label || !option.parameters || !option.parameters.config)
				continue;
			let body = Engine.GetGUIObjectByName(category + "[" + i + "]");
			let label = Engine.GetGUIObjectByName(category + "Label[" + i + "]");
			let config = option.parameters.config;
			g_Controls[config] = {
				"control": setupControl(option, i, category),
				"type": option.type,
				"dependencies": option.dependencies || undefined,
				"parameters": option.parameters
			};
			label.caption = translate(option.label);
			label.tooltip = option.tooltip ? translate(option.tooltip) : "";
			// Move each element to the correct place.
			if (lastSize)
			{
				let newSize = new GUISize();
				newSize.left = lastSize.left;
				newSize.rright = lastSize.rright;
				newSize.top = lastSize.bottom;
				newSize.bottom = newSize.top + 26;
				body.size = newSize;
				lastSize = newSize;
			}
			else
				lastSize = body.size;
			// small right shift of options which depends on another one
			for (let opt of options[category])
			{
				if (!opt.label || !opt.parameters || !opt.parameters.config)
					continue;
				if (!opt.dependencies || opt.dependencies.indexOf(config) === -1)
					continue;
				label.caption = "      " + label.caption;
				break;
			}
			// Show element.
			body.hidden = false;
		}
	}

	updateOptionPanel();
}

/**
 * Setup the apropriate control for a given option.
 *
 * @param option Structure containing the data to setup an option.
 * @param prefix Prefix to use when accessing control, for example "generalSetting" when the tickbox name is generalSettingTickbox[i].
 */
function setupControl(option, i, category)
{
	var control;
	var onUpdate;
	var key = option.parameters.config;

	switch (option.type)
	{
	case "boolean":
	case "invertedboolean":
		// More space for the label
		let text = Engine.GetGUIObjectByName(category + "Label[" + i + "]");
		let size = text.size;
		size.rright = 87;
		text.size = size;
		control = Engine.GetGUIObjectByName(category + "Tickbox[" + i + "]");
		let checked;
		let keyRenderer;

		for (let param of Object.keys(option.parameters))
		{
			switch (param)
			{
			case "config":
				checked = Engine.ConfigDB_GetValue("user", key) === "true";
				break;
			case "renderer":
				keyRenderer = option.parameters.renderer;
				if (!Engine["Renderer_Get" + keyRenderer + "Enabled"])
				{
					warn("Invalid renderer key " + keyRenderer);
					keyRenderer = undefined;
					break;
				}
				if (Engine["Renderer_Get" + keyRenderer + "Enabled"]() !== checked)
				{
					warn("Incompatible renderer option value for " + keyRenderer);
					Engine["Renderer_Set" + keyRenderer + "Enabled"](checked);
				}
				break;
			default:
				warn("Unknown option source type '" + param + "'");
			}
		}
		// invertedboolean when we want to display the opposite of the flag value
		var inverted = option.type === "invertedboolean";
		if (inverted)
			checked = !checked;

		onUpdate = function(key, keyRenderer, inverted)
		{
			return function()
			{
				let val = inverted ? !this.checked : this.checked;
				if (keyRenderer)
					Engine["Renderer_Set" + keyRenderer + "Enabled"](val);
				Engine.ConfigDB_CreateValue("user", key, String(val));
				Engine.ConfigDB_SetChanges("user", true);
				updateOptionPanel();
			};
		}(key, keyRenderer, inverted);

		// Load final data to the control element.
		control.checked = checked;
		control.onPress = onUpdate;
		break;
	case "number":
		// TODO: Slider
	case "string":
		control = Engine.GetGUIObjectByName(category + "Input[" + i + "]");
		let caption;
		let functionBody;
		let minval;
		let maxval;

		for (let param of Object.keys(option.parameters))
		{
			switch (param)
			{
			case "config":
				caption = Engine.ConfigDB_GetValue("user", key);
				break;
			case "function":
				if (Engine[option.parameters.function])
					functionBody = option.parameters.function;
				break;
			case "min":
				minval = option.parameters.min;
				break;
			case "max":
				maxval = option.parameters.max;
				break;
			default:
				warn("Unknown option source type '" + param + "'");
			}
		}

		// as the enter key is not necessarily pressed after modifying an entry, we will register the input also
		// - when the mouse leave the control (MouseLeave event)
		// - or when saving or closing the window (registerChanges function)
		// so we must ensure that something has indeed been modified
		onUpdate = function(key, functionBody, minval, maxval)
		{
			return function()
			{
				if (minval && +minval > +this.caption)
					this.caption = minval;
				if (maxval && +maxval < +this.caption)
					this.caption = maxval;
				if (Engine.ConfigDB_GetValue("user", key) === this.caption)
					return;
				Engine.ConfigDB_CreateValue("user", key, this.caption);
				Engine.ConfigDB_SetChanges("user", true);
				if (functionBody)
					Engine[functionBody](+this.caption);
				updateOptionPanel();
			};
		}(key, functionBody, minval, maxval);

		control.caption = caption;
		control.onPress = onUpdate;
		control.onMouseLeave = onUpdate;
		break;
	case "dropdown":
		control = Engine.GetGUIObjectByName(category + "Dropdown[" + i + "]");
		control.onSelectionChange = function(){};  // just the time to setup the value

		for (let param of Object.keys(option.parameters))
		{
			switch (param)
			{
			case "config":
				let val = +Engine.ConfigDB_GetValue("user", key);
				if (key === "materialmgr.quality")
					val = val > 5 ? 2 : val > 2 ? 1 : 0;
				control.selected = val;
				break;
			case "list":
				control.list = option.parameters.list.map(e => translate(e));
				break;
			case "list_data":
				control.list_data = option.parameters.list_data;
				break;
			default:
				warn("Unknown option source type '" + param + "'");
			}
		}

		onUpdate = function(key)
		{
			return function()
			{
				let val = this.selected;
				if (key === "materialmgr.quality")
					val = val == 0 ? 2 : val == 1 ? 5 : 8;
				Engine.ConfigDB_CreateValue("user", key, val);
				Engine.ConfigDB_SetChanges("user", true);
				updateOptionPanel();
			};
		}(key);

		control.onSelectionChange = onUpdate;
		break;
	default:
		warn("Unknown option type " + option.type + ", assuming string.");
		control = Engine.GetGUIObjectByName(category + "Input[" + i + "]");
		break;
	}
	control.hidden = false;
	control.tooltip = option.tooltip ? translate(option.tooltip) : "";
	return control;
}

function updateOptionPanel()
{
	// Update dependencies
	for (let item in g_Controls)
	{
		let control = g_Controls[item];
		if (control.type !== "boolean" && control.type !== "invertedboolean" || !control.dependencies)
			continue;

		for (let dependency of control.dependencies)
			g_Controls[dependency].control.enabled = control.control.checked;
	}

	// And main buttons
	let hasChanges = Engine.ConfigDB_HasChanges("user");
	Engine.GetGUIObjectByName("revertChanges").enabled = hasChanges;
	Engine.GetGUIObjectByName("saveChanges").enabled = hasChanges;
}

/**
 * Register changes of input (text and number) controls
 */
function registerChanges()
{
	for (let item in g_Controls)
		if (g_Controls[item].type === "number" || g_Controls[item].type === "string")
			g_Controls[item].control.onPress();
}

function setDefaults()
{
	let btCaptions = [translate("No"), translate("Yes")];
	let btCode = [null, function(){ reallySetDefaults(); }];
	messageBox(500, 200, translate("Resetting the options will erase your saved settings. Do you want to continue?"),
		translate("Warning"), 0, btCaptions, btCode);
}

function reallySetDefaults()
{
	for (let item in g_Controls)
		Engine.ConfigDB_RemoveValue("user", item);

	Engine.ConfigDB_WriteFile("user", "config/user.cfg");
	revertChanges();
}

function revertChanges()
{
	Engine.ConfigDB_Reload("user");
	for (let item in g_Controls)
	{
		let control = g_Controls[item];
		// needs to update renderer values (which are all of boolean type)
		if (control.parameters.renderer)
		{
			if (control.type !== "boolean" && control.type !== "invertedboolean")
			{
				warn("Invalid type option " + control.type + " defined in renderer for " + item + ": will not be reverted");
				continue;
			}
			let checked = Engine.ConfigDB_GetValue("user", item) === "true";
			Engine["Renderer_Set" + control.parameters.renderer + "Enabled"](checked);
		}
		// and the possible function calls (which are of number or string types)
		if (control.parameters.function)
		{
			if (control.type !== "string" && control.type !== "number")
			{
				warn("Invalid type option " + control.type + " defined with function for " + item + ": will not be reverted");
				continue;
			}
			let caption = Engine.ConfigDB_GetValue("user", item);
			Engine[control.parameters.function](+caption);
		}
	}
	Engine.ConfigDB_SetChanges("user", false);
	init();
}

function saveChanges()
{
	registerChanges();
	Engine.ConfigDB_WriteFile("user", "config/user.cfg");
	Engine.ConfigDB_SetChanges("user", false);
	updateOptionPanel();
}

/**
 * Close GUI page and call callbacks if they exist.
 **/
function closePage()
{
	registerChanges();
	if (Engine.ConfigDB_HasChanges("user"))
	{
		let btCaptions = [translate("No"), translate("Yes")];
		let btCode = [null, function(){ closePageWithoutConfirmation(); }];
		messageBox(500, 200, translate("You have unsaved changes, do you want to close this window?"),
			translate("Warning"), 0, btCaptions, btCode);
	}
	else
		closePageWithoutConfirmation();
}

function closePageWithoutConfirmation()
{
	if (g_HasCallback)
		Engine.PopGuiPageCB();
	else
		Engine.PopGuiPage();
}