import {SharedConsts} from "../shared/SharedConsts.js";
import {UtilApplications} from "./UtilApplications.js";
import {Util} from "./Util.js";
import {UtilList2} from "./UtilList2.js";
import {FolderPathBuilder, FolderPathBuilderRow} from "./FolderPathBuilder.js";
import {GameStorage} from "./GameStorage.js";
import {Config} from "./Config.js";
import {Vetools} from "./Vetools.js";
import {DataConverter} from "./DataConverter.js";
import {UtilCompendium} from "./UtilCompendium.js";
import {ConfigConsts} from "./ConfigConsts.js";

class ImportList extends Application {
	/**
	 * @param opts.prop
	 * @param opts.importerName
	 */
	static _initCreateSheetItemHook (opts) {
		// Note that this breaks the case where a user manually calls `actor.createEmbeddedDocuments` with an array of
		//   Plutonium-flagged items--the pre-create hook gets passed only one of these items, and `return false`s
		//   the rest.
		// ...or at least, this _was_ the case in 0.7.x; post-0.8.x this may have changed.
		Hooks.on("preCreateItem", (item, itemData, options, itemId) => {
			if (item.parent?.documentName !== "Actor") return;

			const flags = itemData.flags?.[SharedConsts.MODULE_NAME_FAKE] || itemData.flags?.[SharedConsts.MODULE_NAME];
			if (!flags || flags?.propDroppable !== opts.prop) return;
			if (flags.isStandardDragDrop) return;

			const actor = item.parent;

			this._pGetUseImporterDragDrop()
				.then(async isUseImporter => {
					let ent;
					try {
						ent = await Renderer.hover.pCacheAndGet(flags.page, flags.source, flags.hash);
					} catch (e) {
						ui.notifications.error(`Failed to import "${ent?.name ?? flags.hash}"! ${VeCt.STR_SEE_CONSOLE}`);
						throw e;
					}

					if (!ent) {
						const msg = `Failed to import "${flags.hash}"! ${VeCt.STR_SEE_CONSOLE}`;
						ui.notifications.error(msg);
						throw new Error(msg);
					}

					try {
						if (isUseImporter) {
							const imp = new this({actor});
							await imp.pInit();
							await imp.pImportEntry(ent, {filterValues: flags.filterValues});
							ui.notifications.info(`Imported "${ent.name}" via ${opts.importerName} Importer`)
							return;
						}

						itemData = MiscUtil.copy(itemData);
						MiscUtil.set(itemData.flags, SharedConsts.MODULE_NAME_FAKE, "isStandardDragDrop", true);
						await actor.createEmbeddedDocuments("Item", [itemData]);
					} catch (e) {
						ui.notifications.error(`Failed to import "${ent.name}"! ${VeCt.STR_SEE_CONSOLE}`);
						throw e;
					}
				});

			return false;
		});
	}

	static async _pGetUseImporterDragDrop () {
		const dragDropMode = Config.get("import", "dragDropMode");
		if (dragDropMode === ConfigConsts.C_IMPORT_DRAG_DROP_MODE_NEVER) return false;

		if (dragDropMode === ConfigConsts.C_IMPORT_DRAG_DROP_MODE_PROMPT) {
			const isImport = await InputUiUtil.pGetUserBoolean({
				title: `Import via ${Config.get("ui", "isStreamerMode") ? "SRD Importer" : SharedConsts.MODULE_TITLE}? Note that this will ignore any in-Foundry modifications made to the item.`,
				textYes: "Yes, use the importer",
				textNo: "No, use normal drag-drop",
			});

			if (!isImport) return false;
		}

		return true;
	}

	/**
	 * @param applicationOpts Application options. Accepts the same options as `Application`.
	 * @param externalData External data, passed in on creating the importer.
	 * @param [externalData.actor] Actor this importer belongs to.
	 * @param subclassOpts Options provided by subclasses to specify basic behaviour.
	 * @param [subclassOpts.props] JSON data properties for this entity (e.g. `["monster"]`).
	 * @param [subclassOpts.propsBrewAdditionalData] JSON data properties for additional data for this entity (e.g. `["foundryMonster"]`).
	 * @param [subclassOpts.gameProp] Property on `game` object where the collection containing this FVTT entity type is stored.
	 * @param [subclassOpts.titleSearch] Used in prompt text in the search bar.
	 * @param [subclassOpts.sidebarTab] Sidebar tab to open when importer is made active, assuming we're not targeting an actor.
	 * @param [subclassOpts.defaultFolderPath] Default folder path under which content imported should be stored.
	 * @param [subclassOpts.folderType] Folder type for this importer.
	 * @param [subclassOpts.fnListSort] Sort function for list items.
	 * @param [subclassOpts.listInitialSortBy] Initial "sort by" value for list items.
	 * @param [subclassOpts.pageFilter] Page filter instance for this importer.
	 * @param [subclassOpts.namespace] Namespace for this importer (will be prefixed as required (e.g. with "importer_") when used)
	 * @param [subclassOpts.isFolderOnly] If this importer may only target folder (and not compendiums)
	 * @param [subclassOpts.isActorRadio] If this importer is in "radio" mode when an actor is being imported.
	 * @param [subclassOpts.isNonCacheableInstance] If instances of this importer should never be cached.
	 * @param [subclassOpts.page] An associated 5etools page for this entity type.
	 * @param [subclassOpts.isPreviewable] If this importer list should have hoverable previews.
	 * @param [subclassOpts.isModdable] If this importer list should have an alternate import button, with additional (modified) UI flow.
	 * @param [subclassOpts.titleButtonRun] Run button text.
	 * @param [subclassOpts.isDedupable] If a dedupe step should be run on importer content.
	 * @param [subclassOpts.fnLoadSideData] Function which loads side data.
	 */
	constructor (applicationOpts, externalData, subclassOpts) {
		subclassOpts = subclassOpts || {};

		if (!subclassOpts.props && !subclassOpts.namespace) throw new Error(`One of "props" or "namespace" must be provided!`);

		const allApplicationOpts = {
			template: `${SharedConsts.MODULE_LOCATION}/template/ImportList.hbs`,
			width: 960,
			height: Util.getMaxWindowHeight(),
			resizable: true,
		};
		Object.assign(allApplicationOpts, applicationOpts || {});
		super(allApplicationOpts);

		// Fields for descendants to override
		this._props = subclassOpts.props;

		// region TODO link this with "props" to make a "propGroups" option
		this._propsBrewAdditionalData = subclassOpts.propsBrewAdditionalData;
		if (this._props && this._propsBrewAdditionalData && this._props.length !== this._propsBrewAdditionalData.length) throw new Error(`Mismatched number of properties! This is a bug!`);
		// endregion

		this._titleSearch = subclassOpts.titleSearch || "entries";
		this._sidebarTab = subclassOpts.sidebarTab;
		this._gameProp = subclassOpts.gameProp;
		this._defaultFolderPath = subclassOpts.defaultFolderPath;
		this._folderType = subclassOpts.folderType;
		this._fnListSort = subclassOpts.fnListSort;
		this._listInitialSortBy = subclassOpts.listInitialSortBy;
		this._pageFilter = subclassOpts.pageFilter;
		this._namespace = subclassOpts.namespace;
		this._isFolderOnly = !!subclassOpts.isFolderOnly;
		this._isNonCacheableInstance = subclassOpts.isNonCacheableInstance;
		this._page = subclassOpts.page;
		this._isPreviewable = subclassOpts.isPreviewable;
		this._isModdable = subclassOpts.isModdable;
		this._titleButtonRun = subclassOpts.titleButtonRun || "Import";
		this._isDedupable = !!subclassOpts.isDedupable;
		this._fnLoadSideData = subclassOpts.fnLoadSideData;

		// region Local fields
		// Fields that require synchronization
		this._actor = externalData.actor;
		this._isRadio = !!externalData.actor && subclassOpts.isActorRadio;
		this._pack = null;
		this._packCache = null;
		this._folderPathSpec = [];

		this._isClosable = true;
		this._isHidden = false;

		this._isInit = false;
		this._content = null;
		this._list = null;
		this._uploadedFile = null; // This doesn't require syncing, as there is no cache/reload for from-file importers

		this._$bntFilter = null;
		this._$btnReset = null;
		this._$btnToggleSummary = null;
		this._$iptSearch = null;
		this._$dispNumVisible = null;
		this._$cbAll = null;
		this._$btnTogglePreviewAll = null;
		this._$wrpRun = null;
		this._$btnRun = null;
		this._$btnRunWithMods = null;
		this._$wrpBtnsSort = null;
		this._$wrpList = null;
		this._$wrpMiniPills = null;
		// endregion

		// Arbitrary data which is specific to each importer, and set each time it is opened
		this._userData = null;
	}

	get page () { return this._page; }
	get folderType () { return this._folderType; }
	set isClosable (val) { this._isClosable = val; }
	get namespace () { return this._namespace; }
	get props () { return this._props; }
	set pack (val) { this._pack = val; }
	get folderPathSpec () { return this._folderPathSpec; }
	get isFolderOnly () { return this._isFolderOnly; }
	get isNonCacheableInstance () { return !!this._isNonCacheableInstance; }
	get isDedupable () { return !!this._isDedupable; }
	set userData (val) { this._userData = val; }

	get gameProp () { return this._gameProp; }

	get isEscapeable () {
		if (this._isClosable) return true;
		else return !this._isHidden;
	}

	get _propGroups () { return this._props.map((prop, i) => ({prop, propBrewAdditionalData: this._propsBrewAdditionalData?.[i]})); }

	async pSetContent (val) { this._content = val; }

	async pSyncStateFrom (that) {
		this._actor = that._actor;
		this._pack = that._pack;
		await this.pSetFolderPathSpec(that._folderPathSpec);
	}

	close (...args) {
		if (this._isNonCacheableInstance) {
			if (this._pageFilter && this._pageFilter.filterBox) this._pageFilter.filterBox.teardown();
			return super.close(...args);
		}

		if (!this._isClosable) {
			this._isHidden = true;
			this.element.hideVe();
			return;
		}

		if (this._pageFilter && this._pageFilter.filterBox) this._pageFilter.filterBox.teardown();
		return super.close(...args);
	}

	async pPreRender () {}

	render (...args) {
		if (this._isHidden) {
			this.element.showVe();
			this._isHidden = false;
			return;
		}

		return super.render(...args);
	}

	showAndRender (...args) {
		if (this._isHidden) {
			this.element.showVe();
			this._isHidden = false;
		}

		return this.render(...args);
	}

	activateSidebarTab () {
		if (this._pack) ui.sidebar.activateTab("compendium")
		else if (!this._actor && this._sidebarTab) ui.sidebar.activateTab(this._sidebarTab);
	}

	async pInit () {
		if (this._isInit) return;
		this._isInit = true;
		// Do initial load
		await this._pLoadFolderPathSpec();
	}

	async _pLoadFolderPathSpec () {
		this._folderPathSpec = MiscUtil.get((await GameStorage.pGetClient(this._getFullFolderPathSpecKey())), "path");
		if (this._folderPathSpec == null) await this.pSetFolderPathSpec((this._defaultFolderPath || []).map(it => FolderPathBuilderRow.getStateFromString(it)));
	}

	async pSetFolderPathSpec (folderPathSpec) {
		this._folderPathSpec = folderPathSpec;
		return GameStorage.pSetClient(this._getFullFolderPathSpecKey(), {path: this._folderPathSpec});
	}

	_getFullFolderPathSpecKey () { return `${ImportList._STO_K_FOLDER_PATH_SPEC}.${this.constructor.name}`; }

	/**
	 * Used by template engine. This runs before `activateListeners` .
	 * Overwrite as required.
	 */
	getData () {
		return {
			isRadio: this._isRadio,
			isPreviewable: this._isPreviewable,
			titleButtonRun: this._titleButtonRun,
			titleSearch: this._titleSearch,
			cols: [
				{
					name: "Name",
					width: 9,
					field: "name",
				},
				{
					name: "Source",
					width: 2,
					field: "source",
					titleProp: "sourceLong",
					displayProp: "sourceShort",
					classNameProp: "sourceClassName",
					rowClassName: "text-center",
				},
			],
			rows: this._content.map((it, ix) => {
				if (this._pageFilter) this._pageFilter.constructor.mutateForFilters(it);

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

	_activateListeners_doFindUiElements ($html) {
		const root = $html[0];

		const $wrpFilterControls = $(root.children[0]);
		this._$bntFilter = $wrpFilterControls.find(`[name="btn-filter"]`);
		this._$btnReset = $wrpFilterControls.find(`[name="btn-reset"]`);
		this._$btnToggleSummary = $wrpFilterControls.find(`[name="btn-toggle-summary"]`);
		this._$iptSearch = $wrpFilterControls.find(`.search`);
		this._$dispNumVisible = $wrpFilterControls.find(`.lst__wrp-search-visible`);

		this._$wrpMiniPills = $(root.children[1])

		const $wrpBtnsSort = $(root.children[2]);
		this._$cbAll = $wrpBtnsSort.find(`[name="cb-select-all"]`);
		this._$btnTogglePreviewAll = $wrpBtnsSort.find(`[name="btn-toggle-all-previews"]`);
		this._$wrpBtnsSort = $wrpBtnsSort;

		this._$wrpList = $(root.children[3]);

		this._$wrpRun = $(root.children[4]);
		this._$btnRun = this._$wrpRun.find(`[name="btn-run"]`);

		if (this._isModdable) {
			this._$btnRunWithMods = this._$wrpRun.find(`[name="btn-run-mods"]`);
		}
	}

	/**
	 * This runs after `getData`.
	 */
	activateListeners ($html) {
		super.activateListeners($html);

		this._activateListeners_doFindUiElements($html);

		this._activateListeners_initRunButton();

		this._$btnReset.click(() => {
			this._$iptSearch.val("");
			if (this._list) this._list.reset();
		});

		if (this._pageFilter) {
			this._activateListeners_pInitFilteredList()
				.then(() => this._activateListeners_initPreviewHoversAndImportButtons());
		} else {
			this._activateListeners_initList();
		}

		this._list.on("updated", () => this._$dispNumVisible.html(`${this._list.visibleItems.length}/${this._list.items.length}`));
		ListUiUtil.bindSelectAllCheckbox(this._$cbAll, this._list);
		ListUiUtil.bindPreviewAllButton(this._$btnTogglePreviewAll, this._list);

		// Reset list to initial state
		if (this._$btnReset) this._$btnReset.click();
	}

	_activateListeners_initPreviewHoversAndImportButtons () {
		if (!this._isPreviewable) return;

		const items = this._list.items;
		const len = items.length;
		for (let i = 0; i < len; ++i) {
			const item = items[i];
			const eleControlsWrp = item.ele.firstElementChild.children[1];

			const btnShowHidePreview = eleControlsWrp.children[0];
			const btnImport = eleControlsWrp.children[1];

			this._activateListeners_initPreviewButton(item, btnShowHidePreview);
			this._activateListeners_initPreviewImportButton(item, btnImport);
		}
	}

	_activateListeners_initPreviewButton (item, btnShowHidePreview) {
		ListUiUtil.bindPreviewButton(this._page, this._content, item, btnShowHidePreview);
	}

	_activateListeners_initPreviewImportButton (item, btnImport) {
		btnImport.addEventListener("click", async evt => {
			evt.stopPropagation();
			evt.preventDefault();

			if (this._isRadio) this.close();

			const toImport = this._content[item.ix];
			try {
				await this._pHandleClickRunButton_pDoPreCachePack();
				let imported;
				try {
					imported = await this.pImportEntry(toImport);
				} finally {
					this._pHandleClickRunButton_doDumpPackCache();
				}
				if (!imported) return; // If the import was cancelled
				UtilApplications.doShowImportedNotification(imported);
			} catch (e) {
				setTimeout(() => { throw e; });
				UtilApplications.doShowImportedNotification({entity: toImport, status: UtilApplications.TASK_EXIT_FAILED});
			}
		});
	}

	// Overwrite as required
	_activateListeners_initRunButton () {
		this._$btnRun.click(() => this._pHandleClickRunButton());

		if (this._isModdable) {
			this._$btnRunWithMods.click(async () => {
				try {
					this._isUseMods = true;
					await this._pHandleClickRunButton();
				} finally {
					this._isUseMods = false;
				}
			});
		}
	}

	async _pFnPostProcessEntries (entries) {
		return entries; // No-op; overwrite in subclasses
	}

	async _pHandleClickRunButton () {
		if (!this._list) return;

		const selIds = this._list.items
			.filter(it => it.data.cbSel.checked)
			.map(it => it.ix);

		if (!selIds.length) return ui.notifications.warn(`Please select something to import!`);

		if (!this._pack && selIds.length > 100 && !Config.get("ui", "isDisableLargeImportWarning")) {
			const isContinue = await InputUiUtil.pGetUserBoolean({
				title: `Warning: Large Import`,
				htmlDescription: `You have selected ${selIds.length} entities to import.<br>Importing a large number of entities may degrade game performance (consider importing to a compendium instead).<br>Do you wish to continue?`,
				textYesRemember: "Continue and Remember",
				textYes: "Continue",
				textNo: "Cancel",
				fnRemember: val => Config.set("ui", "isDisableLargeImportWarning", val),
			});
			if (isContinue == null || isContinue === false) return;
		}

		this.close();
		this.activateSidebarTab();

		let entries = selIds.map(ix => this._content[ix]);
		entries = await this._pFnPostProcessEntries(entries);
		if (entries == null) return;

		await this._pHandleClickRunButton_pDoPreCachePack();

		const tasks = entries.map(entry => {
			return new Util.Task(
				`${entry._displayName || entry.name} (${Parser.sourceJsonToAbv(entry.source)})`,
				() => this.pImportEntry(entry),
			);
		});
		await UtilApplications.pRunTasks(tasks);

		if (!this._actor) game[this._gameProp].render();

		this._pHandleClickRunButton_doDumpPackCache();

		this._$cbAll.prop("checked", false);
		this._list.items.forEach(item => {
			item.data.cbSel.checked = false;
			item.ele.classList.remove("list-multi-selected");
		});
	}

	// Overwrite as required
	_activateListeners_pInitFilteredList () {
		// Init list library
		this._list = new List({
			$iptSearch: this._$iptSearch,
			$wrpList: this._$wrpList,
			fnSort: this._fnListSort,
			sortByInitial: this._listInitialSortBy,
		});
		SortUtil.initBtnSortHandlers(this._$wrpBtnsSort, this._list);

		return this._pageFilter.pInitFilterBox({
			$iptSearch: this._$iptSearch,
			$btnReset: this._$btnReset,
			$btnOpen: this._$bntFilter,
			$btnToggleSummaryHidden: this._$btnToggleSummary,
			$wrpMiniPills: this._$wrpMiniPills,
			namespace: this._getFilterNamespace(),
		}).then(async () => {
			this._content.forEach(it => this._pageFilter.addToFilters(it));

			this._activateListeners_absorbListItems();
			this._list.init();

			this._pageFilter.filterBox.render();

			await this._pPostFilterRender();

			this._pageFilter.filterBox.on(
				FilterBox.EVNT_VALCHANGE,
				this._handleFilterChange.bind(this),
			);

			this._handleFilterChange();
		});
	}

	/** Implement as required. */
	async _pPostFilterRender () {}

	_activateListeners_initList () {
		// Init list library
		this._list = new List({
			$iptSearch: this._$iptSearch,
			$wrpList: this._$wrpList,
			fnSort: this._fnListSort,
		});
		SortUtil.initBtnSortHandlers(this._$wrpBtnsSort, this._list);

		this._activateListeners_absorbListItems();
		this._list.init();
	}

	// Overwrite as required
	_activateListeners_absorbListItems () {
		this._list.doAbsorbItems(
			this._content,
			{
				fnGetName: it => it.name,
				fnGetValues: it => ({
					source: it.source,
					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),
			},
		);
	}

	_handleFilterChange () {
		const f = this._pageFilter.filterBox.getValues();
		this._list.filter(li => this._pageFilter.toDisplay(f, this._content[li.ix]));
	}

	async pImportEntry () { throw new Error(`Unimplemented!`); }
	async pGetSources () { throw new Error(`Unimplemented!`); }

	getFolderPathMeta () {
		return {
			alpha: {
				label: "First Letter of Name",
				getter: it => it.name.slice(0, 1).toUpperCase(),
			},
			source: {
				label: "Source (Full)",
				getter: it => Parser.sourceJsonToFull(it.source),
			},
			sourceAbbreviation: {
				label: "Source (Abbreviation)",
				getter: it => Parser.sourceJsonToAbv(it.source),
			},
		}
	}

	/**
	 * @param entry
	 * @param [opts]
	 * @param [opts.folderType] External override for the folder type
	 * @param [opts.sorting] Folder sorting type, either `"a"` (alphabetical) or `"m"` (manual). Defaults to alphabetical.
	 */
	async _pImportEntry_pGetFolderId (entry, opts) {
		opts = opts || {};

		const folderType = opts.folderType || this._folderType;

		if (!this._folderPathSpec.length || !folderType) return null;

		const meta = this.getFolderPathMeta();
		const pathStrings = this._folderPathSpec
			.filter(it => it.isFreeText ? it.text && it.text.trim() : it.selectedProp)
			.map(it => it.isFreeText ? it.text : meta[it.selectedProp].getter(entry));

		const stack = [];

		const findFolder = (name, parentId) => {
			const matches = CONFIG.Folder.collection.instance.contents.filter(it => {
				const isNameMatch = it.data.name === name;
				const isTypeMatch = it.data.type === folderType;
				const isParentMatch = parentId ? it.data.parent === parentId : it.data.parent == null;
				return isNameMatch && isTypeMatch && isParentMatch
			});

			if (matches.length > 1) {
				const msg = `Ambiguous folder path! Found multiple folders for ${stack.map(it => it.data.name).join(" > ")}`;
				ui.notifications.error(msg);
				throw new Error(msg)
			} else if (matches.length) return matches[0];
			else return null;
		};

		for (let i = 0; i < pathStrings.length; ++i) {
			const name = pathStrings[i];
			const isLast = i === pathStrings.length - 1;

			const parentId = stack.length ? stack.last().id : null;

			const folder = findFolder(name, parentId);
			if (folder) stack.push(folder);
			else {
				// create a new folder
				const folderData = {
					name,
					parent: parentId,
					type: folderType,
				};
				if (isLast && opts.sorting) folderData.sorting = opts.sorting;
				const nuFolder = await Folder.create(folderData, {});
				stack.push(nuFolder);
			}
		}

		// Sanity check; should never occur
		if (stack.length) return stack.last().id;
		return null;
	}

	async pHandleEditFolderPathClick () {
		await this._pLoadFolderPathSpec();
		const builderApp = new FolderPathBuilderApp(this);
		builderApp.render(true);
	}

	getContent (data) {
		return Vetools.getContent(data, this._props);
	}

	_getFilterNamespace () { return `importer_${this._namespace || this._props.join("_")}`; }

	/**
	 * @param opts
	 * @param [opts.name]
	 * @param [opts.source]
	 * @param [opts.entity]
	 * @param [opts.gameProp] Game prop override
	 */
	_getDuplicateMeta (opts) {
		opts = opts || {};

		const gameProp = opts.gameProp || this._gameProp;

		// Only check the pack if we're using an entity type the pack can store
		const pack = gameProp === this._gameProp ? this._pack : null;

		let existing = null
		switch (gameProp) {
			// region Entities with sources in Foundry
			case "actors":
			case "items": {
				if (!((opts.name && opts.source) || opts.entity)) throw new Error(`Either "name" and "source", or "entity", must be provided!`);

				const cleanName = (opts.name || DataConverter.getNameWithSourcePart(opts.entity)).toLowerCase().trim();
				const cleanSource = (opts.source || DataConverter.getSourceWithPagePart(opts.entity)).toLowerCase().trim();

				switch (gameProp) {
					case "actors": {
						if (pack) {
							const key = this._getDuplicateMeta_getEntityKey({name: cleanName, source: cleanSource});
							existing = (this._packCache || {})[key];
						} else {
							existing = game[gameProp].find(it => this.constructor._getDuplicateMeta_getCleanName(it) === cleanName && (!Config.get("import", "isStrictMatching") || (MiscUtil.get(it, "data", "data", "details", "source") || "").toLowerCase().trim() === cleanSource));
						}
						break;
					}
					case "items": {
						if (pack) {
							const key = this._getDuplicateMeta_getEntityKey({name: cleanName, source: cleanSource});
							existing = (this._packCache || {})[key];
						} else {
							existing = game[gameProp].find(it => this.constructor._getDuplicateMeta_getCleanName(it) === cleanName && (!Config.get("import", "isStrictMatching") || (MiscUtil.get(it, "data", "data", "source") || "").toLowerCase().trim() === cleanSource));
						}
						break;
					}
				}

				break;
			}
			// endregion

			// region Entities without sources in Foundry
			case "journal":
			case "tables": {
				const cleanName = opts.name.toLowerCase().trim();

				if (pack) {
					const key = this._getDuplicateMeta_getEntityKey({name: cleanName});
					existing = (this._packCache || {})[key];
				} else {
					existing = game[gameProp].find(it => this.constructor._getDuplicateMeta_getCleanName(it) === cleanName);
				}
				break;
			}
			// endregion

			default: throw new Error(`Game property "${gameProp}" is not supported!`);
		}

		const mode = Config.get("import", "deduplicationMode");
		return {
			mode,
			existing: existing,
			// Helper values
			isSkip: mode === ConfigConsts.C_IMPORT_DEDUPE_MODE_SKIP && existing != null,
			isOverwrite: mode === ConfigConsts.C_IMPORT_DEDUPE_MODE_OVERWRITE && existing != null,
		};
	}

	static _getDuplicateMeta_getCleanName (it) {
		let out = (MiscUtil.get(it, "data", "name") || "").toLowerCase().trim();

		out = out
			.replace(/\[[^\]]+]/g, "") // Remove tags
			.trim();

		return out;
	}

	_getDuplicateMeta_getEntityKey (obj) {
		return Object.entries(obj)
			.sort(([aK], [bK]) => SortUtil.ascSortLower(aK, bK))
			.map(([k, v]) => `${k}=${`${v}`.trim()}`.toLowerCase())
			.join("::");
	}

	async _pHandleClickRunButton_pDoPreCachePack () {
		if (!this._pack || Config.get("import", "deduplicationMode") === ConfigConsts.C_IMPORT_DEDUPE_MODE_NONE) return;

		this._packCache = {};
		const content = await UtilCompendium.pGetCompendiumData(this._pack, true);

		content.forEach(ent => {
			switch (this._gameProp) {
				case "actors": {
					const cleanName = (MiscUtil.get(ent, "data", "name") || "").toLowerCase().trim();
					const cleanSource = (MiscUtil.get(ent, "data", "data", "details", "source") || "").toLowerCase().trim();

					const key = this._getDuplicateMeta_getEntityKey({name: cleanName, source: cleanSource});
					this._packCache[key] = ent;

					break;
				}
				case "items": {
					const cleanName = (MiscUtil.get(ent, "data", "name") || "").toLowerCase().trim();
					const cleanSource = (MiscUtil.get(ent, "data", "data", "source") || "").toLowerCase().trim();

					const key = this._getDuplicateMeta_getEntityKey({name: cleanName, source: cleanSource});
					this._packCache[key] = ent;

					break;
				}
				case "journal":
				case "tables": {
					const cleanName = (MiscUtil.get(ent, "data", "name") || "").toLowerCase().trim();

					const key = this._getDuplicateMeta_getEntityKey({name: cleanName});
					this._packCache[key] = ent;

					break;
				}
				default: throw new Error(`Game property "${this._gameProp}" is not supported!`);
			}
		});
	}

	_pHandleClickRunButton_doDumpPackCache () { this._packCache = null; }

	async _pImportEntry_pDoUpdateExistingPackEntity (duplicateMeta, itemData) {
		await duplicateMeta.existing.deleteEmbeddedDocuments("ActiveEffect", duplicateMeta.existing.effects.map(it => it.id));
		await duplicateMeta.existing.update(itemData);
		return {imported: [duplicateMeta.existing], status: UtilApplications.TASK_EXIT_COMPLETE_UPDATE_OVERWRITE};
	}

	async _pImportEntry_pDoUpdateExistingDirectoryEntity (duplicateMeta, itemData) {
		await duplicateMeta.existing.update(itemData);
		return {imported: [duplicateMeta.existing], status: UtilApplications.TASK_EXIT_COMPLETE_UPDATE_OVERWRITE};
	}

	async _pImportEntry_pImportToDirectoryGeneric (toImport, importOpts, opts) {
		const impData = await this._pImportEntry_pImportToDirectoryGeneric_pGetImportableData(
			toImport,
			{
				isAddDataFlags: true, // This is implicit for some data types, but explicit for others.
				filterValues: importOpts.filterValues,
				...opts,
				isAddPermission: true,
			},
		);

		const duplicateMeta = this._getDuplicateMeta({name: impData.name, source: MiscUtil.get(impData, "data", "source")});
		if (duplicateMeta.isSkip) return {existing: duplicateMeta.existing, status: UtilApplications.TASK_EXIT_SKIPPED_DUPLICATE};

		let Clazz;
		switch (this._gameProp) {
			case "items": Clazz = Item; break;
			case "journal": Clazz = JournalEntry; break;
			case "tables": Clazz = RollTable; break;
		}

		if (importOpts.isTemp) {
			const imported = await Clazz.create(impData, {renderSheet: true, temporary: true});
			return {imported: [imported], status: UtilApplications.TASK_EXIT_COMPLETE};
		} else if (this._pack) {
			if (duplicateMeta.isOverwrite) return this._pImportEntry_pDoUpdateExistingPackEntity(duplicateMeta, impData);

			const imported = await Clazz.create(impData, {renderSheet: false, temporary: false});
			await this._pack.importDocument(imported);
			return {imported: [imported], status: UtilApplications.TASK_EXIT_COMPLETE};
		} else {
			if (duplicateMeta.isOverwrite) return this._pImportEntry_pDoUpdateExistingDirectoryEntity(duplicateMeta, impData);

			const folderId = await this._pImportEntry_pGetFolderId(toImport);
			if (folderId) impData.folder = folderId;

			const imported = await Clazz.create(impData, {renderSheet: false, temporary: false});

			await game[this._gameProp].set(imported.id, imported);

			return {imported: [imported], status: UtilApplications.TASK_EXIT_COMPLETE};
		}
	}

	/**
	 * @param it
	 * @param getItemOpts
	 * @return {*}
	 */
	_pImportEntry_pImportToDirectoryGeneric_pGetImportableData (it, getItemOpts) { throw new Error(`Unimplemented!`); }

	/** Implement as required. */
	async pGetChooseImporterUserDataForSources () {}
}
ImportList._STO_K_FOLDER_PATH_SPEC = "ImportList.folderKeyPathSpec";

/**
 * A window-based wrapper around the full component.
 */
class FolderPathBuilderApp extends Application {
	constructor (importer) {
		super({
			width: 480,
			height: 480,
			title: "Edit Folder Path",
			template: `${SharedConsts.MODULE_LOCATION}/template/FolderPathBuilder.hbs`,
			resizable: true,
		});
		this._comp = new FolderPathBuilder(importer);
	}

	activateListeners ($html) {
		super.activateListeners($html);
		this._comp.render($html);
	}
}

// TODO refactor parts of this into the main ImportList class (allowing a resolution other than just importing) and use
//   this to handle the "quick import" in-list buttons
const MixinUserChooseImporter = clsImportList => class extends clsImportList {
	constructor (...args) {
		super(...args);
		this._isRadio = true;

		this._isResolveOnClose = true;
		this._fnResolve = null;
		this._fnReject = null;
		this.pResult = null;
	}

	async _pHandleClickRunButton () {
		if (!this._list) return;

		try {
			const selItem = this._list.items
				.find(it => it.data.cbSel.checked);

			if (!selItem) return ui.notifications.warn(`Please select something from the list!`);

			this._isResolveOnClose = false;

			this.close();

			let entries = [this._content[selItem.ix]];

			entries = await this._pFnPostProcessEntries(entries);
			if (entries == null) return;

			const imported = await this.pImportEntry(entries[0], {isTemp: true, isDataOnly: true});
			if (imported.status === UtilApplications.TASK_EXIT_COMPLETE_DATA_ONLY) this._fnResolve(imported?.imported?.[0]);
			else this._fnReject(new Error(`Import exited with status "${imported.status.toString()}"`));

			selItem.data.cbSel.checked = false;
			selItem.ele.classList.remove("list-multi-selected");
		} catch (e) {
			this._fnReject(e);
		}
	}

	_activateListeners_initPreviewImportButton (item, btnImport) {
		btnImport.addEventListener("click", async evt => {
			evt.stopPropagation();
			evt.preventDefault();

			try {
				let entries = [this._content[item.ix]];

				entries = await this._pFnPostProcessEntries(entries);
				if (entries == null) return;

				const imported = await this.pImportEntry(entries[0], {isTemp: true, isDataOnly: true});
				if (imported.status === UtilApplications.TASK_EXIT_COMPLETE_DATA_ONLY) this._fnResolve(imported?.imported?.[0]);
				else this._fnReject(new Error(`Import exited with status "${imported.status.toString()}"`));

				this.close();
			} catch (e) {
				this._fnReject(e);
			}
		});
	}

	async close (...args) {
		await super.close(...args);
		if (this._isResolveOnClose) this._fnResolve(null);
	}

	async pPreRender (preRenderArgs) {
		await super.pPreRender(preRenderArgs);

		if (!preRenderArgs) return;

		const {fnResolve, fnReject, pResult} = preRenderArgs;

		this._isResolveOnClose = true;
		this._fnResolve = fnResolve;
		this._fnReject = fnReject;
		this.pResult = pResult;
	}

	/**
	 * @param mode A predefined mode that the ChooseImporter wizard should use, rather than allowing the user to pick one.
	 * @param namespace A namespace for the ChooseImporter wizard. Useful for non-standard flows.
	 */
	static async pGetUserChoice (mode, namespace) {
		const {ChooseImporter} = await import("./ChooseImporter.js");

		const importer = new this({});
		await importer.pInit();

		let fnResolve = null;
		let fnReject = null;
		const pResult = new Promise((resolve, reject) => {
			fnResolve = resolve;
			fnReject = reject;
		});

		// Avoid passing in the actor, as we'll pull out the imported result and apply it to the actor ourselves
		const chooseImporter = new ChooseImporter(
			null,
			{
				mode: {
					...mode,
					importerInstance: importer,
				},
				namespace,
				isAlwaysCloseWindow: true,
				isTemp: true,
				isNoImport: true,
				importerPreRenderArgs: {
					fnResolve,
					fnReject,
					pResult,
				},
			},
		);
		chooseImporter.render(true);

		return pResult;
	}
};

export {ImportList, MixinUserChooseImporter};
