/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/
"use strict";

const DependencyReference = require("./DependencyReference");
const HarmonyImportDependency = require("./HarmonyImportDependency");
const Template = require("../Template");
const HarmonyLinkingError = require("../HarmonyLinkingError");

/** @typedef {import("../Module")} Module */

/** @typedef {"missing"|"unused"|"empty-star"|"reexport-non-harmony-default"|"reexport-named-default"|"reexport-namespace-object"|"reexport-non-harmony-default-strict"|"reexport-fake-namespace-object"|"rexport-non-harmony-undefined"|"safe-reexport"|"checked-reexport"|"dynamic-reexport"} ExportModeType */

/** @type {Map<string, string>} */
const EMPTY_MAP = new Map();

class ExportMode {
	/**
	 * @param {ExportModeType} type type of the mode
	 */
	constructor(type) {
		/** @type {ExportModeType} */
		this.type = type;
		/** @type {string|null} */
		this.name = null;
		/** @type {Map<string, string>} */
		this.map = EMPTY_MAP;
		/** @type {Module|null} */
		this.module = null;
		/** @type {string|null} */
		this.userRequest = null;
	}
}

const EMPTY_STAR_MODE = new ExportMode("empty-star");

class HarmonyExportImportedSpecifierDependency extends HarmonyImportDependency {
	constructor(
		request,
		originModule,
		sourceOrder,
		parserScope,
		id,
		name,
		activeExports,
		otherStarExports,
		strictExportPresence
	) {
		super(request, originModule, sourceOrder, parserScope);
		this.id = id;
		this.redirectedId = undefined;
		this.name = name;
		this.activeExports = activeExports;
		this.otherStarExports = otherStarExports;
		this.strictExportPresence = strictExportPresence;
	}

	get type() {
		return "harmony export imported specifier";
	}

	get _id() {
		return this.redirectedId || this.id;
	}

	getMode(ignoreUnused) {
		const name = this.name;
		const id = this._id;
		const used = this.originModule.isUsed(name);
		const importedModule = this._module;

		if (!importedModule) {
			const mode = new ExportMode("missing");
			mode.userRequest = this.userRequest;
			return mode;
		}

		if (
			!ignoreUnused &&
			(name ? !used : this.originModule.usedExports === false)
		) {
			const mode = new ExportMode("unused");
			mode.name = name || "*";
			return mode;
		}

		const strictHarmonyModule = this.originModule.buildMeta.strictHarmonyModule;
		if (name && id === "default" && importedModule.buildMeta) {
			if (!importedModule.buildMeta.exportsType) {
				const mode = new ExportMode(
					strictHarmonyModule
						? "reexport-non-harmony-default-strict"
						: "reexport-non-harmony-default"
				);
				mode.name = name;
				mode.module = importedModule;
				return mode;
			} else if (importedModule.buildMeta.exportsType === "named") {
				const mode = new ExportMode("reexport-named-default");
				mode.name = name;
				mode.module = importedModule;
				return mode;
			}
		}

		const isNotAHarmonyModule =
			importedModule.buildMeta && !importedModule.buildMeta.exportsType;
		if (name) {
			let mode;
			if (id) {
				// export { name as name }
				if (isNotAHarmonyModule && strictHarmonyModule) {
					mode = new ExportMode("rexport-non-harmony-undefined");
					mode.name = name;
				} else {
					mode = new ExportMode("safe-reexport");
					mode.map = new Map([[name, id]]);
				}
			} else {
				// export { * as name }
				if (isNotAHarmonyModule && strictHarmonyModule) {
					mode = new ExportMode("reexport-fake-namespace-object");
					mode.name = name;
				} else {
					mode = new ExportMode("reexport-namespace-object");
					mode.name = name;
				}
			}
			mode.module = importedModule;
			return mode;
		}

		const hasUsedExports = Array.isArray(this.originModule.usedExports);
		const hasProvidedExports = Array.isArray(
			importedModule.buildMeta.providedExports
		);
		const activeFromOtherStarExports = this._discoverActiveExportsFromOtherStartExports();

		// export *
		if (hasUsedExports) {
			// reexport * with known used exports
			if (hasProvidedExports) {
				const map = new Map(
					this.originModule.usedExports
						.filter(id => {
							if (id === "default") return false;
							if (this.activeExports.has(id)) return false;
							if (activeFromOtherStarExports.has(id)) return false;
							if (!importedModule.buildMeta.providedExports.includes(id))
								return false;
							return true;
						})
						.map(item => [item, item])
				);

				if (map.size === 0) {
					return EMPTY_STAR_MODE;
				}

				const mode = new ExportMode("safe-reexport");
				mode.module = importedModule;
				mode.map = map;
				return mode;
			}

			const map = new Map(
				this.originModule.usedExports
					.filter(id => {
						if (id === "default") return false;
						if (this.activeExports.has(id)) return false;
						if (activeFromOtherStarExports.has(id)) return false;

						return true;
					})
					.map(item => [item, item])
			);

			if (map.size === 0) {
				return EMPTY_STAR_MODE;
			}

			const mode = new ExportMode("checked-reexport");
			mode.module = importedModule;
			mode.map = map;
			return mode;
		}

		if (hasProvidedExports) {
			const map = new Map(
				importedModule.buildMeta.providedExports
					.filter(id => {
						if (id === "default") return false;
						if (this.activeExports.has(id)) return false;
						if (activeFromOtherStarExports.has(id)) return false;

						return true;
					})
					.map(item => [item, item])
			);

			if (map.size === 0) {
				return EMPTY_STAR_MODE;
			}

			const mode = new ExportMode("safe-reexport");
			mode.module = importedModule;
			mode.map = map;
			return mode;
		}

		const mode = new ExportMode("dynamic-reexport");
		mode.module = importedModule;
		return mode;
	}

	getReference() {
		const mode = this.getMode(false);

		switch (mode.type) {
			case "missing":
			case "unused":
			case "empty-star":
				return null;

			case "reexport-non-harmony-default":
			case "reexport-named-default":
				return new DependencyReference(
					mode.module,
					["default"],
					false,
					this.sourceOrder
				);

			case "reexport-namespace-object":
			case "reexport-non-harmony-default-strict":
			case "reexport-fake-namespace-object":
			case "rexport-non-harmony-undefined":
				return new DependencyReference(
					mode.module,
					true,
					false,
					this.sourceOrder
				);

			case "safe-reexport":
			case "checked-reexport":
				return new DependencyReference(
					mode.module,
					Array.from(mode.map.values()),
					false,
					this.sourceOrder
				);

			case "dynamic-reexport":
				return new DependencyReference(
					mode.module,
					true,
					false,
					this.sourceOrder
				);

			default:
				throw new Error(`Unknown mode ${mode.type}`);
		}
	}

	_discoverActiveExportsFromOtherStartExports() {
		if (!this.otherStarExports) return new Set();
		const result = new Set();
		// try to learn impossible exports from other star exports with provided exports
		for (const otherStarExport of this.otherStarExports) {
			const otherImportedModule = otherStarExport._module;
			if (
				otherImportedModule &&
				Array.isArray(otherImportedModule.buildMeta.providedExports)
			) {
				for (const exportName of otherImportedModule.buildMeta
					.providedExports) {
					result.add(exportName);
				}
			}
		}
		return result;
	}

	getExports() {
		if (this.name) {
			return {
				exports: [this.name],
				dependencies: undefined
			};
		}

		const importedModule = this._module;

		if (!importedModule) {
			// no imported module available
			return {
				exports: null,
				dependencies: undefined
			};
		}

		if (Array.isArray(importedModule.buildMeta.providedExports)) {
			const activeFromOtherStarExports = this._discoverActiveExportsFromOtherStartExports();
			return {
				exports: importedModule.buildMeta.providedExports.filter(
					id =>
						id !== "default" &&
						!activeFromOtherStarExports.has(id) &&
						!this.activeExports.has(id)
				),
				dependencies: [importedModule]
			};
		}

		if (importedModule.buildMeta.providedExports) {
			return {
				exports: true,
				dependencies: undefined
			};
		}

		return {
			exports: null,
			dependencies: [importedModule]
		};
	}

	getWarnings() {
		if (
			this.strictExportPresence ||
			this.originModule.buildMeta.strictHarmonyModule
		) {
			return [];
		}
		return this._getErrors();
	}

	getErrors() {
		if (
			this.strictExportPresence ||
			this.originModule.buildMeta.strictHarmonyModule
		) {
			return this._getErrors();
		}
		return [];
	}

	_getErrors() {
		const importedModule = this._module;
		if (!importedModule) {
			return;
		}

		if (!importedModule.buildMeta || !importedModule.buildMeta.exportsType) {
			// It's not an harmony module
			if (
				this.originModule.buildMeta.strictHarmonyModule &&
				this._id &&
				this._id !== "default"
			) {
				// In strict harmony modules we only support the default export
				return [
					new HarmonyLinkingError(
						`Can't reexport the named export '${this._id}' from non EcmaScript module (only default export is available)`
					)
				];
			}
			return;
		}

		if (!this._id) {
			return;
		}

		if (importedModule.isProvided(this._id) !== false) {
			// It's provided or we are not sure
			return;
		}

		// We are sure that it's not provided
		const idIsNotNameMessage =
			this._id !== this.name ? ` (reexported as '${this.name}')` : "";
		const errorMessage = `"export '${this._id}'${idIsNotNameMessage} was not found in '${this.userRequest}'`;
		return [new HarmonyLinkingError(errorMessage)];
	}

	updateHash(hash) {
		super.updateHash(hash);
		const hashValue = this.getHashValue(this._module);
		hash.update(hashValue);
	}

	getHashValue(importedModule) {
		if (!importedModule) {
			return "";
		}

		const stringifiedUsedExport = JSON.stringify(importedModule.usedExports);
		const stringifiedProvidedExport = JSON.stringify(
			importedModule.buildMeta.providedExports
		);
		return (
			importedModule.used + stringifiedUsedExport + stringifiedProvidedExport
		);
	}

	disconnect() {
		super.disconnect();
		this.redirectedId = undefined;
	}
}

module.exports = HarmonyExportImportedSpecifierDependency;

HarmonyExportImportedSpecifierDependency.Template = class HarmonyExportImportedSpecifierDependencyTemplate extends HarmonyImportDependency.Template {
	harmonyInit(dep, source, runtime, dependencyTemplates) {
		super.harmonyInit(dep, source, runtime, dependencyTemplates);
		const content = this.getContent(dep);
		source.insert(-1, content);
	}

	getHarmonyInitOrder(dep) {
		if (dep.name) {
			const used = dep.originModule.isUsed(dep.name);
			if (!used) return NaN;
		} else {
			const importedModule = dep._module;

			const activeFromOtherStarExports = dep._discoverActiveExportsFromOtherStartExports();

			if (Array.isArray(dep.originModule.usedExports)) {
				// we know which exports are used

				const unused = dep.originModule.usedExports.every(id => {
					if (id === "default") return true;
					if (dep.activeExports.has(id)) return true;
					if (importedModule.isProvided(id) === false) return true;
					if (activeFromOtherStarExports.has(id)) return true;
					return false;
				});
				if (unused) return NaN;
			} else if (
				dep.originModule.usedExports &&
				importedModule &&
				Array.isArray(importedModule.buildMeta.providedExports)
			) {
				// not sure which exports are used, but we know which are provided

				const unused = importedModule.buildMeta.providedExports.every(id => {
					if (id === "default") return true;
					if (dep.activeExports.has(id)) return true;
					if (activeFromOtherStarExports.has(id)) return true;
					return false;
				});
				if (unused) return NaN;
			}
		}
		return super.getHarmonyInitOrder(dep);
	}

	getContent(dep) {
		const mode = dep.getMode(false);
		const module = dep.originModule;
		const importedModule = dep._module;
		const importVar = dep.getImportVar();

		switch (mode.type) {
			case "missing":
				return `throw new Error(${JSON.stringify(
					`Cannot find module '${mode.userRequest}'`
				)});\n`;

			case "unused":
				return `${Template.toNormalComment(
					`unused harmony reexport ${mode.name}`
				)}\n`;

			case "reexport-non-harmony-default":
				return (
					"/* harmony reexport (default from non-harmony) */ " +
					this.getReexportStatement(
						module,
						module.isUsed(mode.name),
						importVar,
						null
					)
				);

			case "reexport-named-default":
				return (
					"/* harmony reexport (default from named exports) */ " +
					this.getReexportStatement(
						module,
						module.isUsed(mode.name),
						importVar,
						""
					)
				);

			case "reexport-fake-namespace-object":
				return (
					"/* harmony reexport (fake namespace object from non-harmony) */ " +
					this.getReexportFakeNamespaceObjectStatement(
						module,
						module.isUsed(mode.name),
						importVar
					)
				);

			case "rexport-non-harmony-undefined":
				return (
					"/* harmony reexport (non default export from non-harmony) */ " +
					this.getReexportStatement(
						module,
						module.isUsed(mode.name),
						"undefined",
						""
					)
				);

			case "reexport-non-harmony-default-strict":
				return (
					"/* harmony reexport (default from non-harmony) */ " +
					this.getReexportStatement(
						module,
						module.isUsed(mode.name),
						importVar,
						""
					)
				);

			case "reexport-namespace-object":
				return (
					"/* harmony reexport (module object) */ " +
					this.getReexportStatement(
						module,
						module.isUsed(mode.name),
						importVar,
						""
					)
				);

			case "empty-star":
				return "/* empty/unused harmony star reexport */";

			case "safe-reexport":
				return Array.from(mode.map.entries())
					.map(item => {
						return (
							"/* harmony reexport (safe) */ " +
							this.getReexportStatement(
								module,
								module.isUsed(item[0]),
								importVar,
								importedModule.isUsed(item[1])
							) +
							"\n"
						);
					})
					.join("");

			case "checked-reexport":
				return Array.from(mode.map.entries())
					.map(item => {
						return (
							"/* harmony reexport (checked) */ " +
							this.getConditionalReexportStatement(
								module,
								item[0],
								importVar,
								item[1]
							) +
							"\n"
						);
					})
					.join("");

			case "dynamic-reexport": {
				const activeExports = new Set([
					...dep.activeExports,
					...dep._discoverActiveExportsFromOtherStartExports()
				]);
				let content =
					"/* harmony reexport (unknown) */ for(var __WEBPACK_IMPORT_KEY__ in " +
					importVar +
					") ";

				// Filter out exports which are defined by other exports
				// and filter out default export because it cannot be reexported with *
				if (activeExports.size > 0) {
					content +=
						"if(" +
						JSON.stringify(Array.from(activeExports).concat("default")) +
						".indexOf(__WEBPACK_IMPORT_KEY__) < 0) ";
				} else {
					content += "if(__WEBPACK_IMPORT_KEY__ !== 'default') ";
				}
				const exportsName = dep.originModule.exportsArgument;
				return (
					content +
					`(function(key) { __webpack_require__.d(${exportsName}, key, function() { return ${importVar}[key]; }) }(__WEBPACK_IMPORT_KEY__));\n`
				);
			}

			default:
				throw new Error(`Unknown mode ${mode.type}`);
		}
	}

	getReexportStatement(module, key, name, valueKey) {
		const exportsName = module.exportsArgument;
		const returnValue = this.getReturnValue(name, valueKey);
		return `__webpack_require__.d(${exportsName}, ${JSON.stringify(
			key
		)}, function() { return ${returnValue}; });\n`;
	}

	getReexportFakeNamespaceObjectStatement(module, key, name) {
		const exportsName = module.exportsArgument;
		return `__webpack_require__.d(${exportsName}, ${JSON.stringify(
			key
		)}, function() { return __webpack_require__.t(${name}); });\n`;
	}

	getConditionalReexportStatement(module, key, name, valueKey) {
		if (valueKey === false) {
			return "/* unused export */\n";
		}
		const exportsName = module.exportsArgument;
		const returnValue = this.getReturnValue(name, valueKey);
		return `if(__webpack_require__.o(${name}, ${JSON.stringify(
			valueKey
		)})) __webpack_require__.d(${exportsName}, ${JSON.stringify(
			key
		)}, function() { return ${returnValue}; });\n`;
	}

	getReturnValue(name, valueKey) {
		if (valueKey === null) {
			return `${name}_default.a`;
		}
		if (valueKey === "") {
			return name;
		}
		if (valueKey === false) {
			return "/* unused export */ undefined";
		}

		return `${name}[${JSON.stringify(valueKey)}]`;
	}
};
