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

"use strict";

const util = require("util");

/** @typedef {import("../../declarations/WebpackOptions").EntryStatic} EntryStatic */
/** @typedef {import("../../declarations/WebpackOptions").EntryStaticNormalized} EntryStaticNormalized */
/** @typedef {import("../../declarations/WebpackOptions").LibraryName} LibraryName */
/** @typedef {import("../../declarations/WebpackOptions").LibraryOptions} LibraryOptions */
/** @typedef {import("../../declarations/WebpackOptions").OptimizationRuntimeChunk} OptimizationRuntimeChunk */
/** @typedef {import("../../declarations/WebpackOptions").OptimizationRuntimeChunkNormalized} OptimizationRuntimeChunkNormalized */
/** @typedef {import("../../declarations/WebpackOptions").OutputNormalized} OutputNormalized */
/** @typedef {import("../../declarations/WebpackOptions").WebpackOptions} WebpackOptions */
/** @typedef {import("../../declarations/WebpackOptions").WebpackOptionsNormalized} WebpackOptionsNormalized */

const handledDeprecatedNoEmitOnErrors = util.deprecate(
	(noEmitOnErrors, emitOnErrors) => {
		if (emitOnErrors !== undefined && !noEmitOnErrors === !emitOnErrors) {
			throw new Error(
				"Conflicting use of 'optimization.noEmitOnErrors' and 'optimization.emitOnErrors'. Remove deprecated 'optimization.noEmitOnErrors' from config."
			);
		}
		return !noEmitOnErrors;
	},
	"optimization.noEmitOnErrors is deprecated in favor of optimization.emitOnErrors",
	"DEP_WEBPACK_CONFIGURATION_OPTIMIZATION_NO_EMIT_ON_ERRORS"
);

/**
 * @template T
 * @template R
 * @param {T|undefined} value value or not
 * @param {function(T): R} fn nested handler
 * @returns {R} result value
 */
const nestedConfig = (value, fn) =>
	value === undefined ? fn(/** @type {T} */ ({})) : fn(value);

/**
 * @template T
 * @param {T|undefined} value value or not
 * @returns {T} result value
 */
const cloneObject = value => {
	return /** @type {T} */ ({ ...value });
};

/**
 * @template T
 * @template R
 * @param {T|undefined} value value or not
 * @param {function(T): R} fn nested handler
 * @returns {R|undefined} result value
 */
const optionalNestedConfig = (value, fn) =>
	value === undefined ? undefined : fn(value);

/**
 * @template T
 * @template R
 * @param {T[]|undefined} value array or not
 * @param {function(T[]): R[]} fn nested handler
 * @returns {R[]|undefined} cloned value
 */
const nestedArray = (value, fn) => (Array.isArray(value) ? fn(value) : fn([]));

/**
 * @template T
 * @template R
 * @param {T[]|undefined} value array or not
 * @param {function(T[]): R[]} fn nested handler
 * @returns {R[]|undefined} cloned value
 */
const optionalNestedArray = (value, fn) =>
	Array.isArray(value) ? fn(value) : undefined;

/**
 * @template T
 * @template R
 * @param {Record<string, T>|undefined} value value or not
 * @param {function(T): R} fn nested handler
 * @param {Record<string, function(T): R>=} customKeys custom nested handler for some keys
 * @returns {Record<string, R>} result value
 */
const keyedNestedConfig = (value, fn, customKeys) => {
	const result =
		value === undefined
			? {}
			: Object.keys(value).reduce(
					(obj, key) => (
						(obj[key] = (
							customKeys && key in customKeys ? customKeys[key] : fn
						)(value[key])),
						obj
					),
					/** @type {Record<string, R>} */ ({})
			  );
	if (customKeys) {
		for (const key of Object.keys(customKeys)) {
			if (!(key in result)) {
				result[key] = customKeys[key](/** @type {T} */ ({}));
			}
		}
	}
	return result;
};

/**
 * @param {WebpackOptions} config input config
 * @returns {WebpackOptionsNormalized} normalized options
 */
const getNormalizedWebpackOptions = config => {
	return {
		amd: config.amd,
		bail: config.bail,
		cache: optionalNestedConfig(config.cache, cache => {
			if (cache === false) return false;
			if (cache === true) {
				return {
					type: "memory",
					maxGenerations: undefined
				};
			}
			switch (cache.type) {
				case "filesystem":
					return {
						type: "filesystem",
						allowCollectingMemory: cache.allowCollectingMemory,
						maxMemoryGenerations: cache.maxMemoryGenerations,
						maxAge: cache.maxAge,
						profile: cache.profile,
						buildDependencies: cloneObject(cache.buildDependencies),
						cacheDirectory: cache.cacheDirectory,
						cacheLocation: cache.cacheLocation,
						hashAlgorithm: cache.hashAlgorithm,
						compression: cache.compression,
						idleTimeout: cache.idleTimeout,
						idleTimeoutForInitialStore: cache.idleTimeoutForInitialStore,
						idleTimeoutAfterLargeChanges: cache.idleTimeoutAfterLargeChanges,
						name: cache.name,
						store: cache.store,
						version: cache.version
					};
				case undefined:
				case "memory":
					return {
						type: "memory",
						maxGenerations: cache.maxGenerations
					};
				default:
					// @ts-expect-error Property 'type' does not exist on type 'never'. ts(2339)
					throw new Error(`Not implemented cache.type ${cache.type}`);
			}
		}),
		context: config.context,
		dependencies: config.dependencies,
		devServer: optionalNestedConfig(config.devServer, devServer => ({
			...devServer
		})),
		devtool: config.devtool,
		entry:
			config.entry === undefined
				? { main: {} }
				: typeof config.entry === "function"
				? (
						fn => () =>
							Promise.resolve().then(fn).then(getNormalizedEntryStatic)
				  )(config.entry)
				: getNormalizedEntryStatic(config.entry),
		experiments: nestedConfig(config.experiments, experiments => ({
			...experiments,
			buildHttp: optionalNestedConfig(experiments.buildHttp, options =>
				Array.isArray(options) ? { allowedUris: options } : options
			),
			lazyCompilation: optionalNestedConfig(
				experiments.lazyCompilation,
				options => (options === true ? {} : options)
			),
			css: optionalNestedConfig(experiments.css, options =>
				options === true ? {} : options
			)
		})),
		externals: config.externals,
		externalsPresets: cloneObject(config.externalsPresets),
		externalsType: config.externalsType,
		ignoreWarnings: config.ignoreWarnings
			? config.ignoreWarnings.map(ignore => {
					if (typeof ignore === "function") return ignore;
					const i = ignore instanceof RegExp ? { message: ignore } : ignore;
					return (warning, { requestShortener }) => {
						if (!i.message && !i.module && !i.file) return false;
						if (i.message && !i.message.test(warning.message)) {
							return false;
						}
						if (
							i.module &&
							(!warning.module ||
								!i.module.test(
									warning.module.readableIdentifier(requestShortener)
								))
						) {
							return false;
						}
						if (i.file && (!warning.file || !i.file.test(warning.file))) {
							return false;
						}
						return true;
					};
			  })
			: undefined,
		infrastructureLogging: cloneObject(config.infrastructureLogging),
		loader: cloneObject(config.loader),
		mode: config.mode,
		module: nestedConfig(config.module, module => ({
			noParse: module.noParse,
			unsafeCache: module.unsafeCache,
			parser: keyedNestedConfig(module.parser, cloneObject, {
				javascript: parserOptions => ({
					unknownContextRequest: module.unknownContextRequest,
					unknownContextRegExp: module.unknownContextRegExp,
					unknownContextRecursive: module.unknownContextRecursive,
					unknownContextCritical: module.unknownContextCritical,
					exprContextRequest: module.exprContextRequest,
					exprContextRegExp: module.exprContextRegExp,
					exprContextRecursive: module.exprContextRecursive,
					exprContextCritical: module.exprContextCritical,
					wrappedContextRegExp: module.wrappedContextRegExp,
					wrappedContextRecursive: module.wrappedContextRecursive,
					wrappedContextCritical: module.wrappedContextCritical,
					// TODO webpack 6 remove
					strictExportPresence: module.strictExportPresence,
					strictThisContextOnImports: module.strictThisContextOnImports,
					...parserOptions
				})
			}),
			generator: cloneObject(module.generator),
			defaultRules: optionalNestedArray(module.defaultRules, r => [...r]),
			rules: nestedArray(module.rules, r => [...r])
		})),
		name: config.name,
		node: nestedConfig(
			config.node,
			node =>
				node && {
					...node
				}
		),
		optimization: nestedConfig(config.optimization, optimization => {
			return {
				...optimization,
				runtimeChunk: getNormalizedOptimizationRuntimeChunk(
					optimization.runtimeChunk
				),
				splitChunks: nestedConfig(
					optimization.splitChunks,
					splitChunks =>
						splitChunks && {
							...splitChunks,
							defaultSizeTypes: splitChunks.defaultSizeTypes
								? [...splitChunks.defaultSizeTypes]
								: ["..."],
							cacheGroups: cloneObject(splitChunks.cacheGroups)
						}
				),
				emitOnErrors:
					optimization.noEmitOnErrors !== undefined
						? handledDeprecatedNoEmitOnErrors(
								optimization.noEmitOnErrors,
								optimization.emitOnErrors
						  )
						: optimization.emitOnErrors
			};
		}),
		output: nestedConfig(config.output, output => {
			const { library } = output;
			const libraryAsName = /** @type {LibraryName} */ (library);
			const libraryBase =
				typeof library === "object" &&
				library &&
				!Array.isArray(library) &&
				"type" in library
					? library
					: libraryAsName || output.libraryTarget
					? /** @type {LibraryOptions} */ ({
							name: libraryAsName
					  })
					: undefined;
			/** @type {OutputNormalized} */
			const result = {
				assetModuleFilename: output.assetModuleFilename,
				asyncChunks: output.asyncChunks,
				charset: output.charset,
				chunkFilename: output.chunkFilename,
				chunkFormat: output.chunkFormat,
				chunkLoading: output.chunkLoading,
				chunkLoadingGlobal: output.chunkLoadingGlobal,
				chunkLoadTimeout: output.chunkLoadTimeout,
				cssFilename: output.cssFilename,
				cssChunkFilename: output.cssChunkFilename,
				clean: output.clean,
				compareBeforeEmit: output.compareBeforeEmit,
				crossOriginLoading: output.crossOriginLoading,
				devtoolFallbackModuleFilenameTemplate:
					output.devtoolFallbackModuleFilenameTemplate,
				devtoolModuleFilenameTemplate: output.devtoolModuleFilenameTemplate,
				devtoolNamespace: output.devtoolNamespace,
				environment: cloneObject(output.environment),
				enabledChunkLoadingTypes: output.enabledChunkLoadingTypes
					? [...output.enabledChunkLoadingTypes]
					: ["..."],
				enabledLibraryTypes: output.enabledLibraryTypes
					? [...output.enabledLibraryTypes]
					: ["..."],
				enabledWasmLoadingTypes: output.enabledWasmLoadingTypes
					? [...output.enabledWasmLoadingTypes]
					: ["..."],
				filename: output.filename,
				globalObject: output.globalObject,
				hashDigest: output.hashDigest,
				hashDigestLength: output.hashDigestLength,
				hashFunction: output.hashFunction,
				hashSalt: output.hashSalt,
				hotUpdateChunkFilename: output.hotUpdateChunkFilename,
				hotUpdateGlobal: output.hotUpdateGlobal,
				hotUpdateMainFilename: output.hotUpdateMainFilename,
				iife: output.iife,
				importFunctionName: output.importFunctionName,
				importMetaName: output.importMetaName,
				scriptType: output.scriptType,
				library: libraryBase && {
					type:
						output.libraryTarget !== undefined
							? output.libraryTarget
							: libraryBase.type,
					auxiliaryComment:
						output.auxiliaryComment !== undefined
							? output.auxiliaryComment
							: libraryBase.auxiliaryComment,
					amdContainer:
						output.amdContainer !== undefined
							? output.amdContainer
							: libraryBase.amdContainer,
					export:
						output.libraryExport !== undefined
							? output.libraryExport
							: libraryBase.export,
					name: libraryBase.name,
					umdNamedDefine:
						output.umdNamedDefine !== undefined
							? output.umdNamedDefine
							: libraryBase.umdNamedDefine
				},
				module: output.module,
				path: output.path,
				pathinfo: output.pathinfo,
				publicPath: output.publicPath,
				sourceMapFilename: output.sourceMapFilename,
				sourcePrefix: output.sourcePrefix,
				strictModuleExceptionHandling: output.strictModuleExceptionHandling,
				trustedTypes: optionalNestedConfig(
					output.trustedTypes,
					trustedTypes => {
						if (trustedTypes === true) return {};
						if (typeof trustedTypes === "string")
							return { policyName: trustedTypes };
						return { ...trustedTypes };
					}
				),
				uniqueName: output.uniqueName,
				wasmLoading: output.wasmLoading,
				webassemblyModuleFilename: output.webassemblyModuleFilename,
				workerPublicPath: output.workerPublicPath,
				workerChunkLoading: output.workerChunkLoading,
				workerWasmLoading: output.workerWasmLoading
			};
			return result;
		}),
		parallelism: config.parallelism,
		performance: optionalNestedConfig(config.performance, performance => {
			if (performance === false) return false;
			return {
				...performance
			};
		}),
		plugins: nestedArray(config.plugins, p => [...p]),
		profile: config.profile,
		recordsInputPath:
			config.recordsInputPath !== undefined
				? config.recordsInputPath
				: config.recordsPath,
		recordsOutputPath:
			config.recordsOutputPath !== undefined
				? config.recordsOutputPath
				: config.recordsPath,
		resolve: nestedConfig(config.resolve, resolve => ({
			...resolve,
			byDependency: keyedNestedConfig(resolve.byDependency, cloneObject)
		})),
		resolveLoader: cloneObject(config.resolveLoader),
		snapshot: nestedConfig(config.snapshot, snapshot => ({
			resolveBuildDependencies: optionalNestedConfig(
				snapshot.resolveBuildDependencies,
				resolveBuildDependencies => ({
					timestamp: resolveBuildDependencies.timestamp,
					hash: resolveBuildDependencies.hash
				})
			),
			buildDependencies: optionalNestedConfig(
				snapshot.buildDependencies,
				buildDependencies => ({
					timestamp: buildDependencies.timestamp,
					hash: buildDependencies.hash
				})
			),
			resolve: optionalNestedConfig(snapshot.resolve, resolve => ({
				timestamp: resolve.timestamp,
				hash: resolve.hash
			})),
			module: optionalNestedConfig(snapshot.module, module => ({
				timestamp: module.timestamp,
				hash: module.hash
			})),
			immutablePaths: optionalNestedArray(snapshot.immutablePaths, p => [...p]),
			managedPaths: optionalNestedArray(snapshot.managedPaths, p => [...p])
		})),
		stats: nestedConfig(config.stats, stats => {
			if (stats === false) {
				return {
					preset: "none"
				};
			}
			if (stats === true) {
				return {
					preset: "normal"
				};
			}
			if (typeof stats === "string") {
				return {
					preset: stats
				};
			}
			return {
				...stats
			};
		}),
		target: config.target,
		watch: config.watch,
		watchOptions: cloneObject(config.watchOptions)
	};
};

/**
 * @param {EntryStatic} entry static entry options
 * @returns {EntryStaticNormalized} normalized static entry options
 */
const getNormalizedEntryStatic = entry => {
	if (typeof entry === "string") {
		return {
			main: {
				import: [entry]
			}
		};
	}
	if (Array.isArray(entry)) {
		return {
			main: {
				import: entry
			}
		};
	}
	/** @type {EntryStaticNormalized} */
	const result = {};
	for (const key of Object.keys(entry)) {
		const value = entry[key];
		if (typeof value === "string") {
			result[key] = {
				import: [value]
			};
		} else if (Array.isArray(value)) {
			result[key] = {
				import: value
			};
		} else {
			result[key] = {
				import:
					value.import &&
					(Array.isArray(value.import) ? value.import : [value.import]),
				filename: value.filename,
				layer: value.layer,
				runtime: value.runtime,
				baseUri: value.baseUri,
				publicPath: value.publicPath,
				chunkLoading: value.chunkLoading,
				asyncChunks: value.asyncChunks,
				wasmLoading: value.wasmLoading,
				dependOn:
					value.dependOn &&
					(Array.isArray(value.dependOn) ? value.dependOn : [value.dependOn]),
				library: value.library
			};
		}
	}
	return result;
};

/**
 * @param {OptimizationRuntimeChunk=} runtimeChunk runtimeChunk option
 * @returns {OptimizationRuntimeChunkNormalized=} normalized runtimeChunk option
 */
const getNormalizedOptimizationRuntimeChunk = runtimeChunk => {
	if (runtimeChunk === undefined) return undefined;
	if (runtimeChunk === false) return false;
	if (runtimeChunk === "single") {
		return {
			name: () => "runtime"
		};
	}
	if (runtimeChunk === true || runtimeChunk === "multiple") {
		return {
			name: entrypoint => `runtime~${entrypoint.name}`
		};
	}
	const { name } = runtimeChunk;
	return {
		name: typeof name === "function" ? name : () => name
	};
};

exports.getNormalizedWebpackOptions = getNormalizedWebpackOptions;
