/*
	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 { runtimeToString, RuntimeSpecMap } = require("./util/runtime");

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

class CodeGenerationResults {
	/**
	 * @param {string | Hash} hashFunction the hash function to use
	 */
	constructor(hashFunction = DEFAULTS.HASH_FUNCTION) {
		/** @type {Map<Module, RuntimeSpecMap<CodeGenerationResult>>} */
		this.map = new Map();
		this._hashFunction = hashFunction;
	}

	/**
	 * @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;
	}

	/**
	 * @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;
	}

	/**
	 * @param {Module} module the module
	 * @param {RuntimeSpec} runtime runtime(s)
	 * @param {string} sourceType the source type
	 * @returns {Source} a source
	 */
	getSource(module, runtime, sourceType) {
		return /** @type {Source} */ (
			this.get(module, runtime).sources.get(sourceType)
		);
	}

	/**
	 * @param {Module} module the module
	 * @param {RuntimeSpec} runtime runtime(s)
	 * @returns {ReadOnlyRuntimeRequirements | null} runtime requirements
	 */
	getRuntimeRequirements(module, runtime) {
		return this.get(module, runtime).runtimeRequirements;
	}

	/**
	 * @param {Module} module the module
	 * @param {RuntimeSpec} runtime runtime(s)
	 * @param {string} key data key
	 * @returns {TODO | undefined} data generated by code generation
	 */
	getData(module, runtime, key) {
		const data = this.get(module, runtime).data;
		return data === undefined ? undefined : data.get(key);
	}

	/**
	 * @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 = /** @type {string} */ (hash.digest("hex")));
	}

	/**
	 * @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, () => new RuntimeSpecMap());
		map.set(runtime, result);
	}
}

module.exports = CodeGenerationResults;
