App.UI.OptionsGroup = (function() {
	/**
	 * @typedef value
	 * @property {*} value
	 * @property {string} [name]
	 * @property {string} mode
	 * @property {number} [compareValue]
	 * @property {string} [descAppend] can be SC markup
	 * @property {boolean} [on]
	 * @property {boolean} [off]
	 * @property {boolean} [neutral]
	 * @property {Function} [callback]
	 */

	const Option = class {
		/**
		 * @param {string} description can be SC markup
		 * @param {string} property
		 * @param {object} [object=V]
		 */
		constructor(description, property, object = V) {
			this.description = description;
			this.property = property;
			this.object = object;
			/**
			 * @type {Array<value>}
			 */
			this.valuePairs = [];
		}

		/**
		 * @param {*} name
		 * @param {*} [value=name]
		 * @param {Function} [callback]
		 * @returns {Option}
		 */
		addValue(name, value = name, callback = undefined) {
			this.valuePairs.push({
				name: name, value: value, mode: "=", callback: callback
			});
			return this;
		}

		/**
		 * @param {Array<*|Array>} values
		 * @returns {Option}
		 */
		addValueList(values) {
			for (const value of values) {
				if (Array.isArray(value)) {
					this.addValue(value[0], value[1]);
				} else {
					this.addValue(value);
				}
			}
			return this;
		}

		/**
		 * @param {*} value
		 * @param {number} compareValue
		 * @param {string} mode on of: "<", "<=", ">", ">="
		 * @param {string} [name=value]
		 */
		addRange(value, compareValue, mode, name = value) {
			this.valuePairs.push({
				name: name, value: value, mode: mode, compareValue: compareValue
			});
			return this;
		}

		/**
		 * @param {string} [unit]
		 * @returns {Option}
		 */
		showTextBox(unit) {
			this.textbox = true;
			if (unit) {
				this.unit = unit;
			}
			return this;
		}

		/**
		 * @param {string} comment can be SC markup
		 * @returns {Option}
		 */
		addComment(comment) {
			this.comment = comment;
			return this;
		}

		/**
		 * Adds a button that executes the callback when clicked AND reloads the passage
		 *
		 * @param {string} name
		 * @param {Function} callback
		 * @param {string} passage
		 */
		customButton(name, callback, passage) {
			this.valuePairs.push({
				name: name, value: passage, callback: callback, mode: "custom"
			});
			return this;
		}

		/**
		 * @param {string} element can be SC markup
		 * @returns {Option}
		 */
		addCustomElement(element) {
			this.valuePairs.push({
				value: element, mode: "plain"
			});
			return this;
		}

		/* modify last added option */

		/**
		 * Added to the description if last added value is selected.
		 * example use: addValue(...).customDescription(...).addValue(...).customDescription(...)
		 * @param {string} description can be SC markup
		 */
		customDescription(description) {
			this.valuePairs.last().descAppend = description;
			return this;
		}

		/**
		 * @param {Function} callback gets executed on every button click. Selected value is given as argument.
		 */
		addCallback(callback) {
			this.valuePairs.last().callback = callback;
			return this;
		}

		/**
		 * Mark option as on to style differently.
		 * @returns {Option}
		 */
		on() {
			this.valuePairs.last().on = true;
			return this;
		}

		/**
		 * Mark option as off to style differently.
		 * @returns {Option}
		 */
		off() {
			this.valuePairs.last().off = true;
			return this;
		}

		/**
		 * Mark option as neutral to style differently.
		 * @returns {Option}
		 */
		neutral() {
			this.valuePairs.last().neutral = true;
			return this;
		}
	};

	return class {
		constructor() {
			/**
			 * @type {Array<Option>}
			 */
			this.options = [];
		}

		/**
		 * @returns {App.UI.OptionsGroup}
		 */
		enableDoubleColumn() {
			this.doubleColumn = true;
			return this;
		}

		addOption(name, property, object = V) {
			const option = new Option(name, property, object);
			this.options.push(option);
			return option;
		}

		render() {
			const container = document.createElement("div");
			container.className = "options-group";
			if (this.doubleColumn) {
				container.classList.add("double");
			}
			for (/** @type {Option} */ const option of this.options) {
				/* left side */
				const desc = document.createElement("div");
				desc.className = "description";
				$(desc).wiki(option.description);
				container.append(desc);

				/* right side */
				const currentValue = option.object[option.property];
				let anySelected = false;

				const buttonGroup = document.createElement("div");
				buttonGroup.classList.add("button-group");
				for (const value of option.valuePairs) {
					if (value.mode === "plain") {
						/* insert custom SC markup and go to next element */
						$(buttonGroup).wiki(value.value);
						continue;
					}
					const button = document.createElement("button");
					button.append(value.name);
					if (value.on) {
						button.classList.add("on");
					} else if (value.off) {
						button.classList.add("off");
					} else if (value.neutral) {
						button.classList.add("neutral");
					}
					if (value.mode === "custom") {
						button.onclick = () => {
							value.callback();
							if (value.value) {
								Engine.play(value.value);
							} else {
								App.UI.reload();
							}
						};
					} else {
						if (value.mode === "=" && currentValue === value.value) {
							button.classList.add("selected", "disabled");
							anySelected = true;
							if (value.descAppend !== undefined) {
								desc.append(" ");
								$(desc).wiki(value.descAppend);
							}
						} else if (!anySelected && inRange(value.mode, value.compareValue, currentValue)) {
							button.classList.add("selected");
							// disable the button if clicking it won't change the variable value
							if (currentValue === value.value) {
								button.classList.add("disabled");
							}
							anySelected = true;
							if (value.descAppend !== undefined) {
								desc.append(" ");
								$(desc).wiki(value.descAppend);
							}
						}
						button.onclick = () => {
							option.object[option.property] = value.value;
							if (value.callback) {
								value.callback();
							}
							App.UI.reload();
						};
					}
					buttonGroup.append(button);
				}
				if (option.textbox) {
					const isNumber = typeof currentValue === "number";
					const textbox = App.UI.DOM.makeTextBox(currentValue, input => {
						option.object[option.property] = input;
						App.UI.reload();
					}, isNumber);
					if (isNumber) {
						textbox.classList.add("number");
					}
					buttonGroup.append(textbox);
					if (option.unit) {
						buttonGroup.append(" ", option.unit);
					}
				}
				if (option.comment) {
					const comment = document.createElement("span");
					comment.classList.add("comment");
					$(comment).wiki(option.comment);
					buttonGroup.append(comment);
				}
				container.append(buttonGroup);
			}

			return container;

			function inRange(mode, compareValue, value) {
				if (mode === "<") {
					return value < compareValue;
				} else if (mode === "<=") {
					return value <= compareValue;
				} else if (mode === ">") {
					return value > compareValue;
				} else if (mode === ">=") {
					return value >= compareValue;
				}
				return false;
			}
		}
	};
})();

src/gui/options.js