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

const mm = require("micromatch");
const HarmonyExportImportedSpecifierDependency = require("../dependencies/HarmonyExportImportedSpecifierDependency");
const HarmonyImportSideEffectDependency = require("../dependencies/HarmonyImportSideEffectDependency");
const HarmonyImportSpecifierDependency = require("../dependencies/HarmonyImportSpecifierDependency");

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

/**
 * @typedef {Object} ExportInModule
 * @property {Module} module the module
 * @property {string} exportName the name of the export
 */

class SideEffectsFlagPlugin {
	apply(compiler) {
		compiler.hooks.normalModuleFactory.tap("SideEffectsFlagPlugin", nmf => {
			nmf.hooks.module.tap("SideEffectsFlagPlugin", (module, data) => {
				const resolveData = data.resourceResolveData;
				if (
					resolveData &&
					resolveData.descriptionFileData &&
					resolveData.relativePath
				) {
					const sideEffects = resolveData.descriptionFileData.sideEffects;
					const hasSideEffects = SideEffectsFlagPlugin.moduleHasSideEffects(
						resolveData.relativePath,
						sideEffects
					);
					if (!hasSideEffects) {
						module.factoryMeta.sideEffectFree = true;
					}
				}

				return module;
			});
			nmf.hooks.module.tap("SideEffectsFlagPlugin", (module, data) => {
				if (data.settings.sideEffects === false) {
					module.factoryMeta.sideEffectFree = true;
				} else if (data.settings.sideEffects === true) {
					module.factoryMeta.sideEffectFree = false;
				}
			});
		});
		compiler.hooks.compilation.tap("SideEffectsFlagPlugin", compilation => {
			compilation.hooks.optimizeDependencies.tap(
				"SideEffectsFlagPlugin",
				modules => {
					/** @type {Map<Module, Map<string, ExportInModule>>} */
					const reexportMaps = new Map();

					// Capture reexports of sideEffectFree modules
					for (const module of modules) {
						/** @type {Dependency[]} */
						const removeDependencies = [];
						for (const dep of module.dependencies) {
							if (dep instanceof HarmonyImportSideEffectDependency) {
								if (dep.module && dep.module.factoryMeta.sideEffectFree) {
									removeDependencies.push(dep);
								}
							} else if (
								dep instanceof HarmonyExportImportedSpecifierDependency
							) {
								if (module.factoryMeta.sideEffectFree) {
									const mode = dep.getMode(true);
									if (mode.type === "safe-reexport") {
										let map = reexportMaps.get(module);
										if (!map) {
											reexportMaps.set(module, (map = new Map()));
										}
										for (const pair of mode.map) {
											map.set(pair[0], {
												module: mode.module,
												exportName: pair[1]
											});
										}
									}
								}
							}
						}
					}

					// Flatten reexports
					for (const map of reexportMaps.values()) {
						for (const pair of map) {
							let mapping = pair[1];
							while (mapping) {
								const innerMap = reexportMaps.get(mapping.module);
								if (!innerMap) break;
								const newMapping = innerMap.get(mapping.exportName);
								if (newMapping) {
									map.set(pair[0], newMapping);
								}
								mapping = newMapping;
							}
						}
					}

					// Update imports along the reexports from sideEffectFree modules
					for (const pair of reexportMaps) {
						const module = pair[0];
						const map = pair[1];
						let newReasons = undefined;
						for (let i = 0; i < module.reasons.length; i++) {
							const reason = module.reasons[i];
							const dep = reason.dependency;
							if (
								dep instanceof HarmonyExportImportedSpecifierDependency ||
								(dep instanceof HarmonyImportSpecifierDependency &&
									!dep.namespaceObjectAsContext)
							) {
								const mapping = map.get(dep.id);
								if (mapping) {
									dep.redirectedModule = mapping.module;
									dep.redirectedId = mapping.exportName;
									mapping.module.addReason(
										reason.module,
										dep,
										reason.explanation
											? reason.explanation +
													" (skipped side-effect-free modules)"
											: "(skipped side-effect-free modules)"
									);
									// removing the currect reason, by not adding it to the newReasons array
									// lazily create the newReasons array
									if (newReasons === undefined) {
										newReasons = i === 0 ? [] : module.reasons.slice(0, i);
									}
									continue;
								}
							}
							if (newReasons !== undefined) newReasons.push(reason);
						}
						if (newReasons !== undefined) {
							module.reasons = newReasons;
						}
					}
				}
			);
		});
	}

	static moduleHasSideEffects(moduleName, flagValue) {
		switch (typeof flagValue) {
			case "undefined":
				return true;
			case "boolean":
				return flagValue;
			case "string":
				if (process.platform === "win32") {
					flagValue = flagValue.replace(/\\/g, "/");
				}
				return mm.isMatch(moduleName, flagValue, {
					matchBase: true
				});
			case "object":
				return flagValue.some(glob =>
					SideEffectsFlagPlugin.moduleHasSideEffects(moduleName, glob)
				);
		}
	}
}
module.exports = SideEffectsFlagPlugin;
