import {ImportListCharacter} from "./ImportListCharacter.js";
import {LGT} from "./Util.js";
import {UtilApplications} from "./UtilApplications.js";
import {DataConverter} from "./DataConverter.js";
import {DataConverterSpell} from "./DataConverterSpell.js";
import {UtilActors} from "./UtilActors.js";
import {Charactermancer_AdditionalSpellsSelect} from "./UtilCharactermancerAdditionalSpells.js";
import {Consts} from "./Consts.js";
import {Charactermancer_AbilityScoreSelect} from "./UtilCharactermancer.js";

// TODO merge parts with `ImportListRace`
/**
 * A generic feature importer.
 * Note that effects are generally handled at an "actor" level, not at an "item" level. This is because, should an
 * effect depend on a choice the user made when importing (e.g. selecting "Constitution" as the ability score for the
 * "Resilience" feat), we need to be able to use this variable data. Therefore, side-loaded effects are handled,
 * generally, by the importer, as opposed to directly adding effects to the items we would import.
 * generally, by the importer, as opposed to directly adding effects to the items we would import.
 */
class ImportListFeature extends ImportListCharacter {
	// region External
	static init () {
		throw new Error(`Unimplemented!`);
	}
	// endregion

	constructor (applicationOpts, externalData, subclassOpts, featureImporterOpts) {
		super(applicationOpts, externalData, subclassOpts);

		this._titleLog = featureImporterOpts.titleLog;
	}

	static async _pGetSideData (actor, entity) {
		throw new Error(`Unimplemented!`);
	}

	static async _pGetEntityItem (actor, entity) {
		throw new Error(`Unimplemented!`);
	}

	static async _pHasSideLoadedEffects (actor, entity) {
		throw new Error("Unimplemented!");
	}

	async _pMutActorUpdateFeature (entity, actUpdate, dataBuilderOpts) {
		throw new Error("Unimplemented!");
	}

	/**
	 * @param entity
	 * @param importOpts Options object.
	 * @param [importOpts.isTemp] if the item should be temporary, and displayed.
	 * @param [importOpts.isDataOnly] If this import should simply return the data, rather than import anything.
	 * @param [importOpts.filterValues] Saved filter values to be used instead of our own.
	 * @param [importOpts.isCharactermancer]
	 * @return {*}
	 */
	async pImportEntry (entity, importOpts) {
		importOpts = importOpts || {};

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

		if (importOpts.isDataOnly) return {imported: [await this.constructor._pGetEntityItem(this._actor, entity)], status: UtilApplications.TASK_EXIT_COMPLETE_DATA_ONLY};

		if (importOpts.isTemp) return this._pImportEntry_pImportToDirectoryGeneric(entity, importOpts);
		else if (this._actor) return this._pImportEntry_pImportToActor(entity, importOpts);
		else return this._pImportEntry_pImportToDirectoryGeneric(entity, importOpts);
	}

	async _pImportEntry_pImportToActor (entity, importOpts) {
		// Build actor update
		const actUpdate = {data: {}};

		const dataBuilderOpts = new ImportListFeature.ImportEntryOpts({
			chosenAbilityScoreIncrease: entity._foundryChosenAbilityScoreIncrease,
			isCharactermancer: !!importOpts.isCharactermancer,
		});

		await this._pImportEntry_pImportToActor_fillFlags(entity, actUpdate, importOpts);
		await this._pImportEntry_pFillAbilities(entity, actUpdate, dataBuilderOpts);
		if (dataBuilderOpts.isCancelled) return {status: UtilApplications.TASK_EXIT_CANCELLED};
		await this._pImportEntry_pFillSkills(entity, actUpdate.data, dataBuilderOpts);
		if (dataBuilderOpts.isCancelled) return {status: UtilApplications.TASK_EXIT_CANCELLED};
		await this._pImportEntry_pFillTraits(entity, actUpdate.data, dataBuilderOpts);
		if (dataBuilderOpts.isCancelled) return {status: UtilApplications.TASK_EXIT_CANCELLED};

		// Add actor items
		await this._pImportEntry_pFillItems(entity, actUpdate, dataBuilderOpts);
		if (dataBuilderOpts.isCancelled) return {status: UtilApplications.TASK_EXIT_CANCELLED};

		// Update actor
		if (Object.keys(actUpdate.data).length) await this._actor.update(actUpdate);

		// region Add any sub-entities
		// E.g. Monk's "Unarmed Strike" sub-entity for the "Martial Arts" feature
		await this._pImportEntry_pAddSubEntities(entity);
		// endregion

		if (this._actor.isToken) this._actor.sheet.render();

		return {imported: [{name: entity.name, actor: this._actor}], status: UtilApplications.TASK_EXIT_COMPLETE};
	}

	_pImportEntry_pImportToActor_fillFlags (feature, actor, importOpts) {
		const flags = {};
		const flagsDnd5e = {};

		this._doPopulateFlags({feature, actor, importOpts})

		if (Object.keys(flagsDnd5e).length) flags.dnd5e = flagsDnd5e;
		if (Object.keys(flags).length) actor.flags = flags;
	}

	_doPopulateFlags ({feature, actor, importOpts, flags, flagsDnd5e}) { /* Implement as required */ }

	async _pImportEntry_pFillAbilities (feature, actUpdate, dataBuilderOpts) {
		const formData = await Charactermancer_AbilityScoreSelect.pFillActorAbilityData(this._actor, feature.ability, actUpdate, dataBuilderOpts);
		if (dataBuilderOpts.isCancelled) return;

		// Pull out the chosen ability scores, so we can pass the maximum one into any potential spell population
		if (formData == null) return;
		dataBuilderOpts.chosenAbilityScoreIncrease = formData.data;
	}

	async _pImportEntry_pFillSkills (feature, data, dataBuilderOpts) {
		await DataConverter.pFillActorSkillData(
			MiscUtil.get(this._actor, "data", "data", "skills"),
			feature.skillProficiencies,
			data,
			dataBuilderOpts,
		);
	}

	async _pImportEntry_pFillTraits (feature, data, dataBuilderOpts) {
		data.traits = {};

		await DataConverter.pFillActorLanguageData(
			MiscUtil.get(this._actor, "data", "data", "traits", "languages"),
			feature.languageProficiencies,
			data,
			dataBuilderOpts,
		);
		if (dataBuilderOpts.isCancelled) return;

		await DataConverter.pFillActorToolProfData(
			MiscUtil.get(this._actor, "data", "data", "traits", "toolProf"),
			feature.toolProficiencies,
			data,
			dataBuilderOpts,
		);
		if (dataBuilderOpts.isCancelled) return;

		await DataConverter.pFillActorArmorProfData(
			MiscUtil.get(this._actor, "data", "data", "traits", "armorProf"),
			feature.armorProficiencies,
			data,
			dataBuilderOpts,
		);
		if (dataBuilderOpts.isCancelled) return;

		await DataConverter.pFillActorWeaponProfData(
			MiscUtil.get(this._actor, "data", "data", "traits", "weaponProf"),
			feature.weaponProficiencies,
			data,
			dataBuilderOpts,
		);
		if (dataBuilderOpts.isCancelled) return;

		await DataConverter.pFillActorImmunityData(
			MiscUtil.get(this._actor, "data", "data", "traits", "di"),
			feature.immune,
			data,
			dataBuilderOpts,
		);
		if (dataBuilderOpts.isCancelled) return;

		await DataConverter.pFillActorResistanceData(
			MiscUtil.get(this._actor, "data", "data", "traits", "dr"),
			feature.resist,
			data,
			dataBuilderOpts,
		);
		if (dataBuilderOpts.isCancelled) return;

		await DataConverter.pFillActorVulnerabilityData(
			MiscUtil.get(this._actor, "data", "data", "traits", "dv"),
			feature.vulnerable,
			data,
			dataBuilderOpts,
		);
		if (dataBuilderOpts.isCancelled) return;

		await DataConverter.pFillActorConditionImmunityData(
			MiscUtil.get(this._actor, "data", "data", "traits", "ci"),
			feature.conditionImmune,
			data,
			dataBuilderOpts,
		);
	}

	async _pImportEntry_pFillItems (feature, actUpdate, dataBuilderOpts) {
		await this._pMutActorUpdateFeature(feature, actUpdate, dataBuilderOpts);
		if (dataBuilderOpts.isCancelled) return;

		const spellHashToItemPosMap = {};

		await this._pImportEntry_pHandleAdditionalSpells(feature, actUpdate, dataBuilderOpts, spellHashToItemPosMap);
		if (dataBuilderOpts.isCancelled) return;

		const hook = DataConverterSpell.doHookSpellLinkRender.bind(null, this._actor.id, spellHashToItemPosMap);
		const featItem = await Renderer.get().pWithPlugin({
			entryType: "link",
			pluginType: "*",
			fnPlugin: hook,
			pFn: async () => {
				const featureItem = await this.constructor._pGetEntityItem(this._actor, feature);
				dataBuilderOpts.items.push(featureItem);
				return featureItem;
			},
		});

		const importedEmbeds = await UtilActors.pAddActorItems(this._actor, dataBuilderOpts.items);

		// region Add item effects
		const effectsToAdd = [];
		if (await this.constructor._pHasSideLoadedEffects(this._actor, feature)) {
			const importedEmbed = DataConverter.getImportedEmbed(importedEmbeds, featItem);

			if (importedEmbed) effectsToAdd.push(...(await this.constructor._pGetItemEffects(this._actor, feature, importedEmbed.document, dataBuilderOpts)));
		}

		await UtilActors.pAddActorEffects(this._actor, effectsToAdd);
		// endregion
	}

	async _pImportEntry_pHandleAdditionalSpells (feature, actUpdate, dataBuilderOpts, spellHashToItemPosMap) {
		const maxAbilityScoreIncrease = Object.entries(dataBuilderOpts.chosenAbilityScoreIncrease || {})
			.sort(([, vA], [, vB]) => SortUtil.ascSort(vB, vA));
		const parentAbilityAbv = maxAbilityScoreIncrease?.[0]?.[0] || null;

		const formData = await Charactermancer_AdditionalSpellsSelect.pGetUserInput({
			additionalSpells: feature.additionalSpells,
			sourceHintText: feature.name,

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

		if (formData == null) return dataBuilderOpts.isCancelled = true;
		if (formData === VeCt.SYM_UI_SKIP) return;

		const totalClassLevels = UtilActors.getTotalClassLevels(this._actor)
		await Charactermancer_AdditionalSpellsSelect.pApplyFormDataToActor(
			this._actor,
			formData,
			{
				parentAbilityAbv: parentAbilityAbv,
				hashToIdMap: spellHashToItemPosMap,
			},
		);
	}

	async _pImportEntry_pAddSubEntities (entity) {
		await this.constructor._pGetClassSubclassFeatureAdditionalEntities(this._actor, entity);
	}

	/** Note that we assume that anything in this sub-entity data is to be imported as an item on the actor. */
	static async _pGetClassSubclassFeatureAdditionalEntities (actor, entity) {
		const sideData = await this._pGetSideData(actor, entity);
		if (!sideData) return [];
		if (!sideData.subEntities) return [];

		const {ChooseImporter} = await import("./ChooseImporter.js");

		for (const prop in sideData.subEntities) {
			if (!sideData.subEntities.hasOwnProperty(prop)) continue;

			const arr = sideData.subEntities[prop];
			if (!(arr instanceof Array)) continue;

			const importer = ChooseImporter.getImporter(prop, actor);
			await importer.pInit();
			for (const ent of arr) {
				await importer.pImportEntry(ent);
			}
		}
	}
}
ImportListFeature.ImportEntryOpts = class extends ImportListCharacter.ImportEntryOpts {
	constructor (opts) {
		opts = opts || {};
		super(opts);

		this.chosenAbilityScoreIncrease = opts.chosenAbilityScoreIncrease;
	}
}

export {ImportListFeature};
