import {ImportListActor} from "./ImportListActor.js";
import {Vetools} from "./Vetools.js";
import {Util, LGT} from "./Util.js";
import {UtilActors} from "./UtilActors.js";
import {DataConverter} from "./DataConverter.js";
import {DataConverterItem} from "./DataConverterItem.js";
import {SharedConsts} from "../shared/SharedConsts.js";
import {Config} from "./Config.js";
import {UtilList2} from "./UtilList2.js";
import {DataConverterSpell} from "./DataConverterSpell.js";
import {DataConverterCreature} from "./DataConverterCreature.js";
import {UtilDataSource} from "./UtilDataSource.js";
import {ConfigConsts} from "./ConfigConsts.js";
import {DataConverterActor} from "./DataConverterActor.js";
import {UtilActiveEffects} from "./UtilActiveEffects.js";

class ImportListCreature extends ImportListActor {
	constructor (externalData, applicationOptsOverride, subclassOptsOverride, actorImporterOptsOverride) {
		externalData = externalData || {};
		applicationOptsOverride = applicationOptsOverride || {};
		subclassOptsOverride = subclassOptsOverride || {};
		actorImporterOptsOverride = actorImporterOptsOverride || {};
		super(
			{
				title: "Import Creatures",
				...applicationOptsOverride,
			},
			externalData,
			{
				props: ["monster"],
				propsBrewAdditionalData: ["foundryMonster"],
				fnLoadSideData: Vetools.pGetCreatureSideData,
				titleSearch: "creatures",
				sidebarTab: "actors",
				gameProp: "actors",
				defaultFolderPath: ["Creatures"],
				folderType: "Actor",
				fnListSort: PageFilterBestiary.sortMonsters,
				pageFilter: new PageFilterBestiary(),
				isActorRadio: true,
				page: UrlUtil.PG_BESTIARY,
				isPreviewable: true,
				isModdable: true,
				isDedupable: true,
				...subclassOptsOverride,
			},
			{
				actorType: "npc",
				...actorImporterOptsOverride,
			},
		);

		this._isUseMods = false;
	}

	async pGetSources () {
		const creatureIndex = await Vetools.pGetCreatureIndex();

		return [
			new UtilDataSource.DataSourceSpecial(
				Config.get("ui", "isStreamerMode") ? "SRD" : "5etools",
				async () => (await Vetools.pGetAllCreatures()).monster,
				{
					cacheKey: `5etools-creatures`,
					filterTypes: [UtilDataSource.SOURCE_TYP_OFFICIAL_ALL],
					isDefault: true,
				},
			),
			new UtilDataSource.DataSourceUrl(
				"Custom URL",
				"",
				{
					filterTypes: [UtilDataSource.SOURCE_TYP_CUSTOM],
				},
			),
			new UtilDataSource.DataSourceFile(
				"Upload File",
				{
					filterTypes: [UtilDataSource.SOURCE_TYP_CUSTOM],
				},
			),
			...Object.entries(creatureIndex).map(([src, filename]) => new UtilDataSource.DataSourceUrl(
				Parser.sourceJsonToFull(src),
				Vetools.getCreatureUrl(filename),
				{
					source: src,
					pPostLoad: ImportListCreature._postLoadVetools.bind(ImportListCreature, src),
					filterTypes: SourceUtil.isNonstandardSource(src) ? [UtilDataSource.SOURCE_TYP_ARCANA] : [UtilDataSource.SOURCE_TYP_OFFICIAL_SINGLE],
				},
			)),
			...(await Vetools.pGetHomebrewSources("creature")).map(({name, url}) => new UtilDataSource.DataSourceUrl(
				name,
				url,
				{
					filterTypes: [UtilDataSource.SOURCE_TYP_BREW],
				},
			)),
		]
	}

	static _postLoadVetools (src, monList) {
		return monList.filter(it => it.source === src);
	}

	async pGetChooseImporterUserDataForSources (sources) {
		return {
			isSingletonSource_officialAdventure: sources.length === 1 && sources[0].source && Parser.SOURCES_ADVENTURES.has(sources[0].source),
		};
	}

	async _pPostFilterRender () {
		if (!this._userData) return;

		// If we're loading an official adventure, and this is the only source, wipe the "Adventure NPC" denial filter
		const isSingletonSourceOfficialAdventure = this._userData.isSingletonSource_officialAdventure;
		this._userData = null;
		if (!isSingletonSourceOfficialAdventure) return;

		const KEY_MISC = "Miscellaneous";
		const miscValues = this._pageFilter.filterBox.getValues()[KEY_MISC];
		this._pageFilter.filterBox.setFromValues({
			[KEY_MISC]: {
				...Object.entries(miscValues)
					.filter(([k]) => !k.startsWith("_"))
					.mergeMap(([k, v]) => ({[k]: v})),
				"Adventure NPC": 0,
			},
		})
	}

	getData () {
		return {
			isRadio: this._isRadio,
			isPreviewable: this._isPreviewable,
			titleButtonRun: this._titleButtonRun,
			titleSearch: this._titleSearch,
			buttonsAdditional: [
				{
					name: "btn-run-mods",
					text: "Import with Scaling/Renames",
				},
			],
			cols: [
				{
					name: "Name",
					width: 4,
					field: "name",
				},
				{
					name: "Type",
					width: 5,
					field: "type",
				},
				{
					name: "CR",
					width: 1,
					field: "cr",
					rowClassName: "text-center",
				},
				{
					name: "Source",
					width: 1,
					field: "source",
					titleProp: "sourceLong",
					displayProp: "sourceShort",
					classNameProp: "sourceClassName",
					rowClassName: "text-center",
				},
			],
			rows: this._content.map((it, ix) => {
				this._pageFilter.constructor.mutateForFilters(it);

				// region Re-used in fnGetValues
				it._vCr = it._pCr || "\u2014";
				// endregion

				return {
					name: it.name,
					type: it._pTypes.asText.uppercaseFirst(),
					cr: it._vCr,
					source: it.source,
					sourceShort: Parser.sourceJsonToAbv(it.source),
					sourceLong: Parser.sourceJsonToFull(it.source),
					sourceClassName: Parser.sourceJsonToColor(it.source),
					ix,
				};
			}),
		};
	}

	_getAbsorbListItemOpts () {
		return {
			fnGetName: it => it.name,
			// values used for sorting/search
			fnGetValues: it => ({
				source: it.source,
				type: it._pTypes.asText,
				cr: it._vCr,
				group: it.group || "",
				alias: (it.alias || []).map(it => `"${it}"`).join(","),
				hash: UrlUtil.URL_TO_HASH_BUILDER[this._page](it),
			}),
			fnGetData: UtilList2.absorbFnGetData,
			fnBindListeners: it => this._isRadio
				? UtilList2.absorbFnBindListenersRadio(this._list, it)
				: UtilList2.absorbFnBindListeners(this._list, it),
		};
	}

	_activateListeners_absorbListItems () { this._list.doAbsorbItems(this._content, this._getAbsorbListItemOpts()); }

	getFolderPathMeta () {
		return {
			...super.getFolderPathMeta(),
			cr: {
				label: "Challenge Rating",
				getter: it => this.constructor._getFolderCr(it),
			},
			type: {
				label: "Type",
				getter: it => this.constructor._getFolderType(it),
			},
			typeTags: {
				label: "Type (With Tags)",
				getter: it => this.constructor._getFolderTypeWithTags(it),
			},
		};
	}

	static _getFolderCr (it) { return it.cr ? (it.cr.cr || it.cr) : "Unknown CR"; }
	static _getFolderType (it) { return Parser.monTypeToFullObj(it.type).type.toTitleCase(); }
	static _getFolderTypeWithTags (it) { return Parser.monTypeToFullObj(it.type).asText.uppercaseFirst(); }

	_pFnPostProcessEntries (entries) {
		if (!this._isUseMods) return entries;

		return new Promise(resolve => {
			const scaler = new ImportListCreature.ScaleRename(entries, resolve, {titleSearch: this._titleSearch});
			scaler.render(true);
		});
	}

	async _pImportEntry_pGetImportMetadata (actor, mon, importOpts) {
		const act = {};

		const fluff = await Renderer.utils.pGetFluff({
			entity: mon,
			pFnPostProcess: Renderer.monster.postProcessFluff.bind(null, mon),
			fluffBaseUrl: `data/bestiary/`,
			fluffProp: "monsterFluff",
		});

		const monOpts = new ImportListCreature.ImportEntryOpts({actor, mon, fluff});

		await this._pImportEntry_pFillBase(mon, act, monOpts.fluff, {isUseTokenImageAsPortrait: Config.get("importCreature", "isUseTokenImageAsPortrait")});

		act.data = {};

		if (!this._isSkipFolder && !importOpts.isTemp && !this._pack) {
			const folderId = await this._pImportEntry_pGetFolderId(mon);
			if (folderId) act.folder = folderId;
		}

		act.permission = {default: Config.get("importCreature", "permissions")};

		this._pImportEntry_fillData_Abilities(mon, act.data, monOpts);
		await this._pImportEntry_pFillData_Attributes(mon, act, monOpts);
		this._pImportEntry_fillData_Details(mon, act.data, monOpts);
		this._pImportEntry_fillData_Skills(mon, act.data, monOpts);
		this._pImportEntry_fillData_Traits(mon, act.data, monOpts);
		this._pImportEntry_fillData_Currency(mon, act.data, monOpts);
		this._pImportEntry_fillData_Spells(mon, act.data, monOpts);
		this._pImportEntry_fillData_Resources(mon, act.data, monOpts);

		await this._pImportEntry_pFillToken(mon, act, "importCreature");

		return {dataBuilderOpts: monOpts, actorData: act};
	}

	_getActorSheetName (it) {
		const out = [DataConverter.getNameWithSourcePart(it)];

		const tagsToAdd = Config.get("importCreature", "nameTags");

		Object.entries(tagsToAdd)
			.filter(([, v]) => v)
			.forEach(([k]) => {
				switch (Number(k)) {
					case ConfigConsts.C_CREATURE_NAMETAGS_CR: out.push(`[${this.constructor._getFolderCr(it)}]`); break;
					case ConfigConsts.C_CREATURE_NAMETAGS_TYPE: out.push(`[${this.constructor._getFolderType(it)}]`); break;
					case ConfigConsts.C_CREATURE_NAMETAGS_TYPE_WITH_TAGS: out.push(`[${this.constructor._getFolderTypeWithTags(it)}]`); break;
				}
			});

		return out.join(" ");
	}

	async _pImportEntry_pFillData_Attributes (mon, act, monOpts) {
		const data = act.data;

		const out = {};

		let acVal = 10;
		if (mon.ac && mon.ac.length) {
			const ac0 = mon.ac[0];

			if (typeof ac0 === "number") acVal = ac0;
			else if (typeof ac0.ac === "number") acVal = ac0.ac;

			acVal = await this._pImportEntry_pFillData_Attributes_Ac(mon, act, monOpts, acVal, ac0);
		}

		out.ac = {
			min: 0,
			value: acVal,
		};

		let hpMax = 0;
		let hpFormula = "";
		if (mon.hp) {
			// Try to parse the "special" value, in case it's a plain number
			if (mon.hp.special && /^\d+$/.test(mon.hp.special)) hpMax = Number(mon.hp.special);

			if (mon.hp.average) hpMax = mon.hp.average;
			if (mon.hp.formula) hpFormula = mon.hp.formula
		}

		out.hp = {
			value: hpMax,
			min: 0,
			max: hpMax,
			temp: 0,
			tempmax: 0,
			formula: hpFormula,
		};

		out.init = {
			value: 0,
			mod: 0,
			bonus: 0,
			total: Parser.getAbilityModNumber(mon.dex),
			prof: 0,
		};

		out.prof = monOpts.pb;

		out.movement = DataConverter.getMovement(mon.speed);

		out.spellcasting = monOpts.spellAbility;

		out.spelldc = monOpts.spellDc;

		out.senses = {
			darkvision: 0,
			blindsight: 0,
			tremorsense: 0,
			truesight: 0,
			units: "ft",
			special: "",
		};

		out.senses.special = (mon.senses || []).filter(sens => {
			sens = sens.replace(Parser._numberCleanRegexp, "");

			const mSense = /(blindsight|darkvision|tremorsense|truesight)\s*(\d+)/i.exec(sens);
			if (!mSense) return true;

			const num = Number(mSense[2]);
			switch (mSense[1]) {
				case "darkvision": out.senses.darkvision = num; break;
				case "blindsight": out.senses.blindsight = num; break;
				case "tremorsense": out.senses.tremorsense = num; break;
				case "truesight": out.senses.truesight = num; break;
			}
			return false;
		}).join(", ");

		data.attributes = out;
	}

	/**
	 * Convert excess AC (beyond that which is provided by equipped items) into an active effect.
	 *   This allows compatibility with e.g. the Dynamic Active Effects module.
	 */
	async _pImportEntry_pFillData_Attributes_Ac (mon, act, monOpts, acVal, ac0) {
		if (!ac0.from) return acVal;

		const acHashes = this.constructor._getItemHashesFromAcItem(ac0);
		if (!acHashes.length) return acVal;

		const acFromItemEffects = await this._pImportEntry_pFillData_Attributes_Ac_getAcFromItemEffects(mon, acHashes);

		// If the computed AC differs from the real AC, create an effect which fills the gap.
		if (acVal !== acFromItemEffects) {
			// N.B.: We *could* add a post-import step where we use a `preCreateItem` hook to then go back and
			//   update this effect by trying to find some sheet item (e.g. Cambion's "Fiendish Blessing") to connect
			//   this effect to. So the flow would be:
			//   - Create "Importing..." actor
			//   - Build data and update (which includes setting the effects)
			//   - Bind hook
			//   - Create sheet items, during which our hook fires and we may update the effect we previously created,
			//     by trying to match on the text in our items for "AC bonus"/etc.
			//   - Unbind hook
			//   - Update the actor again, to persist our changes to the effect (if we made any)
			//  This is a massive amount of complexity (not to mention performance loss) for something so
			//    trivial, however.
			monOpts.effects.push({
				label: "Other AC Bonus",
				icon: `modules/${SharedConsts.MODULE_NAME}/media/icon/mighty-force.svg`,
				changes: [
					{
						key: "data.attributes.ac.value",
						value: acVal - acFromItemEffects,
						mode: CONST.ACTIVE_EFFECT_MODES.ADD,
						priority: UtilActiveEffects.PRIORITY_BONUS,
					},
				],
				disabled: false,
				duration: {
					startTime: null,
					seconds: null,
					rounds: null,
					turns: null,
					startRound: null,
					startTurn: null,
				},
				origin: null,
				tint: "",
				transfer: false,
				flags: {},
			});
		}

		// Fall back on old behaviour (i.e. DAE compatibility) if we're not using item active effects
		if (!Config.get("importItem", "isAddActiveEffects")) return acFromItemEffects;

		// Return a base AC, and allow the active effects to fill out the rest
		return 10 + Parser.getAbilityModNumber(mon.dex);
	}

	async _pImportEntry_pFillData_Attributes_Ac_getAcFromItemEffects (mon, acHashes) {
		let acBase = 10;
		let acBonusShield = 0; // Only one shield can be used at a time--take the highest value we find
		let acBonusOther = 0;
		for (const acHash of acHashes) {
			const item = await Renderer.hover.pCacheAndGetHash(UrlUtil.PG_ITEMS, acHash);
			if (!item) continue;

			let bonusAc = Number(item.bonusAc || 0);
			if (isNaN(bonusAc)) bonusAc = 0;

			switch (item.bardingType || item.type) {
				case "HA": { // Heavy armor
					acBase = Math.max(acBase, (item.ac || 10) + bonusAc);
					break;
				}

				case "MA": { // Medium armor
					const maxDex = item.dexterityMax === null ? Number.MAX_SAFE_INTEGER : item.dexterityMax != null ? item.dexterityMax : 2;
					acBase = Math.max(acBase, (item.ac || 10) + Math.min(maxDex, Parser.getAbilityModNumber(mon.dex || 10)) + bonusAc);
					break;
				}

				case "LA": { // Light armor
					acBase = Math.max(acBase, (item.ac || 10) + Parser.getAbilityModNumber(mon.dex || 10) + bonusAc);
					break;
				}

				case "S": { // Shield
					acBonusShield = Math.max(acBonusShield, 2 + bonusAc);
					break;
				}

				default: {
					// If we're using Plutonium item effects (and so not in e.g. DAE compatibility mode), we can add our
					//   extra bonus AC defined on non-armor items here.
					// DAE won't/can't pick up non-armor items, so otherwise avoid adding this bonus to maintain
					//   compatibility.
					if (Config.get("importItem", "isAddActiveEffects")) acBonusOther += bonusAc;
				}
			}
		}

		return acBase + acBonusShield + acBonusOther;
	}

	_pImportEntry_fillData_Details (mon, data, monOpts) {
		const out = {};

		out.alignment = mon.alignment ? Parser.alignmentListToFull(mon.alignment).toLowerCase() : "";

		out.biography = {
			value: this._getBiographyValue(mon, monOpts.fluff, {isImportText: Config.get("importCreature", "isImportBio"), isImportImages: Config.get("importCreature", "isImportBioImages")}),
		};

		if (monOpts.spellLevel || monOpts.spellClass) {
			out.class = {
				level: monOpts.spellLevel,
				name: monOpts.spellClass,
			};
		} else {
			out.class = {};
		}

		out.spellLevel = monOpts.spellLevel;

		out.race = "";

		ImportListCreature._CREATURE_TYPES = ImportListCreature._CREATURE_TYPES || new Set(Object.keys(CONFIG.DND5E.creatureTypes));

		const typeInfo = Parser.monTypeToFullObj(mon.type);
		if (ImportListCreature._CREATURE_TYPES.has(typeInfo.type)) {
			out.type = {
				value: typeInfo.type,
				subtype: typeInfo.tags.join(", "),
				swarm: typeInfo.swarmSize ? UtilActors.VET_SIZE_TO_ABV[typeInfo.swarmSize] : null,
			};
		} else {
			out.type = {
				custom: Parser.monTypeToFullObj(mon.type).asText,
			};
		}

		out.environment = mon.environment ? mon.environment.sort(SortUtil.ascSortLower).map(it => it.toTitleCase()).join(", ") : "";

		const crNum = Parser.crToNumber(mon.cr);
		out.cr = crNum >= VeCt.CR_CUSTOM ? 0 : crNum;

		out.xp = {
			value: mon.cr ? (Parser.crToXpNumber(mon.cr) || 0) : 0,
		};

		out.source = DataConverter.getSourceWithPagePart(mon);

		data.details = out;
	}

	_pImportEntry_fillData_Skills (mon, data, monOpts) {
		data.skills = DataConverterCreature.getDataSkills(mon, data, monOpts);
	}

	_pImportEntry_fillData_Traits (mon, data, monOpts) {
		const out = {};

		out.size = UtilActors.VET_SIZE_TO_ABV[mon.size] || "med";

		const allLangs = new Set();
		let customLangs = [];

		if (mon.languages) {
			mon.languages.forEach(l => {
				const lMapped = UtilActors.VALID_LANGUAGES[l.toLowerCase().trim()];

				if (lMapped) allLangs.add(lMapped);
				else {
					allLangs.add("custom");
					customLangs.push(l);
				}
			});
		}

		out.languages = {
			value: [...allLangs],
			custom: customLangs.filter(Boolean).join("; "),
		};

		this._pImportEntry_fillConditionsDamage(mon, out);

		data.traits = out;
	}

	_pImportEntry_fillData_Currency (mon, data, monOpts) {
		// Dummy data
		data.currency = {
			pp: 0,
			gp: 0,
			ep: 0,
			sp: 0,
			cp: 0,
		};
	}

	_pImportEntry_fillData_Spells (mon, data, monOpts) {
		data.spells = DataConverterCreature.getDataSpells(mon, data, monOpts);
	}

	_pImportEntry_fillData_Resources (mon, data, monOpts) {
		const out = {};

		let legAct = 0;
		if (mon.legendary) legAct = mon.legendaryActions || 3;

		out.legact = {
			value: legAct,
			max: legAct,
		};

		let legRes = 0;
		if (mon.trait) {
			const legResTrait = mon.trait.find(it => /legendary resistance/gi.test(it.name || ""));
			if (legResTrait) {
				const m = /\((\d+)\/Day\)/i.exec(legResTrait.name);
				if (m) legRes = Number(m[1]);
			}
		}

		out.legres = {
			value: legRes,
			max: legRes,
		};

		out.lair = {
			value: !!(monOpts.legendaryMeta && monOpts.legendaryMeta.lairActions),
			initiative: 20, // is this always correct?
		};

		data.resources = out;
	}

	async _pImportEntry_pFillItems (mon, act, monOpts, importOpts) {
		// Do actions first, as they set the prof bonus if the creature has no CR
		await this._pImportEntry_pFillItems_pActions(mon, act, monOpts);
		await this._pImportEntry_pFillItems_pTraitsReactionsLegendaries(
			mon,
			act,
			monOpts,
			{
				prop: "trait",
				fnPreProcess: trait => {
					if (!/legendary resistance/gi.test(trait.name || "")) return;
					trait._plut_isLegendaryResistance = true;
				},
			},
			{
				consumeType: trait => trait._plut_isLegendaryResistance ? "attribute" : null,
				consumeTarget: trait => trait._plut_isLegendaryResistance ? "resources.legres.value" : null,
				consumeAmount: trait => trait._plut_isLegendaryResistance ? 1 : null,
			},
		);
		await this._pImportEntry_pFillItems_pTraitsReactionsLegendaries(mon, act, monOpts, {prop: "bonus"}, {
			activationType: "bonus",
			activationCost: 1,
		});
		await this._pImportEntry_pFillItems_pTraitsReactionsLegendaries(mon, act, monOpts, {prop: "reaction"}, {
			activationType: "reaction",
			activationCost: 1,
		});
		await this._pImportEntry_pFillItems_pLegendaryHeader(mon, act, monOpts);
		await this._pImportEntry_pFillItems_pTraitsReactionsLegendaries(
			mon,
			act,
			monOpts,
			{
				prop: "legendary",
				fnPreProcess: legEnt => {
					legEnt._plut_legActions = 1;
					if (legEnt.name) {
						legEnt.name = legEnt.name.replace(/\(costs (\d+) actions?\)/i, (...m) => {
							legEnt._plut_legActions = Number(m[1]);
							return "";
						}).trim();
					}
				},
			},
			{
				activationType: "legendary",
				activationCost: legEnt => legEnt._plut_legActions,
				consumeType: "attribute",
				consumeTarget: "resources.legact.value",
				consumeAmount: legEnt => legEnt._plut_legActions,
			},
		);
		await this._pImportEntry_pFillItems_pLairActions(mon, act, monOpts);
		await this._pImportEntry_pFillItems_pRegionalEffects(mon, act, monOpts);

		await this._pImportEntry_pFillItems_pInventory(mon, act, monOpts);

		const isTemporary = importOpts.isTemp || this._pack != null;
		await this._pImportEntry_pFillItems_pSpellcasting(mon, act, isTemporary, monOpts);

		await UtilActors.pAddActorItems(monOpts.actor, monOpts.items, {isTemporary});
	}

	/**
	 * @param mon Monster.
	 * @param monOpts Import options.
	 * @param act Actor update data.
	 * @param it Entry to process.
	 * @param opts Options object.
	 * @param opts.fvttType Foundry type.
	 * @param [opts.activationType] Foundry activation type.
	 * @param [opts.activationCost] Foundry action type (getter function or static value)
	 * @param opts.img Image path.
	 * @param [opts.description] Pre-rendered description.
	 */
	_pImportEntry_pFillItems_addTextOnlyItem (mon, monOpts, act, it, opts) {
		monOpts.items.push(DataConverter.getItemActorPassive(
			it,
			{
				...opts,
				mode: "creature",
				pb: monOpts.assumedPb,
				entity: mon,
				source: mon.source,
				summonSpellLevel: mon._summonedBySpell_level ?? mon._summonedBySpell_levelBase,
				actor: {data: act}, // wrap our update data to give the appearance of a real actor
			},
		));
	}

	async _pImportEntry_pFillItems_pLegendaryHeader (mon, act, monOpts) {
		let legAct = 0;
		if (mon.legendary) legAct = mon.legendaryActions || 3;
		if (!legAct) return;

		const description = `<div>${DataConverter.getWithDescriptionPlugins(() => Renderer.monster.getLegendaryActionIntro(mon))}</div>`;

		const img = await DataConverterCreature.pGetTraitReactionLegendaryImage(mon, {name: "Legendary Actions"}, monOpts);

		monOpts.items.push(DataConverter.getItemActorPassive(
			{name: "Legendary Actions"},
			{
				fvttType: "feat",
				description,
				mode: "creature",
				pb: monOpts.assumedPb,
				entity: mon,
				source: mon.source,
				summonSpellLevel: mon._summonedBySpell_level ?? mon._summonedBySpell_levelBase,
				actor: {data: act}, // wrap our update data to give the appearance of a real actor
				img,
			},
		));
	}

	/**
	 * @param mon Monster.
	 * @param act Actor data.
	 * @param monOpts Import metadata.
	 * @param opts Options object.
	 * @param opts.prop Monster property to loop through (if it exists)
	 * @param opts.img Image path.
	 * @param [opts.fnPreProcess]
	 * @param [optsFoundryData] Properties can be either values of functions.
	 * @param [optsFoundryData.activationType]
	 * @param [optsFoundryData.activationCost]
	 * @param [optsFoundryData.consumeType]
	 * @param [optsFoundryData.consumeTarget]
	 * @param [optsFoundryData.consumeAmount]
	 */
	async _pImportEntry_pFillItems_pTraitsReactionsLegendaries (mon, act, monOpts, opts, optsFoundryData) {
		optsFoundryData = optsFoundryData || {};

		if (!mon[opts.prop]) return;

		for (let ent of mon[opts.prop]) {
			if (opts.fnPreProcess) {
				ent = MiscUtil.copy(ent);
				opts.fnPreProcess(ent);
			}

			const nxtOpts = {};
			Object.entries(optsFoundryData)
				.forEach(([k, v]) => {
					if (typeof v === "function") nxtOpts[k] = v(ent);
					else nxtOpts[k] = MiscUtil.copy(v);
				});

			const img = await DataConverterCreature.pGetTraitReactionLegendaryImage(mon, ent, monOpts, {isLegendary: opts.prop === "legendary"});

			this._pImportEntry_pFillItems_addTextOnlyItem(
				mon,
				monOpts,
				act,
				ent,
				{
					...nxtOpts,
					fvttType: "feat",
					img,
				},
			);
		}
	}

	static _getLairActionName (strEnt) {
		const stripped = Renderer.stripTags(strEnt);

		let truncatedName = "Lair: ";
		const spl = stripped.split(" ")
		for (const pt of spl) {
			if ((truncatedName.length + pt.length) < 50) {
				truncatedName += `${pt} `;
			} else break;
		}
		if (stripped.length > truncatedName.trim().length) truncatedName += "...";
		else truncatedName = truncatedName.trim();

		return truncatedName;
	}

	async _pImportEntry_pFillItems_pLairActions (mon, act, monOpts) {
		if (!monOpts.legendaryMeta?.lairActions) return;

		const lairPart = monOpts.legendaryMeta.lairActions;
		const actionList = lairPart.find(it => it.type === "list");

		if (actionList) {
			for (let i = 0; i < actionList.items.length; ++i) {
				const it = actionList.items[i];
				await this._pImportEntry_pFillItems_lairActions_addLairAction(mon, act, monOpts, it, i);
			}
		} else {
			await this._pImportEntry_pFillItems_lairActions_addLairAction(mon, act, monOpts, lairPart);
		}
	}

	async _pImportEntry_pFillItems_lairActions_addLairAction (mon, act, monOpts, ent, ixAction) {
		if (ent instanceof Array) {
			if (typeof ent[0] === "string") {
				ent = {
					type: "entries",
					name: this.constructor._getLairActionName(ent[0]),
					entries: ent,
				};
			} else {
				// Fallback on making a generically-named lair action
				ent = {
					type: "entries",
					name: `Lair Action${ixAction != null ? ` ${ixAction + 1}` : ""}`,
					entries: ent,
				};
			}
		} else if (typeof ent === "string") {
			ent = {
				type: "entries",
				name: this.constructor._getLairActionName(ent),
				entries: [
					ent,
				],
			}
		} else throw new Error(`Unhandled lair action format!`);

		const strEntries = JSON.stringify(ent);

		const {
			saveAbility,
			saveScaling,
			saveDc,
		} = this._getSavingThrowData(mon, monOpts.assumedPb, strEntries);

		const damageTuples = [];
		let formula = "";
		if (typeof ent.entries[0] === "string") {
			const {damageTupleMetas} = DataConverter.getDamageTupleMetas(ent.entries[0], {summonSpellLevel: mon._summonedBySpell_level ?? mon._summonedBySpell_levelBase});
			const {damageParts, formula: formula_} = DataConverter.getDamagePartsAndOtherFormula(damageTupleMetas);
			damageTuples.push(...damageParts);
			formula = formula_;
		}

		const damageParts = this._getDamageTuplesWithMod(damageTuples);

		// Always name the action "Lair Actions" to match the SRD content
		const img = await DataConverterCreature.pGetLairActionImage(mon, {name: "Lair Actions"}, monOpts);

		this._pImportEntry_pFillItems_addTextOnlyItem(
			mon,
			monOpts,
			act,
			ent,
			{
				fvttType: "feat",
				activationType: "lair",
				activationCost: 1,
				img,
				saveAbility,
				saveDc,
				saveScaling,
				damageParts,
				formula,
			},
		);
	}

	async _pImportEntry_pFillItems_pRegionalEffects (mon, act, monOpts) {
		if (!monOpts.legendaryMeta?.regionalEffects) return;

		const regionalEntry = {
			type: "entries",
			name: "Regional Effects",
			entries: MiscUtil.copy(monOpts.legendaryMeta.regionalEffects),
		};

		const img = await DataConverterCreature.pGetLairActionImage(mon, regionalEntry, monOpts);

		this._pImportEntry_pFillItems_addTextOnlyItem(
			mon,
			monOpts,
			act,
			regionalEntry,
			{
				fvttType: "feat",
				img,
			},
		);
	}

	async _pImportEntry_pFillItems_pActions (mon, act, monOpts) {
		if (!mon.action) return;
		for (const action of mon.action) {
			// Split actions into multiple (fake) sub-actions in the case of e.g. multi-modal breath weapons.
			//   For any other action, a single sub-action, which is simply the original action, will be processed.
			const subActions = this._pImportEntry_pFillItems_getSubActions(action, monOpts);
			for (const subAction of subActions) {
				await this._pImportEntry_pFillItems_pAction(mon, act, subAction, monOpts);
			}
		}
	}

	_pImportEntry_pFillItems_getSubActions (action, monOpts) {
		if (!action.name || !action.entries) return [action];

		// region Breath weapons
		if (
			action.entries.length === 2
			&& typeof action.entries[0] === "string"
			&& action.entries[0].includes("following breath weapon")
			&& action.entries[1].type === "list"
			&& action.entries[1].items.every(li => li.name && li.entry)
		) {
			const out = [];
			const tmpItemIdParent = CryptUtil.uid();

			const breathActions = action.entries[1].items.map(li => {
				const cleanName = li.name.replace(/\.\s*$/, "");
				const itemName = `Breath Weapons: ${cleanName}`
				const tmpItemId = CryptUtil.uid();

				monOpts.postItemItemUpdates.push(async ({actor, isTemp, isPack, pack}) => {
					// Temp items don't have IDs, so we can't link anything up
					if (isTemp) return;

					const parentSheetItem = actor.items.find(it => it.data.flags?.[SharedConsts.MODULE_NAME_FAKE]?._tmpItemId === tmpItemIdParent);
					if (!parentSheetItem) return console.warn(...LGT, `Failed to find parent sheet item for item "${li.name}"`);

					const sheetItem = actor.items.find(it => it.data.flags?.[SharedConsts.MODULE_NAME_FAKE]?._tmpItemId === tmpItemId);
					if (!sheetItem) return console.warn(...LGT, `Failed to find sheet item for item "${li.name}"`);

					const update = {
						_id: sheetItem.id,
						data: {consume: {target: parentSheetItem.id}},
					};
					await actor.updateEmbeddedDocuments("Item", [update]);
				});

				return {
					name: itemName,
					_plut_cleanName: cleanName,
					entries: [
						li.entry,
					],
					_foundryData: {
						"consume.type": "charges",
						"consume.amount": 1,
					},
					_foundryFlags: {
						[SharedConsts.MODULE_NAME_FAKE]: {
							_tmpItemId: tmpItemId,
						},
					},
				}
			});

			out.push({
				name: action.name,
				entries: [
					`${action.entries[0].trim().replace(/[.,!?:]$/, "")}: ${breathActions.map(it => it._plut_cleanName).joinConjunct(", ", " or ")}`,
				],
				_foundryFlags: {
					[SharedConsts.MODULE_NAME_FAKE]: {
						_tmpItemId: tmpItemIdParent,
					},
				},
			});

			breathActions.forEach(it => delete it._plut_cleanName);
			out.push(...breathActions);

			return out;
		}
		// endregion

		// region Eye rays
		if (/^eye ray/gi.test(action.name) && action.entries.find(it => it.type === "list" && (it.items || []).every(li => li.type === "item"))) {
			const out = [];

			const cpy = MiscUtil.copy(action);
			const ixList = action.entries.findIndex(it => it.type === "list" && (it.items || []).every(li => li.type === "item"));
			const lst = action.entries[ixList];
			cpy.entries.splice(ixList, 1);

			out.push(cpy);
			lst.items.forEach(it => {
				const entRay = {
					...it,
					type: "entries",
					name: `Eye Ray: ${it.name.replace(/[.,!:?]\\s*$/, "")}`,
				};

				if (entRay.entry) {
					entRay.entries = [entRay.entry];
					delete entRay.entry;
				}

				delete entRay.style;

				out.push(entRay);
			});

			return out;
		}
		// endregion

		return [action];
	}

	async _pImportEntry_pFillItems_pAction (mon, act, action, monOpts) {
		const description = DataConverter.getEntryDescription(action, {summonSpellLevel: mon._summonedBySpell_level ?? mon._summonedBySpell_levelBase});
		const strEntries = action.entries ? JSON.stringify(action.entries) : null;

		const {
			damageTuples,
			formula,
			offensiveAbility,
			isAttack,
			rangeShort,
			rangeLong,
			actionType,
			isProficient,
			attackBonus,
			_foundryData,
			foundryData,
			_foundryFlags,
			foundryFlags,
			img,
		} = await DataConverterCreature.pGetParsedAction(mon, action, monOpts);

		const damageParts = [];
		damageParts.push(...this._getDamageTuplesWithMod(damageTuples, mon[offensiveAbility]));

		// region Saving throw
		const {
			saveAbility,
			saveScaling,
			saveDc,
		} = this._getSavingThrowData(mon, monOpts.assumedPb, strEntries);
		// endregion

		// If it was an attack, treat is as a weapon. Otherwise, treat it as a generic action.
		if (isAttack) {
			await this._pFillWeaponItem(
				mon,
				act,
				action,
				monOpts,
				{
					offensiveAbility,
					damageParts,
					formula,
					rangeShort,
					rangeLong,
					actionType,
					isProficient,
					description,
					saveAbility,
					saveDc,
					saveScaling,
					attackBonus,
					_foundryData,
					foundryData,
					_foundryFlags,
					foundryFlags,
					img,
					isMagical: (mon.traitTags || []).includes("Magic Weapons"),
				},
			);
		} else {
			this._pImportEntry_pFillItems_addTextOnlyItem(
				mon,
				monOpts,
				act,
				action,
				{
					fvttType: "feat",
					activationType: "action",
					activationCost: 1,
					description,
					saveAbility,
					saveDc,
					saveScaling,
					damageParts,
					formula,
					attackBonus,
					_foundryData,
					foundryData,
					_foundryFlags,
					foundryFlags,
					img,
				},
			);
		}
	}

	_getSavingThrowData (mon, assumedMonProf, strEntries) {
		let {
			saveAbility,
			saveScaling,
			saveDc,
			isFoundParse,
		} = super._getSavingThrowData(strEntries);

		if (isFoundParse) {
			// Try to find an ability to link the scaling to
			//   Order of preference: cha > wis > int > str > dex > con
			//   (Rough heuristic of which saving throw sources are the most common)
			if (assumedMonProf) {
				const fromAbil = saveDc - assumedMonProf - 8;
				for (const abil of ["cha", "wis", "int", "str", "dex", "con"]) {
					const mod = Parser.getAbilityModNumber(mon[abil]);
					if (mod === fromAbil) {
						saveScaling = abil;
						break;
					}
				}
			}
		}

		return {saveAbility, saveScaling, saveDc}
	}

	_getDamageTuplesWithMod (damageTuples, abilityScore) {
		if (!damageTuples.length) return [];

		damageTuples = MiscUtil.copy(damageTuples);

		if (abilityScore) {
			// Try to switch generic +damage for +@mod if the value match
			damageTuples.forEach(dmgTuple => {
				const abMod = Parser.getAbilityModNumber(abilityScore);
				const mBonus = /(\d+d\d+\s*)([-+]\s*\d+)([ -+].*)?$/.exec(dmgTuple[0]);
				if (!mBonus) return;

				const fromAbil = Number(mBonus[2].replace(/\s*/g, ""));
				if (fromAbil === abMod) dmgTuple[0] = `${mBonus[1]} + @mod${mBonus[3] || ""}`;
			});
		}

		return damageTuples.filter(it => it.length);
	}

	static _getItemHashesFromAcItem (ac) {
		const out = []; // Use a list to maintain order

		if (!ac.from || !ac.from.length) return out;

		ac.from.forEach(from => {
			const hashes = this._getItemHashesFromString(from);
			hashes.forEach(hash => {
				if (!out.includes(hash)) out.push(hash)
			});
		});

		return out;
	}

	static _getItemHashesFromString (str) {
		const out = []; // Use a list to maintain order

		str.replace(/{@item ([^}]+)}/gi, (...m) => {
			const [name, source] = m[1].trim().toLowerCase().split("|");

			const hash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ITEMS]({
				name,
				source: source || SRC_DMG,
			});

			if (!out.includes(hash)) out.push(hash);
		});

		return [...out];
	}

	async _pImportEntry_pFillItems_pInventory (mon, act, dataBuilderOpts) {
		const itemHashes = new Set();
		const itemHashesEquipped = new Set();

		(mon.ac || [])
			.forEach((ac, i) => {
				const acHashes = this.constructor._getItemHashesFromAcItem(ac);
				acHashes.forEach(hash => itemHashes.add(hash));
				if (i === 0) acHashes.forEach(hash => itemHashesEquipped.add(hash)); // Assume the creature has its first listed armor set equipped
			});

		for (const itemHash of itemHashes) {
			const item = await Renderer.hover.pCacheAndGetHash(UrlUtil.PG_ITEMS, itemHash);
			if (!item) continue;

			await this._pImportEntry_pFillItems_pAddItem(mon, act, item, dataBuilderOpts, {isEquipped: itemHashesEquipped.has(itemHash)});
		}

		// Do these after, to avoid duplicating AC items
		const itemHashesFromTraits = (mon.trait || [])
			.filter(it => it.name && it.entries && /equipment/i.test(it.name.trim()))
			.map(it => this.constructor._getItemHashesFromString(JSON.stringify(it.entries)))
			.flat()
			.filter(hash => !itemHashes.has(hash));
		for (const itemHash of itemHashesFromTraits) {
			const item = await Renderer.hover.pCacheAndGetHash(UrlUtil.PG_ITEMS, itemHash);
			if (!item) continue;

			// Skip any weapons, as we assume the actions step will take care of these.
			if (item.type === "S" || item.type === "R" || item.weaponCategory) continue;

			await this._pImportEntry_pFillItems_pAddItem(mon, act, item, dataBuilderOpts, {isEquipped: true});
		}
	}

	/**
	 * @param mon
	 * @param act
	 * @param item
	 * @param dataBuilderOpts
	 * @param [opts]
	 * @param [opts.isEquipped]
	 */
	async _pImportEntry_pFillItems_pAddItem (mon, act, item, dataBuilderOpts, opts) {
		opts = opts || {};

		const fluff = await Renderer.item.pGetFluff(item);
		const itemData = await DataConverterItem.pGetItemItem(item, {isActorItem: true, fluff, size: mon.size, isEquipped: opts.isEquipped, dexMod: Parser.getAbilityModNumber(mon.dex)});

		dataBuilderOpts.items.push(itemData);
	}

	async _pImportEntry_pFillItems_pSpellcasting (mon, act, isTemporary, monOpts) {
		if (!mon.spellcasting) return;

		const spellHashToItemPosMasterMap = {};

		const copyToMasterMap = (tempMap) => {
			// Copy any hashes which don't already exist into the master map--just use the first spell when linking
			Object.entries(tempMap).forEach(([k, v]) => {
				spellHashToItemPosMasterMap[k] = spellHashToItemPosMasterMap[k] || v;
			});
		};

		// Assume creatures (i.e. npcs) always prepare all their spells
		const isPrepared = true;

		const exhaustedCopies = [];
		for (const spellcastingRaw of mon.spellcasting) {
			const spellcasting = MiscUtil.copy(spellcastingRaw);

			// region Handle slot-based/pact casters
			if (spellcasting.spells) {
				const spellLevels = Object.keys(spellcasting.spells);

				for (const spellLevel of spellLevels) {
					const levelMeta = spellcasting.spells[spellLevel];

					const preparationMode = levelMeta.lower != null ? "pact" : "prepared";

					const tempMap = await DataConverterSpell.pGetSpellHashToItemPosMapAndFillSpells(
						monOpts.actor,
						JSON.stringify(levelMeta.spells),
						{
							isTemporary,
							optsGetSpellItem: {
								abilityAbv: monOpts.spellAbility,
								isPrepared,
								preparationMode,
							},
						},
					);
					copyToMasterMap(tempMap);
				}

				delete spellcasting.spells;
			}
			// endregion

			// region Handle recharge-based casters
			if (spellcasting.weekly) {
				await this._pImportEntry_pFillItems_pSpellcasting_recharge(
					monOpts,
					spellcasting,
					"weekly",
					copyToMasterMap,
					{
						isTemporary,
						isPrepared,
						usesPer: "charges", // Convert "weekly" to "charges" as Foundry doesn't have "weekly"
					},
				);
			}

			if (spellcasting.daily) {
				await this._pImportEntry_pFillItems_pSpellcasting_recharge(
					monOpts,
					spellcasting,
					"daily",
					copyToMasterMap,
					{
						isTemporary,
						isPrepared,
						usesPer: "day",
					},
				);
			}

			if (spellcasting.rest) {
				await this._pImportEntry_pFillItems_pSpellcasting_recharge(
					monOpts,
					spellcasting,
					"daily",
					copyToMasterMap,
					{
						isTemporary,
						isPrepared,
						usesPer: "sr", // Convert "rest" to "short rest," as this is a subset of a long rest
					},
				);
			}

			if (spellcasting.will) {
				const preparationMode = "innate";

				const tempMap = await DataConverterSpell.pGetSpellHashToItemPosMapAndFillSpells(
					monOpts.actor,
					JSON.stringify(spellcasting.will),
					{
						isTemporary,
						optsGetSpellItem: {
							abilityAbv: monOpts.spellAbility,
							isPrepared,
							preparationMode,
						},
					},
				);
				copyToMasterMap(tempMap);

				delete spellcasting.will;
			}

			if (spellcasting.constant) {
				const preparationMode = "innate";

				const tempMap = await DataConverterSpell.pGetSpellHashToItemPosMapAndFillSpells(
					monOpts.actor,
					JSON.stringify(spellcasting.constant),
					{
						isTemporary,
						optsGetSpellItem: {
							abilityAbv: monOpts.spellAbility,
							isPrepared,
							preparationMode,
							// Give "constant" effects a permanent duration
							durationAmount: null,
							durationUnit: "perm",
						},
					},
				);
				copyToMasterMap(tempMap);

				delete spellcasting.constant;
			}
			// endregion

			exhaustedCopies.push(spellcasting);
		}

		// Handle any leftover spells that we did't pick up above
		await DataConverterSpell.pGetSpellHashToItemPosMapAndFillSpells(
			monOpts.actor,
			JSON.stringify(exhaustedCopies),
			{
				isTemporary,
				hashToIdMap: spellHashToItemPosMasterMap,
				optsGetSpellItem: {
					abilityAbv: monOpts.spellAbility,
				},
			},
		);

		// region Spellcasting traits
		const hook = DataConverterSpell.doHookSpellLinkRender.bind(null, monOpts.actor.id, spellHashToItemPosMasterMap);
		await DataConverter.pGetWithDescriptionPlugins(async () => {
			await Renderer.get().pWithPlugin({
				entryType: "link",
				pluginType: "*",
				fnPlugin: hook,
				pFn: async () => {
					const traits = Renderer.monster.getSpellcastingRenderedTraits(Renderer.get(), mon);
					for (const trait of traits) {
						const img = await DataConverterCreature.pGetSpellcastingImage(mon, trait, monOpts);
						this._pImportEntry_pFillItems_addTextOnlyItem(
							mon,
							monOpts,
							act,
							trait,
							{
								fvttType: "feat",
								img,
								description: trait.rendered,
							},
						);
					}
				},
			});
		});
		// endregion
	}

	async _pImportEntry_pFillItems_pSpellcasting_recharge (monOpts, spellcasting, prop, cbCopyToMasterMap, {isTemporary, isPrepared, usesPer}) {
		const preparationMode = "innate";

		for (let uses = 1; uses <= 9; ++uses) {
			const keyEach = `${uses}e`;

			// Note that Foundry doesn't yet (2020-05-01) support groups of multiple "X/Time" spells having a single
			//   charge between them, so just treat them all as being independent.
			const allSpells = [
				...((spellcasting[prop] || {})[uses] || []),
				...((spellcasting[prop] || {})[keyEach] || []),
			];

			if (!allSpells.length) continue;

			const tempMap = await DataConverterSpell.pGetSpellHashToItemPosMapAndFillSpells(
				monOpts.actor,
				JSON.stringify(allSpells),
				{
					isTemporary,
					optsGetSpellItem: {
						abilityAbv: monOpts.spellAbility,
						isPrepared,
						preparationMode,
						usesCurrent: uses,
						usesMax: uses,
						usesPer,
					},
				},
			);
			cbCopyToMasterMap(tempMap);
		}

		delete spellcasting[prop];
	}

	static _getCasterLevelFromEntry (casting) {
		let casterLevel = 0;
		if (casting.headerEntries) {
			JSON.stringify(casting.headerEntries).replace(/an? (\d+)[A-Za-z]+-level/i, (...m) => {
				const lvl = Number(m[1]);
				if (!isNaN(lvl)) casterLevel = lvl;
			});
		}
		return casterLevel;
	}

	static _getCasterClassFromEntry (casting) {
		let casterClass = "";
		if (casting.headerEntries) {
			JSON.stringify(casting.headerEntries).replace(/(?:^| )(bard|cleric|druid|paladin|ranger|sorcerer|warlock|wizard)(?:\W|$)?/i, (...m) => {
				casterClass = m[1];
			});
		}
		return casterClass;
	}
}

ImportListCreature._CREATURE_TYPES = null;

ImportListCreature.ImportEntryOpts = class extends ImportListActor.ImportEntryOpts {
	constructor (opts) {
		opts = opts || {};
		super(opts);
		this.mon = opts.mon;

		this.pb = Parser.crToPb(this.mon.cr);
		this.assumedPb = this.pb;
		this.legendaryMeta = DataUtil.monster.getMetaGroup(this.mon);

		this.spellAbility = "";
		this.spellDc = 8;
		this.spellLevel = 0;
		this.spellClass = "";
		this._initSpellcasterData(this.mon);

		this._initAssumedPb(this.mon);
	}

	_initAssumedPb (mon) {
		if (!mon.action) return;
		// This will update the `assumedPb` data if required.
		mon.action.forEach(action => DataConverterActor.getParsedActionEntryData(mon, action, this, {mode: "creature", summonSpellLevel: mon._summonedBySpell_level ?? mon._summonedBySpell_levelBase}));
	}

	_initSpellcasterData (mon) {
		if (!mon.spellcasting) return;

		const withAbils = mon.spellcasting
			.filter(it => it.ability)
			.map(it => {
				if (Parser.ABIL_ABVS.includes(it.ability)) return it;

				// If e.g. badly-written homebrew
				const abv = it.ability.substring(0, 3);
				if (Parser.ABIL_ABVS.includes(abv)) {
					it.ability = abv;
					return it;
				}

				return null;
			})
			.filter(Boolean);

		if (withAbils.length) {
			let withAbil = withAbils[0];

			// As the sheet does not support multiple spellcasting traits with different abilities (e.g. Zephyros
			//   from SKT), choose a spellcasting trait to be used as the single representative.
			if (withAbils.length > 1) {
				switch (Config.get("importCreature", "spellcastingPrimaryTraitMode")) {
					// "Most spells" mode
					case 1: {
						withAbil = withAbils.sort((a, b) => {
							const spellCountA = this.constructor._getSpellCount(a);
							const spellCountB = this.constructor._getSpellCount(b);
							return SortUtil.ascSort(spellCountB, spellCountA);
						})[0];

						break;
					}

					// "Maximum ability score" mode
					case 2: {
						withAbil = withAbils.sort((a, b) => SortUtil.ascSort(Parser.getAbilityModNumber(mon[b.ability]), Parser.getAbilityModNumber(mon[a.ability])))[0];

						break;
					}
				}
			}

			const ab = withAbil.ability;
			const abMod = Parser.getAbilityModNumber(mon[ab]);

			this.spellAbility = ab;
			this.spellDc = abMod + this.pb;
		}

		this.spellLevel = mon.spellcasting.map(sc => ImportListCreature._getCasterLevelFromEntry(sc)).find(Boolean) || 0;
		this.spellClass = mon.spellcasting.map(sc => ImportListCreature._getCasterClassFromEntry(sc)).find(Boolean) || "";
	}

	static _getSpellCount (spellcastingItem) {
		// A hacky heuristic--simply count the number of @spell tags in the spell block. Future-proof, and
		//   mostly accurate!
		let cnt = 0;
		JSON.stringify(spellcastingItem).replace(/{@spell [^}]+}/g, () => {
			cnt++;
			return "";
		});
		return cnt;
	}
}

// TODO factor this and `ImportListSpell.DetailPreparation` out
ImportListCreature.ScaleRename = class extends Application {
	/**
	 * @param dataList
	 * @param resolve
	 * @param opts Options object.
	 * @param opts.titleSearch Used in prompt text in the search bar.
	 */
	constructor (dataList, resolve, opts) {
		super({
			title: "Scale and Rename Creatures",
			template: `${SharedConsts.MODULE_LOCATION}/template/ImportListCreatureScaleRename.hbs`,
			width: 960,
			height: Util.getMaxWindowHeight(),
			resizable: true,
		});
		this._dataList = dataList;
		this._resolve = resolve;

		this._titleSearch = opts.titleSearch;

		this._list = null;
		this._$btnReset = null;
	}

	getData () {
		return {
			titleSearch: this._titleSearch,
			rows: this._dataList.map((it, ix) => ({
				name: it.name,
				source: it.source,
				sourceShort: Parser.sourceJsonToAbv(it.source),
				sourceLong: Parser.sourceJsonToFull(it.source),
				sourceClassName: Parser.sourceJsonToColor(it.source),
				cr: Parser.monCrToFull(it.cr),
				isAdjustableCr: Parser.crToNumber(it.cr) < VeCt.CR_CUSTOM,
				isAdjustableSummonSpellLevel: it._summonedBySpell_levelBase != null,
				availableSummonSpellLevels: it._summonedBySpell_levelBase != null
					? [...Renderer.monster.getSelSummonSpellLevel(it).options].map(it => Number(it.value)).filter(it => ~it)
					: null,
				ix,
			})),
		};
	}

	activateListeners ($html) {
		super.activateListeners($html);

		// Init list library
		this._list = new List({
			$iptSearch: $html.find(`.search`),
			$wrpList: $html.find(`.veapp__list`),
			valueNames: ["name", "source", "cr", "ix"],
		});
		SortUtil.initBtnSortHandlers($html.find(`[data-name="wrp-btns-sort"]`), this._list);
		this._list.doAbsorbItems(
			this._dataList,
			{
				fnGetName: it => it.name,
				fnGetValues: it => ({source: it.source, cr: Parser.monCrToFull(it.cr)}),
			},
		);
		this._list.init();

		this._$btnReset = $html.find(`[name="btn-reset"]`).click(() => {
			$html.find(`.search`).val("");
			if (this._list) this._list.reset();
		});

		$html.find(`[name="btn-run"]`).click(async () => {
			const listValues = this._list.items.map(it => {
				const $e = $(it.ele);
				const $selCr = $e.find(`[name="sel-cr"]`);
				const $selSummonSpellLevel = $e.find(`[name="sel-summon-spell-level"]`)
				return {
					ix: it.ix,
					targetCr: $selCr.length ? Number($selCr.val()) === -1 ? null : Number($selCr.val()) : null,
					targetSpellLevel: $selSummonSpellLevel.length ? Number($selSummonSpellLevel.val()) === -1 ? null : Number($selSummonSpellLevel.val()) : null,
					rename: $e.find(`[name="ipt-rename"]`).val().trim(),
				};
			});
			const scaledMons = await Promise.all(listValues.map(async ({ix, targetCr, targetSpellLevel, rename}) => {
				let mon = this._dataList[ix];

				if (targetCr != null && targetCr !== Parser.crToNumber(mon.cr)) mon = await ScaleCreature.scale(mon, targetCr);
				else if (targetSpellLevel != null) mon = await ScaleSummonCreature.scale(mon, targetSpellLevel);
				if (rename) mon = ImportListCreature.ScaleRename._getRenamedMon(mon, rename);

				return mon;
			}));
			this._resolve(scaledMons);
			await this.close();
		});

		// Reset list to initial state
		if (this._$btnReset) this._$btnReset.click();
	}

	static _getRenamedMon (mon, newName) {
		ImportListCreature.ScaleRename._WALER_RENAME = ImportListCreature.ScaleRename._WALER_RENAME || MiscUtil.getWalker();

		mon = MiscUtil.copy(mon);

		const newJsonName = newName
			.replace(/\\/g, `\\\\`) // escape backslashes
			.replace(/"/g, `\\"`) // escape quotes
		;

		const reName = new RegExp(`(^|[^a-zA-Z0-9])${mon.name.escapeRegexp()}([^a-zA-Z0-9]|$)`, "gi")

		const applyTo = (prop) => {
			mon[prop] && mon[prop].forEach(it => {
				if (it.entries) {
					it.entries = ImportListCreature.ScaleRename._WALER_RENAME.walk(
						it.entries,
						{
							string: (str) => str.replace(reName, (...m) => `${m[1]}${newJsonName}${m[2]}`),
						},
					);
				}
				if (it.headerEntries) {
					it.headerEntries = ImportListCreature.ScaleRename._WALER_RENAME.walk(
						it.headerEntries,
						{
							string: (str) => str.replace(reName, (...m) => `${m[1]}${newJsonName}${m[2]}`),
						},
					);
				}
			})
		};

		applyTo("action");
		applyTo("bonus");
		applyTo("reaction");
		applyTo("trait");
		applyTo("legendary");
		applyTo("variant");

		mon._displayName = newName;

		return mon;
	}

	async close () {
		this._resolve(null);
		return super.close()
	}
};
ImportListCreature.ScaleRename._WALER_RENAME = null;

export {ImportListCreature};
