import {SharedConsts} from "../shared/SharedConsts.js";
import {Consts} from "./Consts.js";
import {JqueryExtension} from "./JqueryExtension.js";
import {Config} from "./Config.js";
import {GameStorage} from "./GameStorage.js";
import {LGT} from "./Util.js";

class NotAxios {
	static async get (url) { return {data: (await (await fetch(url)).json())}; }
}

class Vetools {
	// region Meta
	static async pDoPreload () {
		await Promise.all([
			Vetools.BESTIARY_FLUFF_INDEX = await Vetools.pGetCreatureFluffIndex(),
			DataUtil.monster.pPreloadMeta(),
		]);

		// Load this asynchronously, to avoid killing the load if it doesn't exist
		Vetools._pGetHomebrewIndices()
			.then(({source, prop}) => {
				Vetools.HOMEBREW_INDEX__PROP = prop;
				Vetools.HOMEBREW_INDEX__SOURCE = source;
				console.log(...LGT, "Loaded homebrew index.")
			})
			.catch(e => {
				Vetools.HOMEBREW_INDEX__PROP = {};
				Vetools.HOMEBREW_INDEX__SOURCE = {};
				ui.notifications.error(`Failed to load homebrew index! ${VeCt.STR_SEE_CONSOLE}`);
				setTimeout(() => { throw e; });
			});
	}

	static withUnpatchedDiceRendering (fn) {
		Renderer.getRollableEntryDice = Vetools._CACHED_GET_ROLLABLE_ENTRY_DICE;
		const out = fn();
		Renderer.getRollableEntryDice = Vetools._PATCHED_GET_ROLLABLE_ENTRY_DICE;
		return out;
	}

	static withCustomDiceRenderingPatch (fn, fnRender) {
		Renderer.getRollableEntryDice = fnRender;
		const out = fn();
		Renderer.getRollableEntryDice = Vetools._PATCHED_GET_ROLLABLE_ENTRY_DICE;
		return out;
	}

	static getCleanDiceString (diceString) {
		return diceString
			// Use symbols Foundry can understand
			.replace(/×/g, "*")
			.replace(/÷/g, "/")
			// Foundry (as of 2020-11-15) doesn't support prompting for user variables in rolls
			.replace(/#\$.*?\$#/g, "0")
		;
	}

	static doMonkeyPatch () {
		JqueryExtension.init();

		VeCt.STR_SEE_CONSOLE = "See the console (F12 or CTRL+SHIFT+J) for details."

		StorageUtil.pSet = GameStorage.pSetClient.bind(GameStorage);
		StorageUtil.pGet = GameStorage.pGetClient.bind(GameStorage);

		Renderer.get().setBaseUrl(Vetools.BASE_SITE_URL);
		if (Config.get("import", "isUseLocalImages")) Renderer.get().setBaseMediaUrl("img", `modules/${SharedConsts.MODULE_NAME}/`);
		Hooks.on("plutonium.configUpdate", () => Renderer.get().setBaseUrl(Vetools.BASE_SITE_URL));

		Renderer.hover.MIN_Z_INDEX = Consts.Z_INDEX_MAX_FOUNDRY + 1;
		Renderer.hover._MAX_Z_INDEX = Renderer.hover.MIN_Z_INDEX + 10;

		BrewUtil.homebrew = {};
		BrewUtil.homebrewMeta = {sources: []};

		// region Rolls
		Vetools._CACHED_GET_ROLLABLE_ENTRY_DICE = Renderer.getRollableEntryDice;
		Vetools._PATCHED_GET_ROLLABLE_ENTRY_DICE = (
			entry,
			name,
			toDisplay,
			{
				isAddHandlers = true,
				additionalData = null,
			} = {},
		) => {
			const cpy = MiscUtil.copy(entry);

			if (typeof cpy.toRoll !== "string") {
				// handle legacy format
				cpy.toRoll = Renderer.legacyDiceToString(cpy.toRoll);
			}

			// If there's a prompt, find the lowest level at which there is additional text (e.g. level 4 for fireball)
			//   and use that text
			if (cpy.prompt) {
				const minAdditionalDiceLevel = Math.min(...Object.keys(cpy.prompt.options)
					.map(it => Number(it))
					.filter(it => cpy.prompt.options[it]));
				cpy.toRoll = cpy.prompt.options[minAdditionalDiceLevel];
			}

			const toRollClean = this.getCleanDiceString(cpy.toRoll);

			return `[[/r ${toRollClean}]]${toRollClean.toLowerCase().trim() !== toDisplay.toLowerCase().trim() ? ` (${toDisplay})` : ""}`;
		};

		Renderer.getRollableEntryDice = Vetools._PATCHED_GET_ROLLABLE_ENTRY_DICE;

		// Swap the original `getRollableEntryDice` back in when we're about to render a window.
		//   This prevents e.g. "[[/r 1d20]]" from appearing in the rendered content.
		const cachedRenderHoverMethods = {};
		const renderHoverMethods = [
			"$getHoverContent_stats",
			"$getHoverContent_fluff",
			"$getHoverContent_statsCode",
			"$getHoverContent_miscCode",
			"$getHoverContent_generic",
		];
		renderHoverMethods.forEach(methodName => {
			cachedRenderHoverMethods[methodName] = Renderer.hover[methodName];
			Renderer.hover[methodName] = (...args) => {
				Renderer.getRollableEntryDice = Vetools._CACHED_GET_ROLLABLE_ENTRY_DICE;
				const out = cachedRenderHoverMethods[methodName](...args);
				Renderer.getRollableEntryDice = Vetools._PATCHED_GET_ROLLABLE_ENTRY_DICE;
				return out;
			};
		});

		Renderer.dice.rollerClick = (evtMock, ele, packed, name) => {
			const entry = JSON.parse(packed);
			if (entry.toRoll) (new Roll(entry.toRoll)).toMessage();
		};

		Renderer.dice.pRollEntry = (entry, rolledBy, opts) => {
			if (entry.toRoll) (new Roll(entry.toRoll)).toMessage();
		};
		// endregion

		// region Patch over content handler bindings--these should only ever be used in hover windows
		Vetools._CACHED_MONSTER_DO_BIND_COMPACT_CONTENT_HANDLERS = Renderer.monster.doBindCompactContentHandlers;
		Renderer.monster.doBindCompactContentHandlers = (opts) => {
			const nxtOpts = {...opts};
			nxtOpts.fnRender = (...args) => Vetools.withUnpatchedDiceRendering(() => opts.fnRender(...args));
			return Vetools._CACHED_MONSTER_DO_BIND_COMPACT_CONTENT_HANDLERS(nxtOpts);
		};
		// endregion

		JqueryUtil.doToast = (options) => {
			if (typeof options === "string") {
				options = {
					content: options,
					type: "info",
				};
			}
			options.type = options.type || "info";

			switch (options.type) {
				case "warning": return ui.notifications.warn(options.content);
				case "danger": return ui.notifications.error(options.content);
				default: return ui.notifications.info(options.content);
			}
		};

		DataUtil.loadJSON = async (url) => Vetools.pGetWithCache(url);

		BrewUtil.hasSourceJson = (source) => !!Vetools.HOMEBREW_META_SOURCES[(source || "").toLowerCase()];
		BrewUtil.sourceJsonToAbv = (source) => (Vetools.HOMEBREW_META_SOURCES[(source || "").toLowerCase()] || {}).abbreviation || source;
		BrewUtil.sourceJsonToFull = (source) => (Vetools.HOMEBREW_META_SOURCES[(source || "").toLowerCase()] || {}).full || source;
		BrewUtil.sourceJsonToSource = (source) => Vetools.HOMEBREW_META_SOURCES[(source || "").toLowerCase()];
		BrewUtil.sourceJsonToStyle = (source) => {
			const lowSource = (source || "").toLowerCase();
			if (Vetools.HOMEBREW_META_SOURCES[lowSource] && Vetools.HOMEBREW_META_SOURCES[lowSource].color) {
				const validColor = BrewUtil.getValidColor(Vetools.HOMEBREW_META_SOURCES[lowSource].color);
				if (validColor.length) return `style="color: #${validColor};"`;
				return "";
			} else return "";
		};
		BrewUtil.getJsonSources = () => Object.values(Vetools.HOMEBREW_META_SOURCES);
		BrewUtil._STORABLE = [...BrewUtil._STORABLE, "foundrySpell", "foundryClassFeature", "foundrySubclassFeature", "foundryOptionalfeature"];
		BrewUtil._PAGE = UrlUtil.PG_MANAGE_BREW;
		BrewUtil._pCleanSaveBrew = () => { /* No-op */ };
	}

	static addToHomebrewSourceLookup (data) {
		if (data._meta && data._meta.sources) {
			data._meta.sources.forEach(metaSource => Vetools.HOMEBREW_META_SOURCES[(metaSource.json || "").toLowerCase()] = MiscUtil.copy(metaSource));
		}
	}

	/**
	 * Add loaded content to homebrew, so that it can be referenced later in the session.
	 * As the content is not copied during 5etools' homebrew loading, we can get away with doing this without much
	 * memory cost.
	 * @param data The full JSON data.
	 */
	static async pAddToHomebrew (data) {
		// Homebrew files should always have a source header
		const sources = MiscUtil.get(data, "_meta", "sources");
		if (!sources || !(sources instanceof Array) || !sources.every(it => !Parser.SOURCE_JSON_TO_ABV[it.json])) return;
		await BrewUtil.pDoHandleBrewJson(data, "NO_PAGE");
	}
	// endregion

	// region Requests
	static async pGetWithCache (url) {
		if (!url.includes("?")) url = `${url}?t=${Consts.RUN_TIME}`;

		// Strip any excess slashes
		const parts = url.split(/(^https?:\/\/)/).filter(Boolean);
		parts[parts.length - 1] = parts.last().replace(/\/+/g, "/");
		url = parts.join("");

		const urlBase = url.split("?")[0];

		if (Vetools.CACHE_REQUESTS_IN_FLIGHT[urlBase]) {
			await Vetools.CACHE_REQUESTS_IN_FLIGHT[urlBase];
			return Vetools.CACHED_REQUESTS[urlBase];
		}

		Vetools.CACHE_REQUESTS_IN_FLIGHT[urlBase] = (async () => {
			// Use local data where possible
			let data;
			if (!Config.get("import", "isNoLocalData") && url.startsWith(`${Vetools.BASE_SITE_URL}data/`)) {
				const urlPart = url.split(Vetools.BASE_SITE_URL).slice(1).join(Vetools.BASE_SITE_URL);
				const localUrl = `modules/${SharedConsts.MODULE_NAME}/${urlPart}`;

				data = (await NotAxios.get(localUrl)).data;
			} else {
				data = (await NotAxios.get(url)).data;
			}

			Vetools.addToHomebrewSourceLookup(data);
			await Vetools.pAddToHomebrew(data);

			await DataUtil.pDoMetaMerge(urlBase, data);
			return (Vetools.CACHED_REQUESTS[urlBase] = data);
		})();

		await Vetools.CACHE_REQUESTS_IN_FLIGHT[urlBase];
		return Vetools.CACHED_REQUESTS[urlBase];
	}

	static async pLoadImporterSourceSpecial (source) {
		let content;
		if (source.special.cacheKey) content = Vetools.CACHED_REQUESTS[source.special.cacheKey] || (Vetools.CACHED_REQUESTS[source.special.cacheKey] = await source.special.pGet());
		else content = await source.special.pGet();
		return content;
	}

	static async pGetChangelog () { return Vetools.pGetWithCache(`${Vetools.BASE_SITE_URL}data/changelog.json`); }

	static async pGetPackageIndex () { return Vetools.pGetWithCache(Config.get("importAdventure", "indexUrl")); }

	static async pGetSpellIndex () { return Vetools.pGetWithCache(`${Vetools.BASE_SITE_URL}data/spells/index.json`); }
	static getSpellUrl (filename) { return `${Vetools.BASE_SITE_URL}data/spells/${filename}`; }

	static async pGetCreatureIndex () { return Vetools.pGetWithCache(`${Vetools.BASE_SITE_URL}data/bestiary/index.json`); }
	static getCreatureUrl (filename) { return `${Vetools.BASE_SITE_URL}data/bestiary/${filename}`; }
	static async pGetCreatureFluffIndex () { return Vetools.pGetWithCache(`${Vetools.BASE_SITE_URL}data/bestiary/fluff-index.json`); }

	static async pGetItems (addGroups) {
		return {item: await Renderer.item.pBuildList({isAddGroups: !!addGroups})}; // This has built-in caching
	}

	static async pGetItemFluff () {
		const url = `${Vetools.BASE_SITE_URL}data/fluff-items.json`;
		return Vetools.pGetWithCache(url);
	}

	/**
	 * @param [opts] Options object.
	 * @param [opts.isAddBaseRaces] If an entity should be created for each base race.
	 */
	static async pGetRaces (opts) {
		return DataUtil.race.loadJSON(opts);
	}

	static async pGetClasses () {
		return DataUtil.class.loadRawJSON();
	}

	static async pGetClassSubclassFeatures () {
		return DataUtil.class.loadRawJSON();
	}

	static async pGetRollableTables () {
		return DataUtil.table.loadJSON();
	}

	static async _pGetAdventureBookIndex (filename, prop) {
		const url = `${Vetools.BASE_SITE_URL}data/${filename}`;
		const index = await Vetools.pGetWithCache(url);
		index[prop].forEach(it => it._pubDate = new Date(it.published || "1970-01-01"));
		return index;
	}

	static async pGetAdventureIndex () {
		return this._pGetAdventureBookIndex("adventures.json", "adventure");
	}

	static async pGetBookIndex () {
		return this._pGetAdventureBookIndex("books.json", "book");
	}

	static _getAdventureBookUrl (type, id) {
		return `${Vetools.BASE_SITE_URL}data/${type}/${type}-${id.toLowerCase()}.json`;
	}

	static getAdventureUrl (id) {
		return this._getAdventureBookUrl("adventure", id)
	}

	static getBookUrl (id) {
		return this._getAdventureBookUrl("book", id)
	}

	static getImageUrlFromFluff (fluff) {
		if (fluff && fluff.images && fluff.images[0]) {
			const imgEntry = fluff.images[0];
			if (imgEntry.href) {
				if (imgEntry.href.type === "internal") return imgEntry.href.path ? `${Vetools.getInternalImageUrl(imgEntry.href.path)}` : "";
				else if (imgEntry.href.type === "external") return imgEntry.href.url || "";
			}
		}
	}

	static hasTokenUrl (entityType, it) {
		return Vetools._getTokenUrl(entityType, it).hasToken;
	}

	static getTokenUrl (entityType, it) {
		return Vetools._getTokenUrl(entityType, it).url;
	}

	static _getTokenUrl (entityType, it) {
		if (it.tokenUrl) return {url: it.tokenUrl, hasToken: true};

		const fallbackMeta = {
			url: UrlUtil.link(`${Renderer.get().baseMediaUrls["img"] || Renderer.get().baseUrl}img/blank.png`),
			hasToken: false,
		};

		switch (entityType) {
			case "monster": {
				if (it.hasToken) return {url: Renderer.monster.getTokenUrl(it), hasToken: true};
				return fallbackMeta;
			}
			case "vehicle": {
				if (it.hasToken) return {url: Renderer.vehicle.getTokenUrl(it), hasToken: true};
				return fallbackMeta;
			}
			case "object": {
				if (it.hasToken) return {url: Renderer.object.getTokenUrl(it), hasToken: true};
				return fallbackMeta;
			}
			default: throw new Error(`Unhandled entity type "${entityType}"`);
		}
	}

	static getInternalImageUrl (path) {
		return `${Renderer.get().baseMediaUrls["img"] || Renderer.get().baseUrl}img/${encodeURIComponent(path)}`
			.replace(/'/g, "%27"); // URL encode, as Foundry uses these in single-quoted CSS `background-image` strings
	}

	static async pGetAllSpells ({isFilterNonStandard = false, additionalSourcesBrew = []} = {}) {
		const index = await Vetools.pGetSpellIndex();
		const fileData = await Promise.all(
			Object.entries(index)
				.filter(([source]) => !isFilterNonStandard || !SourceUtil.isNonstandardSource(source))
				.map(([_, filename]) => Vetools.pGetWithCache(Vetools.getSpellUrl(filename))),
		);

		if (additionalSourcesBrew?.length) {
			for (const src of additionalSourcesBrew) {
				const brewJson = await DataUtil.pLoadBrewBySource(src);
				if (!brewJson) continue;
				fileData.push(brewJson);
			}
		}

		return {spell: fileData.map(it => MiscUtil.copy(it.spell)).flat()};
	}

	static async pGetAllCreatures (isFilterNonStandard = false) {
		const index = await Vetools.pGetCreatureIndex();
		const fileData = await Promise.all(
			Object.entries(index)
				.filter(([source]) => !isFilterNonStandard || !SourceUtil.isNonstandardSource(source))
				.map(async ([source, filename]) => ({source: source, json: await Vetools.pGetWithCache(Vetools.getCreatureUrl(filename))})),
		);
		// Filter to prevent duplicates from "otherSources" copies
		return {monster: fileData.map(it => MiscUtil.copy(it.json.monster.filter(mon => mon.source === it.source))).flat()};
	}

	static async _pGetHomebrewIndices () {
		const out = {source: {}, prop: {}};

		try {
			const [sourceIndex, propIndex] = await Promise.all([
				DataUtil.brew.pLoadSourceIndex(Config.get("import", "baseBrewUrl")),
				DataUtil.brew.pLoadPropIndex(Config.get("import", "baseBrewUrl")),
			]);
			out.source = sourceIndex;
			out.prop = propIndex;
		} catch (e) {
			ui.notifications.error(`Failed to load homebrew index! ${VeCt.STR_SEE_CONSOLE}`);
			setTimeout(() => { throw e; });
		}

		return out;
	}

	static async pGetHomebrewSources (...dirs) {
		const urlRoot = Config.get("import", "baseBrewUrl");
		const seenPaths = new Set();

		return dirs
			.map(dir => {
				return Object.keys(Vetools.HOMEBREW_INDEX__PROP[BrewUtil.dirToProp(dir)] || {})
					.map((path) => {
						if (seenPaths.has(path)) return null;
						seenPaths.add(path);
						return {
							url: DataUtil.brew.getFileUrl(path, urlRoot),
							name: path.split("/").slice(1).join("/").replace(/\.json$/i, ""),
						};
					});
			})
			.flat()
			.filter(Boolean)
			.sort((a, b) => SortUtil.ascSortLower(a.name, b.name))
	}

	static getContent (data, props) {
		if (!props) return data;

		return props.map(prop => {
			data[prop] = data[prop] || [];
			data[prop].forEach(it => it.__prop = prop);
			return data[prop];
		}).flat();
	}
	// endregion

	// region Additional data
	static async pGetSpellSideData () { return Vetools.pGetWithCache(`${Vetools.BASE_SITE_URL}data/spells/foundry.json`); }
	static async pGetOptionalFeatureSideData () { return Vetools.pGetWithCache(`${Vetools.BASE_SITE_URL}data/foundry-optionalfeatures.json`); }
	static async pGetClassSubclassSideData () { return Vetools.pGetWithCache(`${Vetools.BASE_SITE_URL}data/class/foundry.json`); }
	static async pGetRaceSideData () { return Vetools.pGetWithCache(`${Vetools.BASE_SITE_URL}data/foundry-races.json`); }
	static async pGetItemSideData () { return Vetools.pGetWithCache(`${Vetools.BASE_SITE_URL}data/foundry-items.json`); }
	static async pGetFeatSideData () { return Vetools.pGetWithCache(`${Vetools.BASE_SITE_URL}data/foundry-feats.json`); }
	static async pGetRewardSideData () { return Vetools.pGetWithCache(`${Vetools.BASE_SITE_URL}data/foundry-rewards.json`); }

	// region TODO(Future) enable these when data is available
	static async pGeBackgroundSideData () { return {} || Vetools.pGetWithCache(`${Vetools.BASE_SITE_URL}data/foundry-backgrounds.json`); }
	static async pGetConditionDiseaseSideData () { return {} || Vetools.pGetWithCache(`${Vetools.BASE_SITE_URL}data/foundry-conditionsdiseases.json`); }
	static async pGetPsionicsSideData () { return {} || Vetools.pGetWithCache(`${Vetools.BASE_SITE_URL}data/foundry-psionics.json`); }
	static async pGetVehicleUpgradeSideData () { return {} || Vetools.pGetWithCache(`${Vetools.BASE_SITE_URL}data/foundry-vehicles.json`); }
	static async pGetCreatureSideData () { return {} || Vetools.pGetWithCache(`${Vetools.BASE_SITE_URL}data/bestiary/foundry.json`); }
	static async pGetObjectSideData () { return {} || Vetools.pGetWithCache(`${Vetools.BASE_SITE_URL}data/foundry-objects.json`); }
	static async pGetVehicleSideData () { return {} || Vetools.pGetWithCache(`${Vetools.BASE_SITE_URL}data/foundry-vehicles.json`); }
	// endregion

	// endregion

	// region Icons
	static async pGetIconLookup (entityType) {
		const filename = `icon-${entityType}s.json`;
		const url = `modules/${SharedConsts.MODULE_NAME}/data/${filename}`;
		return Vetools.pGetWithCache(url);
	}
	// endregion

	static get BASE_SITE_URL () {
		let userOverride = Config.get("import", "baseSiteUrl");
		if (userOverride && userOverride.trim()) {
			// ensure trailing slash
			userOverride = userOverride.replace(/^(.*?)([\\/]+)$/, "$1/")
			return userOverride;
		}
		return Vetools._BASE_SITE_URL;
	}

	static get DATA_URL_FEATS () { return `${Vetools.BASE_SITE_URL}data/feats.json`; }
	static get DATA_URL_BACKGROUNDS () { return `${Vetools.BASE_SITE_URL}data/backgrounds.json`; }
	static get DATA_URL_VARIANTRULES () { return `${Vetools.BASE_SITE_URL}data/variantrules.json`; }
	static get DATA_URL_PSIONICS () { return `${Vetools.BASE_SITE_URL}data/psionics.json`; }
	static get DATA_URL_OPTIONALFEATURES () { return `${Vetools.BASE_SITE_URL}data/optionalfeatures.json`; }
	static get DATA_URL_CONDITIONSDISEASES () { return `${Vetools.BASE_SITE_URL}data/conditionsdiseases.json`; }
	static get DATA_URL_VEHICLES () { return `${Vetools.BASE_SITE_URL}data/vehicles.json`; }
	static get DATA_URL_REWARDS () { return `${Vetools.BASE_SITE_URL}data/rewards.json`; }
	static get DATA_URL_OBJECTS () { return `${Vetools.BASE_SITE_URL}data/objects.json`; }
	static get DATA_URL_DEITIES () { return `${Vetools.BASE_SITE_URL}data/deities.json`; }
	static get DATA_URL_RECIPES () { return `${Vetools.BASE_SITE_URL}data/recipes.json`; }
}
// Global data cache
Vetools.CACHED_REQUESTS = {};
Vetools.CACHE_REQUESTS_IN_FLIGHT = {};
// URLs
Vetools._BASE_SITE_URL = "https://5e.tools/";
// Preload content
Vetools.BESTIARY_FLUFF_INDEX = null;
Vetools.BESTIARY_TOKEN_LOOKUP = null;
Vetools.HOMEBREW_COLLECTION_INDEX = {};
// Homebrew meta caching
Vetools.HOMEBREW_META_SOURCES = {};
// Patches
Vetools._CACHED_GET_ROLLABLE_ENTRY_DICE = null;
Vetools._PATCHED_GET_ROLLABLE_ENTRY_DICE = null;
Vetools._CACHED_MONSTER_DO_BIND_COMPACT_CONTENT_HANDLERS = null;
// Homebrew
Vetools.HOMEBREW_INDEX__SOURCE = {};
Vetools.HOMEBREW_INDEX__PROP = {};

export {Vetools};
