/** draw event art, with the option to dress the slave in a particular way
 * @param {Node} node - DOM node to attach art to
 * @param {App.Entity.SlaveState|Array<App.Entity.SlaveState>} slaves - one or several slaves to draw art for
 * @param {string} [clothesMode] - if the slaves' clothing should be overridden, what should they be wearing?
 */
App.Events.drawEventArt = function(node, slaves, clothesMode) {
	// do nothing if the player doesn't want images
	if (!V.seeImages) {
		return;
	}

	// ensure that slaves is an array
	if (!Array.isArray(slaves)) {
		slaves = [slaves];
	}

	// if we were asked to change the slave's clothing, do it now
	let originalClothes = [];
	if (clothesMode) {
		// if there are "themes" of clothes that multiple events want to use ("swimwear", "athletic", "casual", etc), they can be added as special cases here instead of duplicating the logic in every event
		if (App.Data.misc.niceClothes.map(c => c.value).concat(App.Data.misc.harshClothes.map(c => c.value)).includes(clothesMode)) {
			// specific clothes have been selected
			originalClothes = slaves.map((s) => { return {ID: s.ID, clothes: s.clothes}; });
			slaves.forEach(s => s.clothes = clothesMode);
		} else {
			throw "Unrecognized clothes mode for event art";
		}
	}

	// actually draw the art - large if single slave, medium column if multiple slaves
	let artSpan = document.createElement("span");
	artSpan.id = "artFrame";
	if (slaves.length === 1) {
		let refDiv = document.createElement("div");
		refDiv.classList.add("imageRef", V.imageChoice === 1 ? "lrgVector" : "lrgRender");
		let maskDiv = document.createElement("div");
		maskDiv.classList.add("mask");
		maskDiv.appendChild(document.createTextNode("\u00a0"));
		refDiv.appendChild(maskDiv);
		refDiv.appendChild(App.Art.SlaveArtElement(slaves[0], 2, 0));
		artSpan.appendChild(refDiv);
	} else {
		let colDiv = document.createElement("div");
		colDiv.classList.add("imageColumn");
		for (const slave of slaves) {
			let refDiv = document.createElement("div");
			refDiv.classList.add("imageRef", "medImg");
			refDiv.appendChild(App.Art.SlaveArtElement(slave, 2, 0));
			colDiv.appendChild(refDiv);
		}
		artSpan.appendChild(colDiv);
	}
	node.appendChild(artSpan);

	// change clothing back, if necessary
	if (originalClothes.length > 0) {
		originalClothes.forEach((c) => slaves.find(s => s.ID === c.ID).clothes = c.clothes);
	}
};

/** intelligently adds spaces to an array of mixed strings and DOM nodes, merging consecutive strings in the process
 * @param {Array<string|HTMLElement|DocumentFragment>} sentences
 * @returns {Array<string|HTMLElement|DocumentFragment>}
 */
App.Events.spaceSentences = function(sentences) {
	if (sentences.length <= 1) {
		return sentences;
	}
	return sentences.reduce((res, cur) => {
		if (res.length === 0) {
			res.push(cur);
		} else if (typeof (res[res.length-1]) === "string") {
			if (typeof (cur) === "string") {
				res[res.length-1] += " " + cur;
			} else {
				res[res.length-1] += " ";
				res.push(cur);
			}
		} else {
			if (typeof (cur) === "string") {
				res.push(" " + cur);
			} else {
				res.push(" ");
				res.push(cur);
			}
		}
		return res;
	}, []);
};

/** assemble a DOM paragraph from an array of DOM nodes, sentences or sentence fragments (which may contain HTML)
 * @param {Node} node
 * @param {Array<string|HTMLElement|DocumentFragment>} sentences
 */
App.Events.addParagraph = function(node, sentences) {
	let para = document.createElement("p");
	$(para).append(...App.Events.spaceSentences(sentences));
	node.appendChild(para);
};

/** result handler callback - process the result and return an array of mixed strings and DOM nodes
 * @callback resultHandler
 * @returns {Array<string|HTMLElement|DocumentFragment>}
 */
/** a response to an event, and its result */
App.Events.Result = class {
	/** @param {string} [text] - the link text for the response
	 *  @param {resultHandler} [handler] - the function to call to generate the result when the link is clicked
	 *  @param {string} [note] - a note to provide alongside the link (for example, a cost or virginity loss warning)
	 *  To mark an option as disabled, construct the result with only the note.
	 */
	constructor(text, handler, note) {
		this.text = text;
		this.handler = handler;
		this.note = note;
	}

	/** call the result handler, replacing the contents of the given span ID
	 * @param {string} resultSpanID
	 */
	handle(resultSpanID) {
		let frag = document.createDocumentFragment();
		$(frag).append(...App.Events.spaceSentences(this.handler()));
		App.UI.DOM.replace(`#${resultSpanID}`, frag);
	}

	/** build the response DOM (for use by addResponses)
	 * @param {HTMLElement} node
	 * @returns {boolean} - true if something was written, false if not
	 */
	makeResponse(node) {
		let wrote = false;
		if (this.text && this.handler) {
			node.appendChild(App.UI.DOM.link(this.text, () => this.handle(node.id)));
			wrote = true;
		}
		if (wrote && this.note) {
			$(node).append(" ");
		}
		if (this.note) {
			node.appendChild(App.UI.DOM.makeElement("span", this.note, "detail"));
			wrote = true;
		}
		return wrote;
	}
};

/** add a list of results for an event
 * @param {Node} node
 * @param {Array<App.Events.Result>} results
 * @param {string} [elementID="result"]
 */
App.Events.addResponses = function(node, results, elementID="result") {
	let resultSpan = document.createElement("span");
	resultSpan.id = elementID;
	for (const result of results) {
		if (result.makeResponse(resultSpan)) {
			resultSpan.appendChild(document.createElement("br"));
		}
	}
	node.appendChild(resultSpan);
};

src/events/eventUtils.js