globalThis.FutureSocieties = (function() {
	"use strict";
	const FSString2Property = { // blame Hedonism and Eugenics for this - TODO: probably can be cleaned up, maybe eliminated
		Supremacist: "FSSupremacist",
		Subjugationist: "FSSubjugationist",
		GenderRadicalist: "FSGenderRadicalist",
		GenderFundamentalist: "FSGenderFundamentalist",
		Degradationist: "FSDegradationist",
		Paternalist: "FSPaternalist",
		BodyPurist: "FSBodyPurist",
		TransformationFetishist: "FSTransformationFetishist",
		YouthPreferentialist: "FSYouthPreferentialist",
		MaturityPreferentialist: "FSMaturityPreferentialist",
		SlimnessEnthusiast: "FSSlimnessEnthusiast",
		AssetExpansionist: "FSAssetExpansionist",
		Pastoralist: "FSPastoralist",
		PhysicalIdealist: "FSPhysicalIdealist",
		Hedonistic: "FSHedonisticDecadence",
		Hedonism: "FSHedonisticDecadence",
		ChattelReligionist: "FSChattelReligionist",
		Multiculturalist: "FSNull",
		RomanRevivalist: "FSRomanRevivalist",
		EgyptianRevivalist: "FSEgyptianRevivalist",
		EdoRevivalist: "FSEdoRevivalist",
		ArabianRevivalist: "FSArabianRevivalist",
		ChineseRevivalist: "FSChineseRevivalist",
		AztecRevivalist: "FSAztecRevivalist",
		RepopulationFocus: "FSRepopulationFocus",
		Repopulationist: "FSRepopulationFocus",
		Eugenics: "FSRestart",
		IntellectualDependency: "FSIntellectualDependency",
		SlaveProfessionalism: "FSSlaveProfessionalism",
		PetiteAdmiration: "FSPetiteAdmiration",
		StatuesqueGlorification: "FSStatuesqueGlorification"
	};
	const SocietyList = [...new Set(Object.keys(FSString2Property).map(key => FSString2Property[key]))]; // This returns an array containing the unique values of FSString2Property. E.g. "FSSupremacist" and "FSSubjugationist"
	const NPCSocietyList = [ "FSCummunism", "FSIncestFetishist" ]; // NPC arcologies may use these FSes, but the PC can't

	const FSMutexGroups = [
		[ "FSSupremacist" ],
		[ "FSSubjugationist" ],
		[ "FSGenderRadicalist", "FSGenderFundamentalist" ],
		[ "FSDegradationist", "FSPaternalist" ],
		[ "FSBodyPurist", "FSTransformationFetishist" ],
		[ "FSYouthPreferentialist", "FSMaturityPreferentialist" ],
		[ "FSSlimnessEnthusiast", "FSAssetExpansionist" ],
		[ "FSPastoralist", "FSCummunism" ],
		[ "FSPhysicalIdealist", "FSHedonisticDecadence" ],
		[ "FSChattelReligionist", "FSNull" ],
		[ "FSIncestFetishist" ],
		[ "FSRomanRevivalist", "FSEgyptianRevivalist", "FSEdoRevivalist", "FSArabianRevivalist", "FSChineseRevivalist", "FSAztecRevivalist" ],
		[ "FSRepopulationFocus", "FSRestart" ],
		[ "FSIntellectualDependency", "FSSlaveProfessionalism" ],
		[ "FSPetiteAdmiration", "FSStatuesqueGlorification"]
	];

	const DisplayName = {
		FSSupremacist: {noun: "Racial Supremacy", adj: "Supremacist"},
		FSSubjugationist: {noun: "Racial Subjugationism", adj: "Subjugationist"},
		FSGenderRadicalist: {noun: "Gender Radicalism", adj: "Gender Radicalist"},
		FSGenderFundamentalist: {noun: "Gender Fundamentalism", adj: "Gender Fundamentalist"},
		FSDegradationist: {noun: "Degradationism", adj: "Degradationist"},
		FSPaternalist: {noun: "Paternalism", adj: "Paternalist"},
		FSBodyPurist: {noun: "Body Purism", adj: "Body Purist"},
		FSTransformationFetishist: {noun: "Transformation Fetishism", adj: "Transformation Fetishist"},
		FSYouthPreferentialist: {noun: "Youth Preferentialism", adj: "Youth Preferentialist"},
		FSMaturityPreferentialist: {noun: "Maturity Preferentialism", adj: "Maturity Preferentialist"},
		FSSlimnessEnthusiast: {noun: "Slimness Enthusiasm", adj: "Slimness Enthusiast"},
		FSAssetExpansionist: {noun: "Asset Expansionism", adj: "Asset Expansionist"},
		FSPastoralist: {noun: "Pastoralism", adj: "Pastoralist"},
		FSCummunism: {noun: "Cummunism", adj: "Cummunist"},
		FSPhysicalIdealist: {noun: "Physical Idealism", adj: "Physical Idealist"},
		FSHedonisticDecadence: {noun: "Decadent Hedonism", adj: "Decadent Hedonist"},
		FSChattelReligionist: {noun: "Chattel Religionism", adj: "Chattel Religionist"},
		FSNull: {noun: "Multiculturalism", adj: "Multiculturalist"},
		FSIncestFetishist: {noun: "Incest Fetishism", adj: "Incest Fetishist"},
		FSRomanRevivalist: {noun: "Roman Revivalism", adj: "Roman Revivalist"},
		FSEgyptianRevivalist: {noun: "Egyptian Revivalism", adj: "Egyptian Revivalist"},
		FSEdoRevivalist: {noun: "Edo Revivalism", adj: "Edo Revivalist"},
		FSArabianRevivalist: {noun: "Arabian Revivalism", adj: "Arabian Revivalist"},
		FSChineseRevivalist: {noun: "Chinese Revivalism", adj: "Chinese Revivalist"},
		FSAztecRevivalist: {noun: "Aztec Revivalism", adj: "Aztec Revivalist"},
		FSRepopulationFocus: {noun: "Repopulation Focus", adj: "Repopulationist"},
		FSRestart: {noun: "Eugenics", adj: "Eugenics"},
		FSIntellectualDependency: {noun: "Intellectual Dependency", adj: "Intellectual Dependency"},
		FSSlaveProfessionalism: {noun: "Slave Professionalism", adj: "Slave Professional"},
		FSPetiteAdmiration: {noun: "Petite Admiration", adj: "Petite Admiration"},
		FSStatuesqueGlorification: {noun: "Statuesque Glorification", adj: "Statuesque Glorification"}
	};

	return {
		activeFSes: activeFSes,
		activeCount: activeCount,
		applyBroadProgress: applyBroadProgress,
		influenceSources: influenceSources,
		diplomaticFSes: diplomaticFSes,
		displayName: displayName,
		displayAdj: displayAdj,
		decay: decayFSes,
		overflowToInfluence: overflowToInfluence,
		remove: removeFS,
		DecorationCleanup: DecorationCleanup,
		Change: FSChange,
		ChangePorn: FSChangePorn,
		HighestDecoration: FSHighestDecoration
	};

	/** get the list of FSes active for a particular arcology
	 * helper function, not callable externally
	 * @param {number} arcologyID
	 * @returns {Array<string>}
	 */
	function activeFSes(arcologyID) {
		let isSet = (fs) => V.arcologies[arcologyID][fs] !== "unset";
		const npcFSes = arcologyID !== 0 ? NPCSocietyList.filter(isSet) : [];
		return SocietyList.filter(isSet).concat(npcFSes);
	}

	/** call as FutureSocieties.activeCount(arcologyID)
	 * @param {number} arcologyID
	 * @returns {number}
	 */
	function activeCount(arcologyID) {
		return activeFSes(arcologyID).length;
	}

	/** call as FutureSocieties.applyBroadProgress(arcologyID, progress)
	 * @param {number} arcologyID
	 * @param {number} progress
	 */
	function applyBroadProgress(arcologyID, progress) {
		for (const fs of activeFSes(arcologyID)) {
			if (fs !== "FSNull") { // does not progress this way
				V.arcologies[arcologyID][fs] += progress;
			}
		}
	}

	/** converts excess progress into influence bonus
	 * call as FutureSocieties.overflowToInfluence(arcologyID)
	 * @param {number} arcologyID
	 */
	function overflowToInfluence(arcologyID) {
		const arcology = V.arcologies[arcologyID];
		for (const fs of activeFSes(arcologyID)) {
			if (fs !== "FSNull") { // no conventional progress
				if (arcology[fs] > V.FSLockinLevel) {
					arcology.influenceBonus += arcology[fs] - V.FSLockinLevel;
					arcology[fs] = V.FSLockinLevel;
				}
			}
		}
	}

	/** returns an array of FSes which the arcology has developed enough to influence others
	 * call as FutureSocieties.influenceSources(arcologyID)
	 * @param {number} arcologyID
	 */
	function influenceSources(arcologyID) {
		let fses = [];
		const arcology = V.arcologies[arcologyID];
		for (const fs of activeFSes(arcologyID)) {
			if (fs !== "FSNull") { // no influence from Multiculturalism?
				if (arcology[fs] > 60) {
					fses.push(fs);
				}
			}
		}
		return fses;
	}

	/** determines whether two named FSes are naturally conflicting or not
	 * @param {string} left FS
	 * @param {string} right FS
	 * @returns {boolean}
	 */
	function conflictingFSes(left, right) {
		if (left !== right) { // identical FSes are not opposed
			for (const group of FSMutexGroups) {
				if (group.includesAll(left, right)) {
					return true; // but any other FS in the mutex group is
				}
			}
		}
		return false;
	}

	/** returns the set of shared FSes between two arcologies, and the set of conflicts between pairs of FSes between the arcologies
	 * relatively expensive, try not to call frequently
	 * call as FutureSocieties.diplomaticFSes(arc1ID, arc2ID)
	 * @param {number} arc1ID
	 * @param {number} arc2ID
	 * @returns {{shared: string[], conflicting: string[][]}}
	 */
	function diplomaticFSes(arc1ID, arc2ID) {
		let shared = [], conflicting = [];
		const arc1FSes = activeFSes(arc1ID);
		const arc2FSes = activeFSes(arc2ID);
		// find ordinary shared and conflicting FSes
		for (const fs1 of arc1FSes) {
			for (const fs2 of arc2FSes) {
				if (fs1 === fs2) {
					shared.push(fs1);
				} else if (conflictingFSes(fs1, fs2)) {
					conflicting.push([fs1, fs2]);
				}
			}
		}
		// special cases: racial FSes might be conflicting even when shared
		const arc1 = V.arcologies[arc1ID];
		const arc2 = V.arcologies[arc2ID];
		if (shared.contains("FSSupremacist")) {
			// a different race is supreme
			if (arc1.FSSupremacistRace !== arc2.FSSupremacistRace) {
				shared.delete("FSSupremacist");
				conflicting.push(["FSSupremacist", "FSSupremacist"]);
			}
			// subjugating the supreme race
			if (arc2FSes.contains("FSSubjugationist") && arc2.FSSubjugationistRace === arc1.FSSupremacistRace) {
				shared.delete("FSSupremacist");
				conflicting.push(["FSSupremacist", "FSSubjugationist"]);
			}
		}
		if (shared.contains("FSSubjugationist")) {
			// subjugating a different race
			if (arc1.FSSubjugationistRace !== arc2.FSSubjugationistRace) {
				shared.delete("FSSubjugationist");
				conflicting.push(["FSSubjugationist", "FSSubjugationist"]);
			}
			// believe the subjugated race is supreme
			if (arc2FSes.contains("FSSupremacist") && arc2.FSSupremacistRace === arc1.FSSubjugationistRace) {
				shared.delete("FSSubjugationist");
				conflicting.push(["FSSubjugationist", "FSSupremacist"]);
			}
		}
		return {shared, conflicting};
	}

	/** returns the future society display name (typically an "ism") for the given property
	 * @param {string} FSProp
	 * @returns {string}
	 */
	function displayName(FSProp) {
		return DisplayName[FSProp].noun;
	}

	/** returns the future society adjective (typically an "ist") for the given property
	 * @param {string} FSProp
	 * @returns {string}
	 */
	function displayAdj(FSProp) {
		return DisplayName[FSProp].adj;
	}

	/** decays all the FSes adopted by a particular arcology (for example, because of government instability)
	 * call as FutureSocieties.decay(arcologyID)
	 * @param {number} arcologyID
	 * @returns {Array<string>} FSes which purged completely
	 */
	function decayFSes(arcologyID) {
		const fses = activeFSes(arcologyID);
		const arc = V.arcologies[arcologyID];
		let purged = [];
		for (const fs of fses) {
			if (fs !== "FSNull") { // exempt for some reason?
				if (arc[fs] < jsRandom(10, 150)) {
					purged.push(fs);
				} else {
					arc[fs] -= 10;
				}
			}
		}
		return purged;
	}

	/** Remove an FS and all associated attributes from an arcology
	 * call as FutureSocieties.remove(FS)
	 * @param {string} FS (e.g. "FSPaternalist" or "FSDegradationist")
	 */
	function removeFS(FS) {
		const arcology = V.arcologies[0];
		const FSDecoration = `${FS}Decoration`;
		const FSSMR = `${FS}SMR`;
		let FSLaw = `${FS}Law`;
		if (arcology[FS] === undefined) {
			// eslint-disable-next-line no-console
			console.log(`ERROR: bad FS reference, $arcologies[0].${FS} not defined`);
			return;
		}

		if (FS === "FSSupremacist" || FS === "FSSubjugationist") { FSLaw += "ME"; }
		if (FS !== "FSNull") { arcology[FSDecoration] = 20; }
		arcology[FS] = "unset";
		switch (FS) {
			case "FSPaternalist":
				arcology[FSLaw] = 0;
				arcology[FSSMR] = 0;
				V.slaveWatch = 0;
				break;
			case "FSDegradationist":
				arcology[FSLaw] = 0;
				arcology[FSSMR] = 0;
				V.liveTargets = 0;
				break;
			case "FSGenderRadicalist":
				arcology.FSGenderRadicalistLawBeauty = 0;
				arcology.FSGenderRadicalistLawFuta = 0;
				break;
			case "FSGenderFundamentalist":
				arcology.FSGenderFundamentalistLawBeauty = 0;
				arcology.FSGenderFundamentalistLawBimbo = 0;
				arcology.FSGenderFundamentalistSMR = 0;
				break;
			case "FSSupremacist":
				arcology[FSLaw] = 0;
				arcology[FSSMR] = 0;
				V.noSubhumansInArmy = 0;
				break;
			case "FSTransformationFetishist":
			case "FSAssetExpansionist":
				arcology[FSSMR] = 0;
				break;
			case "FSPhysicalIdealist":
				arcology.FSPhysicalIdealistLaw = 0;
				arcology.FSPhysicalIdealistSMR = 0;
				arcology.FSPhysicalIdealistStrongFat = 0;
				V.martialSchool = 0;
				break;
			case "FSHedonisticDecadence":
				arcology.FSHedonisticDecadenceLaw = 0;
				arcology.FSHedonisticDecadenceLaw2 = 0;
				arcology.FSHedonisticDecadenceSMR = 0;
				arcology.FSHedonisticDecadenceStrongFat = 0;
				break;
			case "FSChattelReligionist":
				arcology.FSChattelReligionistLaw = 0;
				arcology.FSChattelReligionistSMR = 0;
				arcology.FSChattelReligionistCreed = 0;
				V.subsidyChurch = 0;
				break;
			case "FSRepopulationFocus":
				arcology[FSLaw] = 0;
				arcology[FSSMR] = 0;
				V.universalRulesChildrenBecomeBreeders = 0;
				V.pregExemption = 0;
				break;
			case "FSRestart":
				arcology[FSLaw] = 0;
				arcology[FSSMR] = 0;
				V.eliteOfficers = 0;
				V.propOutcome = 0;
				V.failedElite = 0;
				break;
			case "FSIntellectualDependency":
				arcology.FSIntellectualDependencyLaw = 0;
				arcology.FSIntellectualDependencyLawBeauty = 0;
				arcology.FSIntellectualDependencySMR = 0;
				break;
			case "FSPetiteAdmiration":
				arcology.FSPetiteAdmirationLaw = 0;
				arcology.FSPetiteAdmirationLaw2 = 0;
				arcology.FSPetiteAdmirationSMR = 0;
				break;
			case "FSStatuesqueGlorification":
				arcology.FSStatuesqueGlorificationLaw = 0;
				arcology.FSStatuesqueGlorificationLaw2 = 0;
				arcology.FSStatuesqueGlorificationSMR = 0;
				break;
			case "FSNull":
				break;
			default: // all others have one law and one SMR
				arcology[FSLaw] = 0;
				arcology[FSSMR] = 0;
				break;
		}

		DecorationCleanup();
		resetFSCredits();
	}

	/* helper function, not callable */
	function resetFSCredits() {
		const arcology = V.arcologies[0];
		let activeFS = 0;
		for (let i = 0; i < SocietyList.length; i++) {
			if (arcology[SocietyList[i]] > 0) {
				activeFS++;
			}
		}
		if (arcology.FSNull > 0) { // multiculturalism is accounted for separately
			if (V.FSCreditCount === 4) {
				activeFS += arcology.FSNull / 25;
			} else if (V.FSCreditCount === 6) {
				activeFS += arcology.FSNull / 17;
			} else if (V.FSCreditCount === 7) {
				activeFS += arcology.FSNull / 15;
			} else {
				activeFS += arcology.FSNull / 20;
			}
		}
		V.FSCredits = Math.max(Math.trunc(V.FSGotRepCredits - activeFS), 0);
	}

	/* call as FutureSocieties.DecorationCleanup() */
	function DecorationCleanup() {
		ValidateFacilityDecoration("brothelDecoration");
		ValidateFacilityDecoration("dairyDecoration");
		ValidateFacilityDecoration("clubDecoration");
		ValidateFacilityDecoration("servantsQuartersDecoration");
		ValidateFacilityDecoration("schoolroomDecoration");
		ValidateFacilityDecoration("spaDecoration");
		ValidateFacilityDecoration("clinicDecoration");
		ValidateFacilityDecoration("arcadeDecoration");
		ValidateFacilityDecoration("cellblockDecoration");
		ValidateFacilityDecoration("masterSuiteDecoration");
		ValidateFacilityDecoration("nurseryDecoration");
		ValidateFacilityDecoration("farmyardDecoration");
	}

	/* helper function, not callable */
	/* decoration should be passed as "facilityDecoration" in quotes. For example, ValidateFacilityDecoration("brothelDecoration"). The quotes are important, do not pass it as a story variable. */
	function ValidateFacilityDecoration(decoration) {
		const FSString = V[decoration].replace(/ /g, ''); // removes spaces
		const activeFS = FSString2Property[FSString]; // gets the property name

		if (FSString === "standard") {
			// nothing to do
		} else if (activeFS === undefined) {
			// eslint-disable-next-line no-console
			console.log(`Error: $${decoration} is ${V[decoration]}`);
			V[decoration] = "standard";
		} else if (!Number.isFinite(V.arcologies[0][activeFS])) {
			if (V.arcologies[0][activeFS] !== "unset") {
				// eslint-disable-next-line no-console
				console.log(`Error: $arcologies[0].${activeFS} is ${V.arcologies[0][activeFS]}`);
			}
			V[decoration] = "standard";
		}
	}

	/* call as FutureSocieties.Change() */
	/* FSString should be in the FSString2Property object above */
	function FSChange(FSString, magnitude, bonusMultiplier = 1) {
		const arcology = V.arcologies[0];
		const activeFS = FSString2Property[FSString];

		if (activeFS === undefined) {
			return "<span class='red'>ERROR: bad FS reference</span>";
		} else if (Number.isFinite(arcology[activeFS])) {
			if (magnitude < 0) {
				repX(magnitude * V.FSSingleSlaveRep * (arcology[activeFS] / V.FSLockinLevel) / 3, 'futureSocieties'); // Reducing the reputation impact of slaves that are not adhering to societal ideals properly
			} else {
				repX(magnitude * V.FSSingleSlaveRep * (arcology[activeFS] / V.FSLockinLevel), 'futureSocieties');
			}
			arcology[activeFS] += 0.05 * magnitude * V.FSSingleSlaveRep * bonusMultiplier;
		}
	}

	/* call as FutureSocieties.ChangePorn() */
	/* FSString should be in the FSString2Property object above */
	function FSChangePorn(FSString, magnitude) {
		return FSChange(FSString, magnitude, V.pornFameBonus);
	}

	/**
	 * Returns the highest decoration level of active future societies, call as FutureSocieties.HighestDecoration()
	 * @returns {number}
	 */
	function FSHighestDecoration() {
		const arcology = V.arcologies[0];
		const decorationList = SocietyList.map(FS => `${FS}Decoration`);
		let level = 20; // All decorations start at 20

		for (let i = 0; i < decorationList.length; i++) {
			if (arcology[decorationList[i]] > level) {
				level = arcology[decorationList[i]];
			}
		}
		return level;
	}
})();

src/js/futureSocietyJS.js