import {Vetools} from "./Vetools.js";
import {UtilActors} from "./UtilActors.js";
import {Config} from "./Config.js";
import {SharedConsts} from "../shared/SharedConsts.js";
import {UtilApplications} from "./UtilApplications.js";
import {LGT} from "./Util.js";
import {UtilEvents} from "./UtilEvents.js";
import {DataConverter} from "./DataConverter.js";
import {UtilCompendium} from "./UtilCompendium.js";

class DataConverterSpell {
	/**
	 *
	 * @param spell The spell entity.
	 * @param [opts] Options object.
	 * @param [opts.abilityAbv] A creature's spellcasting ability attribute (abbreviation).
	 * @param [opts.isAddPermission]
	 * @param [opts.isActorItem]
	 * @param [opts.isPrepared]
	 * @param [opts.preparationMode]
	 * @param [opts.usesCurrent]
	 * @param [opts.usesMax]
	 * @param [opts.usesPer]
	 * @param [opts.consumeType]
	 * @param [opts.consumeAmount]
	 * @param [opts.consumeTarget]
	 * @param [opts.durationAmount]
	 * @param [opts.durationUnit]
	 * @param [opts.castAtLevel] A level to force the spell to cast at *if it is an innate spell* (i.e., scaling dice
	 * will be set appropriately, as one cannot choose the casting level for innate spells).
	 *
	 * These parameters are used to link Charactermancer spells to their respective classes when re-loading the
	 * Charactermancer in order to level up.
	 * @param [opts.parentClassName]
	 * @param [opts.parentClassSource]
	 * @param [opts.parentSubclassName]
	 * @param [opts.parentSubclassSource]
	 *
	 * @return {object} Item data.
	 */
	static async pGetSpellItem (spell, opts) {
		opts = opts || {};

		const description = this._getDescription(spell);

		const entriesStr = JSON.stringify(spell.entries);

		// region Prepared
		// Assume cantrips are always prepared
		let preparationMode = spell.level === 0 ? "always" : opts.preparationMode || "prepared";
		let isPrepared = spell.level === 0 ? true : !!opts.isPrepared;

		if (Config.get("importSpell", "isSpellPoints") && spell.level !== 0 && (preparationMode === "prepared" || preparationMode === "always")) {
			// Avoid "spell slot" controls in spell points mode
			preparationMode = "atwill";
		}
		// endregion

		let actionType = "";
		if (spell.miscTags && spell.miscTags.includes("HL")) actionType = "heal";
		if (spell.savingThrow && spell.savingThrow.length) actionType = "save";
		if (entriesStr.toLowerCase().includes("melee spell attack")) actionType = "msak";
		if (entriesStr.toLowerCase().includes("ranged spell attack")) actionType = "rsak";
		actionType = actionType || "util";

		const school = UtilActors.VET_SPELL_SCHOOL_TO_ABV[spell.school] || "";
		const materials = spell.components && spell.components.m ? spell.components.m !== true ? `${spell.components.m.text || spell.components.m}` : "" : "";

		let durationVal = 0;
		let durationUnit = "";
		const duration0 = spell.duration[0];
		switch (duration0.type) {
			case "instant": durationUnit = "inst"; break;
			case "timed": {
				switch (duration0.duration.type) {
					case "turn": durationUnit = "turn"; durationVal = duration0.duration.amount; break;
					case "round": durationUnit = "round"; durationVal = duration0.duration.amount; break;
					case "minute": durationUnit = "minute"; durationVal = duration0.duration.amount; break;
					case "hour": durationUnit = "hour"; durationVal = duration0.duration.amount; break;
					case "day": durationUnit = "day"; durationVal = duration0.duration.amount; break;
					case "week": durationUnit = "day"; durationVal = duration0.duration.amount * 7; break;
					case "year": durationUnit = "year"; durationVal = duration0.duration.amount; break;
				}
				break;
			}
			case "permanent": durationUnit = "perm"; break;
			case "special": durationUnit = "spec"; break;
		}

		let rangeSize = 0;
		let rangeUnits = "";
		let targetSize = 0;
		let targetUnits = "";
		let targetType = "";
		switch (spell.range.type) {
			case RNG_SPECIAL: rangeUnits = "spec"; break;
			case RNG_POINT: {
				const dist = spell.range.distance;
				switch (dist.type) {
					case RNG_SELF: {
						targetUnits = "self";
						targetType = "self";
						rangeUnits = "self";
						break;
					}
					case RNG_UNLIMITED:
					case RNG_UNLIMITED_SAME_PLANE:
					case RNG_SIGHT:
					case RNG_SPECIAL: {
						targetUnits = "spec";
						rangeUnits = "spec";
						break;
					}
					case RNG_TOUCH: {
						targetUnits = "touch";
						rangeUnits = "touch";
						break;
					}
					case UNT_MILES: {
						// TODO(Future) change units to "km" if/when Foundry 5e supports it
						rangeSize = Config.getMetricNumber("importSpell", dist.amount, UNT_MILES);
						rangeUnits = "mi";
						break;
					}
					case UNT_FEET:
					default: {
						// TODO(Future) change units to "m" if/when Foundry 5e supports it
						rangeSize = Config.getMetricNumber("importSpell", dist.amount, UNT_FEET);
						rangeUnits = "ft";
						break;
					}
				}
				break;
			}
			case RNG_LINE:
			case RNG_CUBE:
			case RNG_CONE:
			case RNG_RADIUS:
			case RNG_SPHERE:
			case RNG_HEMISPHERE:
			case RNG_CYLINDER: {
				targetSize = Config.getMetricNumber("importSpell", spell.range.distance.amount, spell.range.distance.type);

				if (spell.range.distance.type === "miles") targetUnits = "mi";
				else targetUnits = "ft";

				if (spell.range.type === RNG_HEMISPHERE) targetType = "sphere";
				else targetType = spell.range.type; // all others map directly to FVTT values
			}
		}

		let damageParts = [];
		let cantripScaling = null;
		let scaling = null;
		let isCustomDamageParts = false; // Avoid using SRD damage if this is set

		if (spell.scalingLevelDice) { // cantrips with parsed out values
			const scalingLevelDice = [spell.scalingLevelDice].flat(); // convert object version to array

			const getLowestKey = scaling => Math.min(...Object.keys(scaling).map(k => Number(k)));
			const reDamageType = new RegExp(`(${UtilActors.VALID_DAMAGE_TYPES.join("|")})`, "i");

			damageParts.push(...scalingLevelDice.map(scl => {
				const lowKey = getLowestKey(scl.scaling);
				const lowDice = scl.scaling[lowKey];

				const mDamageType = reDamageType.exec(scl.label || "");

				return [
					(lowDice || "").replace(/{{spellcasting_mod}}/g, "@mod"),
					mDamageType ? mDamageType[1].toLowerCase() : null,
				]
			}));

			// TODO(Future): fix this for e.g. Toll the Dead if Foundry supports multiple scaling damage dice in future
			const firstScaling = scalingLevelDice[0];
			const lowKey = getLowestKey(firstScaling.scaling);
			cantripScaling = firstScaling.scaling[lowKey];
		} else {
			const damageTuples = []; // (<damage_dice>, <damage_type>)

			if (spell.damageInflict && spell.damageInflict.length) {
				// Try to extract cantrip scaling first
				if (entriesStr.toLowerCase().includes("when you reach 5th level")) {
					const diceTiers = [];
					// Find cantrip scaling values, which are shown in brackets
					entriesStr.replace(/\({@damage ([^}]+)}\)/g, (...m) => diceTiers.push(m[1]));
					// Cantrips scale at levels 5, 11, and 17
					if (diceTiers.length === 3) {
						// Find dice _not_ in brackets
						const baseVal = /(?:^|[^(]){@damage ([^}]+)}(?:[^)]|$)/.exec(entriesStr);
						if (baseVal) cantripScaling = baseVal[1];
						// failing that, just use the first bracketed value
						else cantripScaling = diceTiers[0];
					}
				}

				this._pGetSpellItem_parseAndAddDamage(entriesStr, damageTuples);
			}

			if (spell.miscTags && spell.miscTags.some(str => str === "HL")) {
				const healingTuple = ["", "healing"];

				// Arbitrarily pick the first dice expression we find
				entriesStr.replace(this._getReDiceYourSpellcastingMod(), (...m) => {
					const [, dicePart, modPart] = m;

					healingTuple[0] = dicePart;
					if (modPart) healingTuple[0] = `${healingTuple[0]} + @mod`;
				});

				damageTuples.push(healingTuple);
			}

			if (spell.entriesHigherLevel) {
				const reHigherLevel = /{@(?:scaledice|scaledamage) ([^}]+)}/gi;

				// For innate spells cast at a higher level, override the damage tuples with the upscaled version of the
				//   spell. We do this as Foundry doesn't let you choose the spell level when casting an innate spell.
				if (opts.castAtLevel != null && opts.castAtLevel !== spell.level && damageTuples.length && preparationMode === "innate") {
					const ixsScaled = new Set(); // Track which we've scaled, to avoid scaling the same tuple multiple times

					JSON.stringify(spell.entriesHigherLevel).replace(reHigherLevel, (...m) => {
						const [base] = m[1].split("|").map(it => it.trim());

						const [tag, text] = Renderer.splitFirstSpace(m[0].slice(1, -1));
						const scaleOptions = Renderer.parseScaleDice(tag, text);

						const ixDamageTuple = damageTuples.findIndex(it => (it[0] || "").trim().toLowerCase() === base.toLowerCase());
						if (!ixsScaled.has(ixDamageTuple) && ~ixDamageTuple) {
							const diceAtLevel = scaleOptions?.prompt?.options?.[opts.castAtLevel];
							if (diceAtLevel) {
								ixsScaled.add(ixDamageTuple);
								damageTuples[ixDamageTuple][0] += `+ ${diceAtLevel}`;
								isCustomDamageParts = true;
							}
						}
					});
				} else {
					// FIXME(future) FVTT doesn't yet support progression in non-linear increments, or multiple scaling
					//   modes, so just pick the first one
					JSON.stringify(spell.entriesHigherLevel).replace(reHigherLevel, (...m) => {
						if (scaling) return;

						const [, , addPerProgress] = m[1].split("|");
						scaling = addPerProgress;
					});
				}
			}

			// Do a final step to scoop up any damage info we might have missed, if we have yet to find any
			if (!damageTuples.length) this._pGetSpellItem_parseAndAddDamage(entriesStr, damageTuples);

			damageParts.push(...damageTuples);
		}
		damageParts = damageParts.filter(Boolean);

		let formula = "";
		// If there are no damage parts, arbitrarily pick the first dice expression and use that as the "other formula"
		if (!damageParts.length) {
			entriesStr.replace(this._getReDiceYourSpellcastingMod(), (...m) => {
				const [, dicePart, modPart] = m;

				formula = dicePart;
				if (modPart) formula = `${formula} + @mod`;
			});
		}

		let savingThrow = "";
		if (spell.savingThrow && spell.savingThrow.length) savingThrow = spell.savingThrow[0].substring(0, 3).toLowerCase();

		let saveScaling = "spell";
		if (opts.abilityAbv) saveScaling = opts.abilityAbv;

		// region Resource consumption
		let consumeType = opts.consumeType ?? "";
		let consumeTarget = opts.consumeTarget ?? null;
		let consumeAmount = opts.consumeAmount ?? null;

		if (Config.get("importSpell", "isSpellPoints") && spell.level !== 0) {
			consumeType = "attribute";
			consumeTarget = Config.get("importSpell", "spellPointsResource");
			consumeAmount = Parser.spLevelToSpellPoints(spell.level);
		}
		// endregion

		const img = (await DataConverter.pGetIconImage("spell", spell))
			|| (await UtilCompendium.pGetCompendiumImage("spell", spell))
			|| `modules/${SharedConsts.MODULE_NAME}/media/icon/scroll-unfurled.svg`;

		// region Apply data from the SRD compendium, if available
		const srdData = await UtilCompendium.getSrdCompendiumEntity("spell", spell);
		if (srdData) {
			targetSize = MiscUtil.get(srdData, "data", "target", "value") || targetSize;
			targetUnits = MiscUtil.get(srdData, "data", "target", "units") || targetUnits;
			targetType = MiscUtil.get(srdData, "data", "target", "type") || targetType;
			if (!isCustomDamageParts) damageParts = MiscUtil.get(srdData, "data", "damage", "parts") || damageParts;
		}
		// endregion

		const additionalData = await this._pGetAdditionalData(spell, targetUnits);

		// region Apply custom options data
		if (opts.durationAmount !== undefined) durationVal = opts.durationAmount;
		if (opts.durationUnit !== undefined) durationUnit = opts.durationUnit;
		// endregion

		const out = {
			name: UtilApplications.getCleanEntityName(DataConverter.getNameWithSourcePart(spell, {isActorItem: opts.isActorItem})),
			type: "spell",
			data: {
				source: DataConverter.getSourceWithPagePart(spell),
				description: {value: description, chat: "", unidentified: ""},

				actionType: actionType,
				level: spell.level,
				school: school,
				components: {
					value: "",
					vocal: spell.components && spell.components.v,
					somatic: spell.components && spell.components.s,
					material: !!(spell.components && spell.components.m),
					ritual: spell.meta && spell.meta.ritual,
					concentration: !!MiscUtil.get(spell, "duration", "0", "concentration"),
				},
				materials: {
					value: materials,
					consumed: !!MiscUtil.get(spell, "components", "m", "consume"),
					cost: Math.round((MiscUtil.get(spell, "components", "m", "cost") || 0) / 100),
					supply: 0,
				},
				target: {value: targetSize, units: targetUnits, type: targetType},
				range: {value: rangeSize, units: rangeUnits, long: null},
				activation: {
					type: spell.time[0].unit,
					cost: spell.time[0].number,
					condition: Renderer.stripTags(spell.time[0].condition || ""),
				},
				duration: {
					value: durationVal,
					units: durationUnit,
				},
				damage: {
					parts: damageParts,
					versatile: "",
				},
				scaling: {
					mode: cantripScaling ? "cantrip" : scaling ? "level" : "none",
					formula: cantripScaling || scaling || "",
				},
				save: {ability: savingThrow, dc: null, scaling: saveScaling},
				ability: opts.abilityAbv || "",
				uses: {
					value: opts.usesCurrent || 0,
					max: opts.usesMax || 0,
					per: opts.usesPer || "",
				},
				attackBonus: 0,
				chatFlavor: "",
				critical: null,
				formula,
				preparation: {
					mode: preparationMode,
					prepared: isPrepared,
				},
				consume: {
					type: consumeType,
					target: consumeTarget,
					amount: consumeAmount,
				},
				...(additionalData || {}),
			},
			img,
			flags: this._getSpellFlags(spell, opts),
			effects: MiscUtil.copy(srdData?.effects || []),
		};

		if (opts.isAddPermission) out.permission = {default: Config.get("importSpell", "permissions")};

		return out;
	}

	static _getSpellFlags (
		spell,
		{
			parentClassName,
			parentClassSource,
			parentSubclassName,
			parentSubclassSource,
		} = {},
	) {
		const out = {
			[SharedConsts.MODULE_NAME_FAKE]: {
				page: UrlUtil.PG_SPELLS,
				source: spell.source,
				hash: UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_SPELLS](spell),
			},
		};

		if (!parentClassName && !parentClassSource && !parentSubclassName && !parentSubclassSource) return out;

		out[SharedConsts.MODULE_NAME_FAKE].parentClassName = parentClassName;
		out[SharedConsts.MODULE_NAME_FAKE].parentClassSource = parentClassSource;
		out[SharedConsts.MODULE_NAME_FAKE].parentSubclassName = parentSubclassName;
		out[SharedConsts.MODULE_NAME_FAKE].parentSubclassSource = parentSubclassSource;

		return out;
	}

	static _pGetSpellItem_parseAndAddDamage (entriesStr, damageTuples) {
		// If there's a damage type, try find the spell's damage
		entriesStr.replace(/{@damage ([^}]+)} ([^ ]+)(, [^ ]+)*(,? or [^ ]+)? damage/ig, (...m) => {
			// TODO(Future): add multiple damage types as choices when Foundry supports this--for now, just
			//   choose the first we find
			damageTuples.push([m[1], m[2]]);
		});
	}

	static _getReDiceYourSpellcastingMod () {
		// Only capture the first result (i.e. no [g]lobal)
		return /{@dice ([^}]+)}(\s*\+\s*your\s+spellcasting\s+ability\s+modifier)?/i;
	}

	static _getDescription (spell) {
		if (!Config.get("importSpell", "isImportDescription")) return "";

		return DataConverter.getWithDescriptionPlugins(() => {
			const entries = DataConverter.getEntryDescription(spell);
			const entriesHigherLevel = spell.entriesHigherLevel ? DataConverter.getEntryDescription(spell, {prop: "entriesHigherLevel"}) : "";

			return [entries, entriesHigherLevel].filter(Boolean).join("");
		});
	}

	static async _pGetAdditionalData (spell, targetUnits) {
		const out = DataConverter.pGetAdditionalData_(spell, {propBrew: "foundrySpell", fnLoadJson: Vetools.pGetSpellSideData, propJson: "spell"});

		// Apply metric conversion to loaded data, if required
		if (out["target.value"]) out["target.value"] = Config.getMetricNumber("importSpell", out["target.value"], targetUnits);

		return out;
	}

	/**
	 * @param actor The actor.
	 * @param tagIdList A list of spells, in tag format e.g. "fireball", "shape water|xge", or `{uid: "fireball", uses: 1, usesPer: "lr"}`
	 * @param [opts] Options object.
	 * @param [opts.hashToIdMap] A pre-made hash-to-item-ID map to use.
	 * @param [opts.isTemporary] If the actor is a temporary one, so cannot gain Items.
	 * @param [opts.optsGetSpellItem] Options to pass to the spell entity -> FVTT item converter.
	 * @param [opts.isIgnoreExisting] If existing spells should be ignored, i.e. a new spell is always imported.
	 * @return {Promise}
	 */
	static async pGetSpellHashToItemPosMapAndFillSpellsFromTagIds (actor, tagIdList, opts) {
		opts = opts || {};

		const hashToIdMap = opts.hashToIdMap || {};

		for (const spellUidOrMeta of tagIdList) {
			const uid = spellUidOrMeta.uid || spellUidOrMeta;
			const spellModOpts = spellUidOrMeta.uid ? spellUidOrMeta : null;

			// Skip "expanded spell list" spells
			if (spellModOpts?.isExpanded) continue;

			let [name, source] = uid.split("|");
			if (!source) source = SRC_PHB;
			const hash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_SPELLS]({name, source});

			if (
				hashToIdMap[hash] != null
				&& !spellModOpts // Never use an existing spell if we have special options when adding
			) {
				await this._pFillSpellPromises_pUpdateSpellItem(actor, hashToIdMap[hash], source, hash, opts)
				continue;
			}

			if (spellModOpts) {
				opts = {...opts};
				opts.optsGetSpellItem = MiscUtil.copy(opts.optsGetSpellItem || {});
				opts.optsGetSpellItem.usesCurrent = spellModOpts.uses;
				opts.optsGetSpellItem.usesMax = spellModOpts.uses;
				opts.optsGetSpellItem.usesPer = spellModOpts.usesPer;
				opts.optsGetSpellItem.consumeType = spellModOpts.consumeType;
				opts.optsGetSpellItem.consumeAmount = spellModOpts.consumeAmount;
				opts.optsGetSpellItem.consumeTarget = spellModOpts.consumeTarget;
				opts.optsGetSpellItem.isPrepared = spellModOpts.isPrepared != null ? spellModOpts.isPrepared : opts.optsGetSpellItem.isPrepared;
				opts.optsGetSpellItem.preparationMode = spellModOpts.preparationMode != null ? spellModOpts.preparationMode : opts.optsGetSpellItem.preparationMode;
				opts.optsGetSpellItem.castAtLevel = spellModOpts.castAtLevel;

				// Never use an existing spell if we have special options when adding
				opts.isIgnoreExisting = true;
			}

			const id = await DataConverterSpell._pFillSpellPromises_pAddSpellItem(actor, source, hash, opts);
			if (
				id == null
				|| spellModOpts // Never use an existing spell if we have special options when adding
			) continue;
			hashToIdMap[hash] = id;
		}

		return hashToIdMap;
	}

	static async _pFillSpellPromises_pUpdateSpellItem (actor, spellItemId, source, hash, opts) {
		const item = actor.items.find(it => it.id === spellItemId);
		if (!item) return;
		const itemDataExisting = item.data;

		opts = opts || {};

		let spell;
		try {
			spell = await Renderer.hover.pCacheAndGet(UrlUtil.PG_SPELLS, source, hash);
		} catch (e) {
			console.error(...LGT, `Failed to import spell with hash "${hash}"!`, e);
			return;
		}

		if (!spell) return;

		const itemData = await DataConverterSpell.pGetSpellItem(
			spell,
			{
				...UtilActors.getActorSpellItemOpts(actor),
				...(opts.optsGetSpellItem || {}),
			},
		);

		const update = {
			_id: item.id,
			data: {},
		};

		let anyMod = false;

		if (MiscUtil.get(itemDataExisting.data, "preparation", "mode") === "prepared" && MiscUtil.get(itemData.data, "preparation", "mode") === "always") {
			anyMod = true;
			MiscUtil.set(update.data, "preparation", "mode", "always");
		}

		if (MiscUtil.get(itemDataExisting.data, "preparation", "prepared") !== true && MiscUtil.get(itemData.data, "preparation", "prepared") === true) {
			anyMod = true;
			MiscUtil.set(update.data, "preparation", "prepared", true);
		}

		if (anyMod) {
			await actor.updateEmbeddedDocuments("Item", [update]);
		}
	}

	/**
	 * @param actor The actor.
	 * @param strEntries Stringified entries to search for spell tags.
	 * @param [opts] Options object.
	 * @param [opts.hashToIdMap] A pre-made hash-to-item-ID map to use.
	 * @param [opts.isTemporary] If the actor is a temporary one, so cannot gain Items.
	 * @param [opts.optsGetSpellItem] Options to pass to the spell entity -> FVTT item converter.
	 * @param [opts.isIgnoreExisting] If existing spells should be ignored, i.e. a new spell is always imported.
	 * @return {Promise}
	 */
	static async pGetSpellHashToItemPosMapAndFillSpells (actor, strEntries, opts) {
		opts = opts || {};

		const tagIds = [];
		const reSpell = /{@spell ([^}]+)}/gi;
		let m;
		while ((m = reSpell.exec(strEntries))) {
			tagIds.push(m[1])
		}

		return this.pGetSpellHashToItemPosMapAndFillSpellsFromTagIds(actor, tagIds, opts);
	}

	/**
	 * @param actor The actor.
	 * @param source Spell source.
	 * @param hash Spell hash.
	 * @param [opts] Options object.
	 * @param [opts.isTemporary] If the actor is a temporary one, so cannot gain Items.
	 * @param [opts.optsGetSpellItem] Options to pass to the spell entity -> FVTT item converter.
	 * @param [opts.isIgnoreExisting] If existing spells should be ignored, i.e. a new spell is always imported.
	 * @return {Promise}
	 */
	static async _pFillSpellPromises_pAddSpellItem (actor, source, hash, opts) {
		opts = opts || {};

		let spell;
		try {
			spell = await Renderer.hover.pCacheAndGet(UrlUtil.PG_SPELLS, source, hash);
		} catch (e) {
			console.error(...LGT, `Failed to import spell with hash "${hash}"!`, e);
			return null;
		}

		if (!spell) return null;

		if (!opts.isIgnoreExisting) {
			const existingSpell = DataConverterSpell.getActorSpell(actor, spell.name, spell.source);
			if (existingSpell) return existingSpell.id;
		}

		const itemData = await DataConverterSpell.pGetSpellItem(
			spell,
			{
				...UtilActors.getActorSpellItemOpts(actor),
				...(opts.optsGetSpellItem || {}),
			},
		);

		const importedEmbeds = await UtilActors.pAddActorItems(actor, [itemData], {isTemporary: opts.isTemporary});
		return importedEmbeds[0]?.document?.id;
	}

	static getActorSpell (actor, name, source) {
		if (!name || !source) return null;
		return actor.items && actor.items.find(it =>
			(it.name || "").toLowerCase() === name.toLowerCase()
			&& (
				!Config.get("import", "isStrictMatching")
				|| (DataConverter.getItemSource(it) || "").toLowerCase() === source.toLowerCase()
			),
		);
	}

	static doHookSpellLinkRender (actorId, hashToPosMap, entry) {
		if (entry.href && entry.href.hover) {
			if (entry.href.hover.page === UrlUtil.PG_SPELLS) {
				const entryHash = entry.href.hashPreEncoded ? entry.href.hash : UrlUtil.encodeForHash(entry.href.hash);
				const hashPos = hashToPosMap[entryHash];
				if (hashPos != null) {
					const asString = JSON.stringify({
						actorId: actorId,
						itemId: hashPos,
					}).escapeQuotes();
					return {
						isDisableEvents: true,
						attributes: `${UtilEvents.LINK_DATA_SHEET_ITEM}="${asString}"`,
					}
				}
			}
		}
		return null;
	}

	static async pSetSpellItemIsPrepared (item, isPrepared) {
		if (!item) return;
		await item.update({data: {preparation: {prepared: isPrepared}}});
	}
}

export {DataConverterSpell};
