import {ImportList} from "./ImportList.js";
import {DataConverter} from "./DataConverter.js";
import {Vetools} from "./Vetools.js";
import {Config} from "./Config.js";
import {UtilApplications} from "./UtilApplications.js";
import {UtilActors} from "./UtilActors.js";
import {LGT} from "./Util.js";
import {Consts} from "./Consts.js";
import {UtilCompendium} from "./UtilCompendium.js";
import {SharedConsts} from "../shared/SharedConsts.js";
import {DataConverterItem} from "./DataConverterItem.js";
import {UtilGameSettings} from "./UtilGameSettings.js";
import {ConfigConsts} from "./ConfigConsts.js";

class ImportListActor extends ImportList {
	/**
	 * @param applicationOpts
	 * @param externalData
	 * @param subclassOpts
	 * @param actorImporterOpts Options object.
	 * @param actorImporterOpts.actorType
	 */
	constructor (applicationOpts, externalData, subclassOpts, actorImporterOpts) {
		super(applicationOpts, externalData, subclassOpts);
		this._actorType = actorImporterOpts.actorType;
		this._isSkipFolder = false;
	}

	// region Shared
	/**
	 * @param imp
	 * @param importOpts Options object.
	 * @param [importOpts.isTemp] if the item should be temporary, and displayed.
	 * @param [importOpts.isDataOnly] If the item should not be imported, but its data should be returned.
	 */
	async pImportEntry (imp, importOpts) {
		importOpts = importOpts || {};

		console.log(...LGT, `Importing ${this._actorType} "${imp.name}" (from "${Parser.sourceJsonToAbv(imp.source)}")`);

		if (this._actor) throw new Error(`Cannot import ${this._actorType} to actor!`);

		let actor;
		const duplicateMeta = this._getDuplicateMeta({entity: imp});
		if (duplicateMeta.isSkip) return {status: UtilApplications.TASK_EXIT_SKIPPED_DUPLICATE};
		else if (duplicateMeta.isOverwrite) {
			this._isSkipFolder = true;
			actor = duplicateMeta.existing;
		} else {
			this._isSkipFolder = false;
			actor = this._pack
				? null
				// Create the entity, so we have access to its ID
				: await Actor.create(
					{name: Consts.ACTOR_TEMP_NAME, type: this._actorType},
					{renderSheet: !!importOpts.isTemp, temporary: !!importOpts.isTemp},
				);
		}

		const {dataBuilderOpts, actorData} = await this._pImportEntry_pGetImportMetadata(actor, imp, importOpts);

		const additionalData = await this._pGetAdditionalData(imp);
		if (additionalData) Object.assign(actorData.data || {}, additionalData);

		if (importOpts.isTemp) {
			actor = await Actor.create({...actorData, type: this._actorType}, {renderSheet: !importOpts.isDataOnly, temporary: true});
			dataBuilderOpts.actor = actor;

			await this._pImportEntry_pFillItems(imp, actorData, dataBuilderOpts, importOpts);
			await this._pImportEntry_pApplyEffects(dataBuilderOpts, importOpts);

			// Handle any post-item item updates
			await this._pImportEntry_pHandlePostItemItemUpdates(actor, importOpts, dataBuilderOpts);

			return importOpts.isDataOnly
				? {imported: [actor], status: UtilApplications.TASK_EXIT_COMPLETE_DATA_ONLY}
				: {imported: [actor], status: UtilApplications.TASK_EXIT_COMPLETE};
		} else if (this._pack) {
			if (duplicateMeta.isOverwrite) {
				dataBuilderOpts.actor = actor;

				await actor.deleteEmbeddedDocuments("Item", actor.items.map(it => it.id));
				await actor.deleteEmbeddedDocuments("ActiveEffect", actor.effects.map(it => it.id));

				await this._pImportEntry_pFillItems(imp, actorData, dataBuilderOpts, importOpts);
				await this._pImportEntry_pApplyEffects(dataBuilderOpts, importOpts);

				await actor.update(actorData);

				// Handle any post-item item updates (use the new actor reference, just in case)
				await this._pImportEntry_pHandlePostItemItemUpdates(actor, importOpts, dataBuilderOpts);

				return {imported: [actor], status: UtilApplications.TASK_EXIT_COMPLETE_UPDATE_OVERWRITE};
			} else {
				actor = await Actor.create({...actorData, type: this._actorType}, {temporary: true});
				dataBuilderOpts.actor = actor;

				await this._pImportEntry_pFillItems(imp, actorData, dataBuilderOpts, importOpts);
				await this._pImportEntry_pApplyEffects(dataBuilderOpts, importOpts);

				const actorImported = await this._pack.importDocument(actor);

				// Handle any post-item item updates; switch to using the "real" actor, which has IDs populated on items
				await this._pImportEntry_pHandlePostItemItemUpdates(actorImported, importOpts, dataBuilderOpts);
			}

			return {imported: [actor], status: UtilApplications.TASK_EXIT_COMPLETE};
		} else {
			// If we're updating an existing entity, strip its embedded documents, as passing in an updated
			//   `items`/`effects` array only adds items.
			if (duplicateMeta.isOverwrite) {
				await actor.deleteEmbeddedDocuments("Item", actor.items.map(it => it.id));
				await actor.deleteEmbeddedDocuments("ActiveEffect", actor.effects.map(it => it.id));
			}

			await this._pImportEntry_pFillItems(imp, actorData, dataBuilderOpts, importOpts);
			await this._pImportEntry_pApplyEffects(dataBuilderOpts, importOpts);

			// Set the actor's data
			await actor.update(actorData);

			await game.actors.set(actor.id, actor);

			// Handle any post-item item updates
			await this._pImportEntry_pHandlePostItemItemUpdates(actor, importOpts, dataBuilderOpts);

			return {imported: [actor], status: duplicateMeta.isOverwrite ? UtilApplications.TASK_EXIT_COMPLETE_UPDATE_OVERWRITE : UtilApplications.TASK_EXIT_COMPLETE};
		}
	}

	/** Run after any item effects have been applied. */
	async _pImportEntry_pApplyEffects (dataBuilderOpts, importOpts) {
		if (!dataBuilderOpts.effects?.length) return;
		const isTemporary = importOpts.isTemp || this._pack != null;
		await UtilActors.pAddActorEffects(dataBuilderOpts.actor, dataBuilderOpts.effects, {isTemporary});
	}

	async _pImportEntry_pHandlePostItemItemUpdates (actor, importOpts, dataBuilderOpts) {
		if (!dataBuilderOpts.postItemItemUpdates) return;

		for (const pFnUpdate of dataBuilderOpts.postItemItemUpdates) {
			await pFnUpdate({
				actor,
				isTemp: importOpts.isTemp,
				isPack: this._pack != null,
				pack: this._pack,
			});
		}
	}

	_pImportEntry_pGetImportMetadata () { throw new Error(`Unimplemented!`); }

	_pImportEntry_pFillItems () { throw new Error(`Unimplemented!`); }

	_pImportEntry_hasTokenImage (it) { return this._props.some(prop => Vetools.hasTokenUrl(prop, it)); }

	_pImportEntry_getTokenImage (it) {
		const prop = this._props.find(prop => Vetools.hasTokenUrl(prop, it));
		if (prop) return Vetools.getTokenUrl(prop, it);
		return `icons/svg/mystery-man.svg`;
	}
	// endregion

	// region Base data
	/**
	 * @param it
	 * @param act
	 * @param fluff
	 * @param [opts]
	 * @param [opts.isUseTokenImageAsPortrait]
	 */
	async _pImportEntry_pFillBase (it, act, fluff, opts) {
		opts = opts || {};

		act.name = this._getActorSheetName(it);

		let img = opts.isUseTokenImageAsPortrait ? null : Vetools.getImageUrlFromFluff(fluff);
		if (!img) {
			if (this._pImportEntry_hasTokenImage(it)) img = this._pImportEntry_getTokenImage(it);
			else {
				const fromCompendium = await this._pImportEntry_pFillBase_pGetCompendiumImage(it);
				if (fromCompendium) img = fromCompendium; // Use the compendium token if it exists
				else img = this._pImportEntry_getTokenImage(it); // fallback on 5etools default "blank token"
			}
		}

		act.img = img;
		act.type = this._actorType;

		act.flags = this._getActorFlags(it);
	}

	_getActorSheetName (it) {
		return DataConverter.getNameWithSourcePart(it);
	}

	_getActorFlags (it) {
		return {
			[SharedConsts.MODULE_NAME_FAKE]: {
				page: this._page,
				source: it.source,
				hash: UrlUtil.URL_TO_HASH_BUILDER[this._page](it),
			},
		};
	}

	async _pImportEntry_pFillBase_pGetCompendiumImage (it) {
		for (const prop of this._props) {
			const fromCompendium = await UtilCompendium.pGetCompendiumImage(prop, it);
			// Avoid using the default "mystery-man" image
			if (fromCompendium && !fromCompendium.toLowerCase().includes("mystery-man.svg")) return fromCompendium;
		}
		return null;
	}
	// endregion

	// region Token data
	async _pImportEntry_pFillToken (it, act, configGroup) {
		let dimensions = 1;
		switch (it.size || SZ_MEDIUM) {
			case "T": dimensions = 0.5; break;
			case "L": dimensions = 2; break;
			case "H": dimensions = 3; break;
			case "G": dimensions = 4; break;
		}

		let maxDimSight = 0;
		let maxBrightSight = 0;
		(it.senses || []).forEach(sens => {
			sens = sens.replace(Parser._numberCleanRegexp, "");
			const mSense = /(blindsight|darkvision|tremorsense|truesight)\s*(\d+)/i.exec(sens);
			if (mSense) {
				const num = Number(mSense[2]);
				switch (mSense[1]) {
					case "darkvision": maxDimSight = Math.max(maxDimSight, num); break;
					case "blindsight":
					case "tremorsense":
					case "truesight": maxBrightSight = Math.max(maxBrightSight, num); break;
				}
			}
		});

		act.token = {
			...this.constructor._getMergedTokenData({configGroup, maxDimSight, maxBrightSight}),
			name: UtilApplications.getCleanEntityName(it._displayName || it.name),
			img: this._pImportEntry_getTokenImage(it),
			width: dimensions,
			height: dimensions,
			scale: 1,
			elevation: 0,
			rotation: 0,

			actorLink: false,
			actorData: {},
			flags: {},
			effects: [],
			randomImg: false,
		}
	}

	static _getMergedTokenData ({configGroup, maxDimSight, maxBrightSight}) {
		const out = UtilGameSettings.getSafe("core", "defaultToken") || {};

		if (Config.get(configGroup, "tokenNameDisplay") !== ConfigConsts.C_USE_GAME_DEFAULT) out.displayName = Config.get(configGroup, "tokenNameDisplay");

		if (Config.get(configGroup, "tokenIsAddVision") !== ConfigConsts.C_USE_GAME_DEFAULT) out.vision = Config.get(configGroup, "tokenIsAddVision") === ConfigConsts.C_BOOL_ENABLED;

		if (Config.get(configGroup, "tokenDisposition") !== ConfigConsts.C_USE_GAME_DEFAULT) out.disposition = Config.get(configGroup, "tokenDisposition");

		if (Config.get(configGroup, "tokenBarDisplay") !== ConfigConsts.C_USE_GAME_DEFAULT) out.displayBars = Config.get(configGroup, "tokenBarDisplay");
		if (Config.get(configGroup, "tokenBar1Attribute") !== ConfigConsts.C_USE_GAME_DEFAULT) MiscUtil.set(out, "bar1", "attribute", Config.get(configGroup, "tokenBar1Attribute"));
		if (Config.get(configGroup, "tokenBar2Attribute") !== ConfigConsts.C_USE_GAME_DEFAULT) MiscUtil.set(out, "bar2", "attribute", Config.get(configGroup, "tokenBar2Attribute"));

		if (Config.get(configGroup, "tokenDimSight") !== ConfigConsts.C_USE_GAME_DEFAULT) out.dimSight = maxDimSight;
		if (Config.get(configGroup, "tokenBrightSight") !== ConfigConsts.C_USE_GAME_DEFAULT) out.brightSight = maxBrightSight;

		// region Unused/use sensible defaults

		// lockRotation: false,
		// dimLight: 0,
		// brightLight: 0,
		// sightAngle: 360,
		// lightAngle: 360,

		// endregion

		return out;
	}
	// endregion

	// region Ability data
	_pImportEntry_fillData_Abilities (it, data, impOpts) {
		const out = {};

		Parser.ABIL_ABVS.forEach(ab => {
			let profType = 0;

			const score = it[ab] ?? 0;

			const mod = Parser.getAbilityModNumber(score);

			if (it.save && it.save[ab]) {
				const saveNum = Number(it.save[ab]);
				if (!isNaN(saveNum)) {
					const abMod = Parser.getAbilityModNumber(score);
					const profValue = abMod + (impOpts.assumedPb || impOpts.pb);
					const expertValue = abMod + (2 * (impOpts.assumedPb || impOpts.pb));

					if (profValue === saveNum) profType = 1;
					else if (expertValue === saveNum) profType = 2;
					else {
						const deltaProf = Math.abs(saveNum - profValue);
						const deltaExpert = Math.abs(saveNum - expertValue);

						if (deltaProf < deltaExpert) profType = 1;
						else profType = 2;
						const finalBonus = profType === 2 ? expertValue : profValue;

						console.warn(...LGT, `Creature "${it.name}" (${it.source}) save "${ab}" bonus "${it.save[ab]}" did not match an expected modifier/proficiency amount! A fallback value ("${profType === 2 ? "expert" : "proficient"}" = "${UiUtil.intToBonus(finalBonus)}") will be used.`);
					}
				}
			}

			out[ab] = {
				value: score,
				proficient: profType,
				mod,
			}
		});

		data.abilities = out;
	}
	// endregion

	// region Details
	_getBiographyValue (entity, fluff, {isImportText, isImportImages} = {}) {
		if (
			(!isImportText && !isImportImages)
			|| !fluff
			|| !((fluff.entries && isImportText) || (fluff.images && isImportImages))
		) return "";

		return [
			isImportImages && fluff.images && fluff.images.length
				? Renderer.get().setFirstSection(true).render({type: "entries", entries: [fluff.images[0]]})
				: null,
			Renderer.utils.getFluffTabContent({entity, fluff, isImageTab: false}),
			isImportImages && fluff.images && fluff.images.length > 1
				? Renderer.get().setFirstSection(true).render({type: "entries", entries: [...fluff.images.slice(1)]})
				: null,
		].filter(Boolean).join("");
	}
	// endregion

	// region Traits
	_pImportEntry_fillConditionsDamage (ent, dataTraits) {
		Object.assign(dataTraits, DataConverter.getActorDamageResImmVulnConditionImm(ent));
	}
	// endregion

	// region Items
	async _pFillWeaponItem (
		entity,
		act,
		action,
		dataBuilderOpts,
		{
			offensiveAbility,
			damageParts,
			formula,
			rangeShort,
			rangeLong,
			actionType,
			isProficient,
			description,
			saveAbility,
			saveDc,
			saveScaling,
			attackBonus,
			_foundryData,
			foundryData,
			_foundryFlags,
			foundryFlags,
			img,
			isSiegeWeapon,
			isMagical,
		},
	) {
		const weaponDetails = await DataConverterItem.pGetActionWeaponDetails({size: entity.size, action, damageParts, isSiegeWeapon, isMagical});
		const combinedFoundryData = DataConverter.getCombinedFoundryData(foundryData, _foundryData);
		const combinedFoundryFlags = DataConverter.getCombinedFoundryFlags(foundryFlags, _foundryFlags);

		const itemData = {
			name: UtilApplications.getCleanEntityName(Renderer.stripTags(action.name || "") || " "),
			type: "weapon",
			data: {
				source: DataConverter.getSourceWithPagePart(entity),
				description: {value: description},

				damage: {
					parts: weaponDetails.damageParts,
					versatile: weaponDetails.damageVersatile,
				},

				range: {
					value: rangeShort,
					long: rangeLong,
					units: "ft",
				},
				actionType: actionType,
				proficient: isProficient,

				quantity: 1,
				weight: weaponDetails.weight,
				price: weaponDetails.price,
				rarity: weaponDetails.rarity,

				weaponType: weaponDetails.weaponType,
				properties: weaponDetails.itemProperties,

				attuned: weaponDetails.attuned,
				// Force attune items if they have attunement requirements
				attunement: weaponDetails.attunement ? CONFIG.DND5E.attunementTypes.ATTUNED : weaponDetails.attunement,
				ability: offensiveAbility,
				equipped: true,
				identified: weaponDetails.identified,
				activation: {
					type: "action",
					cost: 1,
					condition: "",
				},
				duration: {
					value: null,
					units: "",
				},
				target: {
					value: null,
					units: "",
					type: "",
				},
				uses: {
					value: 0,
					max: 0,
					per: null,
				},
				attackBonus: attackBonus || weaponDetails.attackBonus || 0, // Prefer a custom bonus, if we have one
				chatFlavor: "",
				critical: null,

				formula,
				save: {
					ability: saveAbility,
					dc: saveDc,
					scaling: saveScaling,
				},

				...(combinedFoundryData || {}),
			},
			img: weaponDetails.img || img,
			flags: {
				...(combinedFoundryFlags || {}),
			},
			effects: [],
		};

		dataBuilderOpts.items.push(itemData);
	}

	_getSavingThrowData (strEntries) {
		if (!strEntries) return MiscUtil.copy(ImportListActor._DEFAULT_SAVING_THROW_DATA);

		let isFoundParse = false;
		let {
			saveAbility,
			saveScaling,
			saveDc,
		} = MiscUtil.copy(ImportListActor._DEFAULT_SAVING_THROW_DATA);

		const mDc = /(?:{@dc (\d+)}|DC\s*(\d+))\s*(Strength|Dexterity|Constitution|Intelligence|Wisdom|Charisma)/i.exec(strEntries);
		if (mDc) {
			saveDc = Number(mDc[1] || mDc[2]);
			saveAbility = mDc[3].toLowerCase().substring(0, 3);

			saveScaling = "flat";

			isFoundParse = true;
		}

		return {saveAbility, saveScaling, saveDc, isFoundParse};
	}
	// endregion

	// region Additional data
	async _pGetAdditionalData (ent) {
		if (!this._propsBrewAdditionalData?.length || !this._fnLoadSideData) return null;

		const out = {};

		for (const {prop, propBrewAdditionalData} of this._propGroups) {
			const outSub = await DataConverter.pGetAdditionalData_(ent, {propBrew: propBrewAdditionalData, fnLoadJson: this._fnLoadSideData, propJson: prop});
			if (outSub) Object.assign(out, outSub);
		}

		return Object.keys(out).length ? out : null;
	}
	// endregion
}
ImportListActor._DEFAULT_SAVING_THROW_DATA = {
	saveAbility: "",
	saveScaling: "spell",
	saveDc: null,
};

ImportListActor.ImportEntryOpts = class {
	constructor (opts) {
		opts = opts || {};

		this.actor = opts.actor;
		this.fluff = opts.fluff;

		this.items = []; // To be filled
		this.effects = []; // To be filled
		this.postItemItemUpdates = []; // Update that will be applied after all item updates have been made.
	}
};

export {ImportListActor};
