Source: utils.js

var equal = require('fast-deep-equal/es6');

/**
 * General utilities
 * 
 * @class
 */
class Utils {

	/**
	 * Checks whether a variable is a real object or not.
	 * 
	 * This is a more strict version of `typeof x === 'object'` as this example would also succeeds for arrays and `null`.
	 * This function only returns `true` for real objects and not for arrays, `null` or any other data types.
	 * 
	 * @param {*} obj - A variable to check.
	 * @returns {boolean} - `true` is the given variable is an object, `false` otherwise.
	 */
	static isObject(obj) {
		return (typeof obj === 'object' && obj === Object(obj) && !Array.isArray(obj));
	}

	/**
	 * Checks whether a variable is a string and contains at least one character.
	 * 
	 * @param {*} string - A variable to check.
	 * @returns {boolean} - `true` is the given variable is an string with length > 0, `false` otherwise.
	 */
	static hasText(string) {
		return (typeof string === 'string' && string.length > 0);
	}

	/**
	 * Performs a deep comparison between two values to determine if they are equivalent.
	 * 
	 * @param {*} x - The value to compare.
	 * @param {*} y - The other value to compare.
	 * @returns {boolean} - Returns true if the values are equivalent, else false.
	 */
	static equals(x, y) {
		return equal(x, y);
	}

	/**
	 * Creates an object composed of the picked object properties.
	 * 
	 * Returns a shallow copy!
	 * 
	 * @param {object} obj - The source object.
	 * @param {string|array} toPick - The properties to pick.
	 * @returns {object}
	 */
	static pickFromObject(obj, toPick) {
		obj = Object(obj);
		if (typeof toPick === 'string') {
			toPick = [toPick];
		}
		const copy = {};
		toPick.forEach(key => copy[key] = obj[key]);
		return copy;
	}

	/**
	 * This method creates an object composed of the own and inherited enumerable property paths of object that are not omitted.
	 * 
	 * Returns a shallow copy!
	 * 
	 * @param {object} obj - The source object.
	 * @param {string|array} toOmit - The properties to omit.
	 * @returns {object}
	 */
	static omitFromObject(obj, toOmit) {
		obj = Object(obj);
		if (typeof toOmit === 'string') {
			toOmit = [toOmit];
		}
		var copy = Object.assign({}, obj);
		for(let key of toOmit) {
			delete copy[key];
		}
		return copy;
	}

	/**
	 *  Creates an array of values by running each property of `object` thru function.
	 * 
	 * The function is invoked with three arguments: (value, key, object).
	 * 
	 * @param {object} obj 
	 * @param {function} func 
	 * @returns {object}
	 */
	static mapObject(obj, func) {
		// Taken from lodash, see https://github.com/lodash/lodash/blob/master/mapObject.js
		const props = Object.keys(obj);
		const result = new Array(props.length);
		props.forEach((key, index) => {
			result[index] = func(obj[key], key, obj);
		});
		return result;
	}

	/**
	 * Creates an object with the same keys as object and values generated by running each own enumerable string keyed property of object thru the function.
	 * 
	 * The function is invoked with three arguments: (value, key, object).
	 * 
	 * @param {object} obj 
	 * @param {function} func 
	 * @returns {object}
	 */
	static mapObjectValues(obj, func) {
		// Taken from lodash, see https://github.com/lodash/lodash/blob/master/mapValue.js
		obj = Object(obj);
		const result = {};
		Object.keys(obj).forEach((key) => {
			result[key] = func(obj[key], key, obj);
		});
		return result;
	}

	/**
	 * Creates a duplicate-free version of an array.
	 * 
	 * If useEquals is set to true, uses the `Utils.equals` function for comparison instead of 
	 * the JS === operator. Thus, if the array contains objects, you likely want to set 
	 * `useEquals` to `true`.
	 * 
	 * @param {array} array
	 * @param {boolean} useEquals
	 * @returns {array}
	 */
	static unique(array, useEquals = false) {
		if (useEquals) {
			return array.filter((s1, pos, arr) => arr.findIndex(s2 => Utils.equals(s1, s2)) === pos);
		}
		else {
			return [...new Set(array)];
		}
	}
	
	/**
	 * Computes the size of an array (number of array elements) or object (number of key-value-pairs).
	 * 
	 * Returns 0 for all other data types.
	 * 
	 * @param {*} obj 
	 * @returns {integer}
	 */
	static size(obj) {
		if (typeof obj === 'object' && obj !== null) {
			if (Array.isArray(obj)) {
				return obj.length;
			}
			else {
				return Object.keys(obj).length;
			}
		}
		return 0;
	}

	/**
	 * Checks whether a variable is numeric.
	 * 
	 * Numeric is every string with numeric data or a number, excluding NaN and finite numbers.
	 * 
	 * @param {*} n - A variable to check.
	 * @returns {boolean} - `true` is the given variable is numeric, `false` otherwise.
	 */
	static isNumeric(n) {
		return !isNaN(parseFloat(n)) && isFinite(n);
    }
    
    /**
     * Deep clone for JSON-compatible data.
     * 
     * @param {*} x - The data to clone.
     * @returns {*} - The cloned data.
     */
    static deepClone(x) {
		return JSON.parse(JSON.stringify(x));
    }

	/**
	 * Normalize a URL (mostly handling leading and trailing slashes).
	 * 
	 * @static
	 * @param {string} baseUrl - The URL to normalize
	 * @param {string} path - An optional path to add to the URL
	 * @returns {string} Normalized URL.
	 */
	static normalizeUrl(baseUrl, path = null) {
		let url = baseUrl.replace(/\/$/, ""); // Remove trailing slash from base URL
		if (typeof path === 'string') {
			if (path.substr(0, 1) !== '/') {
				path = '/' + path; // Add leading slash to path
			}
			url = url + path.replace(/\/$/, ""); // Remove trailing slash from path
		}
		return url;
	}

	/**
	 * Replaces placeholders in this format: `{var}`.
	 * 
	 * This can be used for the placeholders/variables in the openEO API's errors.json file.
	 * 
	 * @param {string} message - The string to replace the placeholders in.
	 * @param {object} variables - A map with the placeholder names as keys and the replacement value as value.
	 */
	static replacePlaceholders(message, variables = {}) {
		if (typeof message === 'string' && Utils.isObject(variables)) {
			for(var placeholder in variables) {
				let vars = variables[placeholder];
				message = message.replace('{' + placeholder + '}', Array.isArray(vars) ? vars.join("; ") : vars);
			}
		}
		return message;
	}

	/**
	 * Compares two strings case-insensitive, including natural ordering for numbers.
	 * 
	 * @param {string} a 
	 * @param {string} b 
	 * @returns {integer} Numeric value compatible with the [Array.sort(fn) interface](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Parameters).
	 */
    static compareStringCaseInsensitive(a, b) {
        if (typeof a !== 'string') {
            a = String(a);
        }
        if (typeof b !== 'string') {
            b = String(b);
        }
        return a.localeCompare(b, undefined, {numeric: true, sensitivity: 'base'});
    }

	/**
	 * Tries to make a string more readable by capitalizing it.
	 * Only applies to words with more than two characters.
	 * 
	 * Supports converting from:
	 * - Snake Case (abc_def => Abc Def)
	 * - Kebab Case (abc-def => Abc Def)
	 * - Camel Case (abcDef => Abc Def)
	 * 
	 * Doesn't capitalize if the words are not in any of the casing formats above.
	 * 
	 * @param {*} strings - String(s) to make readable
	 * @param {string} arraySep - String to separate array elements with
	 * @returns {string}
	 */
    static prettifyString(strings, arraySep = '; ') {
		if (!Array.isArray(strings)) {
			strings = [String(strings)];
		}
		strings = strings.map(str => {
			if (str.length >= 3) {
				const replacer = (_,a,b) => a + ' ' + b.toUpperCase();
				if (str.includes('_')) {
					// Snake case converter
					str = str.replace(/([a-zA-Z\d])_([a-zA-Z\d])/g, replacer);
				}
				else if (str.includes('-')) {
					// Kebab case converter
					str = str.replace(/([a-zA-Z\d])-([a-zA-Z\d])/g, replacer);
				}
				else {
					// Camelcase converter
					str = str.replace(/([a-z])([A-Z])/g, replacer);
				}
				// Uppercase the first letter in the first word, too.
				return str.charAt(0).toUpperCase() + str.substr(1);
			}
			return str;
		});
		return strings.join(arraySep);	
    }

	/**
	 * Makes link lists from the openEO API more user-friendly.
	 * 
	 * Supports:
	 * - Set a reasonable title, if not available. Make title more readable.
	 * - Sorting by title (see `sort` parameter)
	 * - Removing given relation types (`rel` property, see `ignoreRel` parameter)
	 * 
	 * @param {array} linkList - List of links
	 * @param {boolean} sort - Enable/Disable sorting by title. Enabled (true) by default.
	 * @param {array} ignoreRel - A list of rel types to remove. By default, removes the self links (rel type = `self`).
	 * @returns {array}
	 */
    static friendlyLinks(linkList, sort = true, ignoreRel = ['self']) {
        let links = [];
        if (!Array.isArray(linkList)) {
            return links;
        }

        for(let link of linkList) {
            link = Object.assign({}, link); // Make sure to work on a copy
            if (typeof link.rel === 'string' && ignoreRel.includes(link.rel.toLowerCase())) {
                continue;
            }
            if (typeof link.title !== 'string' || link.title.length === 0) {
                if (typeof link.rel === 'string' && link.rel.length > 1) {
                    link.title = Utils.prettifyString(link.rel);
                }
                else {
                    link.title = link.href.replace(/^https?:\/\/(www.)?/i, '').replace(/\/$/i, '');
                }
            }
            links.push(link);
        }
        if (sort) {
            links.sort((a, b) => Utils.compareStringCaseInsensitive(a.title, b.title));
        }
        return links;
    }

}

module.exports = Utils;