/*
	MIT License http://www.opensource.org/licenses/mit-license.php
*/

"use strict";

const RuntimeGlobals = require("../RuntimeGlobals");
const Template = require("../Template");
const HelperRuntimeModule = require("./HelperRuntimeModule");

/** @typedef {import("../Module").RuntimeRequirements} RuntimeRequirements */
/** @typedef {import("../Module").ExportsType} ExportsType */
/** @typedef {import("../ChunkGraph").ModuleId} ModuleId */

/**
 * @param {ExportsType} exportsType exports type
 * @returns {string} mode
 */
function getMakeDeferredNamespaceModeFromExportsType(exportsType) {
	// number is from createFakeNamespaceObject mode ^ 1
	if (exportsType === "namespace") return `/* ${exportsType} */ 8`;
	if (exportsType === "default-only") return `/* ${exportsType} */ 0`;
	if (exportsType === "default-with-named") return `/* ${exportsType} */ 2`;
	if (exportsType === "dynamic") return `/* ${exportsType} */ 6`;
	throw new Error(`Unknown exports type: ${exportsType}`);
}

/**
 * @param {string} moduleId moduleId
 * @param {ExportsType} exportsType exportsType
 * @param {(ModuleId | null)[]} asyncDepsIds asyncDepsIds
 * @param {RuntimeRequirements} runtimeRequirements runtime requirements
 * @returns {string} call make optimized deferred namespace object
 */
function getOptimizedDeferredModule(
	moduleId,
	exportsType,
	asyncDepsIds,
	runtimeRequirements
) {
	runtimeRequirements.add(RuntimeGlobals.makeOptimizedDeferredNamespaceObject);
	const mode = getMakeDeferredNamespaceModeFromExportsType(exportsType);
	return `${
		RuntimeGlobals.makeOptimizedDeferredNamespaceObject
	}(${moduleId}, ${mode}${
		asyncDepsIds.length > 0
			? `, ${JSON.stringify(asyncDepsIds.filter((x) => x !== null))}`
			: ""
	})`;
}

class MakeOptimizedDeferredNamespaceObjectRuntimeModule extends HelperRuntimeModule {
	/**
	 * @param {boolean} hasAsyncRuntime if async module is used.
	 */
	constructor(hasAsyncRuntime) {
		super("make optimized deferred namespace object");
		/** @type {boolean} */
		this.hasAsyncRuntime = hasAsyncRuntime;
	}

	/**
	 * Generates runtime code for this runtime module.
	 * @returns {string | null} runtime code
	 */
	generate() {
		if (!this.compilation) return null;
		const { runtimeTemplate } = this.compilation;
		const cst = runtimeTemplate.renderConst();
		const lt = runtimeTemplate.renderLet();
		const fn = RuntimeGlobals.makeOptimizedDeferredNamespaceObject;
		const hasAsync = this.hasAsyncRuntime;
		return Template.asString([
			// Note: must be a function (not arrow), because this is used in body!
			`${fn} = function(moduleId, mode${hasAsync ? ", asyncDeps" : ""}) {`,
			Template.indent([
				`${cst} r = this;`,
				hasAsync ? `${cst} isAsync = asyncDeps && asyncDeps.length;` : "",
				`${cst} obj = {`,
				Template.indent([
					"get a() {",
					Template.indent([
						`${lt} exports = r(moduleId);`,
						hasAsync
							? `if(isAsync) exports = exports[${RuntimeGlobals.asyncModuleExportSymbol}];`
							: "",
						// if exportsType is "namespace" we can generate the most optimized code,
						// on the second access, we can avoid trigger the getter.
						// we can also do this if exportsType is "dynamic" and there is a "__esModule" property on it.
						'if(mode & 8 || (mode & 4 && exports.__esModule)) Object.defineProperty(this, "a", { value: exports });',
						"return exports;"
					]),
					"}"
				]),
				"};",
				hasAsync
					? `if(isAsync) obj[${RuntimeGlobals.deferredModuleAsyncTransitiveDependenciesSymbol}] = asyncDeps;`
					: "",
				"return obj;"
			]),
			"};"
		]);
	}
}

class MakeDeferredNamespaceObjectRuntimeModule extends HelperRuntimeModule {
	/**
	 * @param {boolean} hasAsyncRuntime if async module is used.
	 */
	constructor(hasAsyncRuntime) {
		super("make deferred namespace object");
		/** @type {boolean} */
		this.hasAsyncRuntime = hasAsyncRuntime;
	}

	/**
	 * Generates runtime code for this runtime module.
	 * @returns {string | null} runtime code
	 */
	generate() {
		if (!this.compilation) return null;
		const { runtimeTemplate } = this.compilation;
		const cst = runtimeTemplate.renderConst();
		const lt = runtimeTemplate.renderLet();
		const fn = RuntimeGlobals.makeDeferredNamespaceObject;
		const hasAsync = this.hasAsyncRuntime;
		const init = `${runtimeTemplate.optionalChaining("init", "()")};`;
		return `${fn} = ${runtimeTemplate.basicFunction("moduleId, mode", [
			// Per the TC39 import-defer spec, deferred namespaces are
			// distinct from their eager counterparts and the same module
			// referenced from multiple defer-import sites must yield the
			// same object. Cache the Proxy / fake namespace per-moduleId so
			// repeated calls (including across files) share identity.
			//
			// Bit 16 (`createFakeNamespaceObject`'s "return value when
			// it's Promise-like" flag added by
			// `RuntimeTemplate.moduleNamespacePromise` for dynamic
			// imports) is irrelevant for deferred namespaces — the value
			// passed into `createFakeNamespaceObject` here is always the
			// resolved module exports (after unwrapping the async-module
			// export symbol when present), never a Promise. Strip it
			// once so all downstream behavior, the cache key, and the
			// `createFakeNamespaceObject` call below see the same shape
			// mode. This keeps static defer (mode 8) and dynamic
			// `await import.defer` (mode 8 | 16) sharing the same
			// Deferred Module Namespace object, while still keying by
			// `(moduleId, mode)` so distinct exports-type shapes
			// (e.g. one importer treats a CJS module as
			// "default-with-named", another as "namespace") get
			// distinct cache entries.
			"mode &= ~16;",
			`${lt} byMode = __webpack_module_deferred_namespace_cache__[moduleId];`,
			"if (byMode && byMode[mode] !== undefined) return byMode[mode];",
			"if (!byMode) byMode = __webpack_module_deferred_namespace_cache__[moduleId] = {};",
			`${cst} cachedModule = __webpack_module_cache__[moduleId];`,
			"if (cachedModule && cachedModule.error === undefined && !(mode & 8)) {",
			Template.indent([
				`${lt} exports = cachedModule.exports;`,
				hasAsync
					? `if (${RuntimeGlobals.asyncModuleExportSymbol} in exports) exports = exports[${RuntimeGlobals.asyncModuleExportSymbol}];`
					: "",
				`return byMode[mode] = ${RuntimeGlobals.createFakeNamespaceObject}(exports, mode);`
			]),
			"}",
			"",
			`${lt} init = ${runtimeTemplate.basicFunction("", [
				`ns = ${RuntimeGlobals.require}(moduleId);`,
				hasAsync
					? `if (${RuntimeGlobals.asyncModuleExportSymbol} in ns) ns = ns[${RuntimeGlobals.asyncModuleExportSymbol}];`
					: "",
				"init = null;",
				"if (mode & 8 || mode & 4 && ns.__esModule && typeof ns === 'object') {",
				Template.indent([
					// Drop only the read-side traps after init: with the
					// resolved namespace's own keys mirrored onto
					// `ns_target` below, the default `Reflect` behavior
					// returns the right values via the live-binding
					// getters, so we no longer need to intercept `get` /
					// `has` / `ownKeys` / `getOwnPropertyDescriptor`.
					//
					// The mutation traps (`set`, `deleteProperty`,
					// `defineProperty`) are kept because per the TC39
					// import-defer spec, `[[Set]]` / `[[Delete]]` /
					// `[[DefineOwnProperty]]` on a Deferred Module
					// Namespace Exotic Object never succeed — and the
					// proxy target itself remains extensible
					// (architecturally we cannot freeze it up-front),
					// so without these traps `ns.notExported = "x"`
					// after evaluation would silently create a property
					// on the target instead of returning false.
					"delete handler.get;",
					"delete handler.has;",
					"delete handler.ownKeys;",
					"delete handler.getOwnPropertyDescriptor;"
				]),
				"} else {",
				Template.indent([
					`ns = ${RuntimeGlobals.createFakeNamespaceObject}(ns, mode);`
				]),
				"}",
				// Mirror own properties from the resolved namespace onto the proxy
				// target so that proxy invariants hold for callers that structurally
				// introspect via `Object.keys` / `Object.getOwnPropertyNames` /
				// `Object.getOwnPropertyDescriptor`: when our trap reports a
				// non-configurable descriptor for a key, the target must also have
				// that key with a matching descriptor.
				//
				// `__esModule` and `Symbol.toStringTag` are intentionally skipped:
				// the proxy synthesizes "Deferred Module" / true regardless of what
				// the underlying namespace exposes (per the TC39 import-defer
				// proposal, the [[StringTag]] of a Deferred Module Namespace
				// Exotic Object is "Deferred Module"), and the target was already
				// pre-populated with those values below.
				`${cst} keys = Reflect.ownKeys(ns);`,
				"for (var i = 0; i < keys.length; i++) {",
				Template.indent([
					`${cst} k = keys[i];`,
					'if (k === "__esModule" || k === Symbol.toStringTag) continue;',
					`if (!${runtimeTemplate.objectHasOwn("ns_target", "k")}) {`,
					Template.indent([
						"try { Object.defineProperty(ns_target, k, Reflect.getOwnPropertyDescriptor(ns, k)); } catch (_) {}"
					]),
					"}"
				]),
				"}"
			])};`,
			"",
			// The proxy target is a fresh placeholder, separate from
			// `__webpack_module_deferred_exports__[moduleId]` (which is reused
			// by `__webpack_require__` as `module.exports` for deferred-loaded
			// modules and would conflict with our pre-populated synthetic
			// `__esModule` / `Symbol.toStringTag` non-configurable properties).
			// Using a dedicated target keeps the proxy invariant-compliant
			// without interfering with the module's own exports object.
			`${cst} ns_target = { __proto__: null };`,
			// Pre-populate the synthetic deferred-namespace properties with
			// fully non-configurable, non-writable, non-enumerable descriptors
			// (matching the TC39 import-defer spec for Module Namespace
			// Exotic Objects). The trap returns the same descriptors below.
			'Object.defineProperty(ns_target, "__esModule", { value: true });',
			'Object.defineProperty(ns_target, Symbol.toStringTag, { value: "Deferred Module" });',
			`${lt} ns = ns_target;`,
			`${cst} handler = {`,
			Template.indent([
				"__proto__: null,",
				// Per the TC39 import-defer proposal, `IsSymbolLikeNamespaceKey`
				// returns true for any Symbol-keyed access (and for "then"); such
				// accesses go through `OrdinaryGetOwnProperty` and must not
				// trigger evaluation of the deferred module. The Symbol checks
				// below short-circuit to the pre-populated target without
				// running `init()`.
				`${runtimeTemplate.method("get", "_, name", [
					"switch (name) {",
					Template.indent([
						'case "__esModule": return true;',
						'case Symbol.toStringTag: return "Deferred Module";',
						'case "then": return undefined;'
					]),
					"}",
					'if (typeof name === "symbol") return ns_target[name];',
					init,
					"return ns[name];"
				])},`,
				`${runtimeTemplate.method("has", "_, name", [
					"switch (name) {",
					Template.indent(
						[
							'case "__esModule":',
							"case Symbol.toStringTag:",
							hasAsync
								? `case ${RuntimeGlobals.deferredModuleAsyncTransitiveDependenciesSymbol}:`
								: "",
							Template.indent("return true;"),
							'case "then":',
							Template.indent("return false;")
						].filter(Boolean)
					),
					"}",
					'if (typeof name === "symbol") return name in ns_target;',
					init,
					"return name in ns;"
				])},`,
				`${runtimeTemplate.method("ownKeys", "", [
					init,
					`${cst} filtered = Reflect.ownKeys(ns).filter(${runtimeTemplate.expressionFunction(
						'x !== "then" && x !== Symbol.toStringTag',
						"x"
					)});`,
					`${cst} keys = ${
						runtimeTemplate.supportsSpread()
							? "[...filtered, Symbol.toStringTag]"
							: "filtered.concat([Symbol.toStringTag])"
					};`,
					"return keys;"
				])},`,
				`${runtimeTemplate.method("getOwnPropertyDescriptor", "_, name", [
					"switch (name) {",
					Template.indent([
						// Match the descriptors actually defined on `ns_target`
						// (non-configurable, non-writable, non-enumerable) so the
						// proxy invariant holds for both the trap result and any
						// post-init forwarding via the deleted-handler path.
						'case "__esModule": return { value: true, writable: false, enumerable: false, configurable: false };',
						'case Symbol.toStringTag: return { value: "Deferred Module", writable: false, enumerable: false, configurable: false };',
						'case "then": return undefined;'
					]),
					"}",
					'if (typeof name === "symbol") return Reflect.getOwnPropertyDescriptor(ns_target, name);',
					init,
					`${lt} desc = Reflect.getOwnPropertyDescriptor(ns, name);`,
					'if (mode & 2 && name == "default" && !desc) {',
					Template.indent("desc = { value: ns, configurable: true };"),
					"}",
					"return desc;"
				])},`,
				// `defineProperty` always rejects, but per the TC39 spec it
				// must still trigger evaluation for string keys (the spec
				// algorithm calls `[[GetOwnProperty]]` first, which forces
				// evaluation on a deferred namespace). Symbol keys go through
				// OrdinaryDefineOwnProperty and do not trigger eval.
				`${runtimeTemplate.method("defineProperty", "_, name", [
					'if (typeof name === "symbol" || name === "then") return false;',
					init,
					"return false;"
				])},`,
				// `deleteProperty` rejects, but per the TC39 spec it must
				// still trigger evaluation for string keys (the spec
				// algorithm calls `GetModuleExportsList` for non-symbol-like
				// keys, forcing evaluation on a deferred namespace).
				`${runtimeTemplate.method("deleteProperty", "_, name", [
					'if (typeof name === "symbol" || name === "then") return false;',
					init,
					"return false;"
				])},`,
				// `set` always returns false without triggering evaluation —
				// the spec [[Set]] algorithm for Module Namespaces is just
				// "return false" (no [[GetOwnProperty]], no eval).
				`set: ${runtimeTemplate.returningFunction("false")},`
			]),
			"}",
			// we don't fully emulate ES Module semantics in this Proxy to align with normal webpack esm namespace object.
			"return byMode[mode] = new Proxy(ns_target, handler);"
		])};`;
	}
}

module.exports.MakeDeferredNamespaceObjectRuntimeModule =
	MakeDeferredNamespaceObjectRuntimeModule;
module.exports.MakeOptimizedDeferredNamespaceObjectRuntimeModule =
	MakeOptimizedDeferredNamespaceObjectRuntimeModule;
module.exports.getMakeDeferredNamespaceModeFromExportsType =
	getMakeDeferredNamespaceModeFromExportsType;
module.exports.getOptimizedDeferredModule = getOptimizedDeferredModule;
