Source: tools/Resolve.js

const { inspect } = require('util')
, jsNumberRegex = /^(?<sign>\-)?((?<int>(0|([1-9]\d*?))(?<big>n)?)|(?<float>(0|([1-9]\d*?))?\.\d+?))(?<sci>(e|E)(\-|\d)?\d+)?$/
, jsNumberBinaryRegex = /^(?<sign>\-)?(?<prefix>0(b|B))(?<val>[01]+)(?<big>n)?$/
, jsNumberOctalRegex = /^(?<sign>\-)?(?<prefix>0(o|O)?)(?<val>[0-7]+?)(?<big>n)?$/
, jsNumberHexRegex = /^(?<sign>\-)?(?<prefix>0(x|X))(?<val>[0-9a-fA-F]+)(?<big>n)?$/;


/**
 * @author Sebastian Hönel <development@hoenel.net>
 */
class Resolve {
	/**
	 * Check whether or not a value is of a specific type, that can be given
	 * as an example, an actual Type/Class or the name of a class.
	 * 
	 * @param {any} value the value to check
	 * @param {any|String} exampleOrTypeOrClassName an examplary other value you'd
	 * expect, a type (e.g. RegExp) or class or the name of a class or c'tor-function.
	 * @returns {Boolean}
	 */
	static isTypeOf(value, exampleOrTypeOrClassName) {
		if (typeof exampleOrTypeOrClassName === 'string' && typeof value === 'string') {
			// Check if the given example was a string and the value as well:
			return true;
		} else { // The example is not a string
			try {
				if (value instanceof exampleOrTypeOrClassName) { // let's make a quick check first
					return true;
				}
			} catch (e) { }
		}


		try {
			const proto = Object.getPrototypeOf(value)
			, ctor = !!proto && proto.hasOwnProperty('constructor') ? proto.constructor : null;

			if (typeof exampleOrTypeOrClassName === 'string') {
				if (ctor.name === exampleOrTypeOrClassName) {
					return true;
				}
			} else if (Resolve.isFunction(exampleOrTypeOrClassName)) {
				if (ctor === exampleOrTypeOrClassName) {
					return true;
				}
			} else {
				const exProto = Object.getPrototypeOf(exampleOrTypeOrClassName)
				, exCtor = !!exProto && exProto.hasOwnProperty('constructor') ? exProto.constructor : null;

				if (proto === exProto || ctor === exCtor) {
					return true;
				}
			}
		} catch (e) { }


		if (Resolve.isPrimitive(value) && Resolve.isPrimitive(exampleOrTypeOrClassName)
			&& value === exampleOrTypeOrClassName) {
			return true;
		}

		return false;
	};

	/**
	 * @param {any|Number} value 
	 * @param {Boolean} [tryConvert] Optional. Default false. Whether to attempt
	 * converting the given value to Number. Returns true should that succeed.
	 * @returns {Boolean} true, iff the given number or NaN is of type Number,
	 * or if it was requested to be converted and that conversion succeeded.
	 */
	static isNumber(value, tryConvert = false) {
		const isValNumber = Object.prototype.toString.call(value) === '[object Number]';
		if (!isValNumber && tryConvert) {
			return Resolve.isNumber(Resolve.tryAsNumber(value));
		}
		return isValNumber;
	};

	/**
	 * Gets a regular expression that can be used to match valid integers,
	 * floats and floats in scientific notation. Integers are only recognized
	 * if they are in Base 10 (i.e., not binary, octal or hexadecimal; you will
	 * have to use the other regexes for that).
	 * 
	 * @type {RegExp}
	 */
	static get numberRegex() {
		return jsNumberRegex;
	};

	/**
	 * Gets a regular expression that can be used to match valid integers,
	 * in binary notation (base 2). Integers can be BigInts.
	 * 
	 * @type {RegExp}
	 */
	static get numberBinaryRegex() {
		return jsNumberBinaryRegex;
	};

	/**
	 * Gets a regular expression that can be used to match valid integers,
	 * in octal notation (base 8). Integers can be BigInts.
	 * 
	 * @type {RegExp}
	 */
	static get numberOctalRegex() {
		return jsNumberOctalRegex;
	};

	/**
	 * Gets a regular expression that can be used to match valid integers,
	 * in hexadecimal notation (base 16). Integers can be BigInts.
	 * 
	 * @type {RegExp}
	 */
	static get numberHexRegex() {
		return jsNumberHexRegex;
	};

	/**
	 * Attempts conversion of a number in a string to a native number. Supports
	 * literal integers and floats, also in scientific notation. Also supports
	 * integers in literal base 2 (binary), 8 (octal) and 16 (hex) notation.
	 * Integers can be BigInts (ending in n).
	 * 
	 * @param {String} value A number (as int, float or scientific notation) to
	 * be parsed to a native Number object. Also supports the literal 'NaN'.
	 * @throws {Error} if the value cannot be parsed as a number. Also throws an
	 * error if a literal integer in base 2, 8 or 16 is attempted to be converted
	 * to BigInt, in cases where it is larger than @see {Number.MAX_SAFE_INTEGER}.
	 * @returns {Number|BigInt} the value parsed as Number.
	 */
	static asNumber(value) {
		value = `${value}`.trim();
		if (value === 'NaN') {
			return NaN;
		}

		const match_2_8_16 = value.match(Resolve.numberBinaryRegex) ||
			value.match(Resolve.numberOctalRegex) ||
			value.match(Resolve.numberHexRegex);
		if (match_2_8_16 !== null) {
			// Those 3 have the same groups: sign?, prefix, value, big?
			const useBase = /x/i.test(match_2_8_16.groups.prefix) ? 16 :
				(/b/i.test(match_2_8_16.groups.prefix) ? 2 : 8)
			, intVal = parseInt(`${match_2_8_16.groups.val}`, useBase)
			, usesign = match_2_8_16.groups.sign === '-' ? -1 : 1
			, useBig = intVal > Number.MAX_SAFE_INTEGER || match_2_8_16.groups.big === 'n';

			if (useBig) {
				// POTENTIAL ERROR!
				// BigInt cannot currently be constructed from a literal binary, octal
				// or hexadecimal number, and we cannot use the parsed value from
				// parseInt() if it is actually bigger than MAX_SAFE_INTEGER, as we
				// likely have lost precision. Anyhow, we go ahead and attempt construction,
				// as it might work in the future.
				return BigInt(`${match_2_8_16.groups.sign || ''}${intVal}`);
			}

			return usesign * intVal;
		}

		const match = value.match(Resolve.numberRegex);
		if (match === null) {
			throw new Error(`Cannot cast value '${value}' as number. It must be in the format ${Resolve.numberRegex.toString()}.`);
		}

		// let's check the sign and whether it's an int/float/sci:
		const groups = match.groups
		, all = `${groups.int || ''}${groups.float || ''}${groups.sci || ''}`
		, isNeg = groups.sign === '-' ? -1 : 1
		, isBig = groups.big === 'n'
		, isFloatOrSci = typeof groups.float === 'string' || typeof groups.sci === 'string'
		, parsed = isFloatOrSci ? parseFloat(all) : parseInt(all, 10);

		if (!isFloatOrSci && (parsed > Number.MAX_SAFE_INTEGER || isBig)) {
			// This is a BigInt!
			return BigInt(`${groups.sign || ''}${groups.int}`);
		}

		return isNeg * parsed;
	};

	/**
	 * Attempts conversion of a value to number, iff it is a valid number. Otherwise
	 * returns the value as is. Uses @see {asNumber}.
	 * 
	 * @template T Basically any value.
	 * @param {T|String|Number|BigInt} value A value that is checked for whether it is a
	 * valid number or not. If the value is already a Number, it is returned as is.
	 * If it is a String, it is attempted to parse. Should that fail, the value is
	 * returned as is. If the value is not a Number and not a String, it is also
	 * returned as is.
	 * @returns {T|String|Number|BigInt}
	 */
	static tryAsNumber(value) {
		const t = typeof value;

		if (t === 'number' || t === 'bigint') {
			return value;
		} else if (t === 'string') {
			try {
				return Resolve.asNumber(value);
			} catch (e) {
				return value;
			}
		}

		return value;
	};

	/**
	 * @param {any|Function|AsyncFunction} value
	 * @returns {Boolean} true, iff the given value is an (async) function
	 */
	static isFunction(value) {
		return typeof value === 'function';
	};

	/**
	 * @param {any|Promise} value
	 * @returns {Boolean} true, iff the value is an instance of Promise
	 */
	static isPromise(value) {
		return Resolve.isTypeOf(value, Promise);
	};

	/**
	 * @param {Symbol} value 
	 * @returns {Boolean}
	 */
	static isSymbol(value) {
		return typeof value === 'symbol';
	};

	/**
	 * @info {https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures}
	 * @param {any} value 
	 * @returns {Boolean} true, iff the value is primitive
	 */
	static isPrimitive(value) {
		return value === true || value === false || value === void 0 || value === null
			|| typeof value === 'string' || Resolve.isNumber(value) || Resolve.isSymbol(value);
	};

	/**
	 * Similar to @see {toValue}, this method returns the default value for the given
	 * value, should it be undefined. Other than that, this method returns the same as
	 * @see {toValue}.
	 * 
	 * @see {toValue}
	 * @template T
	 * @returns {T} the resolved-to value or its default, should it be undefined.
	 */
	static async optionalToValue(defaultValue, value, exampleOrTypeOrClassName, resolveFuncs = true, resolvePromises = true) {
		if (value === void 0) {
			return defaultValue;
		}

		return await Resolve.toValue(...[...arguments].slice(1));
	};

	/**
	 * Resolve a literal value, a function or Promise to a value. If enabled, deeply
	 * resolves functions or Promises. Attempts to resolve (to a) value until it matches
	 * the expected example, type/class or class name.
	 * 
	 * @see {Resolve.isTypeOf}
	 * @template T
	 * @param {any|T|producerHandler<T>|Promise<T>} value a literal value or an (async) function
	 * or Promise that may produce a value of the expected type or exemplary value.
	 * @param {any|string|T} [exampleOrTypeOrClassName] Optional. If not given, will only
	 * resolve functions and promises to a value that is not a function and not a Promise.
	 * Otherwise, pass in an examplary other value you'd expect, a type (e.g. RegExp) or
	 * class or the name of a class or c'tor-function.
	 * @param {Boolean} [resolveFuncs] Optional. Defaults to true. If true, then functions
	 * will be called and their return value will be checked against the expected type or
	 * exemplary value. Note that this parameter applies recursively, until a function's
	 * returned value no longer is a function.
	 * @param {Boolean} [resolvePromises] Optional. Defaults to true. If true, then Promises
	 * will be awaited and their resolved value will be checked against the expected type or
	 * exemplary value. Note that this parameter applies recursively, until a Promise's
	 * resolved value no longer is a Promise.
	 * @throws {Error} if the value cannot be resolved to the expected type or exemplary
	 * value.
	 * @returns {T} the resolved-to value
	 */
	static async toValue(value, exampleOrTypeOrClassName, resolveFuncs = true, resolvePromises = true) {
		const hasExample = arguments.length > 1
		, checkType = val => {
			return Resolve.isTypeOf(val, exampleOrTypeOrClassName) ||
				(!hasExample && !Resolve.isFunction(val) && !Resolve.isPromise(val));
		} , orgVal = value;

		if (checkType(value)) {
			return value;
		}

		do {
			let isFunc = false, isProm = false;

			if ((resolveFuncs && (isFunc = Resolve.isFunction(value)))
				|| (resolvePromises && (isProm = Resolve.isPromise(value)))) {
				value = isFunc ? value() : await value;
				if (checkType(value)) {
					return value;
				} else {
					continue;
				}
			} else {
				break;
			}
		} while (true);

		throw new Error(`The value '${inspect(orgVal)}' cannot be resolved to
			'${exampleOrTypeOrClassName}' using resolveFuncs=${resolveFuncs}
			and resolvePromises=${resolvePromises}.`);
	};

	/**
	 * @see {Resolve.toValue}
	 * @template T
	 * Convenience method for resolving functions and/or Promises to values. Calls
	 * toValue() without an example which leads to resolving the given function or
	 * Promise until it yields a value that is not a function and not a Promise.
	 * @param {producerHandler<T|Promise<T>>|Promise<T>} asyncFuncOrPromise An (async)
	 * function or a Promise.
	 * @returns {T} The result of deeply resolving the given function or Promise.
	 */
	static async asyncFuncOrPromise(asyncFuncOrPromise) {
		if (!Resolve.isFunction(asyncFuncOrPromise) && !Resolve.isPromise(asyncFuncOrPromise)) {
			throw new Error(`The value given for func is neither an (async) function nor a Promise. The value given was: ${inspect(asyncFuncOrPromise)}`);
		}
		return await Resolve.toValue(asyncFuncOrPromise);
	};
};


module.exports = Object.freeze({
	Resolve
});