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

"use strict";

const Dependency = require("../Dependency");
const { UsageState } = require("../ExportsInfo");
const Template = require("../Template");
const { equals } = require("../util/ArrayHelpers");
const makeSerializable = require("../util/makeSerializable");
const { propertyAccess } = require("../util/property");
const {
	ESM_MODULE_EXPORTS_NAME,
	getRequireEsmModuleExportsAccess,
	handleDependencyBase,
	isRequireEsmModuleExportsModule
} = require("./CommonJsDependencyHelpers");
const ModuleDependency = require("./ModuleDependency");
const processExportInfo = require("./processExportInfo");

/** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */
/** @typedef {import("../Dependency").ExportsSpec} ExportsSpec */
/** @typedef {import("../Dependency").RawReferencedExports} RawReferencedExports */
/** @typedef {import("../Dependency").ReferencedExports} ReferencedExports */
/** @typedef {import("../Dependency").TRANSITIVE} TRANSITIVE */
/** @typedef {import("../DependencyTemplate").DependencyTemplateContext} DependencyTemplateContext */
/** @typedef {import("../ExportsInfo")} ExportsInfo */
/** @typedef {import("../ExportsInfo").ExportInfo} ExportInfo */
/** @typedef {import("../ExportsInfo").ExportInfoName} ExportInfoName */
/** @typedef {import("../Module")} Module */
/** @typedef {import("../ModuleGraph")} ModuleGraph */
/** @typedef {import("../javascript/JavascriptParser").Range} Range */
/** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */
/** @typedef {import("./CommonJsDependencyHelpers").CommonJSDependencyBaseKeywords} CommonJSDependencyBaseKeywords */
/** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext<[undefined | boolean, Range, Range | null, CommonJSDependencyBaseKeywords, ExportInfoName[], ExportInfoName[], boolean, boolean]>} ObjectDeserializerContext */
/** @typedef {import("../serialization/ObjectMiddleware").ObjectSerializerContext<[undefined | boolean, Range, Range | null, CommonJSDependencyBaseKeywords, ExportInfoName[], ExportInfoName[], boolean, boolean]>} ObjectSerializerContext */

const idsSymbol = /** @type {symbol} */ (
	Symbol("CommonJsExportRequireDependency.ids")
);

const EMPTY_OBJECT = {};

/** @typedef {Set<string>} Exports */
/** @typedef {Set<string>} Checked */

class CommonJsExportRequireDependency extends ModuleDependency {
	/**
	 * Creates an instance of CommonJsExportRequireDependency.
	 * @param {Range} range range
	 * @param {Range | null} valueRange value range
	 * @param {CommonJSDependencyBaseKeywords} base base
	 * @param {ExportInfoName[]} names names
	 * @param {string} request request
	 * @param {ExportInfoName[]} ids ids
	 * @param {boolean} resultUsed true, when the result is used
	 */
	constructor(range, valueRange, base, names, request, ids, resultUsed) {
		super(request);
		this.range = range;
		this.valueRange = valueRange;
		/** @type {CommonJSDependencyBaseKeywords} */
		this.base = base;
		/** @type {string[]} */
		this.names = names;
		/** @type {string[]} */
		this.ids = ids;
		/** @type {boolean} */
		this.resultUsed = resultUsed;
		/** @type {undefined | boolean} */
		this.asiSafe = undefined;
		// true when the reexport is a lazy `{ get: () => require(...) }` accessor
		// (as produced by `Object.defineProperty`) rather than an eager value.
		/** @type {boolean} */
		this.getter = false;
	}

	get type() {
		return "cjs export require";
	}

	get category() {
		return "commonjs";
	}

	/**
	 * Could affect referencing module.
	 * @returns {boolean | TRANSITIVE} true, when changes to the referenced module could affect the referencing module; TRANSITIVE, when changes to the referenced module could affect referencing modules of the referencing module
	 */
	couldAffectReferencingModule() {
		return Dependency.TRANSITIVE;
	}

	/**
	 * Returns the imported id.
	 * @param {ModuleGraph} moduleGraph the module graph
	 * @returns {ExportInfoName[]} the imported id
	 */
	getIds(moduleGraph) {
		return moduleGraph.getMeta(this)[idsSymbol] || this.ids;
	}

	/**
	 * Updates ids using the provided module graph.
	 * @param {ModuleGraph} moduleGraph the module graph
	 * @param {ExportInfoName[]} ids the imported ids
	 * @returns {void}
	 */
	setIds(moduleGraph, ids) {
		moduleGraph.getMeta(this)[idsSymbol] = ids;
	}

	/**
	 * Returns list of exports referenced by this dependency
	 * @param {ModuleGraph} moduleGraph module graph
	 * @param {RuntimeSpec} runtime the runtime for which the module is analysed
	 * @returns {ReferencedExports} referenced exports
	 */
	getReferencedExports(moduleGraph, runtime) {
		const ids = this.getIds(moduleGraph);
		const importedModule = moduleGraph.getModule(this);
		if (
			importedModule &&
			isRequireEsmModuleExportsModule(importedModule, moduleGraph)
		) {
			// `require(esm)` unwraps the "module.exports" named export; any
			// further property access lands on that value (which webpack does
			// not model), so only the "module.exports" export is observable.
			return [{ name: [ESM_MODULE_EXPORTS_NAME], canInline: false }];
		}
		const getFullResult = () => {
			if (ids.length === 0) {
				return Dependency.EXPORTS_OBJECT_REFERENCED;
			}
			return [
				{
					name: ids,
					canMangle: false,
					canInline: false
				}
			];
		};
		if (this.resultUsed) return getFullResult();
		/** @type {ExportsInfo | undefined} */
		let exportsInfo = moduleGraph.getExportsInfo(
			/** @type {Module} */ (moduleGraph.getParentModule(this))
		);
		for (const name of this.names) {
			const exportInfo =
				/** @type {ExportInfo} */
				(exportsInfo.getReadOnlyExportInfo(name));
			const used = exportInfo.getUsed(runtime);
			if (used === UsageState.Unused) return Dependency.NO_EXPORTS_REFERENCED;
			if (used !== UsageState.OnlyPropertiesUsed) return getFullResult();
			exportsInfo = exportInfo.exportsInfo;
			if (!exportsInfo) return getFullResult();
		}
		if (exportsInfo.otherExportsInfo.getUsed(runtime) !== UsageState.Unused) {
			return getFullResult();
		}
		/** @type {RawReferencedExports} */
		const referencedExports = [];
		for (const exportInfo of exportsInfo.orderedExports) {
			processExportInfo(
				runtime,
				referencedExports,
				[...ids, exportInfo.name],
				exportInfo,
				false
			);
		}
		return referencedExports.map((name) => ({
			name,
			canMangle: false,
			canInline: false
		}));
	}

	/**
	 * Returns the exported names
	 * @param {ModuleGraph} moduleGraph module graph
	 * @returns {ExportsSpec | undefined} export names
	 */
	getExports(moduleGraph) {
		const importedModule = moduleGraph.getModule(this);
		const esmUnwrap =
			importedModule &&
			isRequireEsmModuleExportsModule(importedModule, moduleGraph);
		if (this.names.length === 1) {
			const ids = this.getIds(moduleGraph);
			const name = this.names[0];
			const from = moduleGraph.getConnection(this);
			if (!from) return;
			const exportChain = esmUnwrap
				? [ESM_MODULE_EXPORTS_NAME, ...ids]
				: ids.length === 0
					? null
					: ids;
			return {
				exports: [
					{
						name,
						from,
						export: exportChain,
						// we can't mangle names that are in an empty object
						// because one could access the prototype property
						// when export isn't set yet
						canMangle: !(name in EMPTY_OBJECT) && false
					}
				],
				dependencies: [from.module]
			};
		} else if (this.names.length > 0) {
			const name = this.names[0];
			return {
				exports: [
					{
						name,
						// we can't mangle names that are in an empty object
						// because one could access the prototype property
						// when export isn't set yet
						canMangle: !(name in EMPTY_OBJECT) && false
					}
				],
				dependencies: undefined
			};
		}
		const from = moduleGraph.getConnection(this);
		if (!from) return;
		if (esmUnwrap) {
			// Full re-export `module.exports = require("./esm")` of a module
			// with a `"module.exports"` named export: the wrapping module's
			// `module.exports` becomes the unwrapped value, whose own
			// properties webpack cannot enumerate statically.
			return {
				exports: true,
				canMangle: false,
				dependencies: [from.module]
			};
		}
		const reexportInfo = this.getStarReexports(
			moduleGraph,
			undefined,
			from.module
		);
		const ids = this.getIds(moduleGraph);
		if (reexportInfo) {
			return {
				exports: Array.from(
					/** @type {Exports} */
					(reexportInfo.exports),
					(name) => ({
						name,
						from,
						export: [...ids, name],
						canMangle: !(name in EMPTY_OBJECT) && false
					})
				),
				dependencies: [from.module]
			};
		}
		return {
			exports: true,
			from: ids.length === 0 ? from : undefined,
			canMangle: false,
			dependencies: [from.module]
		};
	}

	/**
	 * Gets star reexports.
	 * @param {ModuleGraph} moduleGraph the module graph
	 * @param {RuntimeSpec} runtime the runtime
	 * @param {Module} importedModule the imported module (optional)
	 * @returns {{ exports?: Exports, checked?: Checked } | undefined} information
	 */
	getStarReexports(
		moduleGraph,
		runtime,
		importedModule = /** @type {Module} */ (moduleGraph.getModule(this))
	) {
		/** @type {ExportsInfo | undefined} */
		let importedExportsInfo = moduleGraph.getExportsInfo(importedModule);
		const ids = this.getIds(moduleGraph);
		if (ids.length > 0) {
			importedExportsInfo = importedExportsInfo.getNestedExportsInfo(ids);
		}
		/** @type {ExportsInfo | undefined} */
		let exportsInfo = moduleGraph.getExportsInfo(
			/** @type {Module} */ (moduleGraph.getParentModule(this))
		);
		if (this.names.length > 0) {
			exportsInfo = exportsInfo.getNestedExportsInfo(this.names);
		}

		const noExtraExports =
			importedExportsInfo &&
			importedExportsInfo.otherExportsInfo.provided === false;
		const noExtraImports =
			exportsInfo &&
			exportsInfo.otherExportsInfo.getUsed(runtime) === UsageState.Unused;

		if (!noExtraExports && !noExtraImports) {
			return;
		}

		const isNamespaceImport =
			importedModule.getExportsType(moduleGraph, false) === "namespace";

		/** @type {Exports} */
		const exports = new Set();
		/** @type {Checked} */
		const checked = new Set();

		if (noExtraImports) {
			for (const exportInfo of /** @type {ExportsInfo} */ (exportsInfo)
				.orderedExports) {
				const name = exportInfo.name;
				if (exportInfo.getUsed(runtime) === UsageState.Unused) continue;
				if (name === "__esModule" && isNamespaceImport) {
					exports.add(name);
				} else if (importedExportsInfo) {
					const importedExportInfo =
						importedExportsInfo.getReadOnlyExportInfo(name);
					if (importedExportInfo.provided === false) continue;
					exports.add(name);
					if (importedExportInfo.provided === true) continue;
					checked.add(name);
				} else {
					exports.add(name);
					checked.add(name);
				}
			}
		} else if (noExtraExports) {
			for (const importedExportInfo of /** @type {ExportsInfo} */ (
				importedExportsInfo
			).orderedExports) {
				const name = importedExportInfo.name;
				if (importedExportInfo.provided === false) continue;
				if (exportsInfo) {
					const exportInfo = exportsInfo.getReadOnlyExportInfo(name);
					if (exportInfo.getUsed(runtime) === UsageState.Unused) continue;
				}
				exports.add(name);
				if (importedExportInfo.provided === true) continue;
				checked.add(name);
			}
			if (isNamespaceImport) {
				exports.add("__esModule");
				checked.delete("__esModule");
			}
		}

		return { exports, checked };
	}

	/**
	 * Serializes this instance into the provided serializer context.
	 * @param {ObjectSerializerContext} context context
	 */
	serialize(context) {
		context
			.write(this.asiSafe)
			.write(this.range)
			.write(this.valueRange)
			.write(this.base)
			.write(this.names)
			.write(this.ids)
			.write(this.resultUsed)
			.write(this.getter);
		super.serialize(context);
	}

	/**
	 * Restores this instance from the provided deserializer context.
	 * @param {ObjectDeserializerContext} context context
	 */
	deserialize(context) {
		this.asiSafe = context.read();
		const c1 = context.rest;
		this.range = c1.read();
		const c2 = c1.rest;
		this.valueRange = c2.read();
		const c3 = c2.rest;
		this.base = c3.read();
		const c4 = c3.rest;
		this.names = c4.read();
		const c5 = c4.rest;
		this.ids = c5.read();
		const c6 = c5.rest;
		this.resultUsed = c6.read();
		const c7 = c6.rest;
		this.getter = c7.read();
		super.deserialize(c7.rest);
	}
}

makeSerializable(
	CommonJsExportRequireDependency,
	"webpack/lib/dependencies/CommonJsExportRequireDependency"
);

CommonJsExportRequireDependency.Template = class CommonJsExportRequireDependencyTemplate extends (
	ModuleDependency.Template
) {
	/**
	 * Applies the plugin by registering its hooks on the compiler.
	 * @param {Dependency} dependency the dependency for which the template should be applied
	 * @param {ReplaceSource} source the current replace source which can be modified
	 * @param {DependencyTemplateContext} templateContext the context object
	 * @returns {void}
	 */
	apply(
		dependency,
		source,
		{
			module,
			runtimeTemplate,
			chunkGraph,
			moduleGraph,
			runtimeRequirements,
			runtime
		}
	) {
		const dep = /** @type {CommonJsExportRequireDependency} */ (dependency);
		// CJS exports are never inlined
		const used = /** @type {string | string[] | false} */ (
			moduleGraph.getExportsInfo(module).getUsedName(dep.names, runtime)
		);

		const [type, base] = handleDependencyBase(
			dep.base,
			module,
			runtimeRequirements
		);

		const importedModule = moduleGraph.getModule(dep);
		let requireExpr = runtimeTemplate.moduleExports({
			module: importedModule,
			chunkGraph,
			request: dep.request,
			weak: dep.weak,
			runtimeRequirements
		});
		if (importedModule) {
			const ids = dep.getIds(moduleGraph);
			const esmRequireAccess = getRequireEsmModuleExportsAccess(
				importedModule,
				moduleGraph,
				runtime
			);
			if (esmRequireAccess !== null) {
				requireExpr += `${esmRequireAccess}${propertyAccess(ids)}`;
			} else {
				// CJS exports are never inlined
				const usedImported = /** @type {string | string[] | false} */ (
					moduleGraph.getExportsInfo(importedModule).getUsedName(ids, runtime)
				);
				if (usedImported) {
					const comment = equals(usedImported, ids)
						? ""
						: `${Template.toNormalComment(propertyAccess(ids))} `;
					requireExpr += `${comment}${propertyAccess(usedImported)}`;
				}
			}
		}

		switch (type) {
			case "expression":
				source.replace(
					dep.range[0],
					dep.range[1] - 1,
					used
						? `${base}${propertyAccess(used)} = ${requireExpr}`
						: `/* unused reexport */ ${requireExpr}`
				);
				return;
			case "Object.defineProperty": {
				// `Object.defineProperty(exports, "name", { value: require("...") })`
				// or the lazy getter form `{ get: () => require("...") }` used by
				// barrel files (e.g. webpack's own `lib/index.js`).
				const valueRange = /** @type {Range} */ (dep.valueRange);
				if (!used) {
					// A lazy getter never runs `require` until accessed, so an unused
					// one can be dropped entirely; an eager value must keep side effects.
					source.replace(
						dep.range[0],
						dep.range[1] - 1,
						dep.getter
							? "/* unused reexport */ 0"
							: `/* unused reexport */ ${requireExpr}`
					);
					return;
				}
				const descriptor = dep.getter
					? "enumerable: true, get: () => ("
					: "value: (";
				source.replace(
					dep.range[0],
					valueRange[0] - 1,
					`Object.defineProperty(${base}${propertyAccess(
						used.slice(0, -1)
					)}, ${JSON.stringify(used[used.length - 1])}, { ${descriptor}`
				);
				source.replace(valueRange[0], valueRange[1] - 1, requireExpr);
				source.replace(valueRange[1], dep.range[1] - 1, ") })");
				return;
			}
			default:
				throw new Error("Unexpected type");
		}
	}
};

module.exports = CommonJsExportRequireDependency;
module.exports.idsSymbol = idsSymbol;
