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

"use strict";

const { UsageState } = require("../ExportsInfo");
const {
	NUMBER_OF_IDENTIFIER_CONTINUATION_CHARS,
	NUMBER_OF_IDENTIFIER_START_CHARS,
	numberToIdentifier
} = require("../Template");
const { assignDeterministicIds } = require("../ids/IdHelpers");
const { compareSelect, compareStringsNumeric } = require("../util/comparators");

/** @typedef {import("../Compiler")} Compiler */
/** @typedef {import("../ExportsInfo")} ExportsInfo */
/** @typedef {import("../ExportsInfo").ExportInfo} ExportInfo */
/** @typedef {import("../util/concatenate").UsedNames} UsedNames */

/**
 * Defines the comparator type used by this module.
 * @template T
 * @typedef {import("../util/comparators").Comparator<T>} Comparator
 */

/**
 * Checks whether it can mangle.
 * @param {ExportsInfo} exportsInfo exports info
 * @returns {boolean} mangle is possible
 */
const canMangle = (exportsInfo) => {
	if (exportsInfo.otherExportsInfo.getUsed(undefined) !== UsageState.Unused) {
		return false;
	}
	let hasSomethingToMangle = false;
	for (const exportInfo of exportsInfo.exports) {
		if (exportInfo.canMangle === true) {
			hasSomethingToMangle = true;
		}
	}
	return hasSomethingToMangle;
};

// Sort by name
/** @type {Comparator<ExportInfo>} */
const comparator = compareSelect((e) => e.name, compareStringsNumeric);
/**
 * Mangle exports info.
 * @param {boolean} deterministic use deterministic names
 * @param {ExportsInfo} exportsInfo exports info
 * @param {boolean | undefined} isNamespace is namespace object
 * @returns {void}
 */
const mangleExportsInfo = (deterministic, exportsInfo, isNamespace) => {
	if (!canMangle(exportsInfo)) return;
	/** @type {UsedNames} */
	const usedNames = new Set();
	/** @type {ExportInfo[]} */
	const mangleableExports = [];

	// Avoid to renamed exports that are not provided when
	// 1. it's not a namespace export: non-provided exports can be found in prototype chain
	// 2. there are other provided exports and deterministic mode is chosen:
	//    non-provided exports would break the determinism
	let avoidMangleNonProvided = !isNamespace;
	if (!avoidMangleNonProvided && deterministic) {
		for (const exportInfo of exportsInfo.ownedExports) {
			if (exportInfo.provided !== false) {
				avoidMangleNonProvided = true;
				break;
			}
		}
	}
	for (const exportInfo of exportsInfo.ownedExports) {
		const name = exportInfo.name;
		if (!exportInfo.hasUsedName()) {
			if (
				// Can the export be mangled?
				exportInfo.canMangle !== true ||
				// Never rename 1 char exports
				(name.length === 1 && /^[a-z0-9_$]/i.test(name)) ||
				// Don't rename 2 char exports in deterministic mode
				(deterministic &&
					name.length === 2 &&
					/^[a-z_$][a-z0-9_$]|^[1-9][0-9]/i.test(name)) ||
				// Don't rename exports that are not provided
				(avoidMangleNonProvided && exportInfo.provided !== true)
			) {
				exportInfo.setUsedName(name);
				usedNames.add(name);
			} else {
				mangleableExports.push(exportInfo);
			}
		}
		if (exportInfo.exportsInfoOwned) {
			const used = exportInfo.getUsed(undefined);
			if (
				used === UsageState.OnlyPropertiesUsed ||
				used === UsageState.Unused
			) {
				mangleExportsInfo(
					deterministic,
					/** @type {ExportsInfo} */ (exportInfo.exportsInfo),
					false
				);
			}
		}
	}
	if (deterministic) {
		assignDeterministicIds(
			mangleableExports,
			(e) => e.name,
			comparator,
			(e, id) => {
				const name = numberToIdentifier(id);
				const size = usedNames.size;
				usedNames.add(name);
				if (size === usedNames.size) return false;
				e.setUsedName(name);
				return true;
			},
			[
				NUMBER_OF_IDENTIFIER_START_CHARS,
				NUMBER_OF_IDENTIFIER_START_CHARS *
					NUMBER_OF_IDENTIFIER_CONTINUATION_CHARS
			],
			NUMBER_OF_IDENTIFIER_CONTINUATION_CHARS,
			usedNames.size
		);
	} else {
		/** @type {ExportInfo[]} */
		const usedExports = [];
		/** @type {ExportInfo[]} */
		const unusedExports = [];
		for (const exportInfo of mangleableExports) {
			if (exportInfo.getUsed(undefined) === UsageState.Unused) {
				unusedExports.push(exportInfo);
			} else {
				usedExports.push(exportInfo);
			}
		}
		usedExports.sort(comparator);
		unusedExports.sort(comparator);
		let i = 0;
		for (const list of [usedExports, unusedExports]) {
			for (const exportInfo of list) {
				/** @type {string} */
				let name;
				do {
					name = numberToIdentifier(i++);
				} while (usedNames.has(name));
				exportInfo.setUsedName(name);
			}
		}
	}
};

const PLUGIN_NAME = "MangleExportsPlugin";

class MangleExportsPlugin {
	/**
	 * Creates an instance of MangleExportsPlugin.
	 * @param {boolean} deterministic use deterministic names
	 */
	constructor(deterministic) {
		/** @type {boolean} */
		this._deterministic = deterministic;
	}

	/**
	 * Applies the plugin by registering its hooks on the compiler.
	 * @param {Compiler} compiler the compiler instance
	 * @returns {void}
	 */
	apply(compiler) {
		const { _deterministic: deterministic } = this;
		compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
			const moduleGraph = compilation.moduleGraph;
			compilation.hooks.optimizeCodeGeneration.tap(PLUGIN_NAME, (modules) => {
				if (compilation.moduleMemCaches) {
					throw new Error(
						"optimization.mangleExports can't be used with cacheUnaffected as export mangling is a global effect"
					);
				}
				for (const module of modules) {
					const isNamespace =
						module.buildMeta && module.buildMeta.exportsType === "namespace";
					const exportsInfo = moduleGraph.getExportsInfo(module);
					mangleExportsInfo(deterministic, exportsInfo, isNamespace);
				}
			});
		});
	}
}

module.exports = MangleExportsPlugin;
