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

const { ConcatSource, OriginalSource } = require("webpack-sources");
const Template = require("./Template");

/** @typedef {import("../declarations/WebpackOptions").LibraryCustomUmdObject} LibraryCustomUmdObject */
/** @typedef {import("./Compilation")} Compilation */

/**
 * @param {string[]} accessor the accessor to convert to path
 * @returns {string} the path
 */
const accessorToObjectAccess = accessor => {
	return accessor.map(a => `[${JSON.stringify(a)}]`).join("");
};

/**
 * @param {string=} base the path prefix
 * @param {string|string[]} accessor the accessor
 * @param {string=} joinWith the element separator
 * @returns {string} the path
 */
const accessorAccess = (base, accessor, joinWith = ", ") => {
	const accessors = Array.isArray(accessor) ? accessor : [accessor];
	return accessors
		.map((_, idx) => {
			const a = base
				? base + accessorToObjectAccess(accessors.slice(0, idx + 1))
				: accessors[0] + accessorToObjectAccess(accessors.slice(1, idx + 1));
			if (idx === accessors.length - 1) return a;
			if (idx === 0 && base === undefined)
				return `${a} = typeof ${a} === "object" ? ${a} : {}`;
			return `${a} = ${a} || {}`;
		})
		.join(joinWith);
};

/** @typedef {string | string[] | LibraryCustomUmdObject} UmdMainTemplatePluginName */

/**
 * @typedef {Object} AuxiliaryCommentObject
 * @property {string} root
 * @property {string} commonjs
 * @property {string} commonjs2
 * @property {string} amd
 */

/**
 * @typedef {Object} UmdMainTemplatePluginOption
 * @property {boolean=} optionalAmdExternalAsGlobal
 * @property {boolean} namedDefine
 * @property {string | AuxiliaryCommentObject} auxiliaryComment
 */

class UmdMainTemplatePlugin {
	/**
	 * @param {UmdMainTemplatePluginName} name the name of the UMD library
	 * @param {UmdMainTemplatePluginOption} options the plugin option
	 */
	constructor(name, options) {
		if (typeof name === "object" && !Array.isArray(name)) {
			this.name = name.root || name.amd || name.commonjs;
			this.names = name;
		} else {
			this.name = name;
			this.names = {
				commonjs: name,
				root: name,
				amd: name
			};
		}
		this.optionalAmdExternalAsGlobal = options.optionalAmdExternalAsGlobal;
		this.namedDefine = options.namedDefine;
		this.auxiliaryComment = options.auxiliaryComment;
	}

	/**
	 * @param {Compilation} compilation the compilation instance
	 * @returns {void}
	 */
	apply(compilation) {
		const { mainTemplate, chunkTemplate, runtimeTemplate } = compilation;

		const onRenderWithEntry = (source, chunk, hash) => {
			let externals = chunk
				.getModules()
				.filter(
					m =>
						m.external &&
						(m.externalType === "umd" || m.externalType === "umd2")
				);
			const optionalExternals = [];
			let requiredExternals = [];
			if (this.optionalAmdExternalAsGlobal) {
				for (const m of externals) {
					if (m.optional) {
						optionalExternals.push(m);
					} else {
						requiredExternals.push(m);
					}
				}
				externals = requiredExternals.concat(optionalExternals);
			} else {
				requiredExternals = externals;
			}

			const replaceKeys = str => {
				return mainTemplate.getAssetPath(str, {
					hash,
					chunk
				});
			};

			const externalsDepsArray = modules => {
				return `[${replaceKeys(
					modules
						.map(m =>
							JSON.stringify(
								typeof m.request === "object" ? m.request.amd : m.request
							)
						)
						.join(", ")
				)}]`;
			};

			const externalsRootArray = modules => {
				return replaceKeys(
					modules
						.map(m => {
							let request = m.request;
							if (typeof request === "object") request = request.root;
							return `root${accessorToObjectAccess([].concat(request))}`;
						})
						.join(", ")
				);
			};

			const externalsRequireArray = type => {
				return replaceKeys(
					externals
						.map(m => {
							let expr;
							let request = m.request;
							if (typeof request === "object") {
								request = request[type];
							}
							if (request === undefined) {
								throw new Error(
									"Missing external configuration for type:" + type
								);
							}
							if (Array.isArray(request)) {
								expr = `require(${JSON.stringify(
									request[0]
								)})${accessorToObjectAccess(request.slice(1))}`;
							} else {
								expr = `require(${JSON.stringify(request)})`;
							}
							if (m.optional) {
								expr = `(function webpackLoadOptionalExternalModule() { try { return ${expr}; } catch(e) {} }())`;
							}
							return expr;
						})
						.join(", ")
				);
			};

			const externalsArguments = modules => {
				return modules
					.map(
						m =>
							`__WEBPACK_EXTERNAL_MODULE_${Template.toIdentifier(`${m.id}`)}__`
					)
					.join(", ");
			};

			const libraryName = library => {
				return JSON.stringify(replaceKeys([].concat(library).pop()));
			};

			let amdFactory;
			if (optionalExternals.length > 0) {
				const wrapperArguments = externalsArguments(requiredExternals);
				const factoryArguments =
					requiredExternals.length > 0
						? externalsArguments(requiredExternals) +
						  ", " +
						  externalsRootArray(optionalExternals)
						: externalsRootArray(optionalExternals);
				amdFactory =
					`function webpackLoadOptionalExternalModuleAmd(${wrapperArguments}) {\n` +
					`			return factory(${factoryArguments});\n` +
					"		}";
			} else {
				amdFactory = "factory";
			}

			const auxiliaryComment = this.auxiliaryComment;

			const getAuxilaryComment = type => {
				if (auxiliaryComment) {
					if (typeof auxiliaryComment === "string")
						return "\t//" + auxiliaryComment + "\n";
					if (auxiliaryComment[type])
						return "\t//" + auxiliaryComment[type] + "\n";
				}
				return "";
			};

			return new ConcatSource(
				new OriginalSource(
					"(function webpackUniversalModuleDefinition(root, factory) {\n" +
						getAuxilaryComment("commonjs2") +
						"	if(typeof exports === 'object' && typeof module === 'object')\n" +
						"		module.exports = factory(" +
						externalsRequireArray("commonjs2") +
						");\n" +
						getAuxilaryComment("amd") +
						"	else if(typeof define === 'function' && define.amd)\n" +
						(requiredExternals.length > 0
							? this.names.amd && this.namedDefine === true
								? "		define(" +
								  libraryName(this.names.amd) +
								  ", " +
								  externalsDepsArray(requiredExternals) +
								  ", " +
								  amdFactory +
								  ");\n"
								: "		define(" +
								  externalsDepsArray(requiredExternals) +
								  ", " +
								  amdFactory +
								  ");\n"
							: this.names.amd && this.namedDefine === true
							? "		define(" +
							  libraryName(this.names.amd) +
							  ", [], " +
							  amdFactory +
							  ");\n"
							: "		define([], " + amdFactory + ");\n") +
						(this.names.root || this.names.commonjs
							? getAuxilaryComment("commonjs") +
							  "	else if(typeof exports === 'object')\n" +
							  "		exports[" +
							  libraryName(this.names.commonjs || this.names.root) +
							  "] = factory(" +
							  externalsRequireArray("commonjs") +
							  ");\n" +
							  getAuxilaryComment("root") +
							  "	else\n" +
							  "		" +
							  replaceKeys(
									accessorAccess("root", this.names.root || this.names.commonjs)
							  ) +
							  " = factory(" +
							  externalsRootArray(externals) +
							  ");\n"
							: "	else {\n" +
							  (externals.length > 0
									? "		var a = typeof exports === 'object' ? factory(" +
									  externalsRequireArray("commonjs") +
									  ") : factory(" +
									  externalsRootArray(externals) +
									  ");\n"
									: "		var a = factory();\n") +
							  "		for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];\n" +
							  "	}\n") +
						`})(${
							runtimeTemplate.outputOptions.globalObject
						}, function(${externalsArguments(externals)}) {\nreturn `,
					"webpack/universalModuleDefinition"
				),
				source,
				";\n})"
			);
		};

		for (const template of [mainTemplate, chunkTemplate]) {
			template.hooks.renderWithEntry.tap(
				"UmdMainTemplatePlugin",
				onRenderWithEntry
			);
		}

		mainTemplate.hooks.globalHashPaths.tap("UmdMainTemplatePlugin", paths => {
			if (this.names.root) paths = paths.concat(this.names.root);
			if (this.names.amd) paths = paths.concat(this.names.amd);
			if (this.names.commonjs) paths = paths.concat(this.names.commonjs);
			return paths;
		});

		mainTemplate.hooks.hash.tap("UmdMainTemplatePlugin", hash => {
			hash.update("umd");
			hash.update(`${this.names.root}`);
			hash.update(`${this.names.amd}`);
			hash.update(`${this.names.commonjs}`);
		});
	}
}

module.exports = UmdMainTemplatePlugin;
