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

"use strict";

const ModuleNotFoundError = require("../ModuleNotFoundError");
const RuntimeGlobals = require("../RuntimeGlobals");
const WebpackError = require("../WebpackError");
const { parseOptions } = require("../container/options");
const LazySet = require("../util/LazySet");
const createSchemaValidation = require("../util/create-schema-validation");
const { parseRange } = require("../util/semver");
const ConsumeSharedFallbackDependency = require("./ConsumeSharedFallbackDependency");
const ConsumeSharedModule = require("./ConsumeSharedModule");
const ConsumeSharedRuntimeModule = require("./ConsumeSharedRuntimeModule");
const ProvideForSharedDependency = require("./ProvideForSharedDependency");
const { resolveMatchedConfigs } = require("./resolveMatchedConfigs");
const {
	isRequiredVersion,
	getDescriptionFile,
	getRequiredVersionFromDescriptionFile
} = require("./utils");

/** @typedef {import("../../declarations/plugins/sharing/ConsumeSharedPlugin").ConsumeSharedPluginOptions} ConsumeSharedPluginOptions */
/** @typedef {import("../../declarations/plugins/sharing/ConsumeSharedPlugin").ConsumesConfig} ConsumesConfig */
/** @typedef {import("../Compiler")} Compiler */
/** @typedef {import("../ResolverFactory").ResolveOptionsWithDependencyType} ResolveOptionsWithDependencyType */
/** @typedef {import("./ConsumeSharedModule").ConsumeOptions} ConsumeOptions */

const validate = createSchemaValidation(
	require("../../schemas/plugins/sharing/ConsumeSharedPlugin.check.js"),
	() => require("../../schemas/plugins/sharing/ConsumeSharedPlugin.json"),
	{
		name: "Consume Shared Plugin",
		baseDataPath: "options"
	}
);

/** @type {ResolveOptionsWithDependencyType} */
const RESOLVE_OPTIONS = { dependencyType: "esm" };
const PLUGIN_NAME = "ConsumeSharedPlugin";

class ConsumeSharedPlugin {
	/**
	 * @param {ConsumeSharedPluginOptions} options options
	 */
	constructor(options) {
		if (typeof options !== "string") {
			validate(options);
		}

		/** @type {[string, ConsumeOptions][]} */
		this._consumes = parseOptions(
			options.consumes,
			(item, key) => {
				if (Array.isArray(item)) throw new Error("Unexpected array in options");
				/** @type {ConsumeOptions} */
				let result =
					item === key || !isRequiredVersion(item)
						? // item is a request/key
							{
								import: key,
								shareScope: options.shareScope || "default",
								shareKey: key,
								requiredVersion: undefined,
								packageName: undefined,
								strictVersion: false,
								singleton: false,
								eager: false
							}
						: // key is a request/key
							// item is a version
							{
								import: key,
								shareScope: options.shareScope || "default",
								shareKey: key,
								requiredVersion: parseRange(item),
								strictVersion: true,
								packageName: undefined,
								singleton: false,
								eager: false
							};
				return result;
			},
			(item, key) => ({
				import: item.import === false ? undefined : item.import || key,
				shareScope: item.shareScope || options.shareScope || "default",
				shareKey: item.shareKey || key,
				requiredVersion:
					typeof item.requiredVersion === "string"
						? parseRange(item.requiredVersion)
						: item.requiredVersion,
				strictVersion:
					typeof item.strictVersion === "boolean"
						? item.strictVersion
						: item.import !== false && !item.singleton,
				packageName: item.packageName,
				singleton: !!item.singleton,
				eager: !!item.eager
			})
		);
	}

	/**
	 * Apply the plugin
	 * @param {Compiler} compiler the compiler instance
	 * @returns {void}
	 */
	apply(compiler) {
		compiler.hooks.thisCompilation.tap(
			PLUGIN_NAME,
			(compilation, { normalModuleFactory }) => {
				compilation.dependencyFactories.set(
					ConsumeSharedFallbackDependency,
					normalModuleFactory
				);

				let unresolvedConsumes, resolvedConsumes, prefixedConsumes;
				const promise = resolveMatchedConfigs(compilation, this._consumes).then(
					({ resolved, unresolved, prefixed }) => {
						resolvedConsumes = resolved;
						unresolvedConsumes = unresolved;
						prefixedConsumes = prefixed;
					}
				);

				const resolver = compilation.resolverFactory.get(
					"normal",
					RESOLVE_OPTIONS
				);

				/**
				 * @param {string} context issuer directory
				 * @param {string} request request
				 * @param {ConsumeOptions} config options
				 * @returns {Promise<ConsumeSharedModule>} create module
				 */
				const createConsumeSharedModule = (context, request, config) => {
					const requiredVersionWarning = details => {
						const error = new WebpackError(
							`No required version specified and unable to automatically determine one. ${details}`
						);
						error.file = `shared module ${request}`;
						compilation.warnings.push(error);
					};
					const directFallback =
						config.import &&
						/^(\.\.?(\/|$)|\/|[A-Za-z]:|\\\\)/.test(config.import);
					return Promise.all([
						new Promise(resolve => {
							if (!config.import) return resolve();
							const resolveContext = {
								/** @type {LazySet<string>} */
								fileDependencies: new LazySet(),
								/** @type {LazySet<string>} */
								contextDependencies: new LazySet(),
								/** @type {LazySet<string>} */
								missingDependencies: new LazySet()
							};
							resolver.resolve(
								{},
								directFallback ? compiler.context : context,
								config.import,
								resolveContext,
								(err, result) => {
									compilation.contextDependencies.addAll(
										resolveContext.contextDependencies
									);
									compilation.fileDependencies.addAll(
										resolveContext.fileDependencies
									);
									compilation.missingDependencies.addAll(
										resolveContext.missingDependencies
									);
									if (err) {
										compilation.errors.push(
											new ModuleNotFoundError(null, err, {
												name: `resolving fallback for shared module ${request}`
											})
										);
										return resolve();
									}
									resolve(result);
								}
							);
						}),
						new Promise(resolve => {
							if (config.requiredVersion !== undefined)
								return resolve(config.requiredVersion);
							let packageName = config.packageName;
							if (packageName === undefined) {
								if (/^(\/|[A-Za-z]:|\\\\)/.test(request)) {
									// For relative or absolute requests we don't automatically use a packageName.
									// If wished one can specify one with the packageName option.
									return resolve();
								}
								const match = /^((?:@[^\\/]+[\\/])?[^\\/]+)/.exec(request);
								if (!match) {
									requiredVersionWarning(
										"Unable to extract the package name from request."
									);
									return resolve();
								}
								packageName = match[0];
							}

							getDescriptionFile(
								compilation.inputFileSystem,
								context,
								["package.json"],
								(err, result) => {
									if (err) {
										requiredVersionWarning(
											`Unable to read description file: ${err}`
										);
										return resolve();
									}
									const { data, path: descriptionPath } = result;
									if (!data) {
										requiredVersionWarning(
											`Unable to find description file in ${context}.`
										);
										return resolve();
									}
									if (data.name === packageName) {
										// Package self-referencing
										return resolve();
									}
									const requiredVersion = getRequiredVersionFromDescriptionFile(
										data,
										packageName
									);
									if (typeof requiredVersion !== "string") {
										requiredVersionWarning(
											`Unable to find required version for "${packageName}" in description file (${descriptionPath}). It need to be in dependencies, devDependencies or peerDependencies.`
										);
										return resolve();
									}
									resolve(parseRange(requiredVersion));
								}
							);
						})
					]).then(([importResolved, requiredVersion]) => {
						return new ConsumeSharedModule(
							directFallback ? compiler.context : context,
							{
								...config,
								importResolved,
								import: importResolved ? config.import : undefined,
								requiredVersion
							}
						);
					});
				};

				normalModuleFactory.hooks.factorize.tapPromise(
					PLUGIN_NAME,
					({ context, request, dependencies }) =>
						// wait for resolving to be complete
						promise.then(() => {
							if (
								dependencies[0] instanceof ConsumeSharedFallbackDependency ||
								dependencies[0] instanceof ProvideForSharedDependency
							) {
								return;
							}
							const match = unresolvedConsumes.get(request);
							if (match !== undefined) {
								return createConsumeSharedModule(context, request, match);
							}
							for (const [prefix, options] of prefixedConsumes) {
								if (request.startsWith(prefix)) {
									const remainder = request.slice(prefix.length);
									return createConsumeSharedModule(context, request, {
										...options,
										import: options.import
											? options.import + remainder
											: undefined,
										shareKey: options.shareKey + remainder
									});
								}
							}
						})
				);
				normalModuleFactory.hooks.createModule.tapPromise(
					PLUGIN_NAME,
					({ resource }, { context, dependencies }) => {
						if (
							dependencies[0] instanceof ConsumeSharedFallbackDependency ||
							dependencies[0] instanceof ProvideForSharedDependency
						) {
							return Promise.resolve();
						}
						const options = resolvedConsumes.get(resource);
						if (options !== undefined) {
							return createConsumeSharedModule(context, resource, options);
						}
						return Promise.resolve();
					}
				);
				compilation.hooks.additionalTreeRuntimeRequirements.tap(
					PLUGIN_NAME,
					(chunk, set) => {
						set.add(RuntimeGlobals.module);
						set.add(RuntimeGlobals.moduleCache);
						set.add(RuntimeGlobals.moduleFactoriesAddOnly);
						set.add(RuntimeGlobals.shareScopeMap);
						set.add(RuntimeGlobals.initializeSharing);
						set.add(RuntimeGlobals.hasOwnProperty);
						compilation.addRuntimeModule(
							chunk,
							new ConsumeSharedRuntimeModule(set)
						);
					}
				);
			}
		);
	}
}

module.exports = ConsumeSharedPlugin;
