import {
	Charactermancer_ArmorProficiencySelect,
	Charactermancer_ConditionImmunitySelect,
	Charactermancer_DamageImmunitySelect,
	Charactermancer_DamageResistanceSelect,
	Charactermancer_DamageVulnerabilitySelect,
	Charactermancer_ExpertiseSelect,
	Charactermancer_LanguageProficiencySelect,
	Charactermancer_SavingThrowProficiencySelect,
	Charactermancer_SkillProficiencySelect,
	Charactermancer_ToolProficiencySelect,
	Charactermancer_WeaponProficiencySelect,
} from "./UtilCharactermancer.js";
import {Config} from "./Config.js";
import {DataConverter} from "./DataConverter.js";
import {DataConverterClass} from "./DataConverterClass.js";
import {DataConverterClassSubclassFeature} from "./DataConverterClassSubclassFeature.js";
import {DataConverterOptionalfeature} from "./DataConverterOptionalfeature.js";
import {Vetools} from "./Vetools.js";
import {LGT, Util} from "./Util.js";
import {UtilActors} from "./UtilActors.js";
import {Consts} from "./Consts.js";
import {SharedConsts} from "../shared/SharedConsts.js";
import {Charactermancer_AdditionalSpellsSelect} from "./UtilCharactermancerAdditionalSpells.js";
import {UtilApplications} from "./UtilApplications.js";

class PageFilterClassesFoundry extends PageFilterClassesRaw {
	static async _pPreloadSideData () {
		return Promise.all([
			DataConverterClass.pPreloadSideData(),
			DataConverterClassSubclassFeature.pPreloadSideData(),
			DataConverterOptionalfeature.pPreloadSideData(),
		]);
	}

	static async _pGetSideData (entity, type) {
		switch (type) {
			case "class":
			case "subclass": return DataConverterClass.pGetSideData(entity, type);

			case "classFeature":
			case "subclassFeature": return DataConverterClassSubclassFeature.pGetSideData(entity, type);

			case "optionalfeature": return DataConverterOptionalfeature.pGetSideData(entity);

			default: throw new Error(`Unhandled type "${type}"`);
		}
	}

	static _handleReferenceError (msg) { console.error(...LGT, msg); ui.notifications.error(msg); }
}

class Charactermancer_Class_Util {
	static getAllFeatures (cls) {
		let allFeatures = [];
		const seenSubclassFeatureHashes = new Set();

		const gainSubclassFeatureLevels = cls.classFeatures
			.filter(it => it.gainSubclassFeature)
			.map(cf => cf.level ?? DataUtil.class.unpackUidClassFeature(cf.classFeature || cf).level);

		// Fill array with class features; add subclass features as appropriate
		cls.classFeatures.forEach(cf => {
			allFeatures.push(cf);

			if (cf.gainSubclassFeature) {
				const cfLevel = cf.level ?? DataUtil.class.unpackUidClassFeature(cf.classFeature || cf).level;
				const nxtCfLevel = gainSubclassFeatureLevels.includes(cfLevel) ? gainSubclassFeatureLevels[gainSubclassFeatureLevels.indexOf(cfLevel) + 1] : null;

				cls.subclasses.forEach(sc => {
					sc.subclassFeatures
						.filter(scf => {
							const scfHash = scf.hash ?? DataUtil.class.unpackUidSubclassFeature(scf.subclassFeature || scf).hash;
							const scfLevel = scf.level ?? DataUtil.class.unpackUidSubclassFeature(scf.subclassFeature || scf).level;

							if (seenSubclassFeatureHashes.has(scfHash)) return false;

							if (scf.isGainAtNextFeatureLevel) {
								// On first gaining a subclass feature, include anything that we gained access to at earlier levels
								if (cfLevel === gainSubclassFeatureLevels[0] && scfLevel <= cfLevel) {
									return true;
								}

								// Thereafter, only include subclass levels in the range of <this class feature> - <next class feature>
								//   (and that have not already been added)
								if (scfLevel <= cfLevel && (nxtCfLevel == null || scfLevel < nxtCfLevel)) {
									return true;
								}

								return false;
							}

							return scfLevel === cfLevel;
						})
						.forEach(scf => {
							const scfHash = scf.hash ?? DataUtil.class.unpackUidSubclassFeature(scf.subclassFeature || scf).hash;
							seenSubclassFeatureHashes.add(scfHash);

							// Fake e.g. Strixhaven subclass levels as being of the appropriate level
							scf.level = cfLevel;

							allFeatures.push(scf);
						});
				});
			}
		});

		return MiscUtil.copy(allFeatures);
	}

	static getFilteredFeatures (allFeatures, pageFilter, filterValues) {
		return allFeatures.filter(f => {
			const source = f.source
				|| (f.classFeature
					? DataUtil.class.unpackUidClassFeature(f.classFeature).source : f.subclassFeature ? DataUtil.class.unpackUidSubclassFeature(f.subclassFeature) : null);

			// If the source of the parent feature is unwanted, remove it
			if (!pageFilter.sourceFilter.toDisplay(filterValues, source)) return false;

			// If all the sub-features are not to be displayed, remove the parent
			//   This should never occur, as the "loadeds" array contains the loaded parent feature
			f.loadeds = f.loadeds.filter(meta => pageFilter.sourceFilter.toDisplay(filterValues, meta.entity.source));
			return f.loadeds.length;
		});
	}

	static getImportableFeatures (allFeatures) {
		// Avoid features with special meaning; we shouldn't add these as sheet items
		return allFeatures.filter(f => {
			// These should never contain interesting text--homebrew may disagree, but this is counter to the intent
			if (f.gainSubclassFeature) return false;

			const lowName = f.name.toLowerCase();
			switch (lowName) {
				case "proficiency versatility": return false;
				default: return true;
			}
		});
	}

	static doApplyFilterToFeatureEntries (allFeatures, pageFilter, filterValues) {
		// Apply the source filter to loaded class/subclass features entries
		allFeatures.forEach(f => {
			f.loadeds.forEach(loaded => {
				switch (loaded.type) {
					case "classFeature":
					case "subclassFeature": {
						if (loaded.entity.entries) loaded.entity.entries = this.getFilteredEntries(loaded.entity.entries, pageFilter, filterValues);
						break;
					}
				}
			})
		});

		return allFeatures;
	}

	static getFilteredEntries (entries, pageFilter, filterValues) {
		const isDisplayableEntry = (ent) => {
			if (!ent.source) return true;
			return pageFilter.sourceFilter.toDisplay(filterValues, ent.source);
		};

		const recursiveFilter = (entry) => {
			if (entry == null) return entry;
			if (typeof entry === "object") {
				if (entry instanceof Array) {
					entry = entry.filter(it => isDisplayableEntry(it));
					return entry.map(it => recursiveFilter(it));
				} else {
					Object.keys(entry).forEach(k => {
						if (entry[k] instanceof Array) {
							entry[k] = recursiveFilter(entry[k]);
							if (!entry[k].length) delete entry[k];
						} else entry[k] = recursiveFilter(entry[k]);
					});
					return entry;
				}
			} else return entry;
		};

		entries = MiscUtil.copy(entries);
		return recursiveFilter(entries);
	}

	static getFeaturesGroupedByOptionsSet (allFeatures) {
		return allFeatures.map(topLevelFeature => {
			// Collect the features into individual arrays, grouped by which options set they belong to (if any)
			const optionsSets = [];

			let optionsStack = [];
			let lastOptionsSetId = null;
			topLevelFeature.loadeds.forEach(l => {
				const optionsSetId = MiscUtil.get(l, "optionsMeta", "setId") || null;
				if (lastOptionsSetId !== optionsSetId) {
					if (optionsStack.length) optionsSets.push(optionsStack);
					optionsStack = [l];
					lastOptionsSetId = optionsSetId;
				} else {
					optionsStack.push(l);
				}
			});
			if (optionsStack.length) optionsSets.push(optionsStack);

			return {topLevelFeature, optionsSets};
		});
	}

	// region Spells
	static getPreparableSpells (spells, cls, spellLevelLow, spellLevelHigh) {
		Renderer.spell.populateHomebrewLookup(BrewUtil.homebrew, {isForce: true});

		return spells.filter(it => {
			if (!(it.level > 0 // Avoid importing cantrips
				&& it.level >= spellLevelLow && it.level <= spellLevelHigh)) return false;

			Renderer.spell.initClasses(it);

			const fromClassList = Renderer.spell.getCombinedClasses(it, "fromClassList");
			return fromClassList.some(c => (c.name || "").toLowerCase() === cls.name.toLowerCase() && (c.source || SRC_PHB).toLowerCase() === cls.source.toLowerCase())
		});
	}
	// endregion

	static getCasterProgression (cls, sc, {targetLevel, otherExistingClassItems}) {
		let casterProgression;
		let spellAbility;
		let totalSpellcastingLevels = 0; // sum up the total caster levels as defined in Multiclassing rules
		let casterClassCount = 0;
		let maxPactCasterLevel = targetLevel;

		if (otherExistingClassItems && otherExistingClassItems.length) {
			otherExistingClassItems
				.filter(it => it.data && it.data.spellcasting)
				.forEach(it => {
					casterClassCount++;
					const lvl = it.data.data.levels || 0;
					switch (it.data.spellcasting) {
						case "full": totalSpellcastingLevels += lvl; break;
						case "half": totalSpellcastingLevels += Math.floor(lvl / 2); break;
						case "third": totalSpellcastingLevels += Math.floor(lvl / 3); break;
						case "pact": Math.max(maxPactCasterLevel, lvl); break;
						// (Do nothing if unrecognized, as this can be e.g. artificer magic)
					}
				});
		}

		const setCasterVars = (type) => {
			if (clsSpellProgression === type && scSpellProgression === type) {
				casterProgression = type;
				return true;
			} else if (clsSpellProgression === type) {
				casterProgression = type;
				spellAbility = cls.spellcastingAbility;
				return true;
			} else if (scSpellProgression === type) {
				casterProgression = type;
				spellAbility = sc.spellcastingAbility;
				return true;
			}
			return false;
		};

		const clsSpellProgression = cls.casterProgression;
		const scSpellProgression = sc ? sc.casterProgression : null;

		if (clsSpellProgression && scSpellProgression) setCasterVars("full") || setCasterVars("1/2") || setCasterVars("1/3");
		else if (clsSpellProgression && !scSpellProgression) {
			casterProgression = clsSpellProgression;
			spellAbility = cls.spellcastingAbility;
		} else if (!clsSpellProgression && scSpellProgression) {
			casterProgression = scSpellProgression;
			spellAbility = sc.spellcastingAbility;
		}

		if (!spellAbility) spellAbility = (sc ? sc.spellcastingAbility : null) || cls.spellcastingAbility;

		if (casterProgression) {
			// Multiclassing rules say use floor, but single-class half/third casters use ceil.
			//   Therefore, if this is our only caster class, use ceil.
			const fnRound = casterClassCount ? Math.floor : Math.ceil;
			switch (casterProgression) {
				case "full": totalSpellcastingLevels += targetLevel; break;
				case "1/2": totalSpellcastingLevels += fnRound(targetLevel / 2); break;
				case "1/3": totalSpellcastingLevels += fnRound(targetLevel / 3); break;
				// (Do nothing if unrecognized, as this can be e.g. pact magic)
			}
		}

		return {
			casterProgression,
			spellAbility,
			totalSpellcastingLevels,
			maxPactCasterLevel,
		};
	}

	/**
	 * For each class with an optional feature progression, add faux features which allow us to choose from a type of
	 * optional features.
	 * @param classList A list of all classes, which have been run through the feature post-loader.
	 * @param optfeatList A list of all optionalfeature entities.
	 */
	static addFauxOptionalFeatureFeatures (classList, optfeatList) {
		for (const cls of classList) {
			if (!cls.classFeatures || !cls.optionalfeatureProgression?.length) continue;
			for (const optFeatProgression of cls.optionalfeatureProgression) {
				this._addFauxOptionalFeatureFeatures_handleClassProgression(optfeatList, cls, optFeatProgression)
			}
		}
	}

	static _addFauxOptionalFeatureFeatures_handleClassProgression (optfeatList, cls, optFeatProgression) {
		const fauxLoadeds = this._addFauxOptionalFeatureFeatures_getLoadeds(optfeatList, cls, optFeatProgression);

		let cntPrev = 0;
		optFeatProgression.progression.forEach((cntOptFeats, ixLvl) => {
			if (cntOptFeats === cntPrev) return;
			const cntDelta = cntOptFeats - cntPrev;
			if (!~cntDelta) return; // Should never occur

			const lvl = ixLvl + 1;

			const feature = this._addFauxOptionalFeatureFeatures_getFauxFeature(cls, optFeatProgression, lvl, fauxLoadeds, cntDelta);

			// Insert the fake feature into the features array at the appropriate point
			const ixInsertBefore = cls.classFeatures.findIndex(it => {
				return (it.level || DataUtil.class.unpackUidClassFeature(it.classFeature || it).level) > lvl;
			});
			if (~ixInsertBefore) cls.classFeatures.splice(ixInsertBefore, 0, feature);
			else cls.classFeatures.push(feature);

			cntPrev = cntOptFeats;
		});
	}

	static _addFauxOptionalFeatureFeatures_getLoadeds (optfeatList, cls, optFeatProgression) {
		const availOptFeats = optfeatList.filter(it => it.featureType.includes(optFeatProgression.featureType));
		const optionsMeta = {setId: CryptUtil.uid(), name: optFeatProgression.name};
		return availOptFeats.map(it => {
			return {
				type: "optionalfeature",
				entry: `{@optfeature ${it.name}|${it.source}}`,
				entity: MiscUtil.copy(it),
				optionsMeta,
				page: UrlUtil.PG_OPT_FEATURES,
				source: it.source,
				hash: UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_OPT_FEATURES](it),
				isRequiredOption: false,
			};
		});
	}

	static _addFauxOptionalFeatureFeatures_getFauxFeature (cls, optFeatProgression, lvl, fauxLoadeds, cntOptions) {
		const loadeds = MiscUtil.copy(fauxLoadeds);

		loadeds.forEach(l => {
			l.optionsMeta.count = cntOptions;
			PageFilterClassesRaw.populateEntityTempData({
				entity: l.entity,
				ancestorClassName: cls.name,
				ancestorType: "optionalfeature",
				displayName: `${optFeatProgression.name}: ${l.entity.name}`,
				foundryData: {
					requirements: `${cls.name} ${lvl.level}`,
				},
			});
		});

		return {
			name: optFeatProgression.name,
			source: cls.source,
			classFeature: `${optFeatProgression.name}|${cls.name}|${cls.source}|${lvl}|${SRC_5ETOOLS_TMP}`,
			hash: UrlUtil.URL_TO_HASH_BUILDER["classFeature"]({
				name: optFeatProgression.name,
				className: cls.name,
				classSource: cls.source,
				level: lvl,
				source: SRC_5ETOOLS_TMP,
			}),
			level: lvl,
			loadeds: loadeds,
		};
	}

	static getExistingClassItems (actor, cls) {
		if (!cls) return [];

		return actor.items.filter(actItem =>
			actItem.type === "class"
			&& (actItem.name || "").toLowerCase().trim() === cls.name.toLowerCase().trim()
			&& (
				!Config.get("import", "isStrictMatching")
				|| (DataConverter.getItemSource(actItem) || "").toLowerCase() === Parser.sourceJsonToAbv(cls.source).toLowerCase()
			),
		);
	}

	static getClassFromExistingClassItem (existingClassItem, classes) {
		if (!existingClassItem || existingClassItem.type !== "class" || !classes?.length) return null;

		return classes.find(cls =>
			cls.name.toLowerCase().trim() === existingClassItem.name.toLowerCase().trim()
			&& (
				!Config.get("import", "isStrictMatching")
				|| (DataConverter.getItemSource(existingClassItem) || "").toLowerCase() === Parser.sourceJsonToAbv(cls.source).toLowerCase()
			),
		);
	}

	static getSubclassFromExistingClassItem (existingClassItem, cls, subclasses) {
		if (!existingClassItem || existingClassItem.type !== "class" || !subclasses?.length) return null;

		subclasses = subclasses.filter(it => it.className === cls.name && it.classSource === cls.source);

		return subclasses.find(sc =>
			sc.name.toLowerCase().trim() === existingClassItem.data.data.subclass.toLowerCase().trim()
			|| sc.shortName.toLowerCase().trim() === existingClassItem.data.data.subclass.toLowerCase().trim(),
		);
	}
}

Charactermancer_Class_Util.ExistingFeatureChecker = class {
	constructor (actor) {
		this._actor = actor;

		this._existingSheetFeatures = {};
		this._existingImportFeatures = {};

		actor.items
			.filter(it => it.type === "feat")
			.forEach(it => {
				const cleanSource = (DataConverter.getItemSource(it) || "").trim().toLowerCase();
				Charactermancer_Class_Util.ExistingFeatureChecker._getNameAliases(it.name)
					.forEach(alias => this._existingSheetFeatures[alias] = cleanSource);

				const {page, source, hash} = it.data.flags?.[SharedConsts.MODULE_NAME_FAKE] || {};
				if (page && source && hash) this.addImportFeature(page, source, hash);
			});
	}

	static _getNameAliases (name) {
		const cleanName = name.trim().toLowerCase();
		const out = [
			cleanName,
		];

		const mTrailingParens = /^(.*?)\(.*\)$/.exec(cleanName);
		if (mTrailingParens) out.push(mTrailingParens[1].trim());

		// Optional features are imported as e.g. `Eldritch Invocations: Agonizing Blast` by the class importer, so
		//   strip the leading feature type.
		if (cleanName.includes(": ")) {
			const cleanNamePostColon = cleanName.split(":").slice(1).join(":").trim();
			out.push(cleanNamePostColon);
			const mTrailingParensPostColon = /^(.*?)\(.*\)$/.exec(cleanNamePostColon);
			if (mTrailingParensPostColon) out.push(mTrailingParensPostColon[1].trim());
		}

		return out;
	}

	isExistingFeature (name, page, source, hash) {
		if (MiscUtil.get(this._existingImportFeatures, page, source, hash)) return true;

		const searchNameAliases = Charactermancer_Class_Util.ExistingFeatureChecker._getNameAliases(name);
		if (!searchNameAliases.some(it => this._existingSheetFeatures[it])) return false;

		if (!Config.get("import", "isStrictMatching")) return true;

		const searchSource = Parser.sourceJsonToAbv(source).trim().toLowerCase();
		return searchNameAliases.some(it => this._existingSheetFeatures[it] === searchSource);
	}

	addImportFeature (page, source, hash) {
		MiscUtil.set(this._existingImportFeatures, page, source, hash, true);
	}
}

class Charactermancer_Class_LevelSelect extends BaseComponent {
	// region External
	/**
	 * @param opts
	 * @param opts.features
	 * @param opts.isSubclass
	 * @param opts.maxPreviousLevel
	 */
	static async pGetUserInput (opts) {
		return UtilApplications.pGetImportCompModalFormData({
			comp: new this(opts),
			isUnskippable: true,
			fnGetInvalidMeta: (formData) => {
				if (formData.data.length === 0) return {type: "error", message: `Please select some levels first!`};
			},
		});
	}
	// endregion

	/**
	 * @param opts
	 * @param opts.features
	 * @param [opts.isRadio] Select all levels below the currently selected level.
	 * @param [opts.isForceSelect] Force select the next level.
	 * @param [opts.isSubclass]
	 * @param [opts.maxPreviousLevel]
	 */
	constructor (opts) {
		super();

		this._isSubclass = !!opts.isSubclass;
		this._isRadio = !!opts.isRadio;
		this._isForceSelect = !!opts.isForceSelect;
		this._featureArr = this.constructor._getLevelGroupedFeatures(opts.features, this._isSubclass);
		this._maxPreviousLevel = opts.maxPreviousLevel || 0;

		this._list = null;

		this._fnsOnChange = [];
	}

	get modalTitle () { return `Select ${this._isSubclass ? "Subclass" : "Class"} Levels`; }

	onchange (fn) {
		this._fnsOnChange.push(fn);
	}

	_doRunFnsOnchange () {
		this._fnsOnChange.forEach(fn => fn());
	}

	setFeatures (features) {
		this._featureArr = this.constructor._getLevelGroupedFeatures(features, this._isSubclass);
		this._list.items.forEach(it => it.data.fnUpdateRowText());
	}

	render ($wrp) {
		const $cbAll = this._isRadio ? null : $(`<input type="checkbox" name="cb-select-all">`);
		const $wrpList = $(`<div class="veapp__list mb-1"></div>`);

		this._list = new List({
			$wrpList: $wrpList,
			fnSort: null,
			isUseJquery: true,
		});

		for (let ix = 0; ix < this._featureArr.length; ++ix) {
			const $cb = this._render_$getCbRow(ix);

			const $dispFeatures = $(`<span class="col-9-5"></span>`);
			const fnUpdateRowText = () => $dispFeatures.text(this.constructor._getRowText(this._featureArr[ix]));
			fnUpdateRowText();

			const $li = $$`<label class="w-100 flex veapp__list-row veapp__list-row-hoverable ${this._isRadio && this._isForceSelect && ix <= this._maxPreviousLevel ? `list-multi-selected` : ""} ${ix < this._maxPreviousLevel ? `ve-muted` : ""}">
				<span class="col-1 flex-vh-center">${$cb}</span>
				<span class="col-1-5 text-center">${ix + 1}</span>
				${$dispFeatures}
			</label>`
				.click((evt) => {
					this._handleSelectClick(listItem, evt);
				});

			const listItem = new ListItem(
				ix,
				$li,
				"",
				{},
				{
					cbSel: $cb[0],
					fnUpdateRowText,
				},
			);
			this._list.addItem(listItem);
		}

		if (!this._isRadio) ListUiUtil.bindSelectAllCheckbox($cbAll, this._list);

		this._list.init();

		$$`<div class="flex-col min-h-0">
			<div class="flex-v-stretch input-group mb-1 no-shrink">
				<label class="btn btn-5et col-1 px-1 flex-vh-center">${$cbAll}</label>
				<button class="btn-5et col-1-5">Level</button>
				<button class="btn-5et col-9-5">Features</button>
			</div>

			${$wrpList}
		</div>`.appendTo($wrp);
	}

	_render_$getCbRow (ix) {
		if (!this._isRadio) return $(`<input type="checkbox" class="no-events">`);

		const $cb = $(`<input type="radio" class="no-events">`);
		if (ix === this._maxPreviousLevel && this._isForceSelect) $cb.prop("checked", true);
		else if (ix < this._maxPreviousLevel) $cb.prop("disabled", true);

		return $cb;
	}

	_handleSelectClick (listItem, evt) {
		if (!this._isRadio) return ListUiUtil.handleSelectClick(this._list, listItem, evt);

		const isCheckedOld = listItem.data.cbSel.checked;

		const isDisabled = this._handleSelectClickRadio(this._list, listItem, evt);
		if (isDisabled) return;

		const isCheckedNu = listItem.data.cbSel.checked;
		if (isCheckedOld !== isCheckedNu) this._doRunFnsOnchange();
	}

	/** Displays all levels less than the one selected as also being selected. */
	_handleSelectClickRadio (list, item, evt) {
		evt.preventDefault();
		evt.stopPropagation();

		if (item.data.cbSel.disabled) return true;

		list.items.forEach(it => {
			if (it === item) {
				// If we're allowed to toggle, toggle it
				if (it.data.cbSel.checked && !this._isForceSelect) {
					it.data.cbSel.checked = false;
					it.ele.removeClass("list-multi-selected");
					return;
				}

				it.data.cbSel.checked = true;
				it.ele.addClass("list-multi-selected");
			} else {
				it.data.cbSel.checked = false;
				if (it.ix < item.ix) it.ele.addClass("list-multi-selected");
				else it.ele.removeClass("list-multi-selected");
			}
		});
	}

	pGetFormData () {
		let out = this._list.items
			.filter(it => it.data.cbSel.checked)
			.map(it => it.ix);

		// If in radio mode, back-fill all previous levels
		if (this._isRadio && out.length) {
			const max = out[0] + 1;
			out = [];
			for (let i = this._maxPreviousLevel; i < max; ++i) out.push(i);
		}

		return {
			isFormComplete: !!out.length,
			data: out,
		};
	}

	/** Get the current level. */
	getCurLevel () {
		if (this._maxPreviousLevel) return this._maxPreviousLevel;
		return 0;
	}

	/** Get the max selected level. */
	getTargetLevel () {
		const ixs = this._list.items
			.filter(it => it.data.cbSel.checked)
			.map(it => it.ix);
		if (!ixs.length) return null;
		return Math.max(...ixs) + 1;
	}

	static _getRowText (lvl) { return lvl.map(f => f.name).join(", ") || "\u2014"; }

	/**
	 * Convert an array of the form:
	 * [{classFeature: "...", level: 1}, {classFeature: "...", level: 1}, {classFeature: "...", level: 2}]
	 * To an array of arrays of the form:
	 * [
	 *   [{classFeature: "...", level: 1}, {classFeature: "...", level: 1}],
	 *   [{classFeature: "...", level: 2}]
	 * ]
	 * @param allFeatures
	 * @param isSubclass
	 */
	static _getLevelGroupedFeatures (allFeatures, isSubclass) {
		allFeatures = MiscUtil.copy(allFeatures);
		if (!isSubclass) allFeatures = allFeatures.filter(it => it.classFeature); // Only show class feature names
		const allFeaturesByLevel = [];

		let level = 1;
		let stack = [];
		const output = () => {
			allFeaturesByLevel.push(stack);
			stack = [];
		}
		allFeatures.forEach(f => {
			// Ensure all levels are filled, even if it's with an empty array
			while (level < f.level) {
				output();
				level++;
			}
			stack.push(f);
			level = f.level;
		})
		output();

		// Fill out higher levels, to show a complete table
		while (level < Consts.CHAR_MAX_LEVEL) {
			output();
			level++;
		}

		return allFeaturesByLevel;
	}
}

class Charactermancer_Class_HpIncreaseModeSelect extends BaseComponent {
	// region External
	static async pGetUserInput () {
		return UtilApplications.pGetImportCompModalFormData({
			comp: new this(),
		});
	}

	static isHpAvailable (cls) {
		return cls.hd && cls.hd.number && !isNaN(cls.hd.number) && cls.hd.faces && !isNaN(cls.hd.faces);
	}
	// endregion

	pGetFormData () {
		return {
			isFormComplete: true,
			data: this._state.mode,
		};
	}

	get modalTitle () { return `Select Hit Points Increase Mode`; }

	render ($wrp) {
		const $sel = ComponentUiUtil.$getSelEnum(
			this,
			"mode",
			{
				values: [
					Charactermancer_Class_HpIncreaseModeSelect.MODE_TAKE_AVERAGE,
					Charactermancer_Class_HpIncreaseModeSelect.MODE_ROLL,
					Charactermancer_Class_HpIncreaseModeSelect.MODE_DO_NOT_INCREASE,
				],
				fnDisplay: mode => Charactermancer_Class_HpIncreaseModeSelect.DISPLAY_MODES[mode],
			},
		);

		$$`<div class="flex-col min-h-0">
			${$sel}
		</div>`.appendTo($wrp);
	}

	_getDefaultState () {
		return {
			mode: Charactermancer_Class_HpIncreaseModeSelect.MODE_TAKE_AVERAGE,
		};
	}
}
Charactermancer_Class_HpIncreaseModeSelect.MODE_TAKE_AVERAGE = 0;
Charactermancer_Class_HpIncreaseModeSelect.MODE_ROLL = 1;
Charactermancer_Class_HpIncreaseModeSelect.MODE_DO_NOT_INCREASE = 2;

Charactermancer_Class_HpIncreaseModeSelect.DISPLAY_MODES = {
	[Charactermancer_Class_HpIncreaseModeSelect.MODE_TAKE_AVERAGE]: "Take Average",
	[Charactermancer_Class_HpIncreaseModeSelect.MODE_ROLL]: "Roll",
	[Charactermancer_Class_HpIncreaseModeSelect.MODE_DO_NOT_INCREASE]: "Do Not Increase HP",
};

class Charactermancer_Class_HpInfo extends BaseComponent {
	constructor ({className, hitDice}) {
		super();
		this._className = className;
		this._hitDice = hitDice;
	}

	render ($wrp) {
		const hdEntry = Renderer.class.getHitDiceEntry(this._hitDice);

		$$`<div class="flex-col min-h-0 ve-small">
			<div class="block"><div class="inline-block bold mr-1">Hit Dice:</div>${Vetools.withUnpatchedDiceRendering(() => Renderer.getEntryDice(hdEntry, "Hit die"))}</div>
			<div class="block"><div class="inline-block bold mr-1">Hit Points:</div>${Renderer.class.getHitPointsAtFirstLevel(this._hitDice)}</div>
			<div class="block"><div class="inline-block bold mr-1">Hit Points at Higher Levels:</div>${Vetools.withUnpatchedDiceRendering(() => Renderer.class.getHitPointsAtHigherLevels(this._className, this._hitDice, hdEntry))}</div>
		</div>`.appendTo($wrp);
	}
}

class Charactermancer_Class_ProficiencyImportModeSelect extends BaseComponent {
	// region External
	static async pGetUserInput () {
		return UtilApplications.pGetImportCompModalFormData({
			comp: new this(),
			isUnskippable: true,
		});
	}
	// endregion

	pGetFormData () {
		return {
			isFormComplete: true,
			data: this._state.mode,
		};
	}

	get modalTitle () { return `Select Class Proficiency Import Mode`; }

	render ($wrp) {
		const $sel = ComponentUiUtil.$getSelEnum(
			this,
			"mode",
			{
				values: [
					Charactermancer_Class_ProficiencyImportModeSelect.MODE_MULTICLASS,
					Charactermancer_Class_ProficiencyImportModeSelect.MODE_PRIMARY,
					Charactermancer_Class_ProficiencyImportModeSelect.MODE_NONE,
				],
				fnDisplay: mode => Charactermancer_Class_ProficiencyImportModeSelect.DISPLAY_MODES[mode],
			},
		);

		$$`<div class="flex-col min-h-0">
			${$sel}
		</div>`.appendTo($wrp);
	}

	_getDefaultState () {
		return {
			mode: Charactermancer_Class_ProficiencyImportModeSelect.MODE_MULTICLASS,
		};
	}
}
Charactermancer_Class_ProficiencyImportModeSelect.MODE_MULTICLASS = 0;
Charactermancer_Class_ProficiencyImportModeSelect.MODE_PRIMARY = 1;
Charactermancer_Class_ProficiencyImportModeSelect.MODE_NONE = 2;

Charactermancer_Class_ProficiencyImportModeSelect.DISPLAY_MODES = {
	[Charactermancer_Class_HpIncreaseModeSelect.MODE_TAKE_AVERAGE]: "Add multiclass proficiencies (this is my second+ class)",
	[Charactermancer_Class_HpIncreaseModeSelect.MODE_ROLL]: "Add base class proficiencies and equipment (this is my first class)",
	[Charactermancer_Class_HpIncreaseModeSelect.MODE_DO_NOT_INCREASE]: "Do not add proficiencies or equipment",
};

class Charactermancer_Class_FeatureOptionsSelect extends BaseComponent {
	// region External
	/**
	 * @param opts
	 * @param opts.optionsSet
	 * @param opts.actor
	 * @param opts.level
	 * @param [opts.existingFeatureChecker]
	 * @param [opts.featureSourceTracker]
	 */
	static async pGetUserInput (opts) {
		const comp = new this({...opts, isModal: true});
		if (await comp.pIsNoChoice()) {
			comp.render($(document.createElement("div"))); // Stub render, to init sub-components
			return comp.pGetFormData();
		}

		return UtilApplications.pGetImportCompApplicationFormData({
			comp,
			width: 640,
			height: Util.getMaxWindowHeight(),
			isAutoResize: true,
		});
	}

	static doApplyProficiencyFormDataToActorUpdate (actor, actorUpdate, formData) {
		const formDataData = formData.data;
		if (!formDataData) return;

		actorUpdate.data = actorUpdate.data || {};

		// region Apply proficiencies found within features
		for (const formData of formDataData.formDatasSkillProficiencies || []) {
			DataConverter.doApplySkillFormDataToActorUpdate({
				existingProfs: MiscUtil.get(actor, "data", "data", "skills"),
				formData: formData,
				actorData: actorUpdate.data,
			});
		}

		for (const formData of formDataData.formDatasLanguageProficiencies || []) {
			DataConverter.doApplyLanguageProficienciesFormDataToActorUpdate({
				existingProfs: MiscUtil.get(actor, "data", "data", "traits", "languages"),
				formData,
				actorData: actorUpdate.data,
			});
		}

		for (const formData of formDataData.formDatasToolProficiencies || []) {
			DataConverter.doApplyToolProficienciesFormDataToActorUpdate({
				existingProfs: MiscUtil.get(actor, "data", "data", "traits", "toolProf"),
				formData,
				actorData: actorUpdate.data,
			});
		}

		for (const formData of formDataData.formDatasWeaponProficiencies || []) {
			DataConverter.doApplyWeaponProficienciesFormDataToActorUpdate({
				existingProfs: MiscUtil.get(actor, "data", "data", "traits", "weaponProf"),
				formData,
				actorData: actorUpdate.data,
			});
		}

		for (const formData of formDataData.formDatasArmorProficiencies || []) {
			DataConverter.doApplyArmorProficienciesFormDataToActorUpdate({
				existingProfs: MiscUtil.get(actor, "data", "data", "traits", "armorProf"),
				formData,
				actorData: actorUpdate.data,
			});
		}

		for (const formData of formDataData.formDatasSavingThrowProficiencies || []) {
			DataConverter.doApplySavingThrowProficienciesFormDataToActorUpdate({
				existingProfs: MiscUtil.get(actor, "data", "data", "abilities"),
				formData,
				actorData: actorUpdate.data,
			});
		}

		for (const formData of formDataData.formDatasDamageImmunities || []) {
			DataConverter.doApplyDamageImmunityFormDataToActorUpdate({
				existingProfs: MiscUtil.get(actor, "data", "data", "traits", "di"),
				formData,
				actorData: actorUpdate.data,
			});
		}

		for (const formData of formDataData.formDatasDamageResistances || []) {
			DataConverter.doApplyDamageResistanceFormDataToActorUpdate({
				existingProfs: MiscUtil.get(actor, "data", "data", "traits", "dr"),
				formData,
				actorData: actorUpdate.data,
			});
		}

		for (const formData of formDataData.formDatasDamageVulnerabilities || []) {
			DataConverter.doApplyDamageVulnerabilityFormDataToActorUpdate({
				existingProfs: MiscUtil.get(actor, "data", "data", "traits", "dv"),
				formData,
				actorData: actorUpdate.data,
			});
		}

		for (const formData of formDataData.formDatasConditionImmunities || []) {
			DataConverter.doApplyConditionImmunityFormDataToActorUpdate({
				existingProfs: MiscUtil.get(actor, "data", "data", "traits", "ci"),
				formData,
				actorData: actorUpdate.data,
			});
		}

		for (const formData of formDataData.formDatasExpertise || []) {
			DataConverter.doApplyExpertiseFormDataToActorUpdate({
				existingProfs: {
					skillProficiencies: MiscUtil.get(actor, "data", "data", "skills"),
					toolProficiencies: MiscUtil.get(actor, "data", "data", "traits", "toolProf"),
				},
				formData: formData,
				actorData: actorUpdate.data,
			});
		}
		// endregion
	}

	static async pDoApplyAdditionalSpellsFormDataToActor ({actor, formData, abilityAbv, parentAbilityAbv = null}) {
		const formDataData = formData.data;
		if (!formDataData || !formDataData.formDatasAdditionalSpells?.length) return;

		for (const formDataAdditionalSpells of formDataData.formDatasAdditionalSpells) {
			await Charactermancer_AdditionalSpellsSelect.pApplyFormDataToActor(
				actor,
				formDataAdditionalSpells,
				{
					abilityAbv,
					parentAbilityAbv,
				},
			);
		}
	}
	// endregion

	/**
	 * @param opts
	 * @param opts.optionsSet
	 * @param opts.actor
	 * @param opts.level
	 * @param [opts.existingFeatureChecker]
	 * @param [opts.featureSourceTracker]
	 * @param [opts.isModal] If this instance is contained in a modal window.
	 * @param [opts.modalFilterSpells]
	 */
	constructor (opts) {
		super();

		this._optionsSet = opts.optionsSet;
		this._actor = opts.actor;
		this._level = opts.level;
		this._existingFeatureChecker = opts.existingFeatureChecker;
		this._featureSourceTracker = opts.featureSourceTracker;
		this._isModal = !!opts.isModal;
		this._modalFilterSpells = opts.modalFilterSpells;

		// Sort the options, if they are part of an options set
		if (this._isOptions()) {
			this._optionsSet.sort((a, b) => SortUtil.ascSortLower(a.entity.name, b.entity.name) || SortUtil.ascSortLower(Parser.sourceJsonToAbv(a.entity.source), Parser.sourceJsonToAbv(b.entity.source)));
		}

		this._lastMeta = null;
		this._lastSubMetas = [];

		// region Sub-components for proficiencies/etc. derived from `entryData`
		// Current sub-components
		this._subCompsSkillProficiencies = [];
		this._subCompsLanguageProficiencies = [];
		this._subCompsToolProficiencies = [];
		this._subCompsWeaponProficiencies = [];
		this._subCompsArmorProficiencies = [];
		this._subCompsSavingThrowProficiencies = [];
		this._subCompsDamageImmunities = [];
		this._subCompsDamageResistances = [];
		this._subCompsDamageVulnerabilities = [];
		this._subCompsConditionImmunities = [];
		this._subCompsExpertise = [];
		this._subCompsAdditionalSpells = [];

		// Previous iterations of the sub-components
		this._prevSubCompsSkillProficiencies = null;
		this._prevSubCompsLanguageProficiencies = null;
		this._prevSubCompsToolProficiencies = null;
		this._prevSubCompsWeaponProficiencies = null;
		this._prevSubCompsArmorProficiencies = null;
		this._prevSubCompsSavingThrowProficiencies = null;
		this._prevSubCompsDamageImmunities = [];
		this._prevSubCompsDamageResistances = [];
		this._prevSubCompsDamageVulnerabilities = [];
		this._prevSubCompsConditionImmunities = [];
		this._prevSubCompsExpertise = [];
		this._prevSubCompsAdditionalSpells = null;
		// endregion
	}

	get optionSet_ () { return this._optionsSet; }

	/** If the first feature is part of an options set, it's an options set. */
	_isOptions () {
		return !!(this._optionsSet[0] && this._optionsSet[0].optionsMeta);
	}

	unregisterFeatureSourceTracking () {
		if (this._featureSourceTracker) this._featureSourceTracker.unregister(this);
		this._unregisterSubComps();
	}

	async _pIsSubChoice (selectedLoadeds) {
		const isSubChoice_sideDataChooseData = await this._pHasChoiceInSideData_chooseData(selectedLoadeds);
		const isForceDisplay_entryDataSkillProficiencies = await this._pIsForceDisplay_skillProficiencies(selectedLoadeds);
		const isForceDisplay_entryDataLanguageProficiencies = await this._pIsForceDisplay_languageProficiencies(selectedLoadeds);
		const isForceDisplay_entryDataToolProficiencies = await this._pIsForceDisplay_toolProficiencies(selectedLoadeds);
		const isForceDisplay_entryDataWeaponProficiencies = await this._pIsForceDisplay_weaponProficiencies(selectedLoadeds);
		const isForceDisplay_entryDataArmorProficiencies = await this._pIsForceDisplay_armorProficiencies(selectedLoadeds);
		const isForceDisplay_entryDataSavingThrowProficiencies = await this._pIsForceDisplay_savingThrowProficiencies(selectedLoadeds);
		const isForceDisplay_entryDataDamageImmunities = await this._pIsForceDisplay_damageImmunities(selectedLoadeds);
		const isForceDisplay_entryDataDamageResistances = await this._pIsForceDisplay_damageResistances(selectedLoadeds);
		const isForceDisplay_entryDataDamageVulnerabilities = await this._pIsForceDisplay_damageVulnerabilities(selectedLoadeds);
		const isForceDisplay_entryDataConditionImmunities = await this._pIsForceDisplay_conditionImmunities(selectedLoadeds);
		const isForceDisplay_entryDataExpertise = await this._pIsForceDisplay_expertise(selectedLoadeds);
		const isForceDisplay_entryDataAdditionalSpells = await this._pIsForceDisplay_additionalSpells(selectedLoadeds);

		return [
			isSubChoice_sideDataChooseData,
			isForceDisplay_entryDataSkillProficiencies,
			isForceDisplay_entryDataLanguageProficiencies,
			isForceDisplay_entryDataToolProficiencies,
			isForceDisplay_entryDataWeaponProficiencies,
			isForceDisplay_entryDataArmorProficiencies,
			isForceDisplay_entryDataSavingThrowProficiencies,
			isForceDisplay_entryDataDamageImmunities,
			isForceDisplay_entryDataDamageResistances,
			isForceDisplay_entryDataDamageVulnerabilities,
			isForceDisplay_entryDataConditionImmunities,
			isForceDisplay_entryDataExpertise,
			isForceDisplay_entryDataAdditionalSpells,
		].some(Boolean);
	}

	async _pHasChoiceInSideData_chooseData (optionsSet) {
		optionsSet = optionsSet || this._optionsSet;

		for (const loaded of optionsSet) {
			const {entity, type} = loaded;
			if (type === "classFeature" || type === "subclassFeature") {
				const sideData = await DataConverterClassSubclassFeature.pGetSideData(entity, type);
				if (sideData && sideData.chooseData) return true;
			}
		}
		return false;
	}

	async _pHasSubChoice_entryData_skillProficiencies (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_SkillProficiencySelect,
			propProficiencies: "skillProficiencies",
			isRequireChoice: true,
		});
	}

	async _pHasSubChoice_entryData_languageProficiencies (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_LanguageProficiencySelect,
			propProficiencies: "languageProficiencies",
			isRequireChoice: true,
		});
	}

	async _pHasSubChoice_entryData_toolProficiencies (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_ToolProficiencySelect,
			propProficiencies: "toolProficiencies",
			isRequireChoice: true,
		});
	}

	async _pHasSubChoice_entryData_weaponProficiencies (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_WeaponProficiencySelect,
			propProficiencies: "weaponProficiencies",
			isRequireChoice: true,
		});
	}

	async _pHasSubChoice_entryData_armorProficiencies (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_ArmorProficiencySelect,
			propProficiencies: "armorProficiencies",
			isRequireChoice: true,
		});
	}

	async _pHasSubChoice_entryData_savingThrowProficiencies (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_SavingThrowProficiencySelect,
			propProficiencies: "savingThrowProficiencies",
			isRequireChoice: true,
		});
	}

	async _pHasSubChoice_damageImmunities (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_DamageImmunitySelect,
			propProficiencies: "immune",
			isRequireChoice: true,
		});
	}

	async _pHasSubChoice_damageResistances (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_DamageResistanceSelect,
			propProficiencies: "resist",
			isRequireChoice: true,
		});
	}

	async _pHasSubChoice_damageVulnerabilities (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_DamageVulnerabilitySelect,
			propProficiencies: "vulnerable",
			isRequireChoice: true,
		});
	}

	async _pHasSubChoice_conditionImmunities (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_ConditionImmunitySelect,
			propProficiencies: "conditionImmune",
			isRequireChoice: true,
		});
	}

	async _pHasSubChoice_expertise (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_ExpertiseSelect,
			propProficiencies: "expertise",
			isRequireChoice: true,
		});
	}

	async _pHasSubChoice_entryData_additionalSpells (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_AdditionalSpellsSelect,
			propProficiencies: "additionalSpells",
			isRequireChoice: true,
		});
	}

	async _pHasEntryData_otherProficiencies ({optionsSet, CompClass, propProficiencies, isRequireChoice}) {
		optionsSet = optionsSet || this._optionsSet;

		for (const loaded of optionsSet) {
			const {entity} = loaded;

			const proficiencies = entity?.[propProficiencies] || entity?.entryData?.[propProficiencies];
			if (proficiencies) {
				if (!isRequireChoice) return true;
				else {
					const isNoChoice = CompClass.isNoChoice(proficiencies);
					if (!isNoChoice) return true;
				}
			}
		}
		return false;
	}

	async _pIsForceDisplay_skillProficiencies (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_SkillProficiencySelect,
			propProficiencies: "skillProficiencies",
		});
	}

	async _pIsForceDisplay_languageProficiencies (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_LanguageProficiencySelect,
			propProficiencies: "languageProficiencies",
		});
	}

	async _pIsForceDisplay_toolProficiencies (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_ToolProficiencySelect,
			propProficiencies: "toolProficiencies",
		});
	}

	async _pIsForceDisplay_weaponProficiencies (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_WeaponProficiencySelect,
			propProficiencies: "weaponProficiencies",
		});
	}

	async _pIsForceDisplay_armorProficiencies (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_ArmorProficiencySelect,
			propProficiencies: "armorProficiencies",
		});
	}

	async _pIsForceDisplay_savingThrowProficiencies (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_SavingThrowProficiencySelect,
			propProficiencies: "savingThrowProficiencies",
		});
	}

	async _pIsForceDisplay_damageImmunities (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_ConditionImmunitySelect,
			propProficiencies: "immune",
		});
	}

	async _pIsForceDisplay_damageResistances (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_DamageResistanceSelect,
			propProficiencies: "resist",
		});
	}

	async _pIsForceDisplay_damageVulnerabilities (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_DamageVulnerabilitySelect,
			propProficiencies: "vulnerable",
		});
	}

	async _pIsForceDisplay_conditionImmunities (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_ConditionImmunitySelect,
			propProficiencies: "conditionImmune",
		});
	}

	async _pIsForceDisplay_expertise (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_ExpertiseSelect,
			propProficiencies: "expertise",
		});
	}

	_pIsForceDisplay_additionalSpells (optionsSet) {
		return this._pHasEntryData_otherProficiencies({
			optionsSet,
			CompClass: Charactermancer_AdditionalSpellsSelect,
			propProficiencies: "additionalSpells",
		});
	}

	async _pGetLoadedsSideDataRaws (optionsSet) {
		optionsSet = optionsSet || this._optionsSet;
		const out = []
		for (const loaded of optionsSet) {
			const {entity, type} = loaded;
			if (type === "classFeature" || type === "subclassFeature") {
				const sideData = await DataConverterClassSubclassFeature.pGetSideData(entity, type);
				out.push(sideData);
			} else {
				out.push(null);
			}
		}
		return out;
	}

	async pIsNoChoice () {
		if (this._isOptions()) return false;
		if (await this._pHasChoiceInSideData_chooseData()) return false;
		if (await this._pHasSubChoice_entryData_skillProficiencies()) return false;
		if (await this._pHasSubChoice_entryData_languageProficiencies()) return false;
		if (await this._pHasSubChoice_entryData_toolProficiencies()) return false;
		if (await this._pHasSubChoice_entryData_weaponProficiencies()) return false;
		if (await this._pHasSubChoice_entryData_armorProficiencies()) return false;
		if (await this._pHasSubChoice_entryData_savingThrowProficiencies()) return false;
		if (await this._pHasSubChoice_damageImmunities()) return false;
		if (await this._pHasSubChoice_damageResistances()) return false;
		if (await this._pHasSubChoice_damageVulnerabilities()) return false;
		if (await this._pHasSubChoice_conditionImmunities()) return false;
		if (await this._pHasSubChoice_expertise()) return false;
		if (await this._pHasSubChoice_entryData_additionalSpells()) return false;
		return true;
	}

	async pIsForceDisplay () {
		if (await this._pIsForceDisplay_skillProficiencies()) return true;
		if (await this._pIsForceDisplay_languageProficiencies()) return true;
		if (await this._pIsForceDisplay_toolProficiencies()) return true;
		if (await this._pIsForceDisplay_weaponProficiencies()) return true;
		if (await this._pIsForceDisplay_armorProficiencies()) return true;
		if (await this._pIsForceDisplay_savingThrowProficiencies()) return true;
		if (await this._pIsForceDisplay_damageImmunities()) return true;
		if (await this._pIsForceDisplay_damageResistances()) return true;
		if (await this._pIsForceDisplay_damageVulnerabilities()) return true;
		if (await this._pIsForceDisplay_conditionImmunities()) return true;
		if (await this._pIsForceDisplay_expertise()) return true;
		if (await this._pIsForceDisplay_additionalSpells()) return true;
		return false;
	}

	_getTrackableFeatures () {
		const ixs = ComponentUiUtil.getMetaWrpMultipleChoice_getSelectedIxs(this, "ixsChosen");
		const selectedLoadeds = ixs.map(ix => this._optionsSet[ix]);

		return selectedLoadeds.map(({page, hash}) => ({page, hash}));
	}

	/** Find a matching component in an array of (usually previously rendered) components, and copy it to ourselves. */
	findAndCopyStateFrom (comps) {
		if (!comps?.length) return;

		const comp = comps.find(it => CollectionUtil.deepEquals(it.optionSet_, this.optionSet_))
		if (comp) {
			this._proxyAssignSimple("state", MiscUtil.copy(comp.__state));
			this._prevSubCompsSkillProficiencies = comp._subCompsSkillProficiencies;
			this._prevSubCompsLanguageProficiencies = comp._subCompsLanguageProficiencies;
			this._prevSubCompsToolProficiencies = comp._subCompsToolProficiencies;
			this._prevSubCompsWeaponProficiencies = comp._subCompsWeaponProficiencies;
			this._prevSubCompsArmorProficiencies = comp._subCompsArmorProficiencies;
			this._prevSubCompsSavingThrowProficiencies = comp._subCompsSavingThrowProficiencies;
			this._prevSubCompsDamageImmunities = comp._prevSubCompsDamageImmunities;
			this._prevSubCompsDamageResistances = comp._prevSubCompsDamageResistances;
			this._prevSubCompsDamageVulnerabilities = comp._prevSubCompsDamageVulnerabilities;
			this._prevSubCompsConditionImmunities = comp._prevSubCompsConditionImmunities;
			this._prevSubCompsExpertise = comp._prevSubCompsExpertise;
			this._prevSubCompsAdditionalSpells = comp._subCompsAdditionalSpells;
		}
	}

	async pGetFormData () {
		// If there are no choices to be made, and no additional data, simply return the options as-is
		if (await this.pIsNoChoice() && !await this.pIsForceDisplay()) {
			// Bake in any side data beforehand
			const sideDatas = await this._pGetLoadedsSideDataRaws();
			const cpyOptionsSet = MiscUtil.copy(this._optionsSet);
			cpyOptionsSet.forEach((loaded, i) => {
				const sideData = sideDatas[i];
				if (!sideData || !sideData.data) return;

				const {entity} = loaded;
				entity.foundryAdditionalData = MiscUtil.copy(sideData.data);
			});

			return {
				isFormComplete: true,
				data: {
					features: cpyOptionsSet,
				},
			}
		}

		await this._pGate("ixsChosen");

		const selectedLoadeds = this._getSelectedLoadeds();

		const sideDatas = await this._pGetLoadedsSideDataRaws(selectedLoadeds);
		const cpySelectedLoadeds = MiscUtil.copy(selectedLoadeds);

		const outSkillProficiencies = [];
		const outLanguageProficiencies = [];
		const outToolProficiencies = [];
		const outWeaponProficiencies = [];
		const outArmorProficiencies = [];
		const outSavingThrowProficiencies = [];
		const outDamageImmunities = [];
		const outDamageResistances = [];
		const outDamageVulnerabilities = [];
		const outConditionImmunities = [];
		const outExpertise = [];
		const outAdditionalSpells = [];

		for (let i = 0; i < cpySelectedLoadeds.length; ++i) {
			const loaded = cpySelectedLoadeds[i];

			const sideData = sideDatas[i];

			const {entity} = loaded;

			if (sideData) {
				if (sideData.data) entity.foundryAdditionalData = MiscUtil.copy(sideData.data);

				if (sideData.chooseData) {
					const {propChooseData} = this._getProps(i);

					const ixs = ComponentUiUtil.getMetaWrpMultipleChoice_getSelectedIxs(this, propChooseData);
					const selectedChooseDatas = ixs.map(ix => sideData.chooseData[ix]);

					// If there are sub-comps, each of them outputs exactly one item
					if (selectedChooseDatas.length) {
						const selectedChooseData = selectedChooseDatas[0];
						Object.assign(entity.foundryAdditionalData, MiscUtil.copy(selectedChooseData.data));
					}
				}
			}

			// region Skill proficiencies
			if ((entity?.skillProficiencies || entity?.entryData?.skillProficiencies) && this._subCompsSkillProficiencies[i]) {
				const formData = await this._subCompsSkillProficiencies[i].pGetFormData();
				outSkillProficiencies.push(formData);
			}
			// endregion

			// region Language proficiencies
			if ((entity?.languageProficiencies || entity?.entryData?.languageProficiencies) && this._subCompsLanguageProficiencies[i]) {
				const formData = await this._subCompsLanguageProficiencies[i].pGetFormData();
				outLanguageProficiencies.push(formData);
			}
			// endregion

			// region Tool proficiencies
			if ((entity?.toolProficiencies || entity?.entryData?.toolProficiencies) && this._subCompsToolProficiencies[i]) {
				const formData = await this._subCompsToolProficiencies[i].pGetFormData();
				outToolProficiencies.push(formData);
			}
			// endregion

			// region Weapon proficiencies
			if ((entity?.weaponProficiencies || entity?.entryData?.weaponProficiencies) && this._subCompsWeaponProficiencies[i]) {
				const formData = await this._subCompsWeaponProficiencies[i].pGetFormData();
				outWeaponProficiencies.push(formData);
			}
			// endregion

			// region Armor proficiencies
			if ((entity?.armorProficiencies || entity?.entryData?.armorProficiencies) && this._subCompsArmorProficiencies[i]) {
				const formData = await this._subCompsArmorProficiencies[i].pGetFormData();
				outArmorProficiencies.push(formData);
			}
			// endregion

			// region Saving throw proficiencies
			if ((entity?.savingThrowProficiencies || entity?.entryData?.savingThrowProficiencies) && this._subCompsSavingThrowProficiencies[i]) {
				const formData = await this._subCompsSavingThrowProficiencies[i].pGetFormData();
				outSavingThrowProficiencies.push(formData);
			}
			// endregion

			// region Damage immunities
			if ((entity?.immune || entity?.entryData?.immune) && this._subCompsDamageImmunities[i]) {
				const formData = await this._subCompsDamageImmunities[i].pGetFormData();
				outDamageImmunities.push(formData);
			}
			// endregion

			// region Damage resistances
			if ((entity?.resist || entity?.entryData?.resist) && this._subCompsDamageResistances[i]) {
				const formData = await this._subCompsDamageResistances[i].pGetFormData();
				outDamageResistances.push(formData);
			}
			// endregion

			// region Damage vulnerabilities
			if ((entity?.vulnerable || entity?.entryData?.vulnerable) && this._subCompsDamageVulnerabilities[i]) {
				const formData = await this._subCompsDamageVulnerabilities[i].pGetFormData();
				outDamageVulnerabilities.push(formData);
			}
			// endregion

			// region Condition immunities
			if ((entity?.conditionImmune || entity?.entryData?.conditionImmune) && this._subCompsConditionImmunities[i]) {
				const formData = await this._subCompsConditionImmunities[i].pGetFormData();
				outConditionImmunities.push(formData);
			}
			// endregion

			// region Expertise
			if ((entity?.expertise || entity?.entryData?.expertise) && this._subCompsExpertise[i]) {
				const formData = await this._subCompsExpertise[i].pGetFormData();
				outExpertise.push(formData);
			}
			// endregion

			// region Additional spells
			if ((entity?.additionalSpells || entity?.entryData?.additionalSpells) && this._subCompsAdditionalSpells[i]) {
				const formData = await this._subCompsAdditionalSpells[i].pGetFormData();
				outAdditionalSpells.push(formData);
			}
			// endregion
		}

		return {
			isFormComplete: true,
			data: {
				features: cpySelectedLoadeds,
				formDatasSkillProficiencies: outSkillProficiencies,
				formDatasLanguageProficiencies: outLanguageProficiencies,
				formDatasToolProficiencies: outToolProficiencies,
				formDatasWeaponProficiencies: outWeaponProficiencies,
				formDatasArmorProficiencies: outArmorProficiencies,
				formDatasSavingThrowProficiencies: outSavingThrowProficiencies,
				formDatasDamageImmunities: outDamageImmunities,
				formDatasDamageResistances: outDamageResistances,
				formDatasDamageVulnerabilities: outDamageVulnerabilities,
				formDatasConditionImmunities: outConditionImmunities,
				formDatasExpertise: outExpertise,
				formDatasAdditionalSpells: outAdditionalSpells,
			},
		};
	}

	_getOptionsNameAndCount () {
		const {name, count} = this._optionsSet[0].optionsMeta;
		const required = this._optionsSet.map((it, ix) => ({it, ix})).filter(({it}) => it.isRequiredOption).map(({ix}) => ix);
		const dispCount = count - required.length;

		return {name, count, dispCount, required};
	}

	get modalTitle () {
		if (!this._isOptions()) return null;

		const {dispCount, name} = this._getOptionsNameAndCount();
		return `Choose ${dispCount === 1 ? "" : `${dispCount} `}Option${dispCount === 1 ? "" : "s"}: ${name} (Level ${this._level})`;
	}

	static _getLoadedTmpUid (loaded) { return `${loaded.page}__${loaded.hash}`; }

	_getSelectedLoadeds () {
		if (this._isOptions()) {
			const ixs = ComponentUiUtil.getMetaWrpMultipleChoice_getSelectedIxs(this, "ixsChosen");
			const {required} = this._getOptionsNameAndCount();
			return [...ixs, ...required].map(ix => this._optionsSet[ix]);
		} else {
			return this._optionsSet;
		}
	}

	render ($wrp) {
		const $stgSubChoiceData = $$`<div class="w-100 flex-col mt-2"></div>`.hideVe();

		this._render_options();

		$$`<div class="flex-col min-h-0">
			${this._lastMeta?.$ele}
			${$stgSubChoiceData}
		</div>`.appendTo($wrp);

		this._addHookBase(
			ComponentUiUtil.getMetaWrpMultipleChoice_getPropPulse("ixsChosen"),
			() => this._render_pHkIxsChosen({$stgSubChoiceData}),
		);
		return this._render_pHkIxsChosen({$stgSubChoiceData});
	}

	async pRender ($wrp) {
		return this.render($wrp);
	}

	async _render_pHkIxsChosen ({$stgSubChoiceData}) {
		try {
			await this._pLock("ixsChosen");
			await this._render_pHkIxsChosen_({$stgSubChoiceData});
		} finally {
			this._unlock("ixsChosen");
		}
	}

	async _render_pHkIxsChosen_ ({$stgSubChoiceData}) {
		// Reset the sub-state on changing chosen indexes
		const {prefixSubComps} = this._getProps();
		Object.keys(this._state).filter(k => k.startsWith(prefixSubComps)).forEach(k => delete this._state[k]);

		const selectedLoadeds = this._getSelectedLoadeds();

		if (!selectedLoadeds.length) return this._render_noSubChoices({$stgSubChoiceData});
		if (!(await this._pIsSubChoice(selectedLoadeds))) return this._render_noSubChoices({$stgSubChoiceData});

		$stgSubChoiceData.empty();
		this._unregisterSubComps();

		const sideDataRaws = await this._pGetLoadedsSideDataRaws(selectedLoadeds);
		const ptrIsFirstSection = {_: true};

		for (let i = 0; i < selectedLoadeds.length; ++i) {
			const loaded = selectedLoadeds[i];

			// region We run this again, per-entity, since we don't want to render a header for every sub-feature of the
			//   main feature.
			if (!(await this._pIsSubChoice([selectedLoadeds[i]]))) continue;
			// endregion

			const isSubChoice_sideDataChooseData = await this._pHasChoiceInSideData_chooseData([selectedLoadeds[i]]);
			const isForceDisplay_entryDataSkillProficiencies = await this._pIsForceDisplay_skillProficiencies([selectedLoadeds[i]]);
			const isForceDisplay_entryDataLanguageProficiencies = await this._pIsForceDisplay_languageProficiencies([selectedLoadeds[i]]);
			const isForceDisplay_entryDataToolProficiencies = await this._pIsForceDisplay_toolProficiencies([selectedLoadeds[i]]);
			const isForceDisplay_entryDataWeaponProficiencies = await this._pIsForceDisplay_weaponProficiencies([selectedLoadeds[i]]);
			const isForceDisplay_entryDataArmorProficiencies = await this._pIsForceDisplay_armorProficiencies([selectedLoadeds[i]]);
			const isForceDisplay_entryDataSavingThrowProficiencies = await this._pIsForceDisplay_savingThrowProficiencies([selectedLoadeds[i]]);
			const isForceDisplay_entryDataDamageImmunities = await this._pIsForceDisplay_damageImmunities([selectedLoadeds[i]]);
			const isForceDisplay_entryDataDamageResistances = await this._pIsForceDisplay_damageResistances([selectedLoadeds[i]]);
			const isForceDisplay_entryDataDamageVulnerabilities = await this._pIsForceDisplay_damageVulnerabilities([selectedLoadeds[i]]);
			const isForceDisplay_entryDataConditionImmunities = await this._pIsForceDisplay_conditionImmunities([selectedLoadeds[i]]);
			const isForceDisplay_entryDataExpertise = await this._pIsForceDisplay_expertise([selectedLoadeds[i]]);
			const isForceDisplay_entryDataAdditionalSpells = await this._pIsForceDisplay_additionalSpells([selectedLoadeds[i]]);

			const {entity, type} = loaded;

			$stgSubChoiceData.append(this._render_getSubCompTitle(entity));

			if (isSubChoice_sideDataChooseData) {
				const sideDataRaw = sideDataRaws[i];
				if (sideDataRaw?.chooseData) {
					ptrIsFirstSection._ = false;
					this._render_renderSubComp_chooseData(i, $stgSubChoiceData, entity, type, sideDataRaw);
				}
			}

			// region Skill proficiencies
			this._render_pHkIxsChosen_comp({
				ix: i,
				$stgSubChoiceData,
				selectedLoadeds,
				propSubComps: "_subCompsSkillProficiencies",
				propPrevSubComps: "_prevSubCompsSkillProficiencies",
				isAvailable: isForceDisplay_entryDataSkillProficiencies,
				propProficiencies: "skillProficiencies",
				title: "Skill Proficiencies",
				ptrIsFirstSection,
				CompClass: Charactermancer_SkillProficiencySelect,
				propPathActorExistingProficiencies: ["data", "data", "skills"],
				fnSetComp: this._render_pHkIxsChosen_setCompOtherProficiencies.bind(this),
			});
			// endregion

			// region Language proficiencies
			this._render_pHkIxsChosen_comp({
				ix: i,
				$stgSubChoiceData,
				selectedLoadeds,
				propSubComps: "_subCompsLanguageProficiencies",
				propPrevSubComps: "_prevSubCompsLanguageProficiencies",
				isAvailable: isForceDisplay_entryDataLanguageProficiencies,
				propProficiencies: "languageProficiencies",
				title: "Language Proficiencies",
				ptrIsFirstSection,
				CompClass: Charactermancer_LanguageProficiencySelect,
				propPathActorExistingProficiencies: ["data", "data", "traits", "languages"],
				fnSetComp: this._render_pHkIxsChosen_setCompOtherProficiencies.bind(this),
			});
			// endregion

			// region Tool proficiencies
			this._render_pHkIxsChosen_comp({
				ix: i,
				$stgSubChoiceData,
				selectedLoadeds,
				propSubComps: "_subCompsToolProficiencies",
				propPrevSubComps: "_prevSubCompsToolProficiencies",
				isAvailable: isForceDisplay_entryDataToolProficiencies,
				propProficiencies: "toolProficiencies",
				title: "Tool Proficiencies",
				ptrIsFirstSection,
				CompClass: Charactermancer_ToolProficiencySelect,
				propPathActorExistingProficiencies: ["data", "data", "traits", "toolProf"],
				fnSetComp: this._render_pHkIxsChosen_setCompOtherProficiencies.bind(this),
			});
			// endregion

			// region Weapon proficiencies
			this._render_pHkIxsChosen_comp({
				ix: i,
				$stgSubChoiceData,
				selectedLoadeds,
				propSubComps: "_subCompsWeaponProficiencies",
				propPrevSubComps: "_prevSubCompsWeaponProficiencies",
				isAvailable: isForceDisplay_entryDataWeaponProficiencies,
				propProficiencies: "weaponProficiencies",
				title: "Weapon Proficiencies",
				ptrIsFirstSection,
				CompClass: Charactermancer_WeaponProficiencySelect,
				propPathActorExistingProficiencies: ["data", "data", "traits", "weaponProf"],
				fnSetComp: this._render_pHkIxsChosen_setCompOtherProficiencies.bind(this),
			});
			// endregion

			// region Armor proficiencies
			this._render_pHkIxsChosen_comp({
				ix: i,
				$stgSubChoiceData,
				selectedLoadeds,
				propSubComps: "_subCompsArmorProficiencies",
				propPrevSubComps: "_prevSubCompsArmorProficiencies",
				isAvailable: isForceDisplay_entryDataArmorProficiencies,
				propProficiencies: "armorProficiencies",
				title: "Armor Proficiencies",
				ptrIsFirstSection,
				CompClass: Charactermancer_ArmorProficiencySelect,
				propPathActorExistingProficiencies: ["data", "data", "traits", "armorProf"],
				fnSetComp: this._render_pHkIxsChosen_setCompOtherProficiencies.bind(this),
			});
			// endregion

			// region Saving throw proficiencies
			this._render_pHkIxsChosen_comp({
				ix: i,
				$stgSubChoiceData,
				selectedLoadeds,
				propSubComps: "_subCompsSavingThrowProficiencies",
				propPrevSubComps: "_prevSubCompsSavingThrowProficiencies",
				isAvailable: isForceDisplay_entryDataSavingThrowProficiencies,
				propProficiencies: "savingThrowProficiencies",
				title: "Saving Throw Proficiencies",
				ptrIsFirstSection,
				CompClass: Charactermancer_SavingThrowProficiencySelect,
				propPathActorExistingProficiencies: ["data", "data", "abilities"],
				fnSetComp: this._render_pHkIxsChosen_setCompOtherProficiencies.bind(this),
			});
			// endregion

			// region Damage immunities
			this._render_pHkIxsChosen_comp({
				ix: i,
				$stgSubChoiceData,
				selectedLoadeds,
				propSubComps: "_subCompsDamageImmunities",
				propPrevSubComps: "_prevSubCompsDamageImmunities",
				isAvailable: isForceDisplay_entryDataDamageImmunities,
				propProficiencies: "immune",
				title: "Damage Immunities",
				ptrIsFirstSection,
				CompClass: Charactermancer_DamageImmunitySelect,
				propPathActorExistingProficiencies: ["data", "data", "traits", "di"],
				fnSetComp: this._render_pHkIxsChosen_setCompOtherProficiencies.bind(this),
			});
			// endregion

			// region Damage resistances
			this._render_pHkIxsChosen_comp({
				ix: i,
				$stgSubChoiceData,
				selectedLoadeds,
				propSubComps: "_subCompsDamageResistances",
				propPrevSubComps: "_prevSubCompsDamageResistances",
				isAvailable: isForceDisplay_entryDataDamageResistances,
				propProficiencies: "resist",
				title: "Damage Resistances",
				ptrIsFirstSection,
				CompClass: Charactermancer_DamageResistanceSelect,
				propPathActorExistingProficiencies: ["data", "data", "traits", "dr"],
				fnSetComp: this._render_pHkIxsChosen_setCompOtherProficiencies.bind(this),
			});
			// endregion

			// region Damage vulnerabilities
			this._render_pHkIxsChosen_comp({
				ix: i,
				$stgSubChoiceData,
				selectedLoadeds,
				propSubComps: "_subCompsDamageVulnerabilities",
				propPrevSubComps: "_prevSubCompsDamageVulnerabilities",
				isAvailable: isForceDisplay_entryDataDamageVulnerabilities,
				propProficiencies: "vulnerable",
				title: "Damage Vulnerabilities",
				ptrIsFirstSection,
				CompClass: Charactermancer_DamageVulnerabilitySelect,
				propPathActorExistingProficiencies: ["data", "data", "traits", "dv"],
				fnSetComp: this._render_pHkIxsChosen_setCompOtherProficiencies.bind(this),
			});
			// endregion

			// region Condition immunities
			this._render_pHkIxsChosen_comp({
				ix: i,
				$stgSubChoiceData,
				selectedLoadeds,
				propSubComps: "_subCompsConditionImmunities",
				propPrevSubComps: "_prevSubCompsConditionImmunities",
				isAvailable: isForceDisplay_entryDataConditionImmunities,
				propProficiencies: "conditionImmune",
				title: "Condition Immunities",
				CompClass: Charactermancer_ConditionImmunitySelect,
				propPathActorExistingProficiencies: ["data", "data", "traits", "ci"],
				ptrIsFirstSection,
				fnSetComp: this._render_pHkIxsChosen_setCompOtherProficiencies.bind(this),
			});
			// endregion

			// region Expertise
			this._render_pHkIxsChosen_comp({
				ix: i,
				$stgSubChoiceData,
				selectedLoadeds,
				propSubComps: "_subCompsExpertise",
				propPrevSubComps: "_prevSubCompsExpertise",
				isAvailable: isForceDisplay_entryDataExpertise,
				propProficiencies: "expertise",
				title: "Expertise",
				ptrIsFirstSection,
				fnSetComp: this._render_pHkIxsChosen_setCompExpertise.bind(this),
			});
			// endregion

			// region Additional Spells
			this._render_pHkIxsChosen_comp({
				ix: i,
				$stgSubChoiceData,
				selectedLoadeds,
				propSubComps: "_subCompsAdditionalSpells",
				propPrevSubComps: "_prevSubCompsAdditionalSpells",
				isAvailable: isForceDisplay_entryDataAdditionalSpells,
				propProficiencies: "additionalSpells",
				ptrIsFirstSection,
				fnSetComp: this._render_pHkIxsChosen_setCompAdditionalSpells.bind(this),
			});
			// endregion
		}

		this._prevSubCompsSkillProficiencies = null;
		this._prevSubCompsLanguageProficiencies = null;
		this._prevSubCompsToolProficiencies = null;
		this._prevSubCompsWeaponProficiencies = null;
		this._prevSubCompsArmorProficiencies = null;
		this._prevSubCompsSavingThrowProficiencies = null;
		this._prevSubCompsDamageImmunities = null;
		this._prevSubCompsDamageResistances = null;
		this._prevSubCompsDamageVulnerabilities = null;
		this._prevSubCompsConditionImmunities = null;
		this._prevSubCompsExpertise = null;
		this._prevSubCompsAdditionalSpells = null;

		$stgSubChoiceData.showVe();
	}

	_render_pHkIxsChosen_comp (
		{
			ix,
			$stgSubChoiceData,
			propSubComps,
			propPrevSubComps,
			isAvailable,
			selectedLoadeds,
			propProficiencies,
			title,
			CompClass,
			propPathActorExistingProficiencies,
			ptrIsFirstSection,
			fnSetComp,
		},
	) {
		this[propSubComps][ix] = null;
		if (!isAvailable) return;

		const {entity} = selectedLoadeds[ix];

		if (!entity?.[propProficiencies] && !entity?.entryData?.[propProficiencies]) return;

		fnSetComp({
			ix,
			propSubComps,
			propProficiencies,
			CompClass,
			propPathActorExistingProficiencies,
			entity,
		})

		// On the first render, apply previous state, if it exists
		if (this[propPrevSubComps] && this[propPrevSubComps][ix]) {
			this[propSubComps][ix]._proxyAssignSimple("state", MiscUtil.copy(this[propPrevSubComps][ix].__state));
		}

		if (title) $stgSubChoiceData.append(`${ptrIsFirstSection._ ? "" : `<div class="w-100 mt-1 mb-2"></div>`}<div class="bold mb-2">${title}</div>`);
		this[propSubComps][ix].render($stgSubChoiceData);
		ptrIsFirstSection._ = false;
	}

	_render_pHkIxsChosen_setCompOtherProficiencies (
		{
			ix,
			propSubComps,
			propProficiencies,
			CompClass,
			propPathActorExistingProficiencies,
			entity,
		},
	) {
		this[propSubComps][ix] = new CompClass({
			featureSourceTracker: this._featureSourceTracker,
			existing: CompClass.getExisting({[propProficiencies]: MiscUtil.get(this._actor, ...propPathActorExistingProficiencies)}),
			available: entity[propProficiencies] || entity.entryData[propProficiencies],
		});
	}

	_render_pHkIxsChosen_setCompExpertise (
		{
			ix,
			propSubComps,
			propProficiencies,
			entity,
		},
	) {
		this[propSubComps][ix] = new Charactermancer_ExpertiseSelect({
			featureSourceTracker: this._featureSourceTracker,
			existing: Charactermancer_ExpertiseSelect.getExisting({
				skillProficiencies: MiscUtil.get(this._actor, "data", "data", "skills"),
				toolProficiencies: MiscUtil.get(this._actor, "data", "data", "traits", "toolProf"),
			}),
			otherData: Charactermancer_ExpertiseSelect.getOtherData({
				actor: this._actor,
			}),
			available: entity[propProficiencies] || entity.entryData[propProficiencies],
		});
	}

	_render_pHkIxsChosen_setCompAdditionalSpells (
		{
			ix,
			propSubComps,
			propProficiencies,
			entity,
		},
	) {
		this[propSubComps][ix] = Charactermancer_AdditionalSpellsSelect.getComp({
			additionalSpells: entity[propProficiencies] || entity.entryData[propProficiencies],
			modalFilterSpells: this._modalFilterSpells,

			// Force all levels to be added
			curLevel: 0,
			targetLevel: Consts.CHAR_MAX_LEVEL,
			spellLevelLow: 0,
			spellLevelHigh: 9,
		});
	}

	_getProps (ix) {
		return {
			prefixSubComps: "subComp_",
			propChooseData: `subComp_${ix}_chooseData`,
		}
	}

	_unregisterSubComps () {
		if (!this._featureSourceTracker) return;

		this._subCompsSkillProficiencies.filter(Boolean).forEach(comp => this._featureSourceTracker.unregister(comp));
		this._subCompsLanguageProficiencies.filter(Boolean).forEach(comp => this._featureSourceTracker.unregister(comp));
		this._subCompsToolProficiencies.filter(Boolean).forEach(comp => this._featureSourceTracker.unregister(comp));
		this._subCompsWeaponProficiencies.filter(Boolean).forEach(comp => this._featureSourceTracker.unregister(comp));
		this._subCompsArmorProficiencies.filter(Boolean).forEach(comp => this._featureSourceTracker.unregister(comp));
		this._subCompsDamageImmunities.filter(Boolean).forEach(comp => this._featureSourceTracker.unregister(comp));
		this._subCompsDamageResistances.filter(Boolean).forEach(comp => this._featureSourceTracker.unregister(comp));
		this._subCompsDamageVulnerabilities.filter(Boolean).forEach(comp => this._featureSourceTracker.unregister(comp));
		this._subCompsConditionImmunities.filter(Boolean).forEach(comp => this._featureSourceTracker.unregister(comp));
		this._subCompsExpertise.filter(Boolean).forEach(comp => this._featureSourceTracker.unregister(comp));
		this._subCompsSavingThrowProficiencies.filter(Boolean).forEach(comp => this._featureSourceTracker.unregister(comp));
	}

	_render_noSubChoices ({$stgSubChoiceData}) {
		this._lastSubMetas.forEach(it => it.unhook());
		this._lastSubMetas = [];

		this._unregisterSubComps();

		this._subCompsSkillProficiencies = [];
		this._subCompsLanguageProficiencies = [];
		this._subCompsToolProficiencies = [];
		this._subCompsWeaponProficiencies = [];
		this._subCompsArmorProficiencies = [];
		this._subCompsSavingThrowProficiencies = [];
		this._subCompsDamageImmunities = [];
		this._subCompsDamageResistances = [];
		this._subCompsDamageVulnerabilities = [];
		this._subCompsConditionImmunities = [];
		this._subCompsExpertise = [];
		this._subCompsAdditionalSpells = [];

		$stgSubChoiceData.empty().hideVe();
	}

	_render_options () {
		if (!this._isOptions()) return;

		const {count, required} = this._getOptionsNameAndCount();

		const $ptsExisting = {};
		this._lastMeta = ComponentUiUtil.getMetaWrpMultipleChoice(
			this,
			"ixsChosen",
			{
				values: this._optionsSet,
				ixsRequired: required,
				count,
				fnDisplay: v => {
					const ptName = Renderer.get().render(v.entry);

					const $ptExisting = $(`<div class="ml-1 ve-small ve-muted"></div>`);
					$ptsExisting[this.constructor._getLoadedTmpUid(v)] = $ptExisting;

					return $$`<div class="w-100 split-v-center">
						<div class="mr-2 flex-v-center">${ptName}${$ptExisting}</div>
						<div class="${Parser.sourceJsonToColor(v.entity.source)} pr-1" title="${Parser.sourceJsonToFull(v.entity.source)}">${Parser.sourceJsonToAbv(v.entity.source)}</div>
					</div>`
				},
			},
		);

		const hkUpdatePtsExisting = () => {
			const otherStates = this._featureSourceTracker ? this._featureSourceTracker.getStatesForKey("features", {ignore: this}) : null;

			this._optionsSet
				.forEach(v => {
					const tmpUid = this.constructor._getLoadedTmpUid(v);

					if (!$ptsExisting[tmpUid]) return;

					// Value from sheet
					let isExists = this._existingFeatureChecker && this._existingFeatureChecker.isExistingFeature(v.entity._displayName || v.entity.name, v.page, v.source, v.hash);

					// Value from other networked components
					if (otherStates) isExists = isExists || otherStates.some(arr => arr.some(it => it.page === v.page && it.hash === v.hash));

					$ptsExisting[tmpUid]
						.title(isExists ? `Gained from Another Source` : "")
						.html(isExists ? `(<i class="fas fa-check"></i>)` : "")
						.toggleClass("ml-1", isExists)
				});
		};
		if (this._featureSourceTracker) this._featureSourceTracker.addHook(this, "pulseFeatures", hkUpdatePtsExisting);
		hkUpdatePtsExisting();

		// region Networking with other feature select components
		if (this._featureSourceTracker) {
			const hkSetTrackerState = () => this._featureSourceTracker.setState(this, {features: this._getTrackableFeatures()});
			this._addHookBase(this._lastMeta.propPulse, hkSetTrackerState);
			hkSetTrackerState(); // Run this immediately, as we might have loaded state from a predecessor
		}
		// endregion
	}

	_render_getSubCompTitle (entity) {
		const titleIntro = [
			entity.className,
			entity.subclassShortName ? `(${entity.subclassShortName})` : "",
			entity.level ? `Level ${entity.level}` : "",
		].filter(Boolean).join(" ");
		const title = `${titleIntro}${titleIntro ? ": " : ""}${entity.name}`;
		return `${this._isModal ? "" : `<hr class="hr-2">`}<div class="mb-2 bold w-100">${title}</div>`;
	}

	/** Used for e.g. Zealot Barbarian's "Divine Fury" (side-loaded options) */
	_render_renderSubComp_chooseData (ix, $stgSubChoice, entity, type, sideData) {
		const {propChooseData} = this._getProps(ix);

		const htmlDescription = sideData.isChooseDataRenderEntries ? Vetools.withUnpatchedDiceRendering(() => `${(entity.entries || []).map(ent => `<div>${Renderer.get().render(ent)}</div>`).join("")}`) : null;

		const choiceMeta = ComponentUiUtil.getMetaWrpMultipleChoice(
			this,
			propChooseData,
			{
				count: 1,
				fnDisplay: val => val.name,
				values: sideData.chooseData,
			},
		);

		this._lastSubMetas.push(choiceMeta);

		$$`<div class="flex-col w-100">
			${htmlDescription}
			${choiceMeta.$ele}
		</div>`.appendTo($stgSubChoice);
	}

	_getDefaultState () {
		return {
			ixsChosen: [],
		};
	}
}

/** Deals with weapon, armor, and tool proficiencies, which are (generally) static on classes. */
class Charactermancer_Class_StartingProficiencies extends BaseComponent {
	// region External
	static get (
		{
			featureSourceTracker,
			primaryProficiencies,
			multiclassProficiencies,
			savingThrowsProficiencies,
			mode,
			existingProficienciesFvttArmor,
			existingProficienciesFvttWeapons,
			existingProficienciesFvttTools,
			existingProficienciesFvttSavingThrows,
		} = {},
	) {
		const {
			existingProficienciesVetArmor,
			existingProficienciesCustomArmor,

			existingProficienciesVetWeapons,
			existingProficienciesCustomWeapons,

			existingProficienciesVetTools,
			existingProficienciesCustomTools,

			existingProficienciesVetSavingThrows,
		} = this._getExistingProficienciesVet({
			existingProficienciesFvttArmor,
			existingProficienciesFvttWeapons,
			existingProficienciesFvttTools,
			existingProficienciesFvttSavingThrows,
		});

		const comp = new this({
			featureSourceTracker,
			primaryProficiencies,
			multiclassProficiencies,
			savingThrowsProficiencies,
			existingProficienciesArmor: existingProficienciesVetArmor,
			existingProficienciesWeapons: existingProficienciesVetWeapons,
			existingProficienciesTools: existingProficienciesVetTools,
			existingProficienciesSavingThrows: existingProficienciesVetSavingThrows,

			// These are passed through and returned in the form data
			existingProficienciesCustomArmor,
			existingProficienciesCustomWeapons,
			existingProficienciesCustomTools,
		});

		if (mode != null) comp.mode = mode;

		return comp;
	}

	static async pGetUserInput (
		{
			featureSourceTracker,
			primaryProficiencies,
			multiclassProficiencies,
			savingThrowsProficiencies,
			mode,
			existingProficienciesFvttArmor,
			existingProficienciesFvttWeapons,
			existingProficienciesFvttTools,
			existingProficienciesFvttSavingThrows,
		} = {},
	) {
		return this.get({
			featureSourceTracker,
			primaryProficiencies,
			multiclassProficiencies,
			savingThrowsProficiencies,
			mode,
			existingProficienciesFvttArmor,
			existingProficienciesFvttWeapons,
			existingProficienciesFvttTools,
			existingProficienciesFvttSavingThrows,
		}).pGetFormData();
	}

	/** Form data is a list of clean strings per proficiency type. */
	static applyFormDataToActorUpdate (actUpdate, formData) {
		MiscUtil.getOrSet(actUpdate, "data", "traits", {});

		this._applyFormDataToActorUpdate_applyProfList({
			actUpdate,
			profList: [...formData.data?.armor || [], ...formData.existingData?.armor || []],
			propTrait: "armorProf",
			fnGetMapped: UtilActors.getMappedArmorProficiency.bind(UtilActors),
		});

		this._applyFormDataToActorUpdate_applyProfList({
			actUpdate,
			profList: [...formData.data?.weapons || [], ...formData.existingData?.weapons || []],
			propTrait: "weaponProf",
			fnGetMapped: UtilActors.getMappedWeaponProficiency.bind(UtilActors),
		});

		this._applyFormDataToActorUpdate_applyProfList({
			actUpdate,
			profList: [...formData.data?.tools || [], ...formData.existingData?.tools || []],
			propTrait: "toolProf",
			fnGetMapped: UtilActors.getMappedTool.bind(UtilActors),
		});

		const tgtAbils = MiscUtil.getOrSet(actUpdate, "data", "abilities", {});
		[...(formData.data?.savingThrows || []), ...(formData.existingData?.savingThrows || [])]
			.forEach(abv => (tgtAbils[abv] = tgtAbils[abv] || {}).proficient = 1);
	}

	static _applyFormDataToActorUpdate_addIfNotExists (arr, itm) {
		if (!arr.some(it => it.toLowerCase().trim() === itm.toLowerCase().trim())) arr.push(itm);
	}

	static _applyFormDataToActorUpdate_applyProfList ({actUpdate, profList, propTrait, fnGetMapped}) {
		if (!profList?.length) return;

		const tgt = MiscUtil.getOrSet(actUpdate, "data", "traits", propTrait, {});
		tgt.value = tgt.value || [];
		tgt.custom = tgt.custom || "";

		const customArr = tgt.custom.split(";").map(it => it.trim()).filter(Boolean);

		profList.forEach(it => {
			// Try to match as a generic string
			const clean = Renderer.stripTags(it).toLowerCase();
			const mapped = fnGetMapped(clean)
			if (mapped) return this._applyFormDataToActorUpdate_addIfNotExists(tgt.value, mapped);

			// Try to match an item within the string, if one exists
			const [itemTag] = /{@item [^}]+}/i.exec(it) || [];
			if (itemTag) {
				const mappedAlt = fnGetMapped(Renderer.stripTags(itemTag));
				if (mappedAlt) return this._applyFormDataToActorUpdate_addIfNotExists(tgt.value, mappedAlt);
			}

			// Otherwise, add it as a custom proficiency
			this._applyFormDataToActorUpdate_addIfNotExists(customArr, Renderer.stripTags(it));
		});

		tgt.custom = customArr.join("; ");
	}

	static getExistingProficienciesFvttSavingThrows (actor) {
		// While these are _technically_ a list of proficiency types (i.e. proficient/expert/...), treat them as boolean
		//   flags, since nothing in the game gives "saving throw expertise."
		return Object.entries(MiscUtil.get(actor, "data", "data", "abilities") || {})
			.filter(([, abMeta]) => abMeta.proficient)
			.map(([ab]) => ab);
	}
	// endregion

	static _getExistingProficienciesVet ({existingProficienciesFvttArmor, existingProficienciesFvttWeapons, existingProficienciesFvttTools, existingProficienciesFvttSavingThrows}) {
		const vetValidWeapons = new Set();
		const customWeapons = new Set();
		const vetValidArmors = new Set();
		const customArmors = new Set();
		const vetValidTools = new Set();
		const customTools = new Set();

		this._getExistingProficienciesVet_({
			existingFvtt: existingProficienciesFvttWeapons,
			fnGetUnmapped: UtilActors.getUnmappedWeaponProficiency.bind(UtilActors),
			fnCheckUnmappedAlt: UtilActors.getItemUIdFromWeaponProficiency.bind(UtilActors),
			vetValidSet: vetValidWeapons,
			customSet: customWeapons,
		});

		this._getExistingProficienciesVet_({
			existingFvtt: existingProficienciesFvttArmor,
			fnGetUnmapped: UtilActors.getUnmappedArmorProficiency.bind(UtilActors),
			vetValidSet: vetValidArmors,
			customSet: customArmors,
		});

		this._getExistingProficienciesVet_({
			existingFvtt: existingProficienciesFvttTools,
			fnGetUnmapped: UtilActors.getUnmappedTool.bind(UtilActors),
			vetValidSet: vetValidTools,
			customSet: customTools,
		});

		return {
			existingProficienciesVetWeapons: [...vetValidWeapons],
			existingProficienciesCustomWeapons: [...customWeapons],
			existingProficienciesVetArmor: [...vetValidArmors],
			existingProficienciesCustomArmor: [...customArmors],
			existingProficienciesVetTools: [...vetValidTools],
			existingProficienciesCustomTools: [...customTools],
			existingProficienciesVetSavingThrows: existingProficienciesFvttSavingThrows, // Pass-through
		};
	}

	static _getExistingProficienciesVet_ ({
		existingFvtt,
		vetValidSet,
		customSet,
		fnGetUnmapped,
		fnCheckUnmappedAlt,
	}) {
		(existingFvtt?.value || []).forEach(it => {
			const unmapped = fnGetUnmapped(it);
			if (unmapped) vetValidSet.add(unmapped);
			else {
				if (fnCheckUnmappedAlt) {
					const unmappedVet = fnCheckUnmappedAlt(it);
					// If it was in the set, we can map it again later, but keep it as plain text for now
					if (unmappedVet) vetValidSet.add(it);
					else customSet.add(it);
				} else {
					customSet.add(it);
				}
			}
		});

		(existingFvtt?.custom || "").trim().split(";").map(it => it.trim()).filter(Boolean).forEach(it => {
			const low = it.toLowerCase();
			const unmapped = fnGetUnmapped(low);
			if (unmapped) vetValidSet.add(unmapped);
			else {
				if (fnCheckUnmappedAlt) {
					const unmappedVet = fnCheckUnmappedAlt(low);
					// If it was in the set, we can map it again later, but keep it as plain text for now
					if (unmappedVet) vetValidSet.add(low);
					else customSet.add(it);
				} else {
					customSet.add(it);
				}
			}
		});
	}

	/** Convert the 5etools data to arrays of strings. */
	static _getCleanVetProfs (vetProfs) {
		if (!vetProfs) return {};

		const out = {};

		if (vetProfs.armor) out.armor = vetProfs.armor.map(it => it.proficiency || it);
		if (vetProfs.weapons) out.weapons = vetProfs.weapons.map(it => (it.proficiency || it).toLowerCase().trim());
		if (vetProfs.tools) out.tools = vetProfs.tools.map(it => (it.proficiency || it).toLowerCase().trim());

		return out;
	}

	constructor (
		{
			featureSourceTracker,
			primaryProficiencies,
			multiclassProficiencies,
			savingThrowsProficiencies,
			existingProficienciesArmor,
			existingProficienciesWeapons,
			existingProficienciesTools,
			existingProficienciesSavingThrows,
			existingProficienciesCustomArmor,
			existingProficienciesCustomWeapons,
			existingProficienciesCustomTools,
		} = {},
	) {
		super();
		this._featureSourceTracker = featureSourceTracker;
		this._primaryProficiencies = Charactermancer_Class_StartingProficiencies._getCleanVetProfs(primaryProficiencies);
		this._multiclassProficiencies = Charactermancer_Class_StartingProficiencies._getCleanVetProfs(multiclassProficiencies);
		this._savingThrowsProficiencies = savingThrowsProficiencies;
		this._existingProficienciesArmor = existingProficienciesArmor;
		this._existingProficienciesWeapons = existingProficienciesWeapons;
		this._existingProficienciesTools = existingProficienciesTools;
		this._existingProficienciesSavingThrows = existingProficienciesSavingThrows;
		// region Pass-throughs to form data
		this._existingProficienciesCustomArmor = existingProficienciesCustomArmor;
		this._existingProficienciesCustomWeapons = existingProficienciesCustomWeapons;
		this._existingProficienciesCustomTools = existingProficienciesCustomTools;
		// endregion
	}

	set mode (mode) { this._state.mode = mode; }

	_getFormData () {
		const isPrimary = this._state.mode === Charactermancer_Class_ProficiencyImportModeSelect.MODE_PRIMARY;
		const profs = isPrimary ? this._primaryProficiencies : this._multiclassProficiencies;

		if (!profs) return {isFormComplete: true, data: {}, existingData: {}};

		return {
			isFormComplete: true,
			data: {
				armor: profs.armor || [],
				weapons: profs.weapons || [],
				tools: profs.tools || [],
				savingThrows: isPrimary ? (this._savingThrowsProficiencies || []) : [],
			},
			existingData: {
				armor: [
					...(this._existingProficienciesArmor || []),
					...(this._existingProficienciesCustomArmor || []),
				],
				weapons: [
					...(this._existingProficienciesWeapons || []),
					...(this._existingProficienciesCustomWeapons || []),
				],
				tools: [
					...(this._existingProficienciesTools || []),
					...(this._existingProficienciesCustomTools || []),
				],
				savingThrows: this._existingProficienciesSavingThrows || [],
			},
		};
	}

	pGetFormData () { return this._getFormData(); }

	render ($wrp) {
		const $wrpDisplay = $(`<div class="flex-col min-h-0 ve-small"></div>`).appendTo($wrp);

		const fnsCleanup = [];

		const hkMode = () => {
			fnsCleanup.forEach(fn => fn());
			fnsCleanup.splice(0, fnsCleanup.length);

			$wrpDisplay.empty();
			const isPrimary = this._state.mode === Charactermancer_Class_ProficiencyImportModeSelect.MODE_PRIMARY;

			const profs = isPrimary ? this._primaryProficiencies : this._multiclassProficiencies;
			if (profs) {
				this._render_profType({
					profList: profs.armor,
					title: "Armor",
					$wrpDisplay,
					propTracker: "armor",
					propTrackerPulse: "pulseArmor",
					fnsCleanup,
					existing: this._existingProficienciesArmor,
					existingProficienciesCustom: this._existingProficienciesCustomArmor,
					fnDisplay: str => ["light", "medium", "heavy"].includes(str) ? `${str} armor` : str,
				});

				this._render_profType({
					profList: profs.weapons,
					title: "Weapons",
					$wrpDisplay,
					propTracker: "weapons",
					propTrackerPulse: "pulseWeapons",
					fnsCleanup,
					existing: this._existingProficienciesWeapons,
					existingProficienciesCustom: this._existingProficienciesCustomWeapons,
					fnDisplay: str => ["simple", "martial"].includes(str) ? `${str} weapons` : str,
				});

				this._render_profType({
					profList: profs.tools,
					title: "Tools",
					$wrpDisplay,
					propTracker: "tools",
					propTrackerPulse: "pulseTools",
					fnsCleanup,
					existing: this._existingProficienciesTools,
					existingProficienciesCustom: this._existingProficienciesCustomTools,
				});
			}

			if (isPrimary && this._savingThrowsProficiencies) {
				this._render_profType({
					profList: this._savingThrowsProficiencies,
					title: "Saving Throws",
					$wrpDisplay,
					propTracker: "savingThrows",
					propTrackerPulse: "pulseSavingThrows",
					fnsCleanup,
					existing: this._existingProficienciesSavingThrows,
					fnDisplay: str => Parser.attAbvToFull(str),
				});
			}

			// region Networking with other armor proficiency components
			if (this._featureSourceTracker) this._featureSourceTracker.setState(this, this._getStateTrackerData());
			// endregion
		};
		this._addHookBase("mode", hkMode);
		hkMode();
	}

	_getStateTrackerData () {
		const formData = this._getFormData();

		const getNoTags = (arr) => arr.map(it => this.constructor._getUid(it)).filter(Boolean);

		return {
			armor: getNoTags(formData.data?.armor || []).mergeMap(it => ({[it]: true})),
			weapons: getNoTags(formData.data?.weapons || []).mergeMap(it => ({[it]: true})),
			tools: getNoTags(formData.data?.tools || []).mergeMap(it => ({[it]: true})),
		};
	}

	static _getUid (str) {
		if (!str.startsWith("{@item")) return str;

		let [name, source] = Renderer.splitTagByPipe((Renderer.splitFirstSpace(str.slice(1, -1))[1] || "").toLowerCase());
		source = source || SRC_DMG.toLowerCase();
		if (!name) return null;

		return `${name}|${source}`;
	}

	_render_profType ({profList, title, $wrpDisplay, propTracker, propTrackerPulse, fnsCleanup, existing, existingProficienciesCustom, fnDisplay}) {
		if (!profList?.length) return;

		const profListUids = profList.map(prof => this.constructor._getUid(prof));

		const $ptsExisting = {};

		const $wrps = profList.map((it, i) => {
			const $ptExisting = $(`<div class="ve-small veapp__msg-warning inline-block"></div>`);
			const uid = profListUids[i];
			$ptsExisting[uid] = $ptExisting;
			const isNotLast = i < profList.length - 1;
			return $$`<div class="inline-block ${isNotLast ? "mr-1" : ""}">${Renderer.get().render(fnDisplay ? fnDisplay(it) : it)}${$ptExisting}${isNotLast ? `,` : ""}</div>`;
		});

		$$`<div class="block">
			<div class="mr-1 bold inline-block">${title}:</div>${$wrps}
		</div>`.appendTo($wrpDisplay);

		const pHkUpdatePtsExisting = async () => {
			try {
				await this._pLock("updateExisting");
				await pHkUpdatePtsExisting_();
			} finally {
				this._unlock("updateExisting");
			}
		};

		const pHkUpdatePtsExisting_ = async () => {
			const otherStates = this._featureSourceTracker ? this._featureSourceTracker.getStatesForKey(propTracker, {ignore: this}) : null;

			for (const v of profListUids) {
				if (!$ptsExisting[v]) return;

				const parentGroup = await DataConverter.pGetItemWeaponType(v)

				// Value from sheet
				let isExisting = (existing || []).includes(v)
					|| (parentGroup && (existing || []).includes(parentGroup))
					|| (existingProficienciesCustom || []).includes(v)
					|| (parentGroup && (existingProficienciesCustom || []).includes(parentGroup));

				// Value from other networked components
				isExisting = isExisting
					|| (otherStates || []).some(otherState => !!otherState[v] || (parentGroup && !!otherState[parentGroup]));

				$ptsExisting[v]
					.title(isExisting ? "Proficient from Another Source" : "")
					.toggleClass("ml-1", isExisting)
					.html(isExisting ? `(<i class="fas ${UtilActors.PROF_TO_ICON_CLASS[1]}"></i>)` : "");
			}
		};
		if (this._featureSourceTracker) {
			this._featureSourceTracker.addHook(this, propTrackerPulse, pHkUpdatePtsExisting);
			fnsCleanup.push(() => this._featureSourceTracker.removeHook(this, propTrackerPulse, pHkUpdatePtsExisting))
		}
		pHkUpdatePtsExisting();
	}

	_getDefaultState () {
		return {
			mode: Charactermancer_Class_ProficiencyImportModeSelect.MODE_PRIMARY,
		};
	}
}

export {
	PageFilterClassesFoundry,
	Charactermancer_Class_Util,
	Charactermancer_Class_LevelSelect,
	Charactermancer_Class_HpIncreaseModeSelect,
	Charactermancer_Class_HpInfo,
	Charactermancer_Class_ProficiencyImportModeSelect,
	Charactermancer_Class_FeatureOptionsSelect,
	Charactermancer_Class_StartingProficiencies,
};
