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

"use strict";

const { DEFAULTS } = require("./config/defaults");
const { getOrInsert } = require("./util/MapHelpers");
const { first } = require("./util/SetHelpers");
const createHash = require("./util/createHash");
const { RuntimeSpecMap, runtimeToString } = require("./util/runtime");

/** @typedef {import("webpack-sources").Source} Source */
/** @typedef {import("./Module")} Module */
/** @typedef {import("./Module").SourceType} SourceType */
/** @typedef {import("./Module").CodeGenerationResult} CodeGenerationResult */
/** @typedef {import("./Module").CodeGenerationResultData} CodeGenerationResultData */
/** @typedef {import("./Module").ReadOnlyRuntimeRequirements} ReadOnlyRuntimeRequirements */
/** @typedef {import("./util/Hash").HashFunction} HashFunction */
/** @typedef {import("./util/runtime").RuntimeSpec} RuntimeSpec */

/**
 * Stores code generation results keyed by module and runtime so later stages
 * can retrieve emitted sources, metadata, and derived hashes.
 */
class CodeGenerationResults {
	/**
	 * Initializes an empty result store and remembers which hash function should
	 * be used when a result hash needs to be derived lazily.
	 * @param {HashFunction} hashFunction the hash function to use
	 */
	constructor(hashFunction = DEFAULTS.HASH_FUNCTION) {
		/** @type {Map<Module, RuntimeSpecMap<CodeGenerationResult>>} */
		this.map = new Map();
		/** @type {HashFunction} */
		this._hashFunction = hashFunction;
	}

	/**
	 * Returns the code generation result for a module/runtime pair, rejecting
	 * ambiguous lookups where no unique runtime-independent result exists.
	 * @param {Module} module the module
	 * @param {RuntimeSpec} runtime runtime(s)
	 * @returns {CodeGenerationResult} the CodeGenerationResult
	 */
	get(module, runtime) {
		const entry = this.map.get(module);
		if (entry === undefined) {
			throw new Error(
				`No code generation entry for ${module.identifier()} (existing entries: ${Array.from(
					this.map.keys(),
					(m) => m.identifier()
				).join(", ")})`
			);
		}
		if (runtime === undefined) {
			if (entry.size > 1) {
				const results = new Set(entry.values());
				if (results.size !== 1) {
					throw new Error(
						`No unique code generation entry for unspecified runtime for ${module.identifier()} (existing runtimes: ${Array.from(
							entry.keys(),
							(r) => runtimeToString(r)
						).join(", ")}).
Caller might not support runtime-dependent code generation (opt-out via optimization.usedExports: "global").`
					);
				}
				return /** @type {CodeGenerationResult} */ (first(results));
			}
			return /** @type {CodeGenerationResult} */ (entry.values().next().value);
		}
		const result = entry.get(runtime);
		if (result === undefined) {
			throw new Error(
				`No code generation entry for runtime ${runtimeToString(
					runtime
				)} for ${module.identifier()} (existing runtimes: ${Array.from(
					entry.keys(),
					(r) => runtimeToString(r)
				).join(", ")})`
			);
		}
		return result;
	}

	/**
	 * Reports whether a module has a stored result for the requested runtime, or
	 * a single unambiguous result when no runtime is specified.
	 * @param {Module} module the module
	 * @param {RuntimeSpec} runtime runtime(s)
	 * @returns {boolean} true, when we have data for this
	 */
	has(module, runtime) {
		const entry = this.map.get(module);
		if (entry === undefined) {
			return false;
		}
		if (runtime !== undefined) {
			return entry.has(runtime);
		} else if (entry.size > 1) {
			const results = new Set(entry.values());
			return results.size === 1;
		}
		return entry.size === 1;
	}

	/**
	 * Returns a generated source of the requested source type from a stored code
	 * generation result.
	 * @param {Module} module the module
	 * @param {RuntimeSpec} runtime runtime(s)
	 * @param {SourceType} sourceType the source type
	 * @returns {Source} a source
	 */
	getSource(module, runtime, sourceType) {
		return /** @type {Source} */ (
			this.get(module, runtime).sources.get(sourceType)
		);
	}

	/**
	 * Returns the runtime requirements captured during code generation for the
	 * requested module/runtime pair.
	 * @param {Module} module the module
	 * @param {RuntimeSpec} runtime runtime(s)
	 * @returns {ReadOnlyRuntimeRequirements | null} runtime requirements
	 */
	getRuntimeRequirements(module, runtime) {
		return this.get(module, runtime).runtimeRequirements;
	}

	/**
	 * Returns an arbitrary metadata entry recorded during code generation.
	 * @param {Module} module the module
	 * @param {RuntimeSpec} runtime runtime(s)
	 * @param {string} key data key
	 * @returns {ReturnType<CodeGenerationResultData["get"]>} data generated by code generation
	 */
	getData(module, runtime, key) {
		const data = this.get(module, runtime).data;
		return data === undefined ? undefined : data.get(key);
	}

	/**
	 * Returns a stable hash for the generated sources and runtime requirements,
	 * computing and caching it on first access.
	 * @param {Module} module the module
	 * @param {RuntimeSpec} runtime runtime(s)
	 * @returns {string} hash of the code generation
	 */
	getHash(module, runtime) {
		const info = this.get(module, runtime);
		if (info.hash !== undefined) return info.hash;
		const hash = createHash(this._hashFunction);
		for (const [type, source] of info.sources) {
			hash.update(type);
			source.updateHash(hash);
		}
		if (info.runtimeRequirements) {
			for (const rr of info.runtimeRequirements) hash.update(rr);
		}
		return (info.hash = hash.digest("hex"));
	}

	/**
	 * Stores a code generation result for a module/runtime pair, creating the
	 * per-module runtime map when needed.
	 * @param {Module} module the module
	 * @param {RuntimeSpec} runtime runtime(s)
	 * @param {CodeGenerationResult} result result from module
	 * @returns {void}
	 */
	add(module, runtime, result) {
		const map = getOrInsert(
			this.map,
			module,
			() =>
				/** @type {RuntimeSpecMap<CodeGenerationResult>} */
				new RuntimeSpecMap()
		);
		map.set(runtime, result);
	}
}

module.exports = CodeGenerationResults;
