/**
 * This class provides the functionality to render a HTML page of a Settings
 * instance.
 *
 * Copyright 2008 Epic Games, Inc. All Rights Reserved
 *
 * @author  Michiel 'elmuerte' Hendriks
 */
class SettingsRenderer extends Object dependsOn(WebAdminSettings);

`include(WebAdmin.uci)

/**
 * Prefix of all include files.
 */
var protected string prefix;

/**
 * Prefix for variable names
 */
var protected string namePrefix;

/**
 * The base path to load the include files from
 */
var protected string path;

/**
 * Minimum number of options in a idMapped setting before switching to a listbox
 */
var int minOptionListSize;

struct SortedSetting
{
	var string txt;
	/** index of this item in one of the whole lists */
	var int idx;
	/** if true it's a localized setting rather than a property */
	var bool isLocalized;
};

/**
 * Settings group containg a sort list of settings.
 */
struct SettingsGroup
{
	var WebAdminSettings.SettingsGroupSpec spec;
	var array<SortedSetting> settings;
};

var array<SettingsGroup> groups;

var protected WebAdminSettings curSettings;
var protected WebResponse curResponse;

/**
 * Modifiers that will be polled to augment the settings
 */
var array<ISettingsModifier> modifiers;

/**
 * Currently active modifiers, only filled during rendering
 */
var protected array<ISettingsModifier> activeModifiers;

/**
 * Initialization when the instance is created.
 */
function init(string basePath, optional string namePre="settings_", optional string filePrefix="settings_")
{
	prefix = filePrefix;
	path = basePath;
	namePrefix = namePre;
	minOptionListSize=4;
}

function string getPath()
{
	return path;
}

function string getFilePrefix()
{
	return prefix;
}

function string getNamePrefix()
{
	return namePrefix;
}

function cleanup()
{
	curSettings = none;
	curResponse = none;
}

/**
 * Used to initialize the rendered for an IAdvWebAdminSettings instance
 */
function initEx(WebAdminSettings settings, WebResponse response)
{
	curSettings = settings;
	curResponse = response;
}

/**
 * Called after processing submitted values. Should be used to ensure some
 * settings have the correct values. This function is called stand-alone.
 */
function ensureSettingValues(WebAdminSettings settings)
{
	local int i;
	for (i = 0; i < modifiers.Length; ++i)
	{
		modifiers[i].ensureSettingValues(settings);
	}
}

/**
 * Sort all settings based on their name
 */
function sortSettings(int groupId)
{
	local int i, j;
	local SortedSetting sortset;

	groups[groupId].settings.length = 0; // clear old
	for (i = 0; i < curSettings.LocalizedSettingsMappings.length; i++)
	{
		if (curSettings.LocalizedSettingsMappings[i].Id < groups[groupId].spec.lMin) continue;
		if (curSettings.LocalizedSettingsMappings[i].Id >= groups[groupId].spec.lMax) continue;
		if (curSettings.LocalizedSettingsMappings[i].Name == '') continue;
		sortset.idx = i;
		sortset.isLocalized = true;
		sortset.txt = getLocalizedSettingText(curSettings.LocalizedSettingsMappings[i].Id);
		for (j = 0; j < groups[groupId].settings.length; j++)
		{
			if (Caps(groups[groupId].settings[j].txt) > Caps(sortset.txt))
			{
				groups[groupId].settings.Insert(j, 1);
				groups[groupId].settings[j] = sortset;
				break;
			}
		}
		if (j == groups[groupId].settings.length)
		{
			groups[groupId].settings[j] = sortset;
		}
	}
	for (i = 0; i < curSettings.PropertyMappings.length; i++)
	{
		if (curSettings.PropertyMappings[i].Id < groups[groupId].spec.pMin) continue;
		if (curSettings.PropertyMappings[i].Id >= groups[groupId].spec.pMax) continue;
		if (curSettings.PropertyMappings[i].Name == '') continue;
		sortset.idx = i;
		sortset.isLocalized = false;
		sortset.txt = getSettingText(curSettings.PropertyMappings[i].Id);
		for (j = 0; j < groups[groupId].settings.length; j++)
		{
			if (Caps(groups[groupId].settings[j].txt) > Caps(sortset.txt))
			{
				groups[groupId].settings.Insert(j, 1);
				groups[groupId].settings[j] = sortset;
				break;
			}
		}
		if (j == groups[groupId].settings.length)
		{
			groups[groupId].settings[j] = sortset;
		}
	}
}

/**
 * Creates the settings groups
 */
function createGroups()
{
	local SettingsGroup group;
	local array<SettingsGroupSpec> specs;
	local SettingsGroupSpec spec;

	groups.length = 0;
	specs = curSettings.settingGroups();
	// copy the spec to our format
	foreach specs(spec)
	{
		group.spec = spec;
		group.settings.Length = 0;
		groups.AddItem(group);
	}

	// add a dummy group
	if (groups.length == 0)
	{
		group.spec.groupId = "0";
		group.spec.pMin = 0;
		group.spec.pMax = curSettings.PropertyMappings.Length;
		group.spec.lMin = 0;
		group.spec.lMax = curSettings.LocalizedSettingsMappings.length;
		group.settings.Length = 0;
		groups.AddItem(group);
	}
}

/**
 * Render all properties of the given settings instance
 */
function render(WebAdminSettings settings, WebResponse response,
	optional string substName = "settings", optional ISettingsPrivileges privileges)
{
	local string result, entry;
	local int i;

	curSettings = settings;
	curResponse = response;

	activeModifiers.length = 0;
	for (i = 0; i < modifiers.Length; ++i)
	{
		if (modifiers[i].modifierAppliesTo(settings))
		{
			activeModifiers[activeModifiers.Length] = modifiers[i];
		}
	}

	createGroups();
	for (i = 0; i < groups.length; i++)
	{
		sortSettings(i);
	}

	if (groups.length == 1)
	{
		curResponse.Subst("settings", renderGroup(groups[0]));
		result = curResponse.LoadParsedUHTM(path $ "/" $ prefix $ "wrapper_single.inc");
	}
	else {
		result= "";
		for (i = 0; i < groups.length; i++)
		{
			if (groups[i].settings.length == 0) continue;
			if (string(int(groups[i].spec.groupId)) != groups[i].spec.groupId)
			{
				if (privileges != none)
				{
					if (!privileges.hasSettingsGroupAccess(settings.class, groups[i].spec.groupId))
					{
						continue;
					}
				}
			}
			curResponse.Subst("group.id", groups[i].spec.groupId);
			curResponse.Subst("group.title", `HTMLEscape(groups[i].spec.DisplayName));
			curResponse.Subst("group.settings", renderGroup(groups[i]));
			entry = curResponse.LoadParsedUHTM(path $ "/" $ prefix $ "group.inc");
			result $= entry;
		}
		curResponse.Subst("settings", result);
		result = curResponse.LoadParsedUHTM(path $ "/" $ prefix $ "wrapper_group.inc");
	}
	for (i = 0; i < activeModifiers.Length; ++i)
	{
		result $= activeModifiers[i].finalizeAugmentation(curResponse, path);
	}
	activeModifiers.length = 0;

	curResponse.subst(substName, result);
}

/**
 * Render a selection of settings
 */
function string renderGroup(SettingsGroup group)
{
	local string result, entry;
	local int i, j;
	local EPropertyValueMappingType mtype;
	local SettingRendererState renderState;

	renderState = new class'SettingRendererState';
	renderState.WebResponse = curResponse;
	renderState.path = path;

	for (i = 0; i < group.settings.length; i++)
	{
		renderState.reset();
		if (group.settings[i].isLocalized)
		{
			entry = renderLocalizedSetting(curSettings.LocalizedSettingsMappings[group.settings[i].idx].Id, renderState);
		}
		else {
			j = group.settings[i].idx;
			curSettings.GetPropertyMappingType(curSettings.PropertyMappings[j].Id, mtype);
			renderState.mappingType = mtype;
			defaultSubst(curSettings.PropertyMappings[j].Id, renderState);
			switch (mtype)
			{
				case PVMT_PredefinedValues:
					entry = renderPredefinedValues(curSettings.PropertyMappings[j].Id, j, renderState);
					break;
				case PVMT_Ranged:
					entry = renderRanged(curSettings.PropertyMappings[j].Id, renderState);
					break;
				case PVMT_IdMapped:
					entry = renderIdMapped(curSettings.PropertyMappings[j].Id, j, renderState);
					break;
				default:
					entry = renderRaw(curSettings.PropertyMappings[j].Id, j, renderState);
			}
		}
		if (len(entry) > 0 && renderState.bVisible)
		{
			curResponse.subst("setting.html", entry);
			result $= curResponse.LoadParsedUHTM(path $ "/" $ prefix $ "entry.inc");
		}
	}
	return result;
}

/**
 * Get a readable name for the current localized property
 */
function string getLocalizedSettingText(int settingId)
{
	local string val, res, elm;
	local int i;

	val = curSettings.GetStringSettingColumnHeader(settingId);
	if (len(val) > 0) return val;

	val = string(curSettings.GetStringSettingName(settingId));
	// FooBarQuux -> Foo Bar Quux
	res = "";
 	for (i = 0; i < len(val); i++) {
 		elm = Mid(val, i, 1);
 		if (Caps(elm) == elm)
		{
			elm = " "$elm;
 		}
 		else if (i == 0 && Locs(elm) == elm && elm == "b") {
 			// skip the 'b' in 'bDoSomething'
 			continue;
 		}
 		res = res$elm;
 	}
 	return res;
}

/**
 * Render a localized property
 */
function string renderLocalizedSetting(int settingId, SettingRendererState renderState)
{
	local name propname;
	local string options;
	local array<IdToStringMapping> values;
	local int selectedValue;
	local int i;

	propname = curSettings.GetStringSettingName(settingId);
	renderState.settingType = "localizedSetting";
	renderState.settingId = settingId;
	renderState.settingName = propname;
	renderState.formName = namePrefix$propname;
	renderState.label = getLocalizedSettingText(settingId);
	renderState.tooltip = Localize(string(curSettings.class.name)$" Tooltips", string(propname), string(curSettings.class.getPackageName()));

	curSettings.GetStringSettingValue(settingId, selectedValue);
	curSettings.GetStringSettingValueNames(settingId, values);
	options = "";
	if (values.length >= minOptionListSize)
	{
		for (i = 0; i < values.Length; i++)
		{
			renderState.subst("setting.option.value", values[i].id);
			renderState.subst("setting.option.text", `HTMLEscape(values[i].name));
			if (values[i].id == selectedValue)
			{
				renderState.subst("setting.option.selected", "selected=\"selected\"");
			}
			else {
				renderState.subst("setting.option.selected", "");
			}
			options $= renderSetting(renderState, "option.inc", i, true);
		}
		renderState.subst("setting.options", options);
		return renderSetting(renderState, "select.inc");
	}
	else {
		for (i = 0; i < values.Length; i++)
		{
			renderState.subst("setting.radio.index", i);
			renderState.subst("setting.radio.value", values[i].id);
			renderState.subst("setting.radio.text", `HTMLEscape(values[i].name));
			if (values[i].id == selectedValue)
			{
				renderState.subst("setting.radio.selected", "checked=\"checked\"");
			}
			else {
				renderState.subst("setting.radio.selected", "");
			}
			options $= renderSetting(renderState, "radio.inc", i);
		}
		return options;
	}
}

/**
 * Get a name for the current setting property.
 */
function string getSettingText(int settingId)
{
	local string val, res, elm;
	local int i;

	val = curSettings.GetPropertyColumnHeader(settingId);
	if (len(val) > 0) return val;
	val = string(curSettings.GetPropertyName(settingId));

	// FooBarQuux -> Foo Bar Quux
	res = "";
 	for (i = 0; i < len(val); i++) {
 		elm = Mid(val, i, 1);
 		if (Caps(elm) == elm && string(int(elm)) != elm)
		{
			elm = " "$elm;
 		}
 		else if (i == 0 && Locs(elm) == elm && elm == "b") {
 			// skip the 'b' in 'bDoSomething'
 			continue;
 		}
 		res = res$elm;
 	}
 	return res;
}

/**
 * Set the default substitution parts for the current property
 * @return true when the setting should be rendered
 */
function defaultSubst(int settingId, SettingRendererState renderState)
{
	local name propname;

	propname = curSettings.GetPropertyName(settingId);
	renderState.settingId = settingId;
	renderState.settingName = propname;
	renderState.formName = namePrefix$propname;
	renderState.label = getSettingText(settingId);
	renderState.tooltip = Localize(string(curSettings.class.name)$" Tooltips", string(propname), string(curSettings.class.getPackageName()));
	renderState.dataType = curSettings.GetPropertyType(settingId);
}

function string renderPredefinedValues(int settingId, int idx, SettingRendererState renderState)
{
	local string options, selectedValue, part1, part2, valDesc;
	local int i, j;
	local array<SettingsData> values;

	local bool usedPreDef, selected;
	local string svalue;
	local int ivalue;
	local float fvalue;

	renderState.settingType = "predefinedValues";

	selectedValue = curSettings.GetPropertyAsString(settingId);
	values = curSettings.PropertyMappings[idx].PredefinedValues;
	usedPreDef = false;

	for (i = 0; i < values.Length; i++)
	{
		valDesc = "";
		for (j = 0; j < curSettings.PropertyMappings[idx].ValueMappings.Length; j++) {
			if (curSettings.PropertyMappings[idx].ValueMappings[j].Id == i) {
				valDesc = string(curSettings.PropertyMappings[idx].ValueMappings[j].name);
			}
		}

		switch (values[i].Type)
		{
			case SDT_Int32:
			case SDT_Int64:
				ivalue = curSettings.GetSettingsDataInt(values[i]);
				renderState.subst("setting.option.value", string(ivalue));
				if (len(valDesc) == 0) {
					renderState.subst("setting.option.text", string(ivalue));
				}
				else {
					renderState.subst("setting.option.text", valDesc);
				}
				selected = (ivalue == int(selectedValue));
				break;
			case SDT_Double:
			case SDT_Float:
				fvalue = curSettings.GetFloatPredefinedValues(curSettings.PropertyMappings[idx].Id, i);
				renderState.subst("setting.option.value", string(fvalue));
				if (len(valDesc) == 0) {
					renderState.subst("setting.option.text", string(fvalue));
				}
				else {
					renderState.subst("setting.option.text", valDesc);
				}
				selected = (fvalue ~= float(selectedValue));
				break;
			case SDT_String:
				svalue = curSettings.GetStringPredefinedValues(curSettings.PropertyMappings[idx].Id, i);
				renderState.subst("setting.option.value", `HTMLEscape(svalue));
				if (len(valDesc) == 0) {
					renderState.subst("setting.option.text", `HTMLEscape(svalue));
				}
				else {
					renderState.subst("setting.option.text", valDesc);
				}
				selected = (svalue ~= selectedValue);
				break;
			default:
				`Log("Unsupported data type "$values[i].Type$" for setting id "$settingId,,'WebAdmin');
				return "";
		}
		if (selected)
		{
			usedPreDef = true;
			renderState.subst("setting.option.selected", "selected=\"selected\"");
		}
		else {
			renderState.subst("setting.option.selected", "");
		}
		options $= renderSetting(renderState, "option.inc", i, true);
	}
	curResponse.subst("setting.options", options);

	if (!usedPreDef) {
		renderState.formName = namePrefix$curSettings.GetPropertyName(settingId)$"_pre";
	}
	part1 = renderSetting(renderState, "select.inc");
	if (usedPreDef) {
		renderState.formName = namePrefix$curSettings.GetPropertyName(settingId)$"_raw";
	}
	else {
		renderState.formName = namePrefix$curSettings.GetPropertyName(settingId);
	}
	part2 = renderRaw(settingId, idx, renderState);

	renderState.subst("multisetting.predef", part1);
	renderState.subst("multisetting.raw", part2);
	renderState.formName = namePrefix$curSettings.GetPropertyName(settingId);

	renderState.settingType = "predefinedValuesContainer";
	renderState.subst("multisetting.predef.class", usedPreDef?"":"settingsraw");
	renderState.subst("multisetting.rawval.class", usedPreDef?"settingsraw":"");
	return renderSetting(renderState, "multisetting.inc");
}

function string renderRanged(int settingId, SettingRendererState renderState)
{
	local float value, min, max, incr;
	local byte asInt;

	renderState.settingType = "ranged";

	curSettings.GetRangedPropertyValue(settingId, value);
	curSettings.GetPropertyRange(settingId, min, max, incr, asInt);

	if (asInt != 1)
	{
		renderState.subst("setting.value", string(value));
		renderState.subst("setting.minval", string(min));
		renderState.subst("setting.maxval", string(max));
		renderState.subst("setting.increment", string(incr));
		renderState.subst("setting.asint", "false");
	}
	else {
		renderState.subst("setting.value", string(int(value)));
		renderState.subst("setting.minval", string(int(min)));
		renderState.subst("setting.maxval", string(int(max)));
		renderState.subst("setting.increment", string(int(incr)));
		renderState.subst("setting.asint", "true");
	}

	return renderSetting(renderState, "ranged.inc");
}

function string renderIdMapped(int settingId, int idx, SettingRendererState renderState)
{
	local string options;
	local array<IdToStringMapping> values;
	local int selectedValue;
	local int i;

	renderState.settingType = "idMapped";

	curSettings.GetIntProperty(settingId, selectedValue);
	values = curSettings.PropertyMappings[idx].ValueMappings;
	if (values.length >= minOptionListSize)
	{
		for (i = 0; i < values.Length; i++)
		{
			renderState.subst("setting.option.value", values[i].id);
			renderState.subst("setting.option.text", `HTMLEscape(values[i].name));
			if (values[i].id == selectedValue)
			{
				renderState.subst("setting.option.selected", "selected=\"selected\"");
			}
			else {
				renderState.subst("setting.option.selected", "");
			}
			options $= renderSetting(renderState, "option.inc", i, true);
		}
		renderState.subst("setting.options", options);
		return renderSetting(renderState, "select.inc");
	}
	else {
		for (i = 0; i < values.Length; i++)
		{
			renderState.subst("setting.radio.index", i);
			renderState.subst("setting.radio.value", values[i].id);
			renderState.subst("setting.radio.text", `HTMLEscape(values[i].name));
			if (values[i].id == selectedValue)
			{
				renderState.subst("setting.radio.selected", "checked=\"checked\"");
			}
			else {
				renderState.subst("setting.radio.selected", "");
			}
			options $= renderSetting(renderState, "radio.inc", i);
		}
		return options;
	}
}

function string renderRaw(int settingId, int idx, SettingRendererState renderState)
{
	local float min, max, incr;
	renderState.settingType = "raw";
	renderState.subst("setting.value", `HTMLEscape(curSettings.GetPropertyAsString(settingId)));

	min = curSettings.PropertyMappings[idx].MinVal;
	max = curSettings.PropertyMappings[idx].MaxVal;
	incr = curSettings.PropertyMappings[idx].RangeIncrement;
	switch(curSettings.GetPropertyType(settingId))
	{
		case SDT_Empty:
			return  "";
		case SDT_Int32:
		case SDT_Int64:
			if (max != 0)
			{
				renderState.subst("setting.maxval", int(max));
				renderState.subst("setting.minval", int(min));
			}
			else {
				renderState.subst("setting.maxval", "Number.NaN");
				renderState.subst("setting.minval", "Number.NaN");
			}
			if (incr > 0)
			{
				renderState.subst("setting.increment", string(int(incr)));
			}
			renderState.subst("setting.asint", "true");
			return renderSetting(renderState, "int.inc");
		case SDT_Double:
		case SDT_Float:
			if (max != 0)
			{
				renderState.subst("setting.maxval", max);
				renderState.subst("setting.minval", min);
			}
			else {
				renderState.subst("setting.maxval", "Number.NaN");
				renderState.subst("setting.minval", "Number.NaN");
			}
			if (incr > 0)
			{
				renderState.subst("setting.increment", string(incr));
			}
			renderState.subst("setting.asint", "false");
			return renderSetting(renderState, "float.inc");
		default:
			if (max != 0)
			{
				renderState.subst("setting.maxval", max);
				renderState.subst("setting.minval", min);
			}
			else {
				renderState.subst("setting.maxval", "Number.NaN");
				renderState.subst("setting.minval", "Number.NaN");
			}
			if (max > 0 && max > min)
			{
				renderState.subst("setting.maxlength", int(max));
			}
			else {
				renderState.subst("setting.maxlength", "");
			}
			if (max > 256)
			{
				return renderSetting(renderState, "textarea.inc");
			}
			else {
				return renderSetting(renderState, "string.inc");
			}
	}
}

function string renderSetting(SettingRendererState renderState, string filename,
	optional int index = -1, optional bool inContainer)
{
	local int i;
	for (i = 0; i < activeModifiers.length; ++i)
	{
		activeModifiers[i].augmentSetting(renderState, index, inContainer);
	}

	for (i = 0; i < renderState.substitutions.length; ++i)
	{
		curResponse.subst(renderState.substitutions[i].Key, renderState.substitutions[i].Value);
	}

	curResponse.subst("setting.type", renderState.settingType);
	curResponse.subst("setting.id", string(renderState.settingId));
	curResponse.subst("setting.name", renderState.settingName);
	curResponse.subst("setting.formname", renderState.formName);
	curResponse.subst("setting.text", `HTMLEscape(renderState.label));
	curResponse.subst("setting.tooltip", `HTMLEscape(renderState.tooltip));
	curResponse.subst("setting.enabled", renderState.bEnabled?"":"disabled=\"disabled\"");
	curResponse.subst("setting.augmented", renderState.extra);
	curResponse.subst("setting.css", renderState.cssClass);

	return curResponse.LoadParsedUHTM(path $ "/" $ prefix $ filename);
}

defaultproperties
{
	minOptionListSize=4
}