'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
 * }
 * ```
 * @module @wider/registry
 */
const $moduleName = "@wider/registry";
const $objectName = $moduleName.split(/\//)[1];

const modulesLoaded = {};
const root = "<<root>>";

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

/**
 * 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 provided as an object, then the object should have a `.moduleName` property
 */
class loggedError extends Error {
	constructor(message, code, moduleName) {
		super(`${$moduleName || 'anonymousModule'} - ${message}`);
		this.code = (code || "UNSPECIFIED");
		this.moduleName = moduleName.$moduleName || moduleName;
		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 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 {NCName} [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.  It is thrown when a registration is rejected
 */
function register(moduleName, value, objectName, section) {
	switch (Array.isArray(moduleName) ? "array" : typeof moduleName) {
		case "string":
			if (!/^[\w_\-\d\@\/\.]+$/.test(moduleName))
				throw new loggedError(`module name '${moduleName}' is not a valid simple module id`, "INVALID_MODULE_NAME", $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 if (modulesLoaded[moduleName])
				throw new loggedError(`${moduleName} -  You have multiple packages attempting to load versions of the same moduleName`, "DUPLICATE_CALL", $moduleName);

			else {
				const target = section ? widerGlobal[section] || (widerGlobal[section] = {}) : widerGlobal;
				// always add to modulesLoaded

				modulesLoaded[moduleName] = {
					created: Date.now()
				};

				if (value !== undefined) {
					objectName = objectName || value.$objectName;

					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 {

						Object.defineProperty(target, objectName, {
							get() {
								return value;
							},
							enumerable: true
						});

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

		case "array":
			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);

				// create the section if it does not yet exist
				if (!modulesLoaded[section])
					register(section, null, {
						$moduleName: section
					});
			}

			// submit each entry of the dictionary
			entries.forEach(element => {
				const objectName = element.$objectName || element.$moduleName;
				if (!objectName)
					throw new loggedError(`Entry submitted for '${section||root}[${objectName}]' does not have property $moduleName`, "MODULENAME_MISSING", register.$moduleName);
				register(element.$moduleName, element, element.$objectName || element.$moduleName, element.$registrySection || section);
			});

			break;

		default:
			if (moduleName)
				throw new loggedError(`Permitted types for the value of moduleName in CAll to register '${moduleName}]' must be a string or an array`, "MODULENAME_MISSING", register.$moduleName);

	}

	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 = ["\nModules loaded in chronological order of loading\n"];
	const paddingName = 40;
	const paddingWait = 10;
	const paddingPath = 30;
	const started = (modulesLoaded.CONFIG || modulesLoaded[$moduleName]).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 || "").padEnd(paddingPath)
			);
		}
	}

	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 = ["\nModules published in alphabetical order\n"];
	const paddingName = 40;

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

			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];
						const elementStr = (/^(string|number)$/.test(typeof element2) || Object.hasOwnProperty.call(element2 || {}, "toString")) ? element2 + "" : "";
						result.push(
							(key + "." + key2).padEnd(paddingName, " ") + " " +
							(typeof element2) +
							(elementStr ?
								` : ${elementStr.substr(0, 20)} ${ (elementStr.length > 20) ? "\x8230" : ""}` :
								"")
						);

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

// register myself
register(
	[{
		$moduleName: $moduleName,
		$objectName: $objectName,
		describe_loaded: loaded,
		describe_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;