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

class DataConverterItem {
	static async pGetActionWeaponDetails ({size, action, damageParts, isSiegeWeapon, isMagical}) {
		await Renderer.item.populatePropertyAndTypeReference();

		await this._pGetActionWeaponDetails_initCaches();

		let damageVersatile = "";
		let weight = 0;
		let price = null;
		let img = null;
		let rarity = "";
		let weaponType = isSiegeWeapon ? "siege" : "natural";
		let itemProperties = {};
		let attuned = false;
		let attunement = 0;
		let identified = false;
		let attackBonus = 0;

		const weaponLookupName = (action.name || "")
			.replace(/\(.*\)$/, "") // remove parenthetical text (e.g. "(Humanoid or Hybrid Form Only)" off the end
			.toLowerCase()
		;

		const weapon = DataConverterItem._WEAPON_DETAIL_CACHE[weaponLookupName] ? MiscUtil.copy(DataConverterItem._WEAPON_DETAIL_CACHE[weaponLookupName]) : null;

		if (weapon) {
			const {weight: tmpWeight, price: tmpPrice} = DataConverterItem._pGetItemItem_getWeightAndPrice(weapon, size);
			weight = tmpWeight;
			price = tmpPrice;

			rarity = (weapon.rarity || "Unknown").toTitleCase();

			const weaponTypeAndAbility = this._getWeaponTypeAndAbility(weapon);
			weaponType = weaponTypeAndAbility.weaponType;

			itemProperties = this._getWeaponProperties(weapon);

			const {isAttuned, isIdentified, attunement: tmpAttunement} = this._pGetItemItem_getAttunedIdentifiedEquipped(weapon);
			attuned = attuned || isAttuned;
			attunement = attunement || tmpAttunement;
			identified = identified || isIdentified;

			// Custom attribute applied by the cache loader
			img = weapon._fvttImage || img;

			if (weapon.bonusWeapon && !isNaN(weapon.bonusWeapon)) attackBonus = Number(weapon.bonusWeapon);
			// This is more specific, so can override the generic "bonusWeapon" prop
			if (weapon.bonusWeaponAttack && !isNaN(weapon.bonusWeaponAttack)) attackBonus = Number(weapon.bonusWeapon);
		}

		// region Versatile
		if (damageParts.length > 1 && ((weapon && (weapon.property || []).includes("V")) || / (?:two|both) hands/i.test(JSON.stringify(action.entries || [])))) {
			// Find the first damage term that has the same type as the main (first) damage term
			const damageTypePrimary = damageParts[0][1];
			if (damageTypePrimary) {
				const ixDamagePartVersatile = damageParts.slice(1).findIndex(it => it[1] === damageTypePrimary);
				if (~ixDamagePartVersatile) {
					damageParts = MiscUtil.copy(damageParts);
					const [damagePartVersatile] = damageParts.splice(ixDamagePartVersatile + 1, 1);
					damageVersatile = damagePartVersatile[0];
				}
			}
		}
		// endregion

		// region Additional properties
		if (isMagical) itemProperties.mgc = true;
		// endregion

		return {
			damageParts,
			damageVersatile,
			weight,
			price,
			img,
			rarity,
			weaponType,
			itemProperties,
			attuned,
			attunement,
			identified,
			attackBonus,
		};
	}

	static async _pGetActionWeaponDetails_initCaches () {
		if (DataConverterItem._WEAPON_DETAIL_CACHE_INIT) return;

		await DataConverterItem._WEAPON_DETAIL_CACHE_LOCK.pLock();
		try {
			if (DataConverterItem._WEAPON_DETAIL_CACHE_INIT) return;

			console.log(...LGT, "Pre-caching item lookup...");

			// Build a cache of all the items and their fluff
			const {item: items} = await Vetools.pGetItems();

			for (const item of items) {
				if (item.type === "GV") continue;

				const lowName = item.name.toLowerCase();
				// If there's e.g. a " +1" suffix on the end, make a copy with it as a prefix instead
				const prefixBonusKey = lowName.replace(/^(.*?)( \+\d+$)/, (...m) => `${m[2].trim()} ${m[1].trim()}`);

				const itemKeys = [
					lowName,
					prefixBonusKey === lowName ? null : prefixBonusKey,
				].filter(Boolean);

				const cpy = MiscUtil.copy(item);
				const procFluff = await Renderer.item.pGetFluff(cpy);
				cpy._fvttImage = await DataConverterItem._pGetItemItem_pGetImagePath(cpy, procFluff);

				itemKeys.forEach(k => {
					if (!DataConverterItem._WEAPON_DETAIL_CACHE[k]) {
						DataConverterItem._WEAPON_DETAIL_CACHE[k] = cpy;
						return;
					}

					// If there is already something in the cache, prefer DMG + PHB entries, then official sources
					const existing = DataConverterItem._WEAPON_DETAIL_CACHE[k];
					if (
						!(existing.source === SRC_DMG || existing.source === SRC_PHB)
						&& SourceUtil.isNonstandardSource(existing.source)
					) {
						DataConverterItem._WEAPON_DETAIL_CACHE[k] = cpy;
					}
				});
			}

			console.log(...LGT, "Pre-caching complete.");

			DataConverterItem._WEAPON_DETAIL_CACHE_INIT = true;
		} finally {
			DataConverterItem._WEAPON_DETAIL_CACHE_LOCK.unlock();
		}
	}

	/**
	 *
	 * @param item The item entry.
	 * @param [opts] Options object.
	 * @param [opts.fluff] Item fluff.
	 * @param [opts.isAddPermission]
	 * @param [opts.quantity]
	 * @param [opts.isEquipped]
	 * @param [opts.size] Creature this item is to be sized for.
	 * @param [opts.sheetItemsAmmo] Actor sheet items
	 * @param [opts.filterValues] Pre-baked filter values to be re-used when importing this from the item.
	 * @param [opts.dexMod] Pre-baked dexterity modifier to use in the place of a dynamic variable when creating active effects.
	 * @param [opts.isActorItem]
	 * @return {object}
	 */
	static async pGetItemItem (item, opts) {
		opts = opts || {};

		await Renderer.item.populatePropertyAndTypeReference();

		const entriesStr = item.entries ? JSON.stringify(item.entries) : "";

		// Treat item groups as "loot," which is a catch-all for "things with no specific properties"
		if (item._isItemGroup) return this._pGetItemItem_loot(item, opts, entriesStr);

		let out;
		if (await this._pIsInSrd(item)) {
			out = await this._pGetItemItem_fromSrd(item, opts, entriesStr);
		} else {
			const importType = this._getItemImporterType(item);

			switch (importType) {
				case DataConverterItem._IMPORT_TYPE_WEAPON: out = await this._pGetItemItem_weapon(item, opts, entriesStr); break;
				case DataConverterItem._IMPORT_TYPE_TOOL: out = await this._pGetItemItem_tool(item, opts, entriesStr); break;
				case DataConverterItem._IMPORT_TYPE_CONSUMABLE: out = await this._pGetItemItem_consumable(item, opts, entriesStr); break;
				case DataConverterItem._IMPORT_TYPE_EQUIPMENT: out = await this._pGetItemItem_equipment(item, opts, entriesStr); break;
				case DataConverterItem._IMPORT_TYPE_CONTAINER: out = await this._pGetItemItem_container(item, opts, entriesStr); break;
				case DataConverterItem._IMPORT_TYPE_LOOT: out = await this._pGetItemItem_loot(item, opts, entriesStr); break;
				default: throw new Error(`Unhandled importer type "${importType}"`);
			}
		}

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

		return out;
	}

	static _getItemImporterType (item) {
		// TODO factor this out (and convert this method to async) to allow us to fetch types defined in `foundry-items.json`
		if (item.foundryType) {
			const cleanType = item.foundryType.toLowerCase().trim();
			if (DataConverterItem._IMPORT_TYPES[cleanType]) return cleanType;
			else console.warn(...LGT, `Item foundryType "${item.foundryType}" found on item "${item.name}" (${item.source}) is not a supported item type. An auto-generated type will be used instead.`)
		}

		// Anything with damage, treat as a weapon
		if (item.dmg1) return DataConverterItem._IMPORT_TYPE_WEAPON;

		if (
			item.type === "AT" // Artisan's Tool
			|| item.type === "T" // Tool
		) return DataConverterItem._IMPORT_TYPE_TOOL;

		if (
			item.type === "P" // Potion
			|| item.type === "SC" // Scroll
			|| (item.type === "WD" && item.charges) // Wand
			|| (item.type === "RD" && item.charges) // Rod
			|| (item.type === "G" && item.charges) // Adventuring gear
			|| item.poison
			|| item.type === "A" // Ammunition
			|| item.type === "AF" // Ammunition [futuristic]
			|| item.type === "EXP"
		) return DataConverterItem._IMPORT_TYPE_CONSUMABLE;

		if (
			item.type === "HA" // Heavy armor
			|| item.type === "MA" // Medium armor
			|| item.type === "LA" // Light armor
			|| item.type === "S" // Shield
			|| item.bardingType // Barding
		) return DataConverterItem._IMPORT_TYPE_EQUIPMENT;

		if (item.containerCapacity) return DataConverterItem._IMPORT_TYPE_CONTAINER;

		// Classify some "other" items as "trinket"-type equipment
		//  - +AC items (e.g. cloak of protection)
		//  - Ability score modifying items (e.g. belt of giant strength)
		//  - All other wondrous items, as there are few examples that couldn't in some way be seen as "equipment"
		if (
			item.bonusAc
			|| item.ability
			|| item.wondrous
		) return DataConverterItem._IMPORT_TYPE_EQUIPMENT;

		// Try to process various equipment-sounding item names as equipment (e.g. rings, bracers)
		if (this._ITEM_EQUIPMENT_NAME_RES.some(it => it.test(item.name))) return DataConverterItem._IMPORT_TYPE_EQUIPMENT;

		// Treat everything else as loot
		return DataConverterItem._IMPORT_TYPE_LOOT;
	}

	static async _pIsInSrd (item) {
		const srdData = await UtilCompendium.getSrdCompendiumEntity("item", item, {fnGetAliases: this._getSrdAliases});
		return !!srdData;
	}

	static _getSrdAliases (entity) {
		if (!entity.name) return [];

		const out = [];

		// Shitty pluralization--allows conversion of e.g. "Arrow" -> "Arrows" (the SRD name is plural, despite being
		//   a single arrow).
		out.push(`${entity.name}s`);

		if (entity.name.toLowerCase().includes("feet")) out.push(entity.name.replace(/feet/g, "ft."));

		// e.g. "Wand of the War Mage, +1" -> "Wand of the War Mage +1"
		// Note that this also "fixes" 5etools' "shield, +1" being a generic variant, whereas "shield +1" is the specific variant.
		//   If anybody ever decides the generic variant is what they wanted to import, they'll be in for a bad time, but
		//   that's an acceptable loss to reduce the very-reasonable support requests this generates.
		if (entity.name.includes(", ")) out.push(entity.name.replace(/, /g, " "));

		// "Holy Water (Flask)" -> "Flask of Holy Water"
		const mBrackets = /^([^(]+) \(([^)]+)\)$/.exec(entity.name.trim());
		if (mBrackets) out.push(`${mBrackets[2]} of ${mBrackets[1]}`);

		// "Oil (Flask)" -> "Oil Flask"
		if (mBrackets) out.push(`${mBrackets[1]} ${mBrackets[2]}`);

		// "Rations (1 day)" -> "Rations"
		if (mBrackets) out.push(mBrackets[1]);

		return out;
	}

	static _getItemFlags (item, opts) {
		opts = opts || {};

		const out = {
			[SharedConsts.MODULE_NAME_FAKE]: {
				page: UrlUtil.PG_ITEMS,
				source: item.source,
				hash: UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ITEMS](item),
			},
		};

		if (opts.isAddDataFlags) {
			out[SharedConsts.MODULE_NAME_FAKE].propDroppable = "item";
			out[SharedConsts.MODULE_NAME_FAKE].filterValues = opts.filterValues;
		}

		return out;
	}

	static async _pGetItemItem_fromSrd (item, opts, entriesStr) {
		const {weight, price} = this._pGetItemItem_getWeightAndPrice(item, opts.size);
		const {isAttuned, isIdentified, isEquipped, attunement} = this._pGetItemItem_getAttunedIdentifiedEquipped(item);
		const {consumeType, consumeTarget, consumeAmount} = this._pGetItemItem_getAmmoConsumeDetails(item, opts);

		const srdData = await UtilCompendium.getSrdCompendiumEntity("item", item, {fnGetAliases: this._getSrdAliases});

		const consume = {...(srdData.data.consume || {})};
		consume.type = consume.type || consumeType;
		consume.target = consume.target || consumeTarget;
		consume.amount = consume.amount || consumeAmount;

		const additionalData = await this._pGetAdditionalData(item);

		const img = await DataConverterItem._pGetItemItem_pGetImagePath(item, opts.fluff);

		return {
			name: UtilApplications.getCleanEntityName(DataConverter.getNameWithSourcePart(item, {isActorItem: opts.isActorItem})),
			type: srdData.type,
			data: {
				...srdData.data,

				source: DataConverter.getSourceWithPagePart(item),
				description: {value: DataConverterItem._getItemItem_getItemDescription(item), chat: "", unidentified: ""},

				proficient: true,
				quantity: opts.quantity != null ? opts.quantity : (item.quantity || 1),
				weight,
				price,
				attuned: isAttuned,
				identified: isIdentified,
				equipped: opts.isEquipped ?? isEquipped,
				rarity: (item.rarity || "Unknown").toTitleCase(),
				consume,

				attunement,

				...(additionalData || {}),
			},
			img,
			flags: {
				...this._getItemFlags(item, opts),
			},
			effects: [
				// FIXME(Future) probably need to pare this down to avoid duplication
				...MiscUtil.copy(srdData.effects || []),
				...await this._pGetItemEffects(item, img, opts),
			],
		}
	}

	static _pGetItemItem_getWeightAndPrice (item, size) {
		let weight = null;
		let price = null;
		let tmpValue = null;

		if (size == null || size === SZ_MEDIUM) {
			return {
				weight: Config.getMetricNumber("importItem", item.weight || 0, "lb"),
				price: !isNaN(item.value) ? item.value / 100 : null,
			}
		}

		const weightValueScalingMode = Config.get("importCreature", "itemWeightAndValueSizeScaling");

		switch (weightValueScalingMode) {
			// No scaling
			case 1: {
				weight = item.weight || 0;
				tmpValue = item.value;
				break;
			}

			// Multiplicative ("barding") scaling
			case 2: {
				const weightMult = DataConverterItem._SIZE_TO_ITEM_WEIGHT_MULT[size] || 1;
				const valueMult = DataConverterItem._SIZE_TO_ITEM_VALUE_MULT[size] || 1;

				if (item.weight && !isNaN(item.weight)) {
					weight = Number(item.weight) * weightMult;
				}

				if (item.value && !isNaN(item.value)) {
					tmpValue = item.value * valueMult;
				}

				break;
			}

			// Exponential ("gurt's greataxe") scaling
			case 3: {
				const exponent = DataConverterItem._SIZE_TO_ITEM_WEIGHT_AND_VALUE_EXPONENT[size] || 1;

				if (item.weight && !isNaN(item.weight)) {
					weight = Math.floor(item.weight ** exponent);
				}

				if (item.value && !isNaN(item.value)) {
					const factor = item.value < 10 ? 1 : item.value < 100 ? 10 : 100;

					// Convert the value to it's output currency
					tmpValue = item.value / factor;

					// Run the exponentiation on the output currency value, to avoid ridiculous numbers
					tmpValue = Math.floor(tmpValue ** exponent);

					// Convert back to copper prior to running the parsing conversion
					tmpValue *= factor;
				}

				break;
			}
		}

		// Currently (2020-04-26), Foundry does not support units in this field, so convert everything to gold
		if (tmpValue) price = tmpValue / 100;

		return {
			weight: Config.getMetricNumber("importItem", weight || 0, "lb"),
			price,
		};
	}

	static _pGetItemItem_getAttunedIdentifiedEquipped (item) {
		// If it's a magic item, mark it as identified
		// If it's a magic item that requires attunement, mark it as attuned
		const isMundane = item.rarity === "none" || item.rarity === "unknown" || item._category === "basic";
		const isAttuned = !isMundane && !!item.reqAttune;

		let isEquipped;
		const importType = this._getItemImporterType(item);
		switch (importType) {
			case DataConverterItem._IMPORT_TYPE_WEAPON: isEquipped = true; break;
			case DataConverterItem._IMPORT_TYPE_TOOL: isEquipped = false; break;
			case DataConverterItem._IMPORT_TYPE_CONSUMABLE: isEquipped = false; break;
			case DataConverterItem._IMPORT_TYPE_EQUIPMENT: isEquipped = true; break;
			case DataConverterItem._IMPORT_TYPE_CONTAINER: isEquipped = false; break;
			case DataConverterItem._IMPORT_TYPE_LOOT: isEquipped = false; break;
			default: throw new Error(`Unhandled importer type "${importType}"`);
		}

		return {
			isAttuned,
			// As of dnd5e v1.2.0, attunement cannot be e.g. "by <X class>", and instead is a numerical enum
			attunement: item.reqAttune ? Config.get("importItem", "attunementType") : CONFIG.DND5E.attunementTypes.NONE,
			isIdentified: !isMundane,
			isEquipped,
		}
	}

	static _getItemItem_getItemDescription (item) {
		if (!Config.get("importItem", "isImportDescription")) return "";

		return DataConverter.getWithDescriptionPlugins(
			() => this._getItemItem_getItemDescription_(item),
		);
	}

	static _getItemItem_getItemDescription_ (item) {
		// Render everything in the header, as Foundry doesn't have support for e.g. rarity or attunement
		const [damage, damageType, propertiesTxt] = Renderer.item.getDamageAndPropertiesText(item);
		const [typeRarityText, subTypeText, tierText] = Renderer.item.getTypeRarityAndAttunementText(item);

		const headerPart = Config.get("importItem", "isImportDescriptionHeader") ? `<div>
			${Renderer.item.getTypeRarityAndAttunementHtml(typeRarityText, subTypeText, tierText)}
				<div class="flex w-100">
					<div class="col-4">${[Parser.itemValueToFull(item), Parser.itemWeightToFull(item)].filter(Boolean).join(", ").uppercaseFirst()}</div>
					<div class="col-8 text-right">${damage} ${damageType} ${propertiesTxt}</div>
				</div>
			</div>
		<hr>` : "";

		const bodyPart = Renderer.item.getRenderedEntries(
			item,
			{
				isCompact: true,
				wrappedTypeWhitelist: Config.get("importItem", "isImportDescriptionHeader") ? null : new Set(["variant"]),
			},
		);

		return `${headerPart}
		${bodyPart}`;
	}

	static _getItemItem_getItemUses (item) {
		let charges = 0;
		if (item.charges) {
			if (isNaN(item.charges)) {
				const mDice = /{@dice (\d)+d(\d+)\s*(?:[-+]\s*\d+)?}/i.exec(item.charges);
				if (mDice) {
					charges = (Number(mDice[1]) * Number(mDice[2])) + (mDice[3] ? Number(mDice[3].replace(/\s*/g, "")) : 0);
				}
			} else charges = item.charges;
		}
		return charges;
	}

	static _getWeaponTypeAndAbility (item) {
		let weaponType = "";
		let weaponAbility = "str";

		if (item.type === "A" || item.type === "AF") {
			weaponType = "ammo";
			weaponAbility = "dex";
		} else if (item.type === "M" || item.type === "R") {
			if ((item.weaponCategory || "").toLowerCase() === "martial") weaponType = `martial${item.type}`;
			else if ((item.weaponCategory || "").toLowerCase() === "simple") weaponType = `simple${item.type}`;
		}

		if (item.type === "R") weaponAbility = "dex";
		else if (item.property && item.property.includes("F")) weaponAbility = "dex"; // Finesse

		return {weaponType, weaponAbility};
	}

	static _getWeaponProperties (item) {
		const out = item.property ? item.property.map(it => DataConverterItem._ITEM_PROP_MAP[it]).filter(Boolean).map(it => ({[it]: true})).reduce((a, b) => Object.assign(a, b), {}) : {};

		if (item._variantName === "Adamantine Weapon" || item._variantName === "Adamantine Armor" || item._variantName === "Adamantine Ammunition") out.ada = true; // Adamantine
		if (item.focus) out.foc = true; // Focus
		if (!Renderer.item.isMundane(item)) out.mgc = true; // Magical
		if (item._variantName === "Silvered Weapon" || item._variantName === "Silvered Ammunition") out.sil = true; // Silvered
		if (item.firearm) out.fir = true; // Firearm

		return out;
	}

	static async _pGetItemItem_weapon (item, opts, entriesStr) {
		const {weaponType, weaponAbility} = this._getWeaponTypeAndAbility(item);
		const {weight, price} = this._pGetItemItem_getWeightAndPrice(item, opts.size);
		const {isAttuned, isIdentified, isEquipped, attunement} = this._pGetItemItem_getAttunedIdentifiedEquipped(item);
		const {consumeType, consumeTarget, consumeAmount} = this._pGetItemItem_getAmmoConsumeDetails(item, opts);

		// When the sheet renders, it splits this string by comma
		const itemProperties = this._getWeaponProperties(item);

		// Bonuses need to be manually applied to damage as well as the generic "bonus" field for on-hit
		let dmg1 = "";
		if (item.dmg1) {
			dmg1 = item.dmg1;
			if (item.bonusWeapon) dmg1 = `${dmg1}${item.bonusWeapon}`;
			else if (item.bonusWeaponDamage) dmg1 = `${dmg1}${item.bonusWeaponDamage}`;
			dmg1 = `${dmg1}+@mod`;
		}

		let dmg2 = "";
		if (item.dmg2) {
			dmg2 = item.dmg2;
			if (item.bonusWeapon) dmg2 = `${dmg2}${item.bonusWeapon}`;
			else if (item.bonusWeaponDamage) dmg2 = `${dmg2}${item.bonusWeaponDamage}`;
			dmg2 = `${dmg2}+@mod`;
		}

		let rangeShort = 0;
		let rangeLong = 0;
		if (item.range) {
			const cleanRange = `${item.range}`.trim();
			const mRangeLong = /^(\d+)\/(\d+)$/i.exec(cleanRange);
			if (mRangeLong) {
				rangeShort = Number(mRangeLong[1]);
				rangeLong = Number(mRangeLong[2])
			}

			const mRangeNum = /^(\d+)$/i.exec(cleanRange);
			if (mRangeNum) rangeShort = Number(mRangeNum[1]);
		} else if (item.property && item.property.includes("R")) { // Add a default 10 foot range for reach melee weapons
			rangeShort = 10;
		} else { // Add a default 5 foot range for melee weapons
			rangeShort = 5;
		}

		const charges = this._getItemItem_getItemUses(item);

		const additionalData = await this._pGetAdditionalData(item);

		const img = await DataConverterItem._pGetItemItem_pGetImagePath(item, opts.fluff);

		return {
			name: UtilApplications.getCleanEntityName(DataConverter.getNameWithSourcePart(item, {isActorItem: opts.isActorItem})),
			type: "weapon",
			data: {
				source: DataConverter.getSourceWithPagePart(item),
				description: {value: DataConverterItem._getItemItem_getItemDescription(item), chat: "", unidentified: ""},

				proficient: true,
				quantity: opts.quantity || item.quantity || 1,
				weight,
				price,
				attuned: isAttuned,
				identified: isIdentified,
				equipped: opts.isEquipped ?? isEquipped,
				rarity: (item.rarity || "Unknown").toTitleCase(),

				damage: {
					parts: [
						[dmg1, item.dmgType ? Parser.dmgTypeToFull(item.dmgType) : ""],
					],
					versatile: dmg2,
				},
				range: {value: rangeShort, long: rangeLong, units: "ft"},
				weaponType: weaponType,
				ability: weaponAbility,
				properties: itemProperties,

				attunement,

				activation: {type: "action", cost: 1, condition: ""},
				duration: {value: 0, units: ""},
				target: {value: 0, units: "", type: ""},
				uses: {value: charges, max: charges, per: ""},
				actionType: item.type === "R" ? "rwak" : "mwak",
				attackBonus: item.bonusWeapon || item.bonusWeaponAttack || 0,
				chatFlavor: "",
				critical: null,

				formula: "",
				save: {ability: "", dc: 0},
				consume: {type: consumeType, target: consumeTarget, amount: consumeAmount},

				...(additionalData || {}),
			},
			img,
			flags: {
				...this._getItemFlags(item, opts),
			},
			effects: await this._pGetItemEffects(item, img, opts),
		}
	}

	static async _pGetItemItem_equipment (item, opts, entriesStr) {
		const {weight, price} = this._pGetItemItem_getWeightAndPrice(item, opts.size);
		const {isAttuned, isIdentified, isEquipped, attunement} = this._pGetItemItem_getAttunedIdentifiedEquipped(item);

		const {acValue, maxDexBonus} = this._getItemAcMeta(item);

		const charges = this._getItemItem_getItemUses(item);

		const additionalData = await this._pGetAdditionalData(item);

		const img = await DataConverterItem._pGetItemItem_pGetImagePath(item, opts.fluff);

		return {
			name: UtilApplications.getCleanEntityName(DataConverter.getNameWithSourcePart(item, {isActorItem: opts.isActorItem})),
			type: "equipment",
			data: {
				source: DataConverter.getSourceWithPagePart(item),
				description: {value: DataConverterItem._getItemItem_getItemDescription(item), chat: "", unidentified: ""},

				proficient: true,
				quantity: opts.quantity || item.quantity || 1,
				weight,
				price,
				attuned: isAttuned,
				identified: isIdentified,
				equipped: opts.isEquipped ?? isEquipped,
				rarity: (item.rarity || "Unknown").toTitleCase(),

				attunement,

				activation: {type: "", cost: 0, condition: ""},
				duration: {value: null, units: ""},
				target: {value: null, units: "", type: ""},
				range: {value: null, long: null, units: ""},
				uses: {value: charges, max: charges, per: null},
				ability: "",
				actionType: "",
				attackBonus: 0,
				chatFlavor: "",
				critical: null,
				damage: {parts: [], versatile: ""},
				formula: "",
				save: {ability: "", dc: null},
				armor: {
					type: DataConverterItem._ITEM_TYPE_TO_ARMOR_TYPE[item.type] || "",
					value: acValue,
					dex: maxDexBonus,
				},
				strength: item.strength || null,
				stealth: !!item.stealth,

				...(additionalData || {}),
			},
			img,
			flags: {
				...this._getItemFlags(item, opts),
			},
			effects: await this._pGetItemEffects(item, img, opts),
		}
	}

	static async _pGetItemItem_container (item, opts, entriesStr) {
		const {weight, price} = this._pGetItemItem_getWeightAndPrice(item, opts.size);
		const {isAttuned, isIdentified, isEquipped, attunement} = this._pGetItemItem_getAttunedIdentifiedEquipped(item);

		const capacityValue = item.containerCapacity.weight
			? item.containerCapacity.weight.reduce((a, b) => a + b, 0)
			: Math.max(...item.containerCapacity.item.map(itemToCount => Math.max(...Object.values(itemToCount))));

		const additionalData = await this._pGetAdditionalData(item);

		const img = await DataConverterItem._pGetItemItem_pGetImagePath(item, opts.fluff);

		return {
			name: UtilApplications.getCleanEntityName(DataConverter.getNameWithSourcePart(item, {isActorItem: opts.isActorItem})),
			type: "backpack",
			data: {
				source: DataConverter.getSourceWithPagePart(item),
				description: {value: DataConverterItem._getItemItem_getItemDescription(item), chat: "", unidentified: ""},

				quantity: opts.quantity || item.quantity || 1,
				weight,
				price,
				attuned: isAttuned,
				equipped: opts.isEquipped ?? isEquipped,
				identified: isIdentified,
				rarity: (item.rarity || "Unknown").toTitleCase(),
				capacity: {
					type: item.containerCapacity.weight ? "weight" : "items",
					value: capacityValue,
					weightless: !!item.containerCapacity.weightless,
				},

				attunement,

				damage: {parts: []},

				...(additionalData || {}),
			},
			img,
			flags: {
				...this._getItemFlags(item, opts),
			},
			effects: await this._pGetItemEffects(item, img, opts),
		}
	}

	static async _pGetItemItem_consumable (item, opts, entriesStr) {
		const {weight, price} = this._pGetItemItem_getWeightAndPrice(item, opts.size);
		const {isAttuned, isIdentified, isEquipped, attunement} = this._pGetItemItem_getAttunedIdentifiedEquipped(item);

		const consType = (item.poison ? "poison" : DataConverterItem._ITEM_TYPE_TO_CONSUMABLE_TYPE[item.type]) || "";

		let rollOnCons = "";
		let autoDestroy = false;

		entriesStr.replace(/if you expend[^.]+last charge[^.]+roll[^.]+{@dice ([^}]+)}(?:[^.]+)?\.(?:[^.]+)on a[^.]+\d+[^.]+destroyed/ig, (...m) => {
			rollOnCons = m[1];
			autoDestroy = true;
		});
		if (item.type === "SC" || item.type === "P") autoDestroy = true;

		const charges = this._getItemItem_getItemUses(item);

		const additionalData = await this._pGetAdditionalData(item);

		const img = await DataConverterItem._pGetItemItem_pGetImagePath(item, opts.fluff);

		return {
			name: UtilApplications.getCleanEntityName(DataConverter.getNameWithSourcePart(item, {isActorItem: opts.isActorItem})),
			type: "consumable",
			data: {
				source: DataConverter.getSourceWithPagePart(item),
				description: {value: DataConverterItem._getItemItem_getItemDescription(item), chat: "", unidentified: ""},

				quantity: opts.quantity || item.quantity || 1,
				weight,
				price,
				attuned: isAttuned,
				equipped: opts.isEquipped ?? isEquipped,
				identified: isIdentified,
				rarity: (item.rarity || "Unknown").toTitleCase(),

				consumableType: consType,

				attunement,

				activation: {type: "action", cost: 1, condition: ""},
				duration: {value: null, units: ""},
				target: {value: null, units: "", type: ""},
				range: {value: null, long: null, units: ""},
				uses: {value: charges, max: charges, per: null, autoUse: true, autoDestroy: autoDestroy},
				ability: "",
				actionType: null,
				attackBonus: 0,
				chatFlavor: "",
				critical: null,
				damage: {parts: [], versatile: ""},
				formula: rollOnCons, // unsure if this is the best place to put this
				save: {ability: "", dc: null},

				...(additionalData || {}),
			},
			img,
			flags: {
				...this._getItemFlags(item, opts),
			},
			effects: await this._pGetItemEffects(item, img, opts),
		}
	}

	static async _pGetItemItem_tool (item, opts, entriesStr) {
		const {weight, price} = this._pGetItemItem_getWeightAndPrice(item, opts.size);
		const {isAttuned, isIdentified, isEquipped, attunement} = this._pGetItemItem_getAttunedIdentifiedEquipped(item);

		let defaultAbil = DataConverterItem._ITEM_NAME_TO_DEFAULT_ABILITY[item.name] || "int";

		const additionalData = await this._pGetAdditionalData(item);

		const img = await DataConverterItem._pGetItemItem_pGetImagePath(item, opts.fluff);

		return {
			name: UtilApplications.getCleanEntityName(DataConverter.getNameWithSourcePart(item, {isActorItem: opts.isActorItem})),
			type: "tool",
			data: {
				source: DataConverter.getSourceWithPagePart(item),
				description: {value: DataConverterItem._getItemItem_getItemDescription(item), chat: "", unidentified: ""},

				quantity: opts.quantity || item.quantity || 1,
				weight,
				price,
				attuned: isAttuned,
				equipped: opts.isEquipped ?? isEquipped,
				identified: isIdentified,
				rarity: (item.rarity || "Unknown").toTitleCase(),

				proficient: 1, // 0 = not, 1 = proficient, 2 = expert, 0.5 = jack of all trades
				ability: defaultAbil,

				attunement,

				chatFlavor: "",
				damage: {parts: []},

				...(additionalData || {}),
			},
			img,
			flags: {
				...this._getItemFlags(item, opts),
			},
			effects: await this._pGetItemEffects(item, img, opts),
		}
	}

	static async _pGetItemItem_loot (item, opts, entriesStr) {
		const {weight, price} = this._pGetItemItem_getWeightAndPrice(item, opts.size);
		const {isAttuned, isIdentified, isEquipped, attunement} = this._pGetItemItem_getAttunedIdentifiedEquipped(item);

		const additionalData = await this._pGetAdditionalData(item);

		const img = await DataConverterItem._pGetItemItem_pGetImagePath(item, opts.fluff);

		return {
			name: UtilApplications.getCleanEntityName(DataConverter.getNameWithSourcePart(item, {isActorItem: opts.isActorItem})),
			type: "loot",
			data: {
				source: DataConverter.getSourceWithPagePart(item),
				description: {value: DataConverterItem._getItemItem_getItemDescription(item), chat: "", unidentified: ""},

				quantity: opts.quantity || item.quantity || 1,
				weight,
				price,
				attuned: isAttuned,
				equipped: opts.isEquipped ?? isEquipped,
				identified: isIdentified,
				rarity: (item.rarity || "Unknown").toTitleCase(),

				attunement,

				damage: {parts: []},

				...(additionalData || {}),
			},
			img,
			flags: {
				...this._getItemFlags(item, opts),
			},
			effects: await this._pGetItemEffects(item, img, opts),
		}
	}

	static async _pGetItemItem_pGetImagePath (item, fluff) {
		const fromFluff = Vetools.getImageUrlFromFluff(fluff);
		if (fromFluff) return fromFluff;

		const fromCompendium = await this._pGetCompendiumImage(item);

		if (fromCompendium) return fromCompendium;
		return `modules/${SharedConsts.MODULE_NAME}/media/icon/${DataConverterItem._ITEM_TYPE_TO_ICON[item.type] || "crossed-swords"}.svg`;
	}

	static _pGetItemItem_getAmmoConsumeDetails (item, opts) {
		let consumeType = "";
		let consumeTarget = "";
		let consumeAmount = null;

		if (item.ammoType) {
			consumeType = "ammo";
			consumeAmount = 1;

			if (opts.sheetItemsAmmo) {
				const [ammoTypeName, ammoTypeSource] = item.ammoType.toLowerCase().split("|").map(it => it.trim()).filter(Boolean);
				const cleanAmmoTypeSource = (ammoTypeSource || SRC_DMG).toLowerCase();
				const cleanAmmoTypeName = ammoTypeName.replace(/s+$/g, ""); // Remove trailing "s"

				const ammoTypeItems = opts.sheetItemsAmmo.filter(sheetItem => {
					const cleanSheetItemName = sheetItem.name.toLowerCase().trim().replace(/s+$/g, ""); // Remove trailing "s"
					return cleanSheetItemName === cleanAmmoTypeName
						&& (
							!Config.get("import", "isStrictMatching")
							|| (DataConverter.getItemSource(sheetItem) || "").toLowerCase() === cleanAmmoTypeSource
						)
				});

				if (ammoTypeItems.length) {
					// Arbitrarily use the first matching item we find
					consumeTarget = ammoTypeItems[0].id;
				}
			}
		}

		return {
			consumeType,
			consumeTarget,
			consumeAmount,
		}
	}

	static async _pGetCompendiumImage (item) {
		const out = await UtilCompendium.pGetCompendiumImage("item", item, {fnGetAliases: this._getSrdAliases});
		if (out) return out;

		// If the item has an SRD base, attempt to get the image from it
		if (item._baseName) {
			const out = await UtilCompendium.pGetCompendiumImage(
				"item",
				{
					name: item._baseName,
					source: item._baseSource || item.source,
					srd: item._baseSrd,
				},
				{
					fnGetAliases: this._getSrdAliases,
				},
			);
			if (out) return out;
		}

		// If the item has a base item, attempt to get the image from it
		if (item.baseItem && typeof item.baseItem === "string") {
			let [name, source] = item.baseItem.split("|");
			source = source || SRC_DMG;
			return UtilCompendium.pGetCompendiumImage(
				"item",
				{
					name: name,
					source: source,
					srd: true, // Always try to search for SRD
				},
				{
					fnGetAliases: this._getSrdAliases,
				},
			);
		}
	}

	static _getItemAcMeta (item) {
		let acBonus = item.bonusAc ? Number(item.bonusAc) : 0;
		if (isNaN(acBonus)) acBonus = 0;

		const itemType = item.bardingType || item.type;

		const isArmor = itemType === "HA"
			|| itemType === "MA"
			|| itemType === "LA";

		const isArmorOrShield = isArmor || item.type === "S";

		return {
			acValue: isArmorOrShield
				? (item.ac || 10) + acBonus
				: (acBonus || null), // Treat `0` acBonus as `null`
			maxDexBonus: itemType === "MA"
				? item.dexterityMax !== undefined ? item.dexterityMax : 2
				: itemType === "HA" ? 0 : null,
			isBase: isArmor,
		};
	}

	static async _pGetItemEffects (item, img, opts) {
		opts = opts || {};

		if (!Config.get("importItem", "isAddActiveEffects")) return [];

		const importType = this._getItemImporterType(item);
		const isDisabled = importType === DataConverterItem._IMPORT_TYPE_CONSUMABLE || (opts.isEquipped != null && !opts.isEquipped);

		const out = [];

		// region Additional custom effects
		if (await this.pHasItemSideLoadedEffects(null, item)) {
			out.push(...(await this.pGetItemItemEffects(null, item, null, {img})));
		}
		// endregion

		// region AC
		out.push(...this._pGetItemEffects_getAcEffects(item, img, opts, {isDisabled}));
		// endregion

		// region Saving throws
		if (item.bonusSavingThrow) {
			const effect = this._pGetItemEffects_getGenericBonus({
				item,
				img,
				label: "Saving Throw Bonus",
				key: "data.bonuses.abilities.save",
				prop: "bonusSavingThrow",
				isDisabled,
			});
			if (effect) out.push(effect);
		}
		// endregion

		// region Ability checks
		if (item.bonusSavingThrow) {
			const effect = this._pGetItemEffects_getGenericBonus({
				item,
				img,
				label: "Ability Check Bonus",
				key: "data.bonuses.abilities.check",
				prop: "bonusAbilityCheck",
				isDisabled,
			});
			if (effect) out.push(effect);
		}
		// endregion

		// region Spell attack
		if (item.bonusSpellAttack) {
			const effectMelee = this._pGetItemEffects_getGenericBonus({
				item,
				img,
				label: "Spell Attack Bonus (Melee)",
				key: "data.bonuses.msak.attack",
				prop: "bonusSpellAttack",
				isDisabled,
			});
			if (effectMelee) out.push(effectMelee);

			const effectRanged = this._pGetItemEffects_getGenericBonus({
				item,
				img,
				label: "Spell Attack Bonus (Ranged)",
				key: "data.bonuses.rsak.attack",
				prop: "bonusSpellAttack",
				isDisabled,
			});
			if (effectRanged) out.push(effectRanged);
		}
		// endregion

		// region Spell damage
		if (item.bonusSpellAttack) {
			const effectMelee = this._pGetItemEffects_getGenericBonus({
				item,
				img,
				label: "Spell Damage Bonus (Melee)",
				key: "data.bonuses.msak.damage",
				prop: "bonusSpellDamage",
				isDisabled,
			});
			if (effectMelee) out.push(effectMelee);

			const effectRanged = this._pGetItemEffects_getGenericBonus({
				item,
				img,
				label: "Spell Damage Bonus (Ranged)",
				key: "data.bonuses.rsak.damage",
				prop: "bonusSpellDamage",
				isDisabled,
			});
			if (effectRanged) out.push(effectRanged);
		}
		// endregion

		// region Spell save DC
		if (item.bonusSpellSaveDc) {
			const effect = this._pGetItemEffects_getGenericBonus({
				item,
				img,
				label: "Spell Save DC Bonus",
				key: "data.bonuses.spell.dc",
				prop: "bonusSpellSaveDc",
				isDisabled,
			});
			if (effect) out.push(effect);
		}
		// endregion

		// region Proficiency bonus
		if (item.bonusProficiencyBonus) {
			const effect = this._pGetItemEffects_getGenericBonus({
				item,
				img,
				label: "Proficiency Bonus... Bonus?", // :^)
				key: "data.attributes.prof",
				prop: "bonusProficiencyBonus",
				isDisabled,
			});
			if (effect) out.push(effect);
		}
		// endregion

		// region Ability scores
		if (item.ability) {
			if (item.ability.static) {
				Parser.ABIL_ABVS.forEach(ab => {
					if (item.ability.static[ab] == null) return;

					out.push(UtilActiveEffects.getGenericEffect({
						key: `data.abilities.${ab}.value`,
						value: item.ability.static[ab],
						mode: CONST.ACTIVE_EFFECT_MODES.UPGRADE,
						label: `Base ${Parser.attAbvToFull(ab)}`,
						icon: img,
						disabled: isDisabled,
						priority: UtilActiveEffects.PRIORITY_BASE,
					}));
				})
			}

			Parser.ABIL_ABVS.forEach(ab => {
				if (item.ability[ab] == null) return;

				out.push(UtilActiveEffects.getGenericEffect({
					key: `data.abilities.${ab}.value`,
					value: item.ability[ab],
					mode: CONST.ACTIVE_EFFECT_MODES.ADD,
					label: `Bonus ${Parser.attAbvToFull(ab)}`,
					icon: img,
					disabled: isDisabled,
					priority: UtilActiveEffects.PRIORITY_BONUS,
				}));
			})
		}
		// endregion

		// region Damage resistance/immunity/vulnerability; condition immunity
		const actorDataDrDiDvCi = DataConverter.getActorDamageResImmVulnConditionImm(item);
		out.push(...this._pGetItemEffects_getDrDiDvCiEffects({img, label: "Damage Resistance", actProp: "dr", isDisabled, actorDataDrDiDvCi}))
		out.push(...this._pGetItemEffects_getDrDiDvCiEffects({img, label: "Damage Immunity", actProp: "di", isDisabled, actorDataDrDiDvCi}))
		out.push(...this._pGetItemEffects_getDrDiDvCiEffects({img, label: "Damage Vulnerability", actProp: "dv", isDisabled, actorDataDrDiDvCi}))
		out.push(...this._pGetItemEffects_getDrDiDvCiEffects({img, label: "Condition Immunity", actProp: "ci", isDisabled, actorDataDrDiDvCi}))
		// endregion

		// TODO(Future)
		//  - language proficiencies
		//  - save proficiencies?

		return out;
	}

	static _pGetItemEffects_getAcEffects (item, img, opts, {isDisabled}) {
		if (UtilCompat.isDaeGeneratingArmorEffects()) return [];

		const out = [];

		const acMeta = this._getItemAcMeta(item);
		if (acMeta.acValue != null) {
			let value;

			if (acMeta.isBase) {
				if (Config.get("actor", "isUseExtendedActiveEffectsParser") || UtilCompat.isDaeActive()) {
					// Use a dynamic dex mod where we can
					if (acMeta.maxDexBonus === 0) value = acMeta.acValue;
					else if (acMeta.maxDexBonus) value = `${acMeta.acValue} + {${acMeta.maxDexBonus}, @abilities.dex.mod}kl`;
					else value = `${acMeta.acValue} + @abilities.dex.mod`;
				} else {
					// Use a pre-baked dex mod if we have to
					if (acMeta.maxDexBonus === 0) value = acMeta.acValue;
					else if (acMeta.maxDexBonus) value = acMeta.acValue + Math.min(2, opts.dexMod || 0);
					else value = acMeta.acValue + (opts.dexMod || 0);
				}
			} else {
				value = acMeta.acValue;
			}

			// Unfortunately, as of 2021-02-07, there is no "bonus AC" attribute which we could use for our bonus items.
			// Implement custom handling for base AC, since what we want is "UPGRADE" but with "ADD" layered over
			//   the top (for e.g. shields, Ring of Protection, etc.)
			// (Foundry's prioritisation of effects dictates that "UPGRADE" (which is a `Max`-y "OVERRIDE") happens
			//   after "ADD", and so will clobber any "ADD" effects.)
			out.push(UtilActiveEffects.getGenericEffect({
				key: "data.attributes.ac.value",
				value,
				mode: acMeta.isBase ? CONST.ACTIVE_EFFECT_MODES.UPGRADE : CONST.ACTIVE_EFFECT_MODES.ADD,
				label: `${acMeta.isBase ? "Base" : "Bonus"} AC`,
				icon: img,
				disabled: isDisabled,
				priority: acMeta.isBase ? UtilActiveEffects.PRIORITY_BASE : UtilActiveEffects.PRIORITY_BONUS,
			}));
		}

		return out;
	}

	static _pGetItemEffects_getGenericBonus ({item, img, label, key, prop, isDisabled}) {
		const bonus = !isNaN(item[prop]) ? Number(item[prop]) : 0;
		if (!bonus) return null;

		return UtilActiveEffects.getGenericEffect({
			key,
			value: bonus,
			mode: CONST.ACTIVE_EFFECT_MODES.ADD,
			label,
			icon: img,
			disabled: isDisabled,
			priority: UtilActiveEffects.PRIORITY_BONUS,
		});
	}

	static _pGetItemEffects_getDrDiDvCiEffects ({img, label, actProp, isDisabled, actorDataDrDiDvCi}) {
		if (!actorDataDrDiDvCi[actProp]) return [];

		const out = [];

		if (actorDataDrDiDvCi[actProp].value) {
			actorDataDrDiDvCi[actProp].value.forEach(it => {
				out.push(UtilActiveEffects.getGenericEffect({
					key: `data.traits.${actProp}.value`,
					value: it,
					mode: CONST.ACTIVE_EFFECT_MODES.ADD,
					label: label,
					icon: img,
					disabled: isDisabled,
					priority: UtilActiveEffects.PRIORITY_BONUS,
				}));
			})
		}

		if (actorDataDrDiDvCi[actProp].custom?.length) {
			out.push(UtilActiveEffects.getGenericEffect({
				key: `data.traits.${actProp}.custom`,
				value: actorDataDrDiDvCi[actProp].custom,
				mode: CONST.ACTIVE_EFFECT_MODES.ADD,
				label: label,
				icon: img,
				disabled: isDisabled,
				priority: UtilActiveEffects.PRIORITY_BONUS,
			}));
		}

		return out;
	}

	static async _pGetAdditionalData (item) {
		return DataConverter.pGetAdditionalData_(item, {propBrew: "foundryItem", fnLoadJson: Vetools.pGetItemSideData, propJson: "item"});
	}

	static async pHasItemSideLoadedEffects (actor, item) {
		if ((await DataConverter.pGetAdditionalEffectsRaw_(item, {propBrew: "foundryItem", fnLoadJson: Vetools.pGetItemSideData, propJson: "item"}))?.length > 0) return true;

		if (!item._variantName) return false;
		const fauxGeneric = {name: item._variantName, source: item.source};

		return (await DataConverter.pGetAdditionalEffectsRaw_(fauxGeneric, {propBrew: "foundryVariant", fnLoadJson: Vetools.pGetItemSideData, propJson: "variant"}))?.length > 0
	}

	static async pGetItemItemEffects (actor, item, sheetItem, {additionalData, img} = {}) {
		const effectsRaw = await DataConverter.pGetAdditionalEffectsRaw_(item, {propBrew: "foundryItem", fnLoadJson: Vetools.pGetItemSideData, propJson: "item"});
		const effects = UtilActiveEffects.getExpandedEffects(effectsRaw || [], {actor, sheetItem, parentName: item.name, additionalData, img});

		if (!item._variantName) return effects;
		const fauxGeneric = {name: item._variantName, source: item.source};

		const effectsRawVariant = await DataConverter.pGetAdditionalEffectsRaw_(fauxGeneric, {propBrew: "foundryVariant", fnLoadJson: Vetools.pGetItemSideData, propJson: "variant"});
		const effectsVariant = UtilActiveEffects.getExpandedEffects(effectsRawVariant || [], {actor, sheetItem, parentName: fauxGeneric.name, additionalData, img});

		return [...effects, ...effectsVariant];
	}
}

DataConverterItem._WEAPON_DETAIL_CACHE_INIT = false;
DataConverterItem._WEAPON_DETAIL_CACHE = {};
DataConverterItem._WEAPON_DETAIL_CACHE_LOCK = new VeLock();

DataConverterItem._IMPORT_TYPE_WEAPON = "weapon";
DataConverterItem._IMPORT_TYPE_TOOL = "tool";
DataConverterItem._IMPORT_TYPE_CONSUMABLE = "consumable";
DataConverterItem._IMPORT_TYPE_EQUIPMENT = "equipment";
DataConverterItem._IMPORT_TYPE_CONTAINER = "container";
DataConverterItem._IMPORT_TYPE_LOOT = "loot";

DataConverterItem._IMPORT_TYPES = {
	[DataConverterItem._IMPORT_TYPE_WEAPON]: true,
	[DataConverterItem._IMPORT_TYPE_TOOL]: true,
	[DataConverterItem._IMPORT_TYPE_CONSUMABLE]: true,
	[DataConverterItem._IMPORT_TYPE_EQUIPMENT]: true,
	[DataConverterItem._IMPORT_TYPE_CONTAINER]: true,
	[DataConverterItem._IMPORT_TYPE_LOOT]: true,
};

DataConverterItem._ITEM_TYPE_TO_ARMOR_TYPE = {
	"HA": "heavy",
	"MA": "medium",
	"LA": "light",
	"S": "shield",
};
DataConverterItem._ITEM_TYPE_TO_CONSUMABLE_TYPE = {
	A: "ammo",
	AF: "ammo",
	P: "potion",
	SC: "scroll",
	WD: "wand",
	RD: "rod",
};
// NOTE: Unofficial data, loosely based on the skills shown in XGE's expanded "Tool Proficiencies" section
DataConverterItem._ITEM_NAME_TO_DEFAULT_ABILITY = {
	"Alchemist's Supplies": "int",
	"Brewer's Supplies": "int",
	"Calligrapher's Supplies": "int",
	"Carpenter's Tools": "int",
	"Cartographer's Tools": "int",
	"Cobbler's Tools": "int",
	"Cook's Utensils": "wis",
	"Disguise Kit": "cha",
	"Forgery Kit": "int",
	"Glassblower's Tools": "int",
	"Herbalism Kit": "wis",
	"Jeweler's Tools": "int",
	"Leatherworker's Tools": "int",
	"Mason's Tools": "int",
	"Navigator's Tools": "wis",
	"Painter's Supplies": "int",
	"Poisoner's Kit": "wis",
	"Potter's Tools": "int",
	"Smith's Tools": "int",
	"Thieves' Tools": "dex",
	"Tinker's Tools": "int",
	"Weaver's Tools": "int",
	"Woodcarver's Tools": "int",
};
DataConverterItem._ITEM_TYPE_TO_ICON = {
	A: "quiver", // Ammunition
	AF: "quiver", // Ammunition
	AT: "toolbox", // Artisan Tool
	EM: "", // Eldritch Machine
	EXP: "unlit-bomb", // Explosive
	G: "backpack", // Adventuring Gear
	GS: "rolling-dices", // Gaming Set
	HA: "breastplate", // Heavy Armor
	INS: "guitar", // Instrument
	LA: "leather-armor", // Light Armor
	M: "crossed-swords", // Melee Weapon
	MA: "scale-mail", // Medium Armor
	MNT: "horse-head", // Mount
	GV: "", // Generic Variant
	P: "potion-ball", // Potion
	R: "pocket-bow", // Ranged Weapon
	RD: "crystal-wand", // Rod
	RG: "ring", // Ring
	S: "checked-shield", // Shield
	SC: "tied-scroll", // Scroll
	SCF: "orb-wand", // Spellcasting Focus
	OTH: "", // Other
	T: "toolbox", // Tool
	TAH: "", // Tack and Harness
	TG: "barrel", // Trade Good
	VEH: "old-wagon", // Vehicle (land)
	SHP: "galleon", // Vehicle (water)
	AIR: "air-balloon", // Vehicle (air)
	WD: "fairy-wand", // Wand
};

DataConverterItem._ITEM_PROP_MAP = {
	"A": "amm",
	"AF": "amm",
	"BF": "", // Burst Fire has no mapping
	"F": "fin",
	"H": "hvy",
	"L": "lgt",
	"LD": "lod",
	"R": "rch",
	"RLD": "rel",
	"S": "spc",
	"T": "thr",
	"2H": "two",
	"V": "ver",
};
DataConverterItem._ITEM_EQUIPMENT_NAME_RES = [
	"amulet of",
	"badge of",
	"band of",
	"belt of",
	"boots of",
	"bracelet of",
	"bracer of",
	"bracers of",
	"brooch of",
	"cape of",
	"circlet of",
	"clothes of",
	"crown of",
	"eyes of",
	"gauntlets of",
	"gloves of",
	"goggles of",
	"hat of",
	"headband of",
	"helm of",
	"mantle of",
	"mask of",
	"necklace of",
	"periapt of",
	"ring of",
	"rings of",
	"robe of",
	"slippers of",
].map(it => new RegExp(`(?:[ (]|^)${it}`, "i"));
// Based on "Barding" from the PHB
DataConverterItem._SIZE_TO_ITEM_WEIGHT_MULT = {
	[SZ_FINE]: 0.5,
	[SZ_DIMINUTIVE]: 0.5,
	[SZ_TINY]: 0.5,
	[SZ_SMALL]: 1,
	[SZ_MEDIUM]: 1,
	[SZ_LARGE]: 2,
	[SZ_HUGE]: 4,
	[SZ_GARGANTUAN]: 8,
	[SZ_COLOSSAL]: 16,
	[SZ_VARIES]: 1,
};
DataConverterItem._SIZE_TO_ITEM_VALUE_MULT = {
	[SZ_FINE]: 0.25,
	[SZ_DIMINUTIVE]: 0.25,
	[SZ_TINY]: 0.25,
	[SZ_SMALL]: 1,
	[SZ_MEDIUM]: 1,
	[SZ_LARGE]: 4,
	[SZ_HUGE]: 16,
	[SZ_GARGANTUAN]: 64,
	[SZ_COLOSSAL]: 256,
	[SZ_VARIES]: 1,
};
// Based on "Gurt's Greataxe" from SKT
DataConverterItem._SIZE_TO_ITEM_WEIGHT_AND_VALUE_EXPONENT = {
	[SZ_FINE]: 0.5,
	[SZ_DIMINUTIVE]: 0.5,
	[SZ_TINY]: 0.5,
	[SZ_SMALL]: 1,
	[SZ_MEDIUM]: 1,
	[SZ_LARGE]: 2,
	[SZ_HUGE]: 3,
	[SZ_GARGANTUAN]: 4,
	[SZ_COLOSSAL]: 5,
	[SZ_VARIES]: 1,
};

export {DataConverterItem};
