import {LGT} from "./Util.js";
import {Vetools} from "./Vetools.js";
import {UtilActors} from "./UtilActors.js";
import {DataConverter} from "./DataConverter.js";
import {Config} from "./Config.js";
import {UtilList2} from "./UtilList2.js";
import {DataConverterSpell} from "./DataConverterSpell.js";
import {DataConverterRace} from "./DataConverterRace.js";
import {UtilApplications} from "./UtilApplications.js";
import {Charactermancer_AbilityScoreSelect} from "./UtilCharactermancer.js";
import {
	Charactermancer_Race_FeatSelect,
	Charactermancer_Race_SizeSelect,
	Charactermancer_Race_Util,
} from "./UtilCharactermancerRace.js";
import {ImportListFeat} from "./ImportListFeat.js";
import {UtilDataSource} from "./UtilDataSource.js";
import {ImportListCharacter} from "./ImportListCharacter.js";

// TODO merge parts with `ImportListFeature`
class ImportListRace extends ImportListCharacter {
	// region External
	static init () {
		this._initCreateSheetItemHook({
			prop: "race",
			importerName: "Race",
		});
	}
	// endregion

	constructor (externalData) {
		externalData = externalData || {};
		super(
			{title: "Import Races"},
			externalData,
			{
				props: ["race"],
				titleSearch: "races",
				sidebarTab: "items",
				gameProp: "items",
				defaultFolderPath: ["Races"],
				folderType: "Item",
				pageFilter: new PageFilterRaces(),
				isActorRadio: true,
				page: UrlUtil.PG_RACES,
				isPreviewable: true,
				isDedupable: true,
			},
		);
	}

	async _pPostLoad (raceList, fileData) {
		return Charactermancer_Race_Util.pPostLoadBrew(fileData);
	}

	async pGetSources () {
		const nonVetoolsOpts = {pPostLoad: this._pPostLoad.bind(this)};

		return [
			new UtilDataSource.DataSourceSpecial(
				Config.get("ui", "isStreamerMode") ? "SRD" : "5etools",
				async () => (await Vetools.pGetRaces()).race,
				{
					cacheKey: `5etools-races`,
					filterTypes: [UtilDataSource.SOURCE_TYP_OFFICIAL_ALL],
					isDefault: true,
				},
			),
			new UtilDataSource.DataSourceUrl(
				"Custom URL",
				"",
				{
					...nonVetoolsOpts,
					filterTypes: [UtilDataSource.SOURCE_TYP_CUSTOM],
				},
			),
			new UtilDataSource.DataSourceFile(
				"Upload File",
				{
					...nonVetoolsOpts,
					filterTypes: [UtilDataSource.SOURCE_TYP_CUSTOM],
				},
			),
			...(await Vetools.pGetHomebrewSources("race", "subrace")).map(({name, url}) => new UtilDataSource.DataSourceUrl(
				name,
				url,
				{
					...nonVetoolsOpts,
					filterTypes: [UtilDataSource.SOURCE_TYP_BREW],
				},
			)),
		]
	}

	getData () {
		return {
			isRadio: this._isRadio,
			isPreviewable: this._isPreviewable,
			titleButtonRun: this._titleButtonRun,
			titleSearch: this._titleSearch,
			cols: [
				{
					name: "Name",
					width: 4,
					field: "name",
				},
				{
					name: "Ability",
					width: 5,
					field: "ability",
				},
				{
					name: "Size",
					width: 1,
					field: "size",
				},
				{
					name: "Source",
					width: 1,
					field: "source",
					titleProp: "sourceLong",
					displayProp: "sourceShort",
					classNameProp: "sourceClassName",
					rowClassName: "text-center",
				},
			],
			rows: this._content.map((it, ix) => {
				this._pageFilter.constructor.mutateForFilters(it);

				// region Re-used in fnGetValues
				it._vAbility = it.ability ? Renderer.getAbilityData(it.ability).asTextShort : "None";
				it._vSize = (it.size || [SZ_VARIES]).map(sz => Parser.sizeAbvToFull(sz)).join("/");
				// endregion

				return {
					name: it.name,
					ability: it._vAbility,
					size: it._vSize,
					source: it.source,
					sourceShort: Parser.sourceJsonToAbv(it.source),
					sourceLong: Parser.sourceJsonToFull(it.source),
					sourceClassName: Parser.sourceJsonToColor(it.source),
					ix,
				};
			}),
		}
	}

	_activateListeners_absorbListItems () {
		this._list.doAbsorbItems(
			this._content,
			{
				fnGetName: it => it.name,
				// values used for sorting/search
				fnGetValues: it => ({
					source: it.source,
					ability: it._vAbility,
					size: it._vSize,
					hash: UrlUtil.URL_TO_HASH_BUILDER[this._page](it),
				}),
				fnGetData: UtilList2.absorbFnGetData,
				fnBindListeners: it => this._isRadio
					? UtilList2.absorbFnBindListenersRadio(this._list, it)
					: UtilList2.absorbFnBindListeners(this._list, it),
			},
		);
	}

	/**
	 * @param race
	 * @param importOpts Options object.
	 * @param [importOpts.isTemp] if the item should be temporary, and displayed.
	 * @param [importOpts.filterValues] Saved filter values to be used instead of our own.
	 * @param [importOpts.isCharactermancer]
	 * @return *
	 */
	async pImportEntry (race, importOpts) {
		importOpts = importOpts || {};

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

		if (DataConverterRace.isStubRace(race)) return {imported: [], status: UtilApplications.TASK_EXIT_COMPLETE};

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

	async _pImportEntry_pImportToActor (race, importOpts) {
		// Build actor update
		const actUpdate = {
			data: {
				details: {
					race: race.name,
				},
			},
			token: {},
		};

		const level = MiscUtil.get(this._actor, "data", "data", "details", "level", "value") || 1;
		const dataBuilderOpts = new ImportListRace.ImportEntryOpts({
			isCharactermancer: importOpts.isCharactermancer,
			pb: Math.floor((level - 1) / 4) + 2,
		});

		this._pImportEntry_fillFlags(race, actUpdate, dataBuilderOpts);
		await this._pImportEntry_pFillAbilities(race, actUpdate, dataBuilderOpts);
		if (dataBuilderOpts.isCancelled) return {status: UtilApplications.TASK_EXIT_CANCELLED};
		this._pImportEntry_fillAttributes(race, actUpdate.data, dataBuilderOpts);
		await this._pImportEntry_pFillSkills(race, actUpdate.data, dataBuilderOpts);
		if (dataBuilderOpts.isCancelled) return {status: UtilApplications.TASK_EXIT_CANCELLED};
		await this._pImportEntry_pFillTraits(race, actUpdate.data, dataBuilderOpts);
		if (dataBuilderOpts.isCancelled) return {status: UtilApplications.TASK_EXIT_CANCELLED};
		await this._pImportEntry_fillToken(race, actUpdate.token, dataBuilderOpts);
		if (dataBuilderOpts.isCancelled) return {status: UtilApplications.TASK_EXIT_CANCELLED};

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

		// Update actor
		await this._actor.update(actUpdate);

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

		// Handle feats
		await this._pImportEntry_pImportFeats(race, importOpts, dataBuilderOpts);
		if (dataBuilderOpts.isCancelled) return {status: UtilApplications.TASK_EXIT_CANCELLED};

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

	_pImportEntry_pImportToDirectoryGeneric_pGetImportableData (it, getItemOpts) {
		return DataConverterRace.pGetRaceItem(it, getItemOpts);
	}

	_pImportEntry_fillFlags (race, act, dataBuilderOpts) {
		const flags = {};
		const flagsDnd5e = {};

		const hasPowerfulBuild = (race.traitTags && race.traitTags.includes("Powerful Build"))
			|| race.entries.some(it => it.name === "Powerful Build");
		if (hasPowerfulBuild) flagsDnd5e.powerfulBuild = true;

		if (race.entries.some(it => it.name === "Savage Attacks")) flagsDnd5e.savageAttacks = true;
		if (race.entries.some(it => it.name === "Lucky") && race._baseName === "Halfling") flagsDnd5e.halflingLucky = true;

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

	async _pImportEntry_pFillAbilities (race, actUpdate, dataBuilderOpts) {
		await Charactermancer_AbilityScoreSelect.pFillActorAbilityData(this._actor, race.ability, actUpdate, dataBuilderOpts);
	}

	_pImportEntry_fillAttributes (race, data, dataBuilderOpts) {
		const out = {};

		if (race.speed != null) out.movement = DataConverter.getMovement(race.speed);

		if (race.darkvision && race.darkvision > (this._actor.data.data.attributes?.senses?.darkvision || 0)) {
			out.senses = {
				darkvision: race.darkvision,
				units: "ft",
			};
		}

		data.attributes = out;
	}

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

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

		await this._pImportEntry_pHandleSize(race, data, dataBuilderOpts);
		if (dataBuilderOpts.isCancelled) return;

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

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

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

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

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

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

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

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

	_pImportEntry_fillToken (race, token, dataBuilderOpts) {
		if (race.darkvision) {
			const {dimSight: curDimSight} = this._actor.data.token || {};
			if (!curDimSight || (!isNaN(curDimSight) && Number(curDimSight) < race.darkvision)) {
				token.dimSight = race.darkvision;
				token.vision = true;
			}
		}
	}

	static _isSkippableFeature (ent) {
		if (typeof ent === "string") return true; // Skip pure string entries; these are notes.
		if (!ent.name) return true; // Skip un-named entries, e.g. Tortle "AL" note
		if (ent.name === "Size") return true; // Skip "Size" entries
		return false;
	}

	async _pImportEntry_pFillItems (race, actUpdate, dataBuilderOpts) {
		const spellHashToItemPosMap = {};

		const raceFeaturesRequiresEffects = [];

		await this._pApplyAllAdditionalSpellsToActor({entity: race, dataBuilderOpts, spellHashToItemPosMap});
		if (dataBuilderOpts.isCancelled) return;

		// region Race Features
		for (const ent of race.entries) {
			if (this.constructor._isSkippableFeature(ent)) continue;

			const fauxRaceFeature = DataConverterRace.getFauxRaceFeature(race, ent);
			await DataConverterRace.pMutActorUpdateRaceFeature(this._actor, actUpdate, fauxRaceFeature, dataBuilderOpts);
			if (dataBuilderOpts.isCancelled) return;
		}

		const hook = DataConverterSpell.doHookSpellLinkRender.bind(null, this._actor.id, spellHashToItemPosMap);
		await Renderer.get().pWithPlugin({
			entryType: "link",
			pluginType: "*",
			fnPlugin: hook,
			pFn: async () => {
				for (const ent of race.entries) {
					if (this.constructor._isSkippableFeature(ent)) continue;

					const fauxRaceFeature = DataConverterRace.getFauxRaceFeature(race, ent);

					const raceFeatureItem = await DataConverterRace.pGetRaceFeatureItem(race, fauxRaceFeature, {actor: this._actor});
					dataBuilderOpts.items.push(raceFeatureItem);

					if (await DataConverterRace.pHasRaceFeatureSideLoadedEffects(this._actor, fauxRaceFeature)) {
						raceFeaturesRequiresEffects.push(
							new ImportListRace.RaceFeatureRequiresEffects({
								actor: this._actor,
								raceFeature: fauxRaceFeature,
								itemData: raceFeatureItem,
							}),
						);
					}
				}
			},
		});
		// endregion

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

		// region Add item effects
		const effectsToAdd = [];
		if (raceFeaturesRequiresEffects.length) {
			for (const raceFeatureRequiresEffects of raceFeaturesRequiresEffects) {
				const importedEmbed = DataConverter.getImportedEmbed(importedEmbeds, raceFeatureRequiresEffects.itemData);

				if (!importedEmbed) continue;

				effectsToAdd.push(...(await DataConverterRace.pGetRaceFeatureItemEffects(this._actor, race, raceFeatureRequiresEffects.raceFeature, importedEmbed.document)));
			}
		}

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

	async _pImportEntry_pImportFeats (race, importOpts, dataBuilderOpts) {
		if (!race.feats) return;

		const formData = await Charactermancer_Race_FeatSelect.pGetUserInput(race.feats);
		if (!formData) return dataBuilderOpts.isCancelled = true;
		if (formData === VeCt.SYM_UI_SKIP) return;

		if (!(formData.data || []).length) return;

		const importListFeat = new ImportListFeat({actor: this._actor});
		await importListFeat.pInit();

		for (const {page, source, hash} of (formData.data || [])) {
			const feat = await Renderer.hover.pCacheAndGet(page, source, hash);
			await importListFeat.pImportEntry(feat, importOpts);
		}
	}

	async _pImportEntry_pHandleSize (race, actUpdate, dataBuilderOpts) {
		const formData = await Charactermancer_Race_SizeSelect.pGetUserInput({
			sizes: race.size,
		});

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

		actUpdate.traits.size = UtilActors.VET_SIZE_TO_ABV[formData.data || SZ_MEDIUM] || "med";
	}
}

ImportListRace.ImportEntryOpts = class extends ImportListCharacter.ImportEntryOpts {
	constructor (opts) {
		opts = opts || {};
		super(opts);

		this.pb = opts.pb;
	}
}

ImportListRace.RaceFeatureRequiresEffects = class {
	/**
	 * Represents a tuple of: <loaded 5etools raceFeature>, <raw Foundry sheet item data>
	 * @param actor
	 * @param raceFeature
	 * @param itemData Sheet item data, which we expect will be loaded as an embedded item.
	 */
	constructor ({actor, raceFeature, itemData}) {
		this.actor = actor;
		this.raceFeature = raceFeature;
		this.itemData = itemData;
	}
};

export {ImportListRace};
