/**
 * @file Functions for rendering lists of slave summaries for various purposes. This includes
 * lists for the penthouse/facilities, selecting a slaves, facility leaders.
 *
 * For documentation see devNotes/slaveListing.md
 */

App.UI.SlaveList = {};

/**
 * @callback slaveTextGenerator
 * @param {App.Entity.SlaveState} slave
 * @returns {string}
 */

/**
 * @callback slaveToElement
 * @param {App.Entity.SlaveState} slave
 * @returns {HTMLElement|DocumentFragment}
 */

App.UI.SlaveList.render = function() {
	const facilityPassages = new Set(
		["Main", "Head Girl Suite", "Spa", "Brothel", "Club", "Arcade", "Clinic", "Schoolroom", "Dairy", "Farmyard", "Servants' Quarters", "Master Suite", "Cellblock"]);

	/** @type {string} */
	let passageName;

	// potentially can be a problem if played long enough to reach Number.MAX_SAFE_INTEGER
	let listID = Number.MIN_SAFE_INTEGER;

	/** @type {Object.<number, Node>}*/
	const readyResults = {

	};

	/** @type {App.Art.SlaveArtBatch?} */
	let batchRenderer = null;

	return {
		listMarkup: listMarkup,
		listDOM: listDOM
	};

	/**
	 * @param {number[]} IDs
	 * @param {Array.<{id: number, rejects: string[]}>} rejectedSlaves
	 * @param {slaveToElement} interactionLink
	 * @param {slaveToElement} [postNote]
	 * @returns {DocumentFragment}
	 */
	function listDOM(IDs, rejectedSlaves, interactionLink, postNote) {
		passageName = passage();

		let res = document.createDocumentFragment();

		if ((V.seeImages === 1) && (V.seeSummaryImages === 1)) {
			batchRenderer = new App.Art.SlaveArtBatch(IDs, 1);
			res.appendChild(batchRenderer.writePreamble());
		}

		if (V.useSlaveListInPageJSNavigation === 1) {
			res.appendChild(createQuickList(IDs));
		}

		const fcs = App.Entity.facilities;
		const penthouse = fcs.penthouse;

		let anyFacilityExists = false;

		for (const f of Object.values(fcs)) {
			if (f !== penthouse && f.established) {
				anyFacilityExists = true;
				break;
			}
		}

		let showTransfers = false;
		if (anyFacilityExists) {
			if (facilityPassages.has(passageName)) {
				V.returnTo = passageName;
				showTransfers = true;
			}
		}

		for (const _sid of IDs) {
			let ss = renderSlave(_sid, interactionLink, showTransfers, postNote);
			let slaveDiv = document.createElement("div");
			slaveDiv.id = `slave-${_sid}`;
			slaveDiv.classList.add("slaveSummary");
			if (V.slavePanelStyle === 2) {
				slaveDiv.classList.add("card");
			}
			slaveDiv.appendChild(ss);
			res.appendChild(slaveDiv);
		}

		for (const rs of rejectedSlaves) {
			const slave = slaveStateById(rs.id);
			const rejects = rs.rejects;
			const slaveName = SlaveFullName(slave);
			let slaveDiv = document.createElement("div");
			slaveDiv.id = `slave-${slave.ID}`;
			slaveDiv.style.setProperty("clear", "both");
			if (rejects.length === 1) {
				slaveDiv.innerHTML = rejects[0];
			} else {
				slaveDiv.appendChild(document.createTextNode(`${slaveName}: `));
				let ul = document.createElement("ul");
				for (const rr of rejects) {
					const li = document.createElement("li");
					li.innerHTML = rr;
					ul.appendChild(li);
				}
				slaveDiv.appendChild(ul);
			}
			res.appendChild(slaveDiv);
		}

		batchRenderer = null; // release reference

		return res;
	}

	/**
	 * @param {number[]} IDs
	 * @param {Array.<{id: number, rejects: string[]}>} rejectedSlaves
	 * @param {slaveToElement} interactionLink
	 * @param {slaveToElement} [postNote]
	 * @returns {string}
	 */
	function listMarkup(IDs, rejectedSlaves, interactionLink, postNote) {
		const listIDStr = `slave-list-${listID}`;
		readyResults[listID] = listDOM(IDs, rejectedSlaves, interactionLink, postNote);

		$(document).one(':passagedisplay', function() {
			for (const e of document.querySelectorAll('[id^=slave-list]')) {
				const id = e.getAttribute("data-listId");
				if (readyResults.hasOwnProperty(id)) {
					if (e.childNodes.length > 0) {
						e.innerHTML = '';
					}
					e.appendChild(readyResults[id]);
					delete readyResults[id];
				}
			}
		});

		let resMarkup = `<div id="${listIDStr}" data-listId="${listID}"></div>`;
		++listID;

		return resMarkup;
	}


	/**
	 * @param {number} id
	 * @param {slaveToElement} interactionLink
	 * @param {boolean} showTransfers
	 * @param {slaveToElement} [postNote]
	 * @returns {DocumentFragment}
	 */
	function renderSlave(id, interactionLink, showTransfers, postNote) {
		let res = document.createDocumentFragment();
		if (V.slavePanelStyle === 0) {
			res.appendChild(document.createElement("br"));
		} else if (V.slavePanelStyle === 1) {
			const hr = document.createElement("hr");
			hr.style.margin = "0";
			res.appendChild(hr);
		}
		const slave = slaveStateById(id);

		if (batchRenderer) {
			let imgDiv = document.createElement("div");
			imgDiv.classList.add("imageRef");
			imgDiv.classList.add("smlImg");
			imgDiv.appendChild(batchRenderer.render(slave));
			res.appendChild(imgDiv);
		}
		// res.push(dividerAndImage(slave));
		res.appendChild(interactionLink(slave));

		if ((slave.choosesOwnClothes === 1) && (slave.clothes === "choosing her own clothes")) {
			const _oldDevotion = slave.devotion;
			App.SlaveAssignment.choosesOwnClothes(slave);
			slave.devotion = _oldDevotion; /* restore devotion value so repeatedly changing clothes isn't an exploit */
		}

		SlaveStatClamp(slave);
		slave.trust = Math.trunc(slave.trust);
		slave.devotion = Math.trunc(slave.devotion);
		slave.health.condition = Math.trunc(slave.health.condition);
		slave.health.shortDamage = Math.trunc(slave.health.shortDamage);
		slave.health.longDamage = Math.trunc(slave.health.longDamage);
		slave.health.illness = Math.trunc(slave.health.illness);
		slave.health.tired = Math.trunc(slave.health.tired);
		slave.health.health = Math.trunc(slave.health.health);
		res.appendChild(document.createTextNode(' will '));
		const assignment = document.createElement("span");
		if ((slave.assignment === Job.REST) && (slave.health.condition >= -20)) {
			assignment.className = "freeAssignment";
			assignment.innerText = slave.assignment;
		} else if ((slave.assignment === Job.CONFINEMENT) && ((slave.devotion > 20) || ((slave.trust < -20) && (slave.devotion >= -20)) || ((slave.trust < -50) && (slave.devotion >= -50)))) {
			assignment.innerText = slave.assignment;
			if (slave.sentence > 0) {
				assignment.innerText += ` (${slave.sentence} weeks)`;
			}
		} else if (slave.choosesOwnAssignment === 1) {
			assignment.innerText = `choose ${getPronouns(slave).possessive} own job`;
		} else {
			assignment.innerText = slave.assignment;
			if (slave.sentence > 0) {
				assignment.innerText += ` ${slave.sentence} weeks`;
			}
		}
		if (slave.assignment === Job.CLINIC) {
			let list = [];
			let i;
			if (slave.health.condition <= 40) {
				list.push(`poor health`);
			}
			if (slave.health.shortDamage >= 10) {
				list.push(`injuries`);
			}
			if (V.Nurse !== 0) {
				if ((slave.chem > 15) && (V.clinicUpgradeFilters === 1)) {
					list.push(`unhealthy chemicals`);
				}
				if ((V.clinicInflateBelly > 0) && (slave.bellyImplant >= 0) && (slave.bellyImplant <= (V.arcologies[0].FSTransformationFetishistResearch ? 800000 : 130000))) {
					list.push(`implant filling`);
				}
				if ((slave.pregKnown === 1) && (V.clinicSpeedGestation > 0 || slave.pregControl === "speed up") && ((slave.pregAdaptation*1000 < slave.bellyPreg || slave.preg > slave.pregData.normalBirth/1.33))) {
					list.push(`observation of accelerated pregnancy`);
				} else if ((slave.pregKnown === 1) && (V.clinicSpeedGestation > 0 || slave.pregControl === "speed up")) {
					list.push(`safely hurrying pregnancy along`);
				} else if ((slave.pregAdaptation*1000 < slave.bellyPreg || slave.preg > slave.pregData.normalBirth/1.33)) {
					list.push(`observation during pregnancy`);
				}
			}
			if (list.length > 0) {
				assignment.innerText += " for ";
			} else {
				assignment.innerText += ", preparing to check out";
			}
			for (i = 0; i < list.length; i++) {
				assignment.innerText += list[i];
				if (i === (list.length - 2)) {
					assignment.innerText += " and ";
				} else if (i < (list.length - 2)) {
					assignment.innerText += ", ";
				}
			}
		} else if (slave.assignment === Job.SPA) {
			let list = [];
			let i;
			if (slave.fetish === "mindbroken") {
				assignment.innerText += `, mindbroken`;
			} else {
				if ((slave.sexualFlaw !== "none") || (slave.behavioralFlaw !== "none")) {
					list.push(`overcoming flaws`);
				}
				if ((slave.trust < 60) || (slave.devotion < 60)) {
					list.push(`learning to accept life as a slave`);
				}
				if (slave.health.condition < 20) {
					list.push(`improving in health`);
				}
				if (list.length > 0) {
					assignment.innerText += ", ";
				} else {
					assignment.innerText += ", preparing to move out";
				}
				for (i = 0; i < list.length; i++) {
					assignment.innerText += list[i];
					if (i === (list.length - 2)) {
						assignment.innerText += " and ";
					} else if (i < (list.length - 2)) {
						assignment.innerText += ", ";
					}
				}
			}
		} else if (slave.assignment === Job.SCHOOL) {
			let lessons = [];
			let i;
			if ((V.schoolroomRemodelBimbo === 1 && slave.intelligenceImplant > -15) || (V.schoolroomRemodelBimbo === 0 && slave.intelligenceImplant < 30)) {
				lessons.push("general");
			}
			if (!((slave.voice === 0) || (slave.accent <= 1) || ((V.schoolroomUpgradeLanguage === 0) && (slave.accent <= 2)))) {
				lessons.push("speech");
			}
			if (!((slave.skill.oral > 30) || ((V.schoolroomUpgradeSkills === 0) && (slave.skill.oral > 10)))) {
				lessons.push("oral");
			}
			if (!((slave.skill.whoring > 30) || ((V.schoolroomUpgradeSkills === 0) && (slave.skill.whoring > 10)))) {
				lessons.push("whoring");
			}
			if (!((slave.skill.entertainment > 30) || ((V.schoolroomUpgradeSkills === 0) && (slave.skill.entertainment > 10)))) {
				lessons.push("entertainment");
			}
			if (!((slave.skill.anal > 30) || ((V.schoolroomUpgradeSkills === 0) && (slave.skill.anal > 10)))) {
				lessons.push("anal");
			}
			if (!((slave.skill.vaginal > 30) || ((V.schoolroomUpgradeSkills === 0) && (slave.skill.vaginal > 10)) || (slave.vagina < 0))) {
				lessons.push("vaginal");
			}
			if (lessons.length > 0) {
				assignment.innerText += ", practicing: ";
			} else {
				assignment.innerText += ", studying for finals";
			}
			for (i = 0; i < lessons.length; i++) {
				assignment.innerText += lessons[i];
				if (i === (lessons.length - 2)) {
					assignment.innerText += " and ";
				} else if (i < (lessons.length - 2)) {
					assignment.innerText += ", ";
				}
			}
		} else if (slave.assignment === Job.SUBORDINATE) {
			if (slave.subTarget === -1) {
				assignment.innerText += ", serving as your Stud";
			} else if (slave.subTarget !== 0) {
				const domSlave = getSlave(slave.subTarget);
				if (domSlave) {
					assignment.innerText += ", serving " + SlaveFullName(domSlave) + " exclusively";
				} else {
					slave.subTarget = 0;
				}
			}
		} else if (slave.assignment === Job.AGENT) {
			const arc = V.arcologies.find((a) => a.leaderID === slave.ID);
			if (arc) {
				assignment.innerText += `, leading `;
				if (passageName === "Neighbor Interact") {
					assignment.appendChild(App.UI.DOM.makeElement("span", arc.name, "name"));
				} else {
					assignment.appendChild(App.UI.DOM.passageLink(arc.name, "Neighbor Interact"));
				}
			}
		} else if (slave.assignment === Job.AGENTPARTNER) {
			const arc = V.arcologies.find((a) => a.leaderID === slave.relationshipTarget);
			if (arc) {
				assignment.innerText += ` in `;
				if (passageName === "Neighbor Interact") {
					assignment.appendChild(App.UI.DOM.makeElement("span", arc.name, "name"));
				} else {
					assignment.appendChild(App.UI.DOM.passageLink(arc.name, "Neighbor Interact"));
				}
			}
		}
		assignment.appendChild(document.createTextNode('.'));
		res.appendChild(assignment);
		if (V.assignmentRecords[slave.ID]) {
			res.appendChild(document.createTextNode(` Last assigned to ${V.assignmentRecords[slave.ID]}.`));
		}
		res.appendChild(document.createTextNode(' '));

		if ((V.displayAssignments === 1) && (passageName === "Main") && (slave.ID !== V.HeadGirl.ID) && (slave.ID !== V.Recruiter.ID) && (slave.ID !== V.Bodyguard.ID)) {
			res.appendChild(App.UI.jobLinks.assignmentsFragment(slave.ID, "Main", (slave, assignment) => { App.UI.SlaveList.ScrollPosition.record(); assignJob(slave, assignment); }));
		}
		if (showTransfers) {
			res.appendChild(document.createElement("br"));
			res.appendChild(document.createTextNode('Transfer to: '));
			res.appendChild(App.UI.jobLinks.transfersFragment(slave.ID, (slave, assignment) => { App.UI.SlaveList.ScrollPosition.record(); assignJob(slave, assignment); }));
		}

		res.appendChild(App.UI.SlaveSummary.render(slave));

		if (postNote) {
			const pn = postNote(slave);
			if (pn) {
				let r = document.createElement("p");
				r.classList.add("si");
				r.appendChild(pn);
				res.appendChild(r);
			}
		}

		return res;
	}

	/**
	 * @param {number[]} IDs
	 * @returns {DocumentFragment}
	 */
	function createQuickList(IDs) {
		/**
		 *
		 * @param {Node} container
		 * @param {string} tagName
		 * @param {string} [content]
		 * @param {string|string[]} [classNames]
		 * @param {Object.<string, any>} [attributes]
		 * @returns {HTMLElement}
		 */
		function makeElement(container, tagName, content, classNames, attributes) {
			let res = document.createElement(tagName);
			container.appendChild(res);
			if (content) {
				res.textContent = content;
			}
			if (Array.isArray(classNames)) {
				for (const c of classNames) {
					res.classList.add(c);
				}
			} else if (classNames !== undefined) {
				res.classList.add(classNames);
			}

			if (attributes) {
				for (const [k, v] of Object.entries(attributes)) {
					res.setAttribute(k, v);
				}
			}
			return res;
		}

		const res = document.createDocumentFragment();

		/* Useful for finding weird combinations — usages of this passage that don't yet generate the quick index.
		*	<<print 'pass/count/indexed/flag::[' + passageName + '/' + _Count + '/' + _indexed + '/' + $SlaveSummaryFiler + ']'>>
		*/

		if (IDs.length > 1 && passageName === "Main") {
			const _buttons = [];
			let _offset = -50;
			if (/Select/i.test(passageName)) {
				_offset = -25;
			}
			res.appendChild(document.createElement("br"));
			/*
			 * we want <button data-quick-index="<<= listID>>">...
			 */
			const quickIndexBtn = document.createElement("button");
			res.appendChild(quickIndexBtn);
			quickIndexBtn.id = `quick-list-toggle${listID}`;
			quickIndexBtn.setAttribute('data-quick-index', listID.toString());
			quickIndexBtn.onclick = function(ev) {
				let which = ev.target.attributes["data-quick-index"].value;
				let quick = $("div#list_index" + which);
				quick.toggleClass("hidden");
			};
			/*
			 * we want <div id="list_index3" class=" hidden">...
			 */
			const listIndex = makeElement(res, "div", undefined, "hidden");
			listIndex.id = `list_index${listID}`;

			for (const sID of IDs) {
				const _IndexSlave = slaveStateById(sID);
				const _indexSlaveName = SlaveFullName(_IndexSlave);
				const _devotionClass = getSlaveDevotionClass(_IndexSlave);
				const _trustClass = getSlaveTrustClass(_IndexSlave);
				_buttons.push({
					"data-name": _indexSlaveName,
					"data-scroll-to": `#slave-${_IndexSlave.ID}`,
					"data-scroll-offset": _offset,
					"data-devotion": _IndexSlave.devotion,
					"data-trust": _IndexSlave.trust,
					"class": `${_devotionClass} ${_trustClass}`
				});
			}
			if (_buttons.length > 0) {
				V.sortQuickList = V.sortQuickList || 'Devotion';
				makeElement(listIndex, "em", "Sorting: ");
				const qlSort = makeElement(listIndex, "span", V.sortQuickList, "strong");
				qlSort.id = "qlSort";
				listIndex.appendChild(document.createTextNode(". "));
				const linkSortByDevotion = makeElement(listIndex, "a", "Sort by Devotion");
				linkSortByDevotion.onclick = (ev) => {
					ev.preventDefault();
					V.sortQuickList = "Devotion";
					$("#qlSort").text(V.sortQuickList);
					$("#qlWrapper").removeClass("trust").addClass("devotion");
					sortButtonsByDevotion();
				};
				const linkSortByTrust = makeElement(listIndex, "a", "Sort by Trust");
				linkSortByTrust.onclick = (ev) => {
					ev.preventDefault();
					V.sortQuickList = "Trust";
					$("#qlSort").text(V.sortQuickList);
					$("#qlWrapper").removeClass("devotion").addClass("trust");
					sortButtonsByTrust();
				};
				makeElement(listIndex, "br");
				const qlWrapper = makeElement(listIndex, "div", undefined, ["quicklist", "devotion"]);
				qlWrapper.id = "qlWrapper";
				for (const _button of _buttons) {
					const btn = makeElement(listIndex, 'button', _button['data-name'], undefined, _button);
					btn.onclick = App.UI.quickBtnScrollToHandler;
				}
			}
		}
		return res;
	}
}();

App.UI.SlaveList.Decoration = {};
/**
 * returns "HG", "BG", "PA", and "RC" prefixes
 * @param {App.Entity.SlaveState} slave
 * @returns {HTMLElement}
 */
App.UI.SlaveList.Decoration.penthousePositions = function(slave) {
	const fcs = App.Entity.facilities;
	if (fcs.headGirlSuite.manager.isEmployed(slave)) {
		return App.UI.DOM.makeElement("span", 'HG', ['lightcoral', 'strong']);
	}
	if (fcs.penthouse.manager.isEmployed(slave)) {
		return App.UI.DOM.makeElement("span", 'RC', ['lightcoral', 'strong']);
	}
	if (fcs.armory.manager.isEmployed(slave)) {
		return App.UI.DOM.makeElement("span", 'BG', ['lightcoral', 'strong']);
	}
	if (Array.isArray(V.personalAttention) && V.personalAttention.some(s => s.ID === slave.ID)) {
		return App.UI.DOM.makeElement("span", 'PA', ['lightcoral', 'strong']);
	}
	return null;
};

App.UI.SlaveList.ScrollPosition = (function() {
	let lastPassage = null;
	let position = 0;

	return {
		reset : function() {
			lastPassage = null;
			position = 0;
		},

		record : function() {
			lastPassage = passage();
			position = window.pageYOffset;
		},

		restore : function() {
			$(document).one(':passageend', () => {
				if (lastPassage === passage()) {
					window.scrollTo(0, position);
				}
				this.reset();
			});
		}
	};
})();

App.UI.SlaveList.SlaveInteract = {};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {HTMLElement}
 */
App.UI.SlaveList.SlaveInteract.stdInteract = function(slave) {
	return App.UI.DOM.passageLink(SlaveFullName(slave), 'Slave Interact', () => {
		App.UI.SlaveList.ScrollPosition.record();
		V.activeSlave = slave;
	});
};


/**
 * @param {App.Entity.SlaveState} slave
 * @returns {DocumentFragment|HTMLElement}
 */
App.UI.SlaveList.SlaveInteract.penthouseInteract = function(slave) {
	let decoration = App.UI.SlaveList.Decoration.penthousePositions(slave);
	let stdLink = App.UI.SlaveList.SlaveInteract.stdInteract(slave);
	if (decoration) {
		let fr = document.createDocumentFragment();
		fr.appendChild(decoration);
		fr.appendChild(document.createTextNode(' '));
		fr.appendChild(stdLink);
		return fr;
	}
	return stdLink;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {HTMLElement}
 */
App.UI.SlaveList.SlaveInteract.personalAttention = (slave) =>
	App.UI.DOM.passageLink(SlaveFullName(slave), undefined,
		() => { App.UI.selectSlaveForPersonalAttention(slave.ID); });

/**
 * Adds/removes a slave with the given id to/from the personal attention array
 * @param {number} id slave id
 */
App.UI.selectSlaveForPersonalAttention = function(id) {
	if (!Array.isArray(V.personalAttention)) {
		/* first PA target */
		V.personalAttention = [{
			ID: id,
			trainingRegimen: "undecided"
		}];
	} else {
		const _pai = V.personalAttention.findIndex(s => s.ID === id);
		if (_pai === -1) {
			/* not already a PA target; add */
			V.activeSlave = getSlave(id);
			V.personalAttention.push({
				ID: id,
				trainingRegimen: "undecided"
			});
		} else {
			/* already a PA target; remove */
			V.personalAttention.deleteAt(_pai);
			if (V.personalAttention.length === 0) {
				V.personalAttention = "sex";
			}
		}
	}
	SugarCube.Engine.play("Personal Attention Select");
};

/**
 * Generates fragment with sorting options, that link to the given passage
 * @param {string} passage The passage to link to
 * @returns {string}
 */
App.UI.SlaveList.sortingLinks = function(passage) {
	let r = '&nbsp;&nbsp;&nbsp;&nbsp;Sort by: ';
	let textify = (cc) => capFirstChar(cc.replace(/([A-Z])/g, " $1"));
	r += ["devotion", "name", "assignment", "seniority", "actualAge", "visualAge", "physicalAge", "weeklyIncome"]
		.map(so => V.sortSlavesBy !== so ?
			App.UI.passageLink(textify(so), passage, `$sortSlavesBy = "${so}"`) : textify(so))
		.join("&thinsp;|&thinsp;");

	r += '&nbsp;&nbsp;&nbsp;&nbsp;Sort: ';
	r += ["descending", "ascending"].map(so => V.sortSlavesOrder !== so ?
		App.UI.passageLink(capFirstChar(so), passage, `$sortSlavesOrder = "${so}"`) : capFirstChar(so))
		.join("&thinsp;|&thinsp;");
	return r;
};

/**
 * @param {string} passage
 * @returns {*}
 */
App.UI.SlaveList.sortingLinksDOM = function(passage) {
	const outerDiv = document.createElement("div");
	outerDiv.classList.add("flex-container");
	const textify = string => capFirstChar(string.replace(/([A-Z])/g, " $1"));

	let innerDiv = App.UI.DOM.makeElement("div", "Sort by: ", "indent");
	let order = ["devotion", "name", "assignment", "seniority", "actualAge", "visualAge", "physicalAge", "weeklyIncome"]
		.map(so => V.sortSlavesBy !== so ?
			App.UI.DOM.passageLink(textify(so), passage, () => { V.sortSlavesBy = so; }) : textify(so));
	innerDiv.append(App.UI.DOM.arrayToList(order, " | ", " | "));
	outerDiv.append(innerDiv);

	innerDiv = App.UI.DOM.makeElement("div", "Sort: ", "indent");
	order = ["descending", "ascending"].map(so => V.sortSlavesOrder !== so ?
		App.UI.DOM.passageLink(textify(so), passage, () => { V.sortSlavesOrder = so; }) : textify(so));
	innerDiv.append(App.UI.DOM.arrayToList(order, " | ", " | "));
	outerDiv.append(innerDiv);

	return outerDiv;
};

/**
 * Standard tabs for a facility with a single job (SJ)
 * @param {App.Entity.Facilities.Facility} facility
 * @param {string} [facilityPassage]
 * @param {boolean} [showTransfersTab=false]
 * @param {{assign: string, remove: string, transfer: (string| undefined)}} [tabCaptions]
 * @returns {string}
 */
App.UI.SlaveList.listSJFacilitySlaves = function(facility, facilityPassage, showTransfersTab = false, tabCaptions = undefined) {
	const job = facility.job();

	facilityPassage = facilityPassage || passage();
	tabCaptions = tabCaptions || {
		assign: 'Assign a slave',
		remove: 'Remove a slave',
		transfer: 'Transfer from Facility'
	};
	let r = '';
	if (V.sortSlavesMain) {
		r += this.sortingLinks(facilityPassage) + '<br>';
	}
	r += '<div class="tabbar">' +
		App.UI.tabbar.tabButton('assign', tabCaptions.assign) +
		App.UI.tabbar.tabButton('remove', tabCaptions.remove) +
		(showTransfersTab ? App.UI.tabbar.tabButton('transfer', tabCaptions.transfer) : '') +
		'</div>';

	const facilitySlaves = [...job.employeesIDs()];
	if (facilitySlaves.length > 0) {
		SlaveSort.IDs(facilitySlaves);
		r += App.UI.tabbar.makeTab("remove", App.UI.SlaveList.render.listMarkup(facilitySlaves, [],
			App.UI.SlaveList.SlaveInteract.stdInteract,
			(slave) => App.UI.DOM.link(`Retrieve ${getPronouns(slave).object} from ${facility.name}`, () => removeJob(slave, job.desc.assignment), [], facilityPassage)
		));
	} else {
		r += App.UI.tabbar.makeTab("remove", `<em>${capFirstChar(facility.name)} is empty for the moment</em>`);
	}

	/**
	 * @param {number[]} slaveIDs
	 * @returns {string}
	 */
	function assignableTabContent(slaveIDs) {
		SlaveSort.IDs(slaveIDs);
		let rejectedSlaves = [];
		let passedSlaves = [];
		slaveIDs.forEach((id) => {
			const rejects = facility.canHostSlave(slaveStateById(id));
			if (rejects.length > 0) {
				rejectedSlaves.push({id: id, rejects: rejects});
			} else {
				passedSlaves.push(id);
			}
		}, []);
		return App.UI.SlaveList.render.listMarkup(passedSlaves, rejectedSlaves,
			App.UI.SlaveList.SlaveInteract.stdInteract,
			(slave) => App.UI.DOM.link(`Send ${getPronouns(slave).object} to ${facility.name}`, () => { assignmentTransition(slave, job.desc.assignment, facilityPassage); }));
	}
	if (facility.hasFreeSpace) {
		const assignableSlaveIDs = job.desc.partTime ?
			V.slaves.map(slave => slave.ID) : // all slaves can work here
			[...App.Entity.facilities.penthouse.employeesIDs()]; // only slaves from the penthouse can be transferred here
		r += App.UI.tabbar.makeTab("assign", assignableTabContent(assignableSlaveIDs));
	} else {
		r += App.UI.tabbar.makeTab("assign", `<strong>${capFirstChar(facility.name)} is full and cannot hold any more slaves</strong>`);
	}

	if (showTransfersTab) {
		if (facility.hasFreeSpace) {
			// slaves from other facilities can be transferred here
			const transferableIDs = V.slaves.reduce((acc, slave) => {
				if (!assignmentVisible(slave) && !facility.isHosted(slave)) {
					acc.push(slave.ID);
				}
				return acc;
			}, []);
			r += App.UI.tabbar.makeTab("transfer", assignableTabContent(transferableIDs));
		} else {
			r += App.UI.tabbar.makeTab("transfer", `<strong>${capFirstChar(facility.name)} is full and cannot hold any more slaves</strong>`);
		}
	}
	App.UI.tabbar.handlePreSelectedTab();

	return r;
};

/**
 * @param {string[]} classNames
 */
App.UI.SlaveList.makeNameDecorator = function(classNames) {
	return (slave) => {
		const r = document.createElement("span");
		for (const c of classNames) {
			r.classList.add(c);
		}
		r.textContent = SlaveFullName(slave);
		return r;
	};
};

/**
 * @returns {string}
 */
App.UI.SlaveList.listNGPSlaves = function() {
	const thisPassage = 'New Game Plus';
	let r = this.sortingLinks(thisPassage) + '<br>';

	r += '<div class="tabbar">' +
		App.UI.tabbar.tabButton('assign', 'Import a slave') +
		App.UI.tabbar.tabButton('remove', 'Remove from import') +
		'</div>';

	const NGPassignment = Job.IMPORTED;
	/** @type {App.Entity.SlaveState[]} */
	const slaves = V.slaves;

	/* handle the legacy assignment string */
	for (const slave of slaves) {
		if (slave.assignment === "be imported") {
			slave.assignment = Job.IMPORTED;
		}
	}

	if (V.slavesToImport > 0) {
		const importedSlavesIDs = slaves.reduce((acc, s) => {
			if (s.assignment === NGPassignment) {
				acc.push(s.ID);
			}
			return acc;
		}, []);
		SlaveSort.IDs(importedSlavesIDs);
		r += App.UI.tabbar.makeTab("remove", App.UI.SlaveList.render.listMarkup(importedSlavesIDs, [],
			App.UI.SlaveList.makeNameDecorator(["emphasizedSlave", "pink"]),
			(s) => App.UI.DOM.passageLink('Remove from import list', thisPassage,
				() => { V.slavesToImport -= 1; removeJob(s, NGPassignment); }
			)));
	} else {
		r += App.UI.tabbar.makeTab("remove", `<em>No slaves will go with you to the new game</em>`);
	}

	if (V.slavesToImport < V.slavesToImportMax) {
		const slavesToImportIDs = slaves.reduce((acc, s) => { if (s.assignment !== NGPassignment) { acc.push(s.ID); } return acc; }, []);
		SlaveSort.IDs(slavesToImportIDs);
		r += App.UI.tabbar.makeTab("assign", App.UI.SlaveList.render.listMarkup(slavesToImportIDs, [],
			App.UI.SlaveList.makeNameDecorator(["emphasizedSlave", "pink"]),
			(s) => App.UI.DOM.passageLink('Add to import list', thisPassage,
				() => { V.slavesToImport += 1; assignJob(s, NGPassignment); }
			)));
	} else {
		r += App.UI.tabbar.makeTab("assign", `<strong>Slave import limit reached</strong>`);
	}

	App.UI.tabbar.handlePreSelectedTab();
	return r;
};

/**
 * Renders facility manager summary or a note with a link to select one
 * @param {App.Entity.Facilities.Facility} facility
 * @param {string} [selectionPassage] passage name for manager selection. "${Manager} Select" if omitted
 * @returns {string}
 */
App.UI.SlaveList.displayManager = function(facility, selectionPassage) {
	const managerCapName = capFirstChar(facility.desc.manager.position);
	selectionPassage = selectionPassage || `${managerCapName} Select`;
	const manager = facility.manager.currentEmployee;
	if (manager) {
		return this.render.listMarkup([manager.ID], [],
			App.UI.SlaveList.SlaveInteract.stdInteract,
			() => App.UI.DOM.passageLink(`Change or remove ${managerCapName}`, selectionPassage));
	} else {
		return `You do not have a slave serving as a ${managerCapName}. ${App.UI.passageLink(`Appoint one`, selectionPassage, "")}`;
	}
};

/**
 * Displays standard facility page with manager and list of workers
 * @param {App.Entity.Facilities.Facility} facility
 * @param {boolean} [showTransfersPage]
 * @returns {string}
 */
App.UI.SlaveList.stdFacilityPage = function(facility, showTransfersPage) {
	return this.displayManager(facility) + '<br><br>' + this.listSJFacilitySlaves(facility, passage(), showTransfersPage);
};

App.UI.SlaveList.penthousePage = function() {
	const ph = App.Entity.facilities.penthouse;

	function overviewTabContent() {
		const fragment = document.createDocumentFragment();
		const A = V.arcologies[0];

		let slaveWrapper = document.createElement("div"); // first is a div so we have no space between slave and buttons
		if (V.HeadGirl) {
			/** @type {App.Entity.SlaveState} */
			const HG = V.HeadGirl;
			slaveWrapper.append(App.UI.DOM.makeElement("span", SlaveFullName(HG), "slave-name"),
				" is serving as your Head Girl");
			if (A.FSEgyptianRevivalistLaw === 1) {
				slaveWrapper.append(" and Consort");
			}
			slaveWrapper.append(". ");
			const link = App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Manage Head Girl", "HG Select"), "major-link");
			link.id = "manageHG";
			slaveWrapper.append(link, " ", App.UI.DOM.makeElement("span", "[H]", "hotkey"));
			slaveWrapper.append(App.UI.SlaveList.render.listDOM([HG.ID], [],
				App.UI.SlaveList.SlaveInteract.penthouseInteract));
		} else {
			if (V.slaves.length > 1) {
				slaveWrapper.append("You have ", App.UI.DOM.makeElement("span", "not", "warning"), " selected a Head Girl");
				if (A.FSEgyptianRevivalistLaw === 1) {
					slaveWrapper.append(" and Consort");
				}
				slaveWrapper.append(". ", App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Select One", "HG Select"), "major-link"),
					" ", App.UI.DOM.makeElement("span", "[H]", "hotkey"));
				slaveWrapper.id = "manageHG";
				if (V.slavePanelStyle === 2) {
					slaveWrapper.classList.add("slaveSummary", "card");
				}
			} else {
				slaveWrapper.append("You do not have enough slaves to keep a Head Girl");
				slaveWrapper.classList.add("note");
			}
		}
		fragment.append(slaveWrapper);

		slaveWrapper = document.createElement("p");
		if (V.Recruiter) {
			/** @type {App.Entity.SlaveState} */
			const RC = V.Recruiter;
			const {he} = getPronouns(RC);
			slaveWrapper.append(App.UI.DOM.makeElement("span", SlaveFullName(RC), "slave-name"),
				" is working");
			if (V.recruiterTarget !== "other arcologies") {
				slaveWrapper.append(" to recruit girls");
			} else {
				slaveWrapper.append(" as a Sexual Ambassador");
				if (A.influenceTarget === -1) {
					slaveWrapper.append(", but ", App.UI.DOM.makeElement("span", `${he} has no target to influence.`, "warning"));
				} else {
					const targetName = V.arcologies.find(a => a.direction === A.influenceTarget).name;
					slaveWrapper.append(` to ${targetName}.`);
				}
			}
			slaveWrapper.append(". ");
			const link = App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Manage Recruiter", "Recruiter Select"), "major-link");
			link.id = "manageRecruiter";
			slaveWrapper.append(link, " ", App.UI.DOM.makeElement("span", "[U]", "hotkey"));
			slaveWrapper.append(App.UI.SlaveList.render.listDOM([RC.ID], [],
				App.UI.SlaveList.SlaveInteract.penthouseInteract));
		} else {
			slaveWrapper.append("You have ", App.UI.DOM.makeElement("span", "not", "warning"), " selected a Recruiter. ",
				App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Select one", "Recruiter Select"), "major-link"),
				" ", App.UI.DOM.makeElement("span", "[U]", "hotkey"));
			slaveWrapper.id = "manageRecruiter";
			if (V.slavePanelStyle === 2) {
				slaveWrapper.classList.add("slaveSummary", "card");
			}
		}
		fragment.append(slaveWrapper);

		if (V.dojo) {
			slaveWrapper = document.createElement("p");
			/** @type {App.Entity.SlaveState} */
			const BG = V.Bodyguard;
			if (BG) {
				slaveWrapper.append(App.UI.DOM.makeElement("span", SlaveFullName(BG), "slave-name"),
					" is serving as your bodyguard. ");
				const link = App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Manage Bodyguard", "BG Select"), "major-link");
				link.id = "manageBG";
				slaveWrapper.append(link, " ", App.UI.DOM.makeElement("span", "[B]", "hotkey"));
				slaveWrapper.append(App.UI.SlaveList.render.listDOM([BG.ID], [],
					App.UI.SlaveList.SlaveInteract.penthouseInteract));
				slaveWrapper.append(App.MainView.useGuard());
			} else {
				slaveWrapper.append("You have ", App.UI.DOM.makeElement("span", "not", "warning"), " selected a Bodyguard. ",
					App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Select one", "BG Select"), "major-link"),
					" ", App.UI.DOM.makeElement("span", "[B]", "hotkey"));
				slaveWrapper.id = "manageBG";
				if (V.slavePanelStyle === 2) {
					slaveWrapper.classList.add("slaveSummary", "card");
				}
			}

			fragment.append(slaveWrapper);
		}
		return fragment;
	}

	/**
	 * @param {string} job
	 * @returns {{n: number, dom: DocumentFragment}}
	 */
	function _slavesForJob(job) {
		const employeesIDs = job === 'all' ? [...ph.employeesIDs()] : [...ph.job(job).employeesIDs()];
		if (employeesIDs.length === 0) {
			return {n: 0, dom: document.createDocumentFragment()};
		}

		SlaveSort.IDs(employeesIDs);
		return {
			n: employeesIDs.length,
			dom: App.UI.SlaveList.render.listDOM(employeesIDs, [], App.UI.SlaveList.SlaveInteract.penthouseInteract,
				V.fucktoyInteractionsPosition === 1 && job === "fucktoy" ? App.MainView.useFucktoy : null)
		};
	}


	/**
	 * @typedef tabDesc
	 * @property {string} tabName
	 * @property {string} caption
	 * @property {Node} content
	 */

	/**
	 * @param {string} tabName
	 * @param {string} caption
	 * @param {Node} content
	 * @returns {tabDesc}
	 */
	function makeTabDesc(tabName, caption, content) {
		return {
			tabName: tabName,
			caption: caption,
			content: content
		};
	}

	/**
	 * Displays encyclopedia entries for occupations at the top of the tab, if enabled
	 * @returns {HTMLSpanElement}
	 */
	function encycTips(jn) {
		const span = document.createElement("span");
		span.classList.add("note");
		if (V.showTipsFromEncy) {
			switch (jn) {
				case "rest":
					span.append(App.Encyclopedia.Entries.rest());
					break;
				case "chooseOwn":
					break; /* no entry written for choose own */
				case "fucktoy":
					span.append(App.Encyclopedia.Entries.fucktoy());
					break;
				case "classes":
					span.append(App.Encyclopedia.Entries.attendingClasses());
					break;
				case "houseServant":
					span.append(App.Encyclopedia.Entries.servitude());
					break;
				case Job.WHORE:
					span.append(App.Encyclopedia.Entries.whoring());
					break;
				case "publicServant":
					span.append(App.Encyclopedia.Entries.publicService());
					break;
				case "subordinateSlave":
					span.append(App.Encyclopedia.Entries.sexualServitude());
					break;
				case "cow":
					span.append(App.Encyclopedia.Entries.milking());
					break;
				case "gloryhole":
					span.append(App.Encyclopedia.Entries.gloryHole());
					break;
				case "confinement":
					span.append(App.Encyclopedia.Entries.confinement());
					break;
				default:
					span.append(App.UI.DOM.makeElement("span", "missing tip for this tab", "error"));
					break;
			}
		}
		return span;
	}

	function allTab() {
		const penthouseSlavesIDs = [...ph.employeesIDs()];
		if (S.HeadGirl) {
			penthouseSlavesIDs.push(S.HeadGirl.ID);
		}
		if (S.Recruiter) {
			penthouseSlavesIDs.push(S.Recruiter.ID);
		}
		if (S.Bodyguard) {
			penthouseSlavesIDs.push(S.Bodyguard.ID);
		}
		SlaveSort.IDs(penthouseSlavesIDs);
		return makeTabDesc('all', `All${V.useSlaveSummaryTabs > 0 ? ` (${penthouseSlavesIDs.length})` : ""}`,
			App.UI.SlaveList.render.listDOM(penthouseSlavesIDs, [], App.UI.SlaveList.SlaveInteract.penthouseInteract));
	}

	let fragment = document.createDocumentFragment();

	if (V.positionMainLinks >= 0) {
		fragment.append(App.UI.DOM.makeElement("div", App.UI.View.mainLinks(), "center"));
	}

	if (V.sortSlavesMain) {
		fragment.append(App.UI.SlaveList.sortingLinksDOM("Main"));
	}

	/** @type {tabDesc[]} */
	let tabs = [];

	// Overview tab
	if (V.useSlaveSummaryOverviewTab) {
		tabs.push(makeTabDesc('overview', "Special Roles", overviewTabContent()));
	}

	if (V.useSlaveSummaryTabs === 0) {
		tabs.push(allTab());
	}

	// tabs for each assignment
	for (const jn of ph.jobsNames) {
		const slaves = _slavesForJob(jn);
		if (slaves.n > 0) {
			tabs.push(makeTabDesc(jn, `${ph.desc.jobs[jn].position}${V.useSlaveSummaryTabs > 0 ? ` (${slaves.n})` : ""}`, App.UI.DOM.combineNodes(encycTips(jn), slaves.dom)));
		} else if (V.useSlaveSummaryTabs === 0) {
			tabs.push(makeTabDesc(jn, ph.desc.jobs[jn].position, encycTips(jn)));
		}
	}

	if (V.useSlaveSummaryTabs > 0) {
		tabs.push(allTab());
	}

	const div = document.createElement("div");
	div.classList.add("tabbar");
	if (V.useSlaveSummaryTabs === 0) {
		const links = [];
		for (const tab of tabs) {
			links.push(App.UI.tabbar.tabButtonDOM(tab.tabName, tab.caption, true));
		}
		div.append(App.UI.DOM.arrayToList(links, " | ", " | "));
	} else {
		for (const tab of tabs) {
			const button = App.UI.tabbar.tabButtonDOM(tab.tabName, tab.caption);
			if (V.useSlaveSummaryTabs === 2) {
				button.classList.add("card");
			}
			div.append(button);
		}
	}
	fragment.append(div);

	for (const tab of tabs) {
		const div = App.UI.tabbar.makeTabDOM(tab.tabName, tab.content);
		if (V.useSlaveSummaryTabs === 0) {
			div.classList.add("noFade");
		} else if (V.useSlaveSummaryTabs === 2) {
			div.classList.add("card");
		}
		fragment.append(div);
	}

	if (V.positionMainLinks <= 0) {
		fragment.append(App.UI.DOM.makeElement("div", App.UI.View.mainLinks(), "center"));
	}

	App.UI.tabbar.handlePreSelectedTab();
	return fragment;
};

/**
 * @callback assignmentFilterGenerateCallback
 * @param {string} value
 * @returns {string}
 */

/**
 * @callback slaveFilterCallbackReasoned
 * @param {App.Entity.SlaveState} slave
 * @returns {string[]}
 */

/**
 * @callback slaveFilterCallbackSimple
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */

App.UI.SlaveList.slaveSelectionList = function() {
	const selectionElementId = "slaveSelectionList";

	return selection;

	/**
	 * @typedef ListOptions
	 * @property {slaveFilterCallbackReasoned|slaveFilterCallbackSimple} filter
	 * @property {slaveTestCallback} [expCheck]
	 * @property {slaveToElement} interactionLink
	 * @property {slaveToElement} [postNote]
	 */

	/**
	 * @param {slaveFilterCallbackReasoned|slaveFilterCallbackSimple} filter
	 * @param {slaveToElement} interactionLink
	 * @param {slaveTestCallback} [experianceChecker]
	 * @param {slaveToElement} [postNote]
	 * @returns {string}
	 */
	function selection(filter, interactionLink, experianceChecker, postNote) {
		if (experianceChecker === null) { experianceChecker = undefined; }
		State.temporary.slaveSelection = {
			filter: filter,
			expCheck: experianceChecker,
			interactionLink: interactionLink,
			postNote: postNote,
			update: _updateList
		};

		$(document).one(':passagedisplay', () => { _updateList('all'); });
		return `<div>${_assignmentFilter(s => `<<run _slaveSelection.update('${s}')>>`, experianceChecker !== undefined)} <div id=${selectionElementId}></div></div>`;
	}

	function _updateList(assignment) {
		const e = document.getElementById(selectionElementId);
		e.innerHTML = '';
		e.appendChild(_listSlaves(assignment, State.temporary.slaveSelection));
	}
	/**
	 * Displays assignment filter links, whose action are generated by the callback
	 * @param {assignmentFilterGenerateCallback} callback
	 * @param {boolean} includeExperienced
	 * @returns {string}
	 */
	function _assignmentFilter(callback, includeExperienced) {
		let filters = {
			all: "All"
		};
		let fNames = Object.keys(App.Entity.facilities);
		fNames.sort();
		for (const fn of fNames) {
			/** @type {App.Entity.Facilities.Facility} */
			const f = App.Entity.facilities[fn];
			if (f.established && f.hasEmployees) {
				filters[fn] = f.name;
			}
		}
		let links = [];
		/* seems like SC2 does not process data-setter when data-passage is not set
		for (const f in filters) {
			links.push(App.UI.passageLink(filters[f], passage, callback(f)));
		}
		if (includeExperienced) {
			links.push(`<span class="lime">${App.UI.passageLink('Experienced', passage, callback('experienced'))}</span>`);
		}*/
		for (const f in filters) {
			links.push(`<<link "${filters[f]}">>${callback(f)}<</link>>`);
		}
		if (includeExperienced) {
			links.push(`<span class="lime"><<link "Experienced">>${callback('experienced')}<</link>></span>`);
		}

		return links.join('&thinsp;|&thinsp;');
	}

	/**
	 *
	 * @param {string} assignmentStr
	 * @param {ListOptions} options
	 * @returns {DocumentFragment}
	 */
	function _listSlaves(assignmentStr, options) {
		const slaves = V.slaves;
		let unfilteredIDs = [];
		switch (assignmentStr) {
			case 'all':
				unfilteredIDs = slaves.map(s => s.ID);
				break;
			case 'experienced':
				unfilteredIDs = slaves.reduce((acc, s, idx) => {
					if (options.expCheck(s)) {
						acc.push(s.ID);
					}
					return acc;
				}, []);
				break;
			default:
				unfilteredIDs = [...App.Entity.facilities[assignmentStr].employeesIDs()]; // set to array
				break;
		}
		SlaveSort.IDs(unfilteredIDs);
		let passingIDs = [];
		let rejects = [];

		unfilteredIDs.forEach(id => {
			const fr = options.filter(slaveStateById(id));
			if (fr === true || (Array.isArray(fr) && fr.length === 0)) {
				passingIDs.push(id);
			} else {
				if (Array.isArray(fr)) { rejects.push({id: id, rejects: fr}); }
			}
		});

		// clamsi fragment to create a function which combines results of two optional tests
		// done this way to test for tests presence only once
		const listPostNote = options.expCheck ?
			(options.postNote ?
				s => options.expCheck(s) ? App.UI.DOM.combineNodes(
					App.UI.DOM.makeElement("span", "Has applicable career experience.", "lime"),
					document.createElement("br"),
					options.postNote(s)
				) : options.postNote(s) :
				s => options.expCheck(s) ? App.UI.DOM.makeElement("span", "Has applicable career experience.", "lime") : null) :
			options.postNote ?
				s => options.postNote(s) :
				() => null;

		return App.UI.SlaveList.render.listDOM(passingIDs, rejects, options.interactionLink, listPostNote);
	}
}();

/**
 * @param {App.Entity.Facilities.Facility} facility
 * @param {string} passage go here after the new facility manager is selected
 * @returns {string}
 */
App.UI.SlaveList.facilityManagerSelection = function(facility, passage) {
	return this.slaveSelectionList(slave => facility.manager.canEmploy(slave),
		(slave) => App.UI.DOM.passageLink(SlaveFullName(slave), passage,
			() => { assignJob(slave, facility.manager.desc.assignment); }),
		slave => facility.manager.slaveHasExperience(slave));
};

src/js/slaveListing.js