'use strict';

/**
 * A framework for all $wider bundles but also a tracker for different modules of the same name being loaded by different packages of the same 
 * 
 * creates 
 * ```javascript
 * global.$wider = { // container for other wider globals
 * 	moduleLoaded(moduleName){} // which throws error on duplicate loads
 * }
 * ```
 * @module @wider/registry
 */
const $moduleName = "@wider/registry";
const $objectName = $moduleName.split(/\//)[1];
const widerGlobal = {};
const modulesLoaded = {};
const root = "<<root>>";
/**
 * Extends `Error@ -with its additional properties and also logs the error to the console
 * @property {String} message - the error message
 * @property {NCName} code a code word for optional programmatic use by code that traps the error
 * @property {id|Object} moduleName the name of the module affected if present.  If provided as an object, then the object should have a `.moduleName` property
 */
class loggedError extends Error {
	constructor(message, code, defaultExport) {
		super(`${$moduleName || 'anonymousModule'} - ${message}`);
		this.code = (code || "UNSPECIFIED");
		this.moduleName = defaultExport.$moduleName || defaultExport;
		console.error(this);
	}
}
loggedError.$moduleName = `${$moduleName}[throw loggedError]`;

/**
 * Registers an object typically as it is being constructed via require or import.  The purpose being to ensure that multiple packages within your runtime 
 * on a given cluster do not load the same  module more than once - as can happen when a package is a dependency of several other packages.  This function should *only* 
 * only be called from within the module level code of the module that is registering itself
 * 
 * @param {String|Dictionary} [moduleName] - the name of the item to be registered.  The string should be the same as used as might be used as id in `require` 
 * or `import`, or as used in `npm install` 
 * 
 * If moduleName is an object it is treated as a dictionary of registrations where the keyName is si the moduleName and the associated value
 * is an object with the properties ***value*** and ***objectName***.  It may also contain ***section^^^ which if present will override the section parameter of the call to `.register()`
 * 
 *  If *moduleName* is absent then nothing is registered and the only action is to return the output Dictionary bundle
 * 
 * @param {Object} [section] if absent then the value is saved under ***objectName*** at the top level of the output Dictionary, otherwise this defines the section 
 * name in the dictionary under which the value is filed
 * @param {*} [value] - if present then  the ***moduleName***  must be supplied and this ***value***  is added to the bundle.  
 * 
 * If absent, then the ***moduleName***, if provided, is registered in the dictionary and nothing is added to the bundle.
 * 
 * If this is an object (class, function, json, plain object) then it should have a `$moduleName` static property which is the same as the parameter `moduleName`. 
 * @param {NCName} [objectName] if objectName is present, then ***value*** must be supplied and he value is stored in the response of this `@wider/registry
 *  response under this name available for direct use by any other module without use of `require` or `import`.  If ***objectName*** is not provided then it will
 *  be assumed to be the same as ***moduleName***
 * 
 * @returns {Dictionary} containing all registered items under their objectname and section.
 * @throws {loggedError} an extension of the javascript Error object thrown when a registration is rejected
 */
function register(moduleName, section, value, objectName) {
	if (moduleName) {
		if (typeof moduleName === "string") {
			if (!/^[\w_\-\d\@\/\.]+$/.test(moduleName))
				throw new loggedError(`module name '${moduleName}' is not a valid simple module id`, "INVALID_MODULE_NAME", $moduleName);
			if (modulesLoaded[moduleName])
				throw new loggedError(`${moduleName} -  You have multiple packages attempting to load versions of the same moduleName`, "DUPLICATE_CALL", $moduleName);
			else if (objectName && !/^\w[\w\d_]*$/)
				throw new loggedError(`${moduleName} -  Invalid format for name of objectName ${objectName}`, "BAD_OBJECTNAME", $moduleName);
			else if (section && !/^\w[\w\d_]*$/)
				throw new loggedError(`${moduleName} -  Invalid format for name of objectName ${section}`, "BAD_SECTION", $moduleName);
			else {
				modulesLoaded[moduleName] = {
					created: Date.now(),
				};

				if (value !== undefined) {
					const target = section ? widerGlobal[section] || (widerGlobal[section] = {}) : widerGlobal;

					if (!objectName)
						throw new loggedError(`${moduleName} - objectName must be provided when value is provided`, "MISSING_OBJECTNAME", $moduleName);
					else if (target[objectName])
						throw new loggedError(`Duplicate attempt to save member ${objectName} into $wider`, "DUPLICATE_GLOBAL_MEMBER", $moduleName);
					else if (value.$moduleName != moduleName && value.$objectName != moduleName)
						throw new loggedError(`The submitted object does not possess a $moduleName which is the same as the name of the registry name or a matching alias defined by $objectName `, "$MODULENAME_MISSING", $moduleName);

					else {
						target[moduleName] = value;

						Object.assign(modulesLoaded[moduleName], {
							objectName: objectName,
							section: section,
							path: (section ? section + "." : "") + objectName,
							value: value
						});
					}
				}
			}
		} else {
			const entries = moduleName;

			if (objectName || value)
				throw new loggedError("Objectname and value not permitted when submitting a batch of registrations", "INVALID_PARAMS", register.$moduleName);
			else if (section) {
				if (typeof section != "string")
					throw new loggedError("Section name missing or not a string", "SECTION_MISSING", register.$moduleName);

				if (!modulesLoaded[section])
					register(section, null, {
						$moduleName: section
					});
			}

			for (const key in entries) {
				if (Object.hasOwnProperty.call(entries, key)) {
					const element = entries[key];
					if (!element.$moduleName)
						throw new loggedError(`Entry submitted for '${section||root}[${key}]' does not have property $moduleName`, "MODULENAME_MISSING", register.$moduleName);
					register(key, element.section || section, element, element.objectName || element.$moduleName);
				}
			}
		}
	}

	// makes a normally hidden non-enumerable member of global - we must always be the first to set global.$wider
	if (!global.$wider) {
		Object.defineProperties(global, {
			"$wider": {
				get: () => {
					return widerGlobal;
				}
			},
		});
	}

	return widerGlobal;
}
register.$moduleName = `${$moduleName[register]}`;

/**
 * Generate a report of the items that have been loaded and how long into the run session it was
 * when they are loaded.  NB not all loaded items are published.
 * @returns {String} a tabular report of items loaded
 */
function loaded() {
	const result = ["Modules loaded in chronological order of loading\n\n"];
	const paddingName = 40;
	const paddingWait = 10;
	const started = (modulesLoaded.CONFIG || modulesLoaded[$objectName]).created;

	for (const key in modulesLoaded) {
		if (Object.hasOwnProperty.call(modulesLoaded, key)) {
			const element = modulesLoaded[key];

			result.push(key.padEnd(paddingName, " ") + " " +
				((element.created - started) / 1000).toFixed(3).padStart(paddingWait, " ") + " " +
				(element.path || ""));
		}
	}

	return result.join("\n");
}

/**
 * A report of the structure in the object that is published via the common object
 * @returns {String} a tabular report of items published
 */
function published() {
	const result = ["Modules published in alphabetical order\n\n"];
	const paddingName = 40;
	//const paddingWait = 10;

	for (const key in widerGlobal) {
		if (Object.hasOwnProperty.call(modulesLoaded, key)) {
			const element = modulesLoaded[key];

			result.push(key.padEnd(paddingName, " ") + " " +
				(element.path || ""));

			if (element.value.$moduleName)
				for (const key2 in element.value) {
					if (Object.hasOwnProperty.call(element.value, key2)) {
						const element2 = element.value[key2];

						result.push((key + "." + key2).padEnd(paddingName, " ") + " " +
							(typeof element2) +
							((key2==="$moduleName" || key2==="$objectName") ? ` : ${element2}`:""));
					}
				}
		}
	}
	return result.sort().join("\n");
}

// register myself
register({
	registry: {
		$moduleName: $moduleName,
		$objectName: $objectName,
		loaded: loaded,
		published: published,
		register: register,
		loggedError: loggedError
	}
});


// there is a risk that this module itself is being multiple loaded so we have to use the version in global even if we didn't create it
//register($moduleName); // to stop ourselves being loaded multiple times

module.exports = global.$wider;