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

"use strict";

const ConcatenationScope = require("../ConcatenationScope");
const Dependency = require("../Dependency");
const InitFragment = require("../InitFragment");
const RuntimeGlobals = require("../RuntimeGlobals");
const { InlinedUsedName } = require("../optimize/InlineExports");
const makeSerializable = require("../util/makeSerializable");
const { propertyAccess } = require("../util/property");
const ExportBindingInitFragment = require("./ExportBindingInitFragment");
const HarmonyExportInitFragment = require("./HarmonyExportInitFragment");
const NullDependency = require("./NullDependency");

const DEFAULT_EXPORT_NAME = "default";

/** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */
/** @typedef {import("../Dependency").ExportsSpec} ExportsSpec */
/** @typedef {import("../Dependency").LazyUntil} LazyUntil */
/** @typedef {import("../DependencyTemplate").DependencyTemplateContext} DependencyTemplateContext */
/** @typedef {import("../ModuleGraph")} ModuleGraph */
/** @typedef {import("../ModuleGraphConnection").ConnectionState} ConnectionState */
/** @typedef {import("../optimize/InlineExports").InlinedValue} InlinedValue */
/** @typedef {import("../javascript/JavascriptParser").Range} Range */
/** @typedef {string | { id?: string | undefined, range: Range, prefix: string, suffix: string }} DeclarationId */
/** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext<[Range, Range, string, DeclarationId | undefined, boolean]>} ObjectDeserializerContext */
/** @typedef {import("../serialization/ObjectMiddleware").ObjectSerializerContext<[Range, Range, string, DeclarationId | undefined, boolean]>} ObjectSerializerContext */
/** @typedef {import("./HarmonyExportInitFragment").ExportMap} ExportMap */
/** @typedef {import("../javascript/JavascriptModule").JavascriptModuleBuildInfo} JavascriptModuleBuildInfo */

class HarmonyExportExpressionDependency extends NullDependency {
	/**
	 * Creates an instance of HarmonyExportExpressionDependency.
	 * @param {Range} range range
	 * @param {Range} rangeStatement range statement
	 * @param {string} prefix prefix
	 * @param {string | { id?: string | undefined, range: Range, prefix: string, suffix: string }=} declarationId declaration id
	 * @param {InlinedValue=} constValue inlined primitive when `export default <const>` is inline-eligible
	 */
	constructor(range, rangeStatement, prefix, declarationId, constValue) {
		super();
		this.range = range;
		this.rangeStatement = rangeStatement;
		/** @type {string} */
		this.prefix = prefix;
		this.declarationId = declarationId;
		this.constValue = constValue;
		/** @type {boolean} */
		this.isAnonymousDefault = false;
	}

	get type() {
		return "harmony export expression";
	}

	/**
	 * Returns how this dependency may be deferred when its parent module is side-effect-free (lazy barrel optimization).
	 * @returns {LazyUntil | null} lazy classification, null when it must be processed eagerly
	 */
	getLazyUntil() {
		return Dependency.LAZY_UNTIL_LOCAL;
	}

	/**
	 * Returns the export name for a `LAZY_UNTIL_LOCAL`/`LAZY_UNTIL_ID` classification (lazy barrel optimization).
	 * @returns {string | null} export name, null when not applicable
	 */
	getLazyName() {
		return DEFAULT_EXPORT_NAME;
	}

	/**
	 * Returns the exported names
	 * @param {ModuleGraph} moduleGraph module graph
	 * @returns {ExportsSpec | undefined} export names
	 */
	getExports(moduleGraph) {
		const module = moduleGraph.getParentModule(this);
		const pureFunctions =
			module &&
			module.buildInfo &&
			/** @type {JavascriptModuleBuildInfo} */ (module.buildInfo).pureFunctions;
		const isPure = Boolean(
			pureFunctions && pureFunctions.has(DEFAULT_EXPORT_NAME)
		);

		return {
			exports: [
				{
					name: DEFAULT_EXPORT_NAME,
					isPure: isPure || undefined,
					inlined: this.constValue
				}
			],
			priority: 1,
			terminalBinding: true,
			dependencies: undefined
		};
	}

	/**
	 * Gets module evaluation side effects state.
	 * @param {ModuleGraph} moduleGraph the module graph
	 * @returns {ConnectionState} how this dependency connects the module to referencing modules
	 */
	getModuleEvaluationSideEffectsState(moduleGraph) {
		// The expression/declaration is already covered by SideEffectsFlagPlugin
		return false;
	}

	/**
	 * Serializes this instance into the provided serializer context.
	 * @param {ObjectSerializerContext} context context
	 */
	serialize(context) {
		context
			.write(this.range)
			.write(this.rangeStatement)
			.write(this.prefix)
			.write(this.declarationId)
			.write(this.isAnonymousDefault)
			.write(this.constValue);
		super.serialize(context);
	}

	/**
	 * Restores this instance from the provided deserializer context.
	 * @param {ObjectDeserializerContext} context context
	 */
	deserialize(context) {
		this.range = context.read();
		const c1 = context.rest;
		this.rangeStatement = c1.read();
		const c2 = c1.rest;
		this.prefix = c2.read();
		const c3 = c2.rest;
		this.declarationId = c3.read();
		const c4 = c3.rest;
		this.isAnonymousDefault = c4.read();
		const c5 = c4.rest;
		this.constValue = c5.read();
		super.deserialize(c5.rest);
	}
}

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

HarmonyExportExpressionDependency.Template = class HarmonyExportDependencyTemplate extends (
	NullDependency.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,
			moduleGraph,
			runtimeTemplate,
			runtimeRequirements,
			initFragments,
			runtime,
			concatenationScope
		}
	) {
		const dep = /** @type {HarmonyExportExpressionDependency} */ (dependency);
		const { declarationId } = dep;
		const exportsName = module.exportsArgument;
		if (declarationId) {
			/** @type {string} */
			let name;
			if (typeof declarationId === "string") {
				name = declarationId;
			} else {
				name = ConcatenationScope.DEFAULT_EXPORT;
				source.replace(
					declarationId.range[0],
					declarationId.range[1] - 1,
					`${declarationId.prefix}${name}${declarationId.suffix}`
				);
			}

			const used = concatenationScope
				? undefined
				: moduleGraph
						.getExportsInfo(module)
						.getUsedName(DEFAULT_EXPORT_NAME, runtime);

			if (concatenationScope) {
				concatenationScope.registerExport(DEFAULT_EXPORT_NAME, name);
			} else if (used) {
				/** @type {ExportMap} */
				const map = new Map();
				map.set(used, `/* export default binding */ ${name}`);
				initFragments.push(new HarmonyExportInitFragment(exportsName, map));
			}

			source.replace(
				dep.rangeStatement[0],
				dep.range[0] - 1,
				`/* harmony default export */ ${dep.prefix}`
			);

			if (
				typeof declarationId !== "string" &&
				dep.isAnonymousDefault &&
				(concatenationScope || used)
			) {
				// Fix .name for anonymous default export function declarations
				// see test/test262-cases/test/language/module-code/instn-named-bndng-dflt-fun-anon.js cspell:disable-line
				runtimeRequirements.add(RuntimeGlobals.setAnonymousDefaultName);
				initFragments.push(
					new InitFragment(
						`${RuntimeGlobals.setAnonymousDefaultName}(${name});\n`,
						InitFragment.STAGE_HARMONY_EXPORTS,
						2
					)
				);
			}
		} else {
			/** @type {string} */
			let content;
			let name = ConcatenationScope.DEFAULT_EXPORT;
			let defaultIsUsed = Boolean(concatenationScope);
			if (runtimeTemplate.supportsConst()) {
				content = `/* harmony default export */ const ${name} = `;
				if (concatenationScope) {
					concatenationScope.registerExport(DEFAULT_EXPORT_NAME, name);
				} else {
					const used = moduleGraph
						.getExportsInfo(module)
						.getUsedName(DEFAULT_EXPORT_NAME, runtime);
					if (used instanceof InlinedUsedName) {
						content = `/* inlined harmony default export */ const ${name} = `;
					} else if (used) {
						defaultIsUsed = true;
						runtimeRequirements.add(RuntimeGlobals.exports);
						// `export default <expr>` binds an immutable const declaration
						const canUseValueBinding =
							/** @type {import("../Module").BuildInfo} */ (module.buildInfo)
								.isCircular === false;
						if (canUseValueBinding) {
							initFragments.push(
								new ExportBindingInitFragment(
									exportsName,
									[
										{
											name: used,
											value: `/* export default binding */ ${name}`,
											bindingType: "value"
										}
									],
									true
								)
							);
						} else {
							/** @type {ExportMap} */
							const map = new Map();
							map.set(used, name);
							initFragments.push(
								new HarmonyExportInitFragment(exportsName, map)
							);
						}
					} else {
						content = `/* unused harmony default export */ var ${name} = `;
					}
				}
			} else if (concatenationScope) {
				content = `/* harmony default export */ var ${name} = `;
				concatenationScope.registerExport(DEFAULT_EXPORT_NAME, name);
			} else {
				const used = moduleGraph
					.getExportsInfo(module)
					.getUsedName(DEFAULT_EXPORT_NAME, runtime);
				if (used instanceof InlinedUsedName) {
					content = `/* inlined harmony default export */ var ${name} = `;
				} else if (used) {
					defaultIsUsed = true;
					runtimeRequirements.add(RuntimeGlobals.exports);
					// This is a little bit incorrect as TDZ is not correct, but we can't use const.
					// No local `__WEBPACK_DEFAULT_EXPORT__` binding is created in this path,
					// so the anonymous-default `.name` fix-up below must reference the actual
					// assignment target instead. See issue #20793.
					name = `${exportsName}${propertyAccess(
						typeof used === "string" ? [used] : used
					)}`;
					content = `/* harmony default export */ ${name} = `;
				} else {
					content = `/* unused harmony default export */ var ${name} = `;
				}
			}

			if (dep.range) {
				source.replace(
					dep.rangeStatement[0],
					dep.range[0] - 1,
					`${content}(${dep.prefix}`
				);
				if (dep.isAnonymousDefault && defaultIsUsed) {
					// Fix .name for anonymous default export expressions
					// see test/test262-cases/test/language/module-code/eval-export-dflt-cls-anon.js cspell:disable-line
					runtimeRequirements.add(RuntimeGlobals.setAnonymousDefaultName);
					source.replace(
						dep.range[1],
						dep.rangeStatement[1] - 0.5,
						`);\n${RuntimeGlobals.setAnonymousDefaultName}(${name});`
					);
				} else {
					source.replace(dep.range[1], dep.rangeStatement[1] - 0.5, ");");
				}
				return;
			}

			source.replace(dep.rangeStatement[0], dep.rangeStatement[1] - 1, content);
		}
	}
};

module.exports = HarmonyExportExpressionDependency;
