Source: util.js

/**
 * @author Boris GBAHOUE
 * @file Library of misc utility functions
 * @namespace amiwo/util
*/

// =======================================================================
// BASE SETUP
// =======================================================================
// Load our packages
var merge           = require('merge');
var uuid            = require('node-uuid');
var request         = require('request-promise');
var when            = require('when');
var node_util       = require('util');
var GenericError    = require('./error/GenericError');

// =======================================================================
// CONSTRUCTOR
// =======================================================================
 function util() {
	 // empty
 }

// =======================================================================
// PUBLIC METHODS
// =======================================================================
 /**
  * Fuse 'obj1' and 'obj2' and return the result.
  * The result will be of the same type as 'obj1' for Array & Object
  *     - Array + Array => Array (behaves as concat modulo options.array.no_copy)
  *     - Array + Object|value => Array
  *     - Object + Array => Object with a new property 'array' set to Array
  *     - Object + Object => Object (if o1 and o2 have an identical key, o2's is kept)
  *     - Object + value => Object with a new property 'value' set to 'obj2'
  *     - value + Array => Array with value as first entry
  *
  * Fuse is different from merge in the sense that when if attempting to fuse an Object with a non Object, 
  * 1. 'merge()' will replace o1 property by o2's no matter what o2 is;
  * 2. 'fuse()' will try to combine them (i.e. "adding" o2's to o1 but keeping o1)
  * 3. if 'obj2' is null (or in case of a conflict deeper in obj1/obj2) fuse will use obj2 while merge will keep null
  * 
  * @param {any} obj1
  * @param {any} obj2
  * @param {Object} options
  * @param {Object} options.array.no_copy : don't copy existing values when concatenating 2 arrays (to avoid duplicates) [set to true by default (i.e. NOT behaving like concat)]
  *
  * @return a fused object (sse above)
  * @memberof amiwo/util
  */
util.fuse = function(o1, o2, options) {
    if (util.isEmpty(options)) options = {};
    if (util.isEmpty(options.array)) options.array = {};
    
    if ((options.array.no_copy == false) || (options.array.no_copy == "false") || (options.array.no_copy == 0)) {
        options.array.no_copy = false;
    } else {
        options.array.no_copy = true;
    }
    
    var obj1 = (o1 instanceof Object) ? this.clone(o1) : o1;
    var obj2 = (o2 instanceof Object) ? this.clone(o2) : o2;

    // Basic sanity checks
    if (obj2 == null) return obj1;
    if (obj1 == null) return obj2;

    if (obj1 instanceof Array) {
        if (obj2 instanceof Array) {
            // Array + Array => Array
            var resultArray = [];
            if (options && options.array && options.array.no_copy) {
                for (var i = 0 ; i < obj1.length ; i++) {
                    resultArray[i] = obj1[i];
                }
                
                var copy = true;
                for (var i = 0 ; i < obj2.length ; i++) {
                    copy = true;
                    for (var j = 0; j < obj1.length; j++) {
                        if (util.smartDeepEqual(obj2[i], obj1[j])) {
                            copy = false;
                            break;
                        }
                    }
                    if (copy) {
                        resultArray.push(obj2[i]);
                    }
                }
                
                return resultArray;
            } else {
                return obj1.concat(obj2);
            }
        } else {
            // Array + Object|value => Array
            obj1.push(obj2);
            return obj1;
        }
    } else if (obj1 instanceof Object) {
        if (obj2 instanceof Array) {
            // Object + Array => Object with a new property 'array' set to Array
            obj1.array = obj2;
            return obj1;
        } else if (obj2 instanceof Object) {
            // Object + Object => merged Object
            return util.merge(obj1, obj2, options, true);
        } else {
            // Object + value => Object with a new property 'value' set to 'obj2'
            obj1.value = obj2;
            return obj1;
        }
    } else {
        if (obj2 instanceof Array) {
            // value + Array => Array with value as first entry
            obj2.unshift(obj1);
            return obj2;
        } else if (obj2 instanceof Object) {
            // value + Object => Object with a new property 'value' set to 'obj2'
            obj2.value = obj1;
            return obj2;
        } else {
            // value + value => return o2's value
            return (obj2);
        }
    }
};

/**
 * Merge that works on complex structures. In case of conflicts, value from o2 takes precedence over o1's.
 * Merge is different from fuse in the sense when if attempting to merge an Object with a non Object, merge will replace o1 property by o2's not matter what o2 is. Fuse will try to combine them (i.e. "adding" o2's to o1 but keeping o1)
 * 
 * @param {any} o1
 * @param {any} o2
 * @param {Object} options (passed to util::fuse() mostly; array.no_copy set to false by default (behaves LIKE concat for array))
 * 
 * @returns {Object} returns a new Object (o1 & o2 are not modified)
 * @memberof amiwo/util
 */
util.merge = function(o1, o2, options, fuse) {
    if (o1 == null) return o2;
    if (o2 == null) return o1;
    if (fuse != true) fuse = false;
    
    // options processing
    if (options == null) options = {};
    if (util.isEmpty(options.array)) options.array = {no_copy: false};
    if (options.array.no_copy == null) options.array.no_copy = false;
    
    if (Array.isArray(o1) || Array.isArray(o2)) {
        return util.fuse(o1, o2, options);
    } else if ((o1 instanceof Object) && (o2 instanceof Object)) {
        // Now we're talking
        var k1 = Object.keys(o1);
        var k2 = Object.keys(o2);
        
        //var util = require('util');
        //console.log("Merging %s with %s", util.inspect(o1), util.inspect(o2));
        
        var result = this.clone(o1);
        // Deal with o1 keys first
        for (var i = 0; i < k1.length; i++) {
            //console.log("\t Processing key : %s", k1[i]);
            if (k2.indexOf(k1[i]) == -1) {
                // this key doesn't exist in o2 => keep o1's
                continue;
            } else {
                if ((result[k1[i]] instanceof Object) && (o2[k1[i]] instanceof Object)) {
                    //console.log("\t\t Key exist and complex");
                    result[k1[i]] = util.merge(result[k1[i]], o2[k1[i]], options); // go deeper to resolve conflict
                } else {
                    if (fuse) {
                        result[k1[i]] = util.fuse(result[k1[i]], o2[k1[i]], options);
                    } else {
                        // whether o1 is an Object, o2 a value; o1 & o2 are values or o1 is a value and o2 is an object we can't go any deeper
                        // o2 takes precedence => erase o1
                        result[k1[i]] = o2[k1[i]];
                    }
                }
            }
        }
        
        // add all the properties from o2 that don't exist in o1
        for (var i = 0; i < k2.length; i++) {
            if (k1.indexOf(k2[i]) == -1) {
                // this key doesn't exist in o1 => add it
                result[k2[i]] = o2[k2[i]];
            } else {
                // this key exists in o1 => was already handled in the previous loop
                continue;
            }
        }
        
        return result;
    } else {
        return util.fuse(o1, o2, options);
    }
}

/**
 * Create a clone of anything
 *
 * @param {any} from
 * @return a clone of 'from'
 * @memberof amiwo/util
 */
util.clone = function(from) {
    if (from == null) return from; // null & undefined

    if (from instanceof Array) {
        var cloneArray = [];
        for (var i = 0; i < from.length; i++) {
            cloneArray.push(util.clone(from[i]));
        };
        return cloneArray;
    } else if (from instanceof Number) {
        return new Number(from);
    } else if (from instanceof String) {
        return new String(from);
    } else if (from instanceof Date) {
        return new Date(from.getTime());
    } else if (from instanceof Object) {
        var cloneObj = {};
        for (var key in from) {
            cloneObj[key] = util.clone(from[key]);
        }
        return cloneObj;
    } else {
        return from;
    }
};

/**
 * Parse a Date in the format ISO 8601 standard ie 'yyyy-mm-ddThh: mm: ssZ', where hh is in 24h format
 *
 * @param {String} str
 * @return a Date object or NaN
 * @memberof amiwo/util
 */
util.parseDateFromString = function(str) {
    if ((str == null) || (str == "")) return Number.NaN;

    // Remove trailing 'Z'
    var a = str.split('Z');
    if (a.length != 2) return Number.NaN;

    a = a[0].split("T");
    if (a.length != 2) return Number.NaN;

    var dateStr = a[0];
    var timeStr = a[1];

    var dateElts = dateStr.split("-");
    if (dateElts.length != 3) return Number.NaN;
    var timeElts = timeStr.split(":");
    if (timeElts.length != 3) return Number.NaN;

    // Remove 1 to month
    dateElts[1] = Number(dateElts[1])-1;

    return new Date(Date.UTC(dateElts[0].trim(), dateElts[1], dateElts[2].trim(), timeElts[0].trim(), timeElts[1].trim(), timeElts[2]));
};

/**
 * Parse local path : check if the first char is a '/' and if so, remove it.
 * Doesn't modify 'str'
 * 
 * @memberof amiwo/util
 */
util.removeFirstSlash = function(str) {
    if ((str == null) || (str == "")) return str;

    return (str[0] == "/") ? str.slice(1) : str;
}

/**
 * Depending on the type of obj
 *      - String : return true if null or equal to ""
 *      - Array : return true if null or length == 0
 *      - Object : return true if null or keys.length == 0
 *      - else : return true if null
 *
 * @param {any} obj: the Object to test
 * @memberof amiwo/util
 */
util.isEmpty = function(obj) {
    if (Array.isArray(obj)) {
        return ((obj == null) || (obj.length == 0));
    } else if (obj instanceof Date) {
        return (obj == null);
    } else if (typeof obj == "string") {
        return ((obj == null) || (obj == ""));
    } else if (typeof obj == "object") {
        var keys = (obj == null) ? null : Object.keys(obj);
        return ((obj == null) || (keys == null) || (keys.length == 0));
    } else {
        return (obj == null);
    }
}

/**
 * Depending on the type of obj
 *      - String : return true if not null and not equal to ""
 *      - Array : return true if not null and length > 0
 *      - Object : return true if not null and keys.length > 0
 *      - else : return true if not null
 *
 * @param {any} obj: the Object to test
 * @memberof amiwo/util
 */
util.isNotEmpty = function(obj) {
    return !util.isEmpty(obj);
}

/**
 * Test if 'obj[property]' is == to 'value' if obj is not null
 *
 * @param {Object} obj
 * @param {any} property
 * @param {any} value
 * @return false if obj is null, property is null or values don't match
 * @memberof amiwo/util
 */
util.equal = function(obj, property, value) {
    if (obj == null) return false;
    if (property == null) return false;

    return (obj[property] == value);
}

/**
 * Test if 'obj[property]' is === to 'value' if obj is not null
 *
 * @param {Object} obj
 * @param {any} property
 * @param {any} value
 * @return false if obj is null or values don't match
 * @memberof amiwo/util
 */
util.deepEqual = function(obj, property, value) {
    if (obj == null) return false;
    if (property == null) return false;

    return (obj[property] === value);
}

/**
 * Test if 'obj[property]' is != to 'value' if obj is not null
 *
 * @param {Object} obj
 * @param {any} property
 * @param {any} value
 * @return false if obj is null or values don't match
 * @memberof amiwo/util
 */
util.notEqual = function(obj, property, value) {
    if (obj == null) return false;
    return (obj[property] != value);
}

/**
 * Test if 'obj[property]' is !== to 'value' if obj is not null
 *
 * @param {Object} obj
 * @param {any} property
 * @param {any} value
 * @return false if obj is null or values don't match
 * @memberof amiwo/util
 */
util.notDeepEqual = function(obj, property, value) {
    if (obj == null) return false;
    return (obj[property] !== value);
}

/**
 * Add 'value' to property 'property' of 'obj'.
 * Tests if 'obj' is null and property is 'null' and if not add value to the existing value
 *      . if 'obj[property]' is an Array => adds it to the end
 *      . if 'obj[property]' is an Object => adds it to the 'valueProperty' using util.add(obj[property], valueProperty, value, null)
 *      . if 'obj[property]' is a String => concatenate String
 *      . else => replace old value

 * @param {Object} obj
 * @param {any} property
 * @param {any} value
 * @param {any} [valueProperty]: used when obj[property is an object].
 *      . If not null : it represents the property name to use to add 'value' as a new property/value of the existing obj[property] (basically obj[property][valueProperty] = value)
 *      . If null : it creates an array of object (regardless of the value of options.create_array) (basically obj[proprerty] = [obj[property], value])
 * @param {any} [options]
 * @param {boolean} [options.create_array] => create an Array when adding {any} to a String or in the 'else' case (default false; alias 'createArray')
 * @param {boolean} [options.concat_arrays] => if true, when 'obj[property]' and value are both Arrays, concat the Arrays instead of pushing 'value' in 'obj[property]' (default false; aliases 'concat', 'concatArray')
 * @param {boolean} [options.merge] => if true, when 'obj[property]' and value are both Objects, merge the Objects instead of pushing 'value' in 'obj[property]' (default false)
 *
 * @returns {boolean} 'true' if all went well, 'false' if 'obj' or 'property' is null
 * @memberof amiwo/util
 */
util.add = function(obj, property, value, valueProperty, options) {
    if (obj == null) return false;
    if (property == null) return false;

    var existing = obj[property];

    // Process option flags
    var createArrayFlag = (options && (options.createArray || options.create_array)) ? true : false;
    var concatFlag = (options && (options.concatArray || options.concat_array || options.concat || options.concat_arrays || options.concatArrays)) ? true : false;
    var mergeFlag = (options && options.merge) ? true : false;

    // Start the cooking
    if (existing) {
        if (Array.isArray(existing)) {
            if (Array.isArray(value) && concatFlag) {
                obj[property] = existing.concat(value);
            } else {
                existing.push(value);
            }

        } else if (typeof existing == "object") {
            if (valueProperty) {
                return util.add(existing, valueProperty, value, null, options);
            } else if ((typeof value == "object") && mergeFlag) {
                merge(obj[property], value);
            } else {
                obj[property] = [existing, value];
            }

        } else if (typeof existing == "string") {
            if (createArrayFlag) {
                obj[property] = [existing, value];
            } else {
                obj[property] = existing + value;
            }

        } else {
            if (createArrayFlag) {
                obj[property] = [existing, value];
            } else {
                obj[property] = value;
            }
        }
    } else {
        obj[property] = value;
    }

    return true;
}

/**
 * Equals that works for Object and Arrays
 *
 * @param {any} obj1
 * @param {any} obj2
 * @param {boolean} [ignoreOrder=false]: for arrays only, default to false
 * @param {String[]} [ignoreProperties]: entries or properties to ignore in the comparison
 * @returns {boolean}
 * @memberof amiwo/util
 */
util.smartEqual = function(obj1, obj2, ignoreOrder, ignoreProperties) {
    if (obj1 == null) return obj2 == null;
    if (obj2 == null) return false;
    
    // Arg check
    if ((ignoreProperties == null) && Array.isArray(ignoreOrder)) {
        ignoreProperties = ignoreOrder;
    }
    if (ignoreProperties == null) ignoreProperties = [];
    
    if (Array.isArray(obj1)) {
        if (!Array.isArray(obj2)) return false;
        
        if (ignoreProperties != null) {
            var clone1 = [];
            var clone2 = [];
            
            for (var i = 0; i < obj1.length; i++) {
                if (ignoreProperties.indexOf(obj1[i]) == -1) {
                    clone1.push(obj1[i]);
                }
            }

            for (var i = 0; i < obj2.length; i++) {
                if (ignoreProperties.indexOf(obj2[i]) == -1) {
                    clone2.push(obj2[i]);
                }
            }
            
            obj1 = clone1;
            obj2 = clone2;
        }
        
        if (obj2.length != obj1.length) return false;

        if (!ignoreOrder) {
            for (var i = 0; i < obj1.length; i++) {
                if (!util.smartEqual(obj2[i],obj1[i])) return false;
            }
        } else {
            for (var i = 0; i < obj1.length; i++) {
                if (obj1[i] == null) {
                    return (obj2[i] == null);
                } else {
                    return (obj2.indexOf(obj1[i]) > -1);
                }
            }
        }
    } else if ((obj1 instanceof Date) || (obj2 instanceof Date)) {
        var time1 = util.getTime(obj1);
        var time2 = util.getTime(obj2);
        
        if (isNaN(time1) || isNaN(time2)) return false; // obj1 or obj2 is a Date
        return (time1 == time2);

    } else if (typeof obj1 == "object") {
        if (typeof obj2 != "object") return false;
        var keys1 = Object.keys(obj1);
        var keys2 = Object.keys(obj2);

        // Check if keys match
        if (!util.smartEqual(keys1, keys2, true, ignoreProperties)) return false;

        // Check if values match
        for (var i = 0; i < keys1.length; i++) {
            if (ignoreProperties.indexOf(keys1[i]) > -1) continue;
            if (!util.smartEqual(obj1[keys1[i]], obj2[keys1[i]])) return false;
        }
    } else {
        return (obj1 == obj2);
    }

    return true;
}

/**
 * Deep equals that works for Object and Arrays
 *
 * @param {any} obj1
 * @param {any} obj2
 * @param {boolean} [ignoreOrder]: for arrays only, default to false
 * @param {String[]} [ignoreProperties]: entries or properties to ignore in the comparison
 * @returns {boolean}
 * @memberof amiwo/util
 */
util.smartDeepEqual = function(obj1, obj2, ignoreOrder, ignoreProperties) {
    if (obj1 == null) return obj2 === obj1;
    if (obj2 == null) return false;
    
    // Arg check
    if ((ignoreProperties == null) && Array.isArray(ignoreOrder)) {
        ignoreProperties = ignoreOrder;
    }
    if (ignoreProperties == null) ignoreProperties = [];

    if (Array.isArray(obj1)) {
        if (!Array.isArray(obj2)) return false;
        
        if (ignoreProperties != null) {
            var clone1 = [];
            var clone2 = [];
            
            for (var i = 0; i < obj1.length; i++) {
                if (ignoreProperties.indexOf(obj1[i]) == -1) {
                    clone1.push(obj1[i]);
                }
            }

            for (var i = 0; i < obj2.length; i++) {
                if (ignoreProperties.indexOf(obj2[i]) == -1) {
                    clone2.push(obj2[i]);
                }
            }
            
            obj1 = clone1;
            obj2 = clone2;
        }

        if (obj2.length != obj1.length) return false;

        if (!ignoreOrder) {
            for (var i = 0; i < obj1.length; i++) {
                if (!util.smartDeepEqual(obj2[i],obj1[i])) return false;
            }
        } else {
            for (var i = 0; i < obj1.length; i++) {
                if (obj1[i] == null) {
                    return (obj2[i] === obj1[i]);
                } else {
                    return (obj2.indexOf(obj1[i]) > -1);
                }
            }
        }
    } else if ((obj1 instanceof Date) || (obj2 instanceof Date)) {
        var time1 = util.getTime(obj1);
        var time2 = util.getTime(obj2);
        
        if (isNaN(time1) || isNaN(time2)) return false; // obj1 or obj2 is a Date
        return (time1 === time2);

    } else if (typeof obj1 == "object") {
        if (typeof obj2 != "object") return false;
        var keys1 = Object.keys(obj1);
        var keys2 = Object.keys(obj2);

        // Check if keys match
        if (!util.smartDeepEqual(keys1, keys2, true)) return false;

        // Check if values match
        for (var i = 0; i < keys1.length; i++) {
            if (ignoreProperties.indexOf(keys1[i]) > -1) continue;
            if (!util.smartDeepEqual(obj1[keys1[i]], obj2[keys1[i]])) return false;
        }
    } else {
        return (obj1 === obj2);
    }

    return true;
}

/**
 * Equivalent of smartEqual() function for Array's indexOf (which native implementation does === test)
 *
 * @param {Array} array (can safely be null)
 * @param {any} object: object which index we are looking for
 * @returns {Number} the index of 'object' if it's contained by 'array'; -1 if not (or if 'array' is null)
 * @memberof amiwo/util
 * @public
 */
util.indexOf = function(array, object) {
    if (array == null) return -1;

    for (var i = 0; i < array.length; i++) {
        if (util.smartEqual(object, array[i])) return i;
    }
    return -1;
}

/**
 * Replacement for Express' req.param() function which got deprecated for some reason
 * Return the value of param `name` when present or `defaultValue`.
 *
 *  - Checks route placeholders, ex: _/user/:id_
 *  - Checks body params, ex: id=12, {"id":12}
 *  - Checks query string params, ex: ?id=12
 *
 * To utilize request bodies, `req.body`
 * should be an object. This can be done by using
 * the `bodyParser()` middleware.
 *
 * @param {Request} req: Express Request object
 * @param {String} name
 * @param {Mixed} [defaultValue] : the value to return if no parameter with name 'name' has been found
 * @return {String}
 * @memberof amiwo/util
 * @public
 */
util.expressParam = function(req, name, defaultValue) {
    function $get(obj) {
        if ($nested) {
            return util.getProperty($baseObj, $nestedName);
        } else {
            return obj[name];
        }
    }

    if (req == null) return null;

    var params = req.params || {};
    var body = req.body || {};
    var query = req.query || {};

    const $idx = name.indexOf('.');
    const $nested = ($idx > -1);
    let $baseObj, $nestedName;
    if ($nested) {
        try {
            $nestedName = name.substr($idx+1);
            $baseObj = JSON.parse(util.expressParam(req, name.substr(0, $idx)));
        } catch(err) {
            throw err;
        }
    }

    /*
    Bug in node v6 => https://github.com/prerender/prerender-node/pull/103
    if (params.hasOwnProperty(name)) return params[name];
    if (body.hasOwnProperty(name)) return body[name];
    if (query.hasOwnProperty(name)) return query[name];
    */
    let $tmp = $get(params);
    if (($tmp !== undefined) && (typeof $tmp !== 'function')) return $tmp;
    $tmp = $get(body);
    if (($tmp !== undefined) && (typeof $tmp !== 'function')) return $tmp;
    $tmp = $get(query);
    if (($tmp !== undefined) && (typeof $tmp !== 'function')) return $tmp;

    return defaultValue;
};

/**
 * Test if 'object' has all the properties listed in 'properties'
 *
 * @param {Object} object
 * @param {String[]} properties
 * @returns {Boolean}
 * @memberof amiwo/util
 * @public
 */
util.hasOwnProperties = function(object, properties) {
    if (object == null) return (properties == null);
    if (util.isEmpty(properties)) return (util.isEmpty(object) === true);

    for (var i = 0; i < properties.length; i++) {
        if (!object.hasOwnProperty(properties[i])) return false;
    }
    return true;
}

/**
 * Wraps request to add an 'amiwo' parameter with a UUID and in case of error, unwrap the StatusCodeError to send directly the associated Error (i.e. StatusCodeError.error)
 * 
 * @param {Object} options: options to be passed to request but the following
 * @param {boolean} [options.uuid=true]: set to true to tag the request ('amiwo' parameter)
 * @param {boolean} [options.rawError=false]: set to true to send the unprocessed Error
 * @param {function} [debug]: debug method (should behave as 'debug()' or 'console.log')
 * 
 * @returns {Promise}
 * @memberof amiwo/util
 */
util.request = function(options, debug) {
    var amiwo = "";
    var $options = util.clone(options);

    // Own options processing
    //  - uuid
    let $uuid = true;
    if (""+$options.uuid === "false") {
        $uuid = false;
    }
    delete $options.uuid;
    //  - rawError
    let $rawError = false;
    if (""+$options.rawError === "true") {
        $rawError = true;
    }
    delete $options.rawError;
    
    // Add UUID to request
    if ($uuid) {
        if ($options.qs) {
            amiwo = uuid.v4();
            $options.qs.amiwo = amiwo;
        } else if ($options.body) {
            amiwo = uuid.v4();
            $options.body.amiwo = amiwo;        
        }
    }
        
    // Send request
    if (typeof debug == 'function') debug("::AMIWO::REQUEST::LOG Sending request %s @ %s (uuid = %s)", $options.method, $options.uri, amiwo);
    return request($options).then(function(result) {
        return when.resolve(result);
    }).catch(function (err) {
        if ($rawError) return when.reject(err);

        var error = err;
        if (err && err.constructor.name == "StatusCodeError") {
            error = err.error;
        }

        if (!(error instanceof GenericError)) {
            error = new GenericError(error.message || "Unexpected error", error);
        }
        return when.reject(error);
    });
}

/**
 * Flatten an object transforming {obj: {subobj1: {subsubobj1: "data 1", subsubobj2: "data 2", ...}, ... } } into {obj.subobj1.subsubobj1: "data 1", obj.subobj1.subsubobj2: "data 2", ...}
 * 
 * @param {Object} object
 * @param {String} [root] of the properties to use
 * @param {Object} [options]
 * @param {boolean} [options.ignoreArray=false] set to true to navigate through Arrays to flatten its components (else flatten Arrays components but keep the Array structure)
 * 
 * @returns {Object}
 * 
 * @memberof amiwo/util
 * @public
 */
util.flatten = function(object, root, options, depth=1) {
    if (util.isEmpty(object)) return {};

    if ((options === undefined) && (root instanceof Object)) {
        options = root;
        root = null
    }

    if (options == null) options = {};
    if (options.ignoreArray == null) options.ignoreArray = false;
    
    if (root == null) {
        root = "";
    }
    
    var output;
    if ((Array.isArray(object) && options.ignoreArray) || util.isObject(object)) {
        output = {};
        if ((root.length > 0) && (root[root.length-1] != ".")) root += ".";

        for (let key in object) {
            output = merge(output, util.flatten(object[key], root+key, options, depth+1));
        }
    } else {
        if (depth == 1) {
            output = object;
        } else {
            output = {};
            output[root] = object;
        }
    }

    return output;
}

/**
 * Parses 'obj' keys and return an Array containing all the properties which keys match 'expr'
 * 
 * @returns [any]
 * 
 * @memberof amiwo/util
 * @public
 */
util.get = function(obj, expr) {
    if (obj == null) return null;
    if (expr == null) return null;
 
    expr = expr.toUpperCase();
    
    var array = [];
    for (var key in obj) {
        if (key.toUpperCase().indexOf(expr) > -1) {
            array.push(obj[key]);
        }
    }
    
    return array;
}

/**
 * Returns a Date 'days' days from today (uses local TimeZone)
 * 
 * @param {Number} days
 * 
 * @returns {Date}
 * 
 * @memberof amiwo/util
 * @public
 */
util.today = function(day) {
    day = Number(day);
    day = (isNaN(day)) ? 0 : day;
    
    var today = new Date();
    today.setDate(today.getDate() + day);
    
    return today;
}

/**
 * Convenience function; equivalent to today(-1)
 * @returns {Date}
 * 
 * @memberof amiwo/util
 * @public
 */
util.yesterday = function() {
    return util.today(-1);
}

/**
 * Adds 'n' days to 'date'
 * 
 * @param {Date} date the date
 * @param {Number} n the number of days to add/remove
 * 
 * @returns {Date}
 * 
 * @memberof amiwo/util
 * @public
 */
util.addDays = function(date, n) {
    if (date == null) return date;
    
    var nb = Number(n);
    if (isNaN(nb)) return date;
    
    var day = 1000*60*60*24;
    
    return new Date(date.getTime() + nb*day);
}

/**
 * Adds 'n' months to 'date'
 * 
 * @param {Date} date the date
 * @param {Number} n the number of months to add/remove
 * 
 * @returns {Date}
 * 
 * @memberof amiwo/util
 * @public
 */
util.addMonths = function(date, n) {
    if (date == null) return date;
    
    var nb = Number(n);
    if (isNaN(nb)) return date;
    
    var result = new Date(date.getTime());
    result.setMonth(date.getMonth()+nb);
    
    return result;
}

/**
 * Adds 'n' years to 'date'
 * @returns {Date}
 * 
 * @memberof amiwo/util
 * @public
 */
util.addYears = function(date, n) {
    if (date == null) return date;
    
    var nb = Number(n);
    if (isNaN(nb)) return date;
    
    var result = new Date(date.getTime());
    result.setFullYear(date.getFullYear()+nb);
    
    return result;

}

/**
 * Get the week number
 * Source = http://weeknumber.net/how-to/javascript
 * 
 * @param {Date} d
 * @param {Object} options
 * @param {Object} options.iso if set to false return -1 for wk53 of in January, -2 for wk54
 * @returns {Number}
 * 
 * @memberof amiwo/util
 */
util.getWeek = function(d, options) {
    if (options == null) options = {};
    
    var date = new Date(d.getTime());
    date.setHours(0, 0, 0, 0);
    // Thursday in current week decides the year.
    date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
    // January 4 is always in week 1.
    var week1 = new Date(date.getFullYear(), 0, 4);
    // Adjust to Thursday in week 1 and count number of weeks from date to week1.
    
    var isoWkNum = 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000
                        - 3 + (week1.getDay() + 6) % 7) / 7);
    if (options.iso == false) {
        var wkNum = isoWkNum;
        switch (isoWkNum) {
            case 53: 
                if (d.getMonth() == 0) wkNum = -1;
                break; 
            case 54: 
                if (d.getMonth() == 0) wkNum = -2;
                break; 
        }
        return wkNum;
    } else {
        return isoWkNum;
    }
}

/**
 * Calculate the average of datas contained in 'data' (or 'data[i].key' if 'key' is not nully)
 * 
 * @param {Array} array : data to process (Array of numbers or Array of {<key>: number})
 * @param {String} [key] : the property to use if data's content are Objects
 * @param {Object} [options]
 * @param {boolean} options.ignore_nan_values: if set to true ignore NaN values to compute the average (else add 0)
 * 
 * @return {Number} the average or NaN if no average could be computed
 * @memberof amiwo/util
 */
util.average = function(array, key, options) {
    function $getData(obj) {
        if (util.isNotEmpty(key)) {
            return obj[key];
        }
        return obj;
    }
    
    if (util.isEmpty(array) || (! Array.isArray(array))) return Number.NaN;
    if (array.length == 1) return $getData(array[0]);

    // Accept 2 parameters only
    if (key instanceof Object) {
        options = key;
    }
    
    var num = 0;
    var denum = 0;
    var nb;
    for (var i = 0 ; i < array.length ; i++) {
        nb = Number($getData(array[i]));
        if (isNaN(nb)) {
            if (options.ignore_nan_values != true) {
                denum++;
            }
        } else {
            num += nb;
            denum++;
        }
    }
    
    if (denum == 0) return Number.NaN;
    return num/denum;
}

/**
 * Returns the max value contained in 'data' (or 'data.key' if 'key' is not nully)
 * 
 * @param [any] data : array to process
 * @param {String} [key] : the property to use if data's content are Objects
 * @param {Object} [options]
 * 
 * @return {Number/Date} the max or NaN/null if no max could be computed
 * @memberof amiwo/util
 */
util.max = function(array, key, options) {
    function $getData(obj) {
        if (util.isNotEmpty(key)) {
            return obj[key];
        }
        return obj;
    }
    
    function $convert(value, date) {
        if (date) {
            return new Date(value);
        } else {
            return new Number(value);
        }
    }
    
    function $isOk(value, date) {
        if (date) {
            return (!isNaN(Date.parse(value)));
        } else {
            return !isNaN(value);
        }
    }
    
    if (util.isEmpty(array) || (! Array.isArray(array))) return Number.NaN;
    if (array.length == 1) return $getData(array[0]);

    // Accept 2 parameters only
    if (key instanceof Object) {
        options = key;
    }
    
    var workingWithDates = false;
    
    var zero = $getData(array[0]);
    
    if (zero instanceof Date) {
        workingWithDates = true;
    } else {
        var test = new Date(zero);
        workingWithDates = $isOk(test, true);
    }
    var max = $convert(zero, workingWithDates);
    var val;
    for (var i = 1 ; i < array.length ; i++) {
        val = $convert($getData(array[i]), workingWithDates);
        if ($isOk(max) && (val>max)) max = val; // will ignore NaN values for nb
    }
    
    return max;
}

/**
 * Returns the min value contained in 'data' (or 'data.key' if 'key' is not nully)
 * 
 * @param [any] data : data to process
 * @param {String} [key] : the property to use if data's content are Objects
 * @param {Object} [options]
 * 
 * @return {Number/Date} the max or NaN/null if no max could be computed
 * @memberof amiwo/util
 */
util.min = function(array, key, options) {
    function $getData(obj) {
        if (util.isNotEmpty(key)) {
            return obj[key];
        }
        return obj;
    }
    
    function $convert(value, date) {
        if (date) {
            return new Date(value);
        } else {
            return new Number(value);
        }
    }
    
    function $isOk(value, date) {
        if (date) {
            return (!isNaN(Date.parse(value)));
        } else {
            return !isNaN(value);
        }
    }
    
    if (util.isEmpty(array) || (! Array.isArray(array))) return Number.NaN;
    if (array.length == 1) return $getData(array[0]);

    // Accept 2 parameters only
    if (key instanceof Object) {
        options = key;
    }
    
    var workingWithDates = false;
    
    var zero = $getData(array[0]);
    
    if (zero instanceof Date) {
        workingWithDates = true;
    } else {
        var test = new Date(zero);
        workingWithDates = $isOk(test, true);
    }
    var min = $convert(zero, workingWithDates);
    var val;
    for (var i = 1 ; i < array.length ; i++) {
        val = $convert($getData(array[i]), workingWithDates);
        if ($isOk(min) && (val<min)) min = val; // ignore invalid values
    }
    
    return min;
}

/**
 * Get the property 'property' from 'obj'. Works with nested structures using a dot syntax
 * 
 * @param {Object} obj to search
 * @param {String} property the name of the property to look for
 * @param {Object} options supported options : upsert (create intermediate objects if they doesn't exist and return null)
 * 
 * @return {any} or null if the property doesn't exist
 * 
 * @memberof amiwo/util
 * @public
 */
util.getProperty = function(obj, property, options) {
    if (obj == null) return null;
    if (util.isEmpty(property)) return null;
    
    if (options == null) options = {};
    
    var split = property.split('.');
    if (split.length == 1) {
        if (options.upsert && !obj.hasOwnProperty(split[0])) {
            obj[split[0]] = null; // does nothing if obj[split[0]] is a value
        }
        return obj[split[0]];
    } else {
        if (obj.hasOwnProperty(split[0])) {
            // Go recursively
            return util.getProperty(obj[split[0]], split.slice(1).join("."), options);
        } else {
            if (options.upsert) {
                obj[split[0]] = {};
                return util.getProperty(obj[split[0]], split.slice(1).join("."), options);
            } else {
                return undefined;
            }
        }
    }
}

/**
 * Set the property 'property' from 'obj' to 'value'. Works with nested structures using a dot syntax
 * 
 * @param {Object} obj to search
 * @param {String} property the name of the property to look for
 * @param {any} value
 * @param {Object} [options]
 * @param {Object} [options.push=false] : if true push, when 'property' references an Array, push value into it instead of replacing it
 * 
 * @memberof amiwo/util
 * @public
 */
// Added in v4.1
util.setProperty = function(obj, property, value, options) {
    if (obj == null) return null;
    if (util.isEmpty(property)) return null;

    if (options == null) options = {};
    if (options.push === undefined) options.push = false;
    
    var split = property.split('.');
    if (split.length == 1) {
        if (obj[split[0]] == null) obj[split[0]] = {};
        if (Array.isArray(obj[split[0]]) && options.push) {
            obj[split[0]].push(value);
        } else {
            obj[split[0]] = value;
        }
    } else {
        // Go recursively
        if (!obj.hasOwnProperty(split[0])) {
            // Create missing interim structures
            obj[split[0]] = {};
        } else {
            if ((split.length > 1) && !(obj[split[0]] instanceof Object)) { // Array or Object
                throw new Error(node_util.format("Invalid state, obj[%s] exists and is not an Object => can't set %s", split[0], split.slice(1).join(".")));
            }
        }
        return util.setProperty(obj[split[0]], split.slice(1).join("."), value, options);
    }
}

/**
 * @private
 */
var __argv;

/**
 * Lookup command line argument 'name'
 * Sample command line node xxx.js argWithNoValue arg=value
 * 
 * 
 * @return {Boolean|String} true if command line argument was found but with no value (i.e. argWithNoValue), value if a value was found (i.e. value for 'arg'), undefined if argv doesn't exist
 * @memberof amiwo/util
 */
util.argv = function(name) {
    // Parses argv the first time it's called and build hash
    if (__argv === undefined) {
        __argv = {};
        
        var tmp;
        process.argv.slice(2).forEach(function (val, index, array) {
            tmp = val.split("=");
            __argv[tmp[0]] = tmp[1] || true;
        });
    }
    
    return __argv[name];
}

// v2.6
// -----------------------------------------------------------------------
/**
 * @private
 */
var $nativeNodeObjects = [
    "Object",
    "Function",
    "Boolean",
    "Symbol",
    "Error",
    "EvalError",
    "InternalError",
    "RangeError",
    "ReferenceError",
    "StopIteration",
    "SyntaxError",
    "TypeError",
    "URIError",
    "Number",
    "Math",
    "Date",
    "String",
    "RegExp",
    "Buffer",
    "URL",
    "Array",
    "Int8Array",
    "Uint8Array",
    "Uint8ClampedArray",
    "Int16Array",
    "Uint16Array",
    "Int32Array",
    "Uint32Array",
    "Float32Array",
    "Float64Array",
    "Map",
    "Set",
    "WeakMap",
    "WeakSet",
    "ArrayBuffer",
    "SharedArrayBuffer",
    "Atomics",
    "DataView",
    "JSON",
    "Promise",
    "Generator",
    "GeneratorFunction",
    "Reflect",
    "Proxy",
    "Intl",
    "Intl.Collator",
    "Intl.DateTimeFormat",
    "Intl.NumberFormat",
    "Iterator",
    "ParallelArray",
    "StopIteration"
];
$nativeNodeObjectsLC = $nativeNodeObjects.map(function(str) {return str.toLowerCase()});

/**
 * Returns the type of 'obj' distinguishing properly Arrays from Object, Date from Object etc.
 * Any non native Object (e.g., class) will be returned as 'object'
 * 
 * @param {any} obj : obj to get the typeof
 * 
 * @return {String|null} null for null 'obj', lower case class name otherwise (object, date, array, number, ...)
 * @memberof amiwo/util
 */
util.typeOf = function(obj) {
    if (obj == null) return obj; // null or undefined
    
    var toClass = {}.toString;
    var str = toClass.call(obj);
    return str.substring(str.indexOf(' ')+1, str.length-1).toLowerCase();
}

/**
 * Tests if 'obj' is an Object (excluding Date, Number, Array, ...)
 * See util::typeOf
 * 
 * @param {any} obj
 * @param {Boolean} [native=false] if true, tests if 'obj' is a native Object and not a non native object (i.e. Class)
 * 
 * @return {Boolean}, false if 'obj' is null/undefined
 * @memberof amiwo/util
 */
util.isObject = function(obj, native) {
    if (obj == null) return false;
    
    if (native === true) {
        return ((util.typeOf(obj) == "object") && (obj.constructor.name == "Object"));
    } else { 
        return (util.typeOf(obj) == "object");
    }
}

/**
 * Checks if 'a' is an Object
 * From http://stackoverflow.com/questions/8834126/how-to-efficiently-check-if-variable-is-array-or-object-in-nodejs-v8
 * 
 * @deprecated
 * @memberof amiwo/util
 */
util.isObject_v1 = function(a) {
    return (!!a) && (a.constructor === Object);
};

/**
 * Tests if 'any' is a native JS object
 * 
 * @param {any} any
 * 
 * @returns {Boolean}, false if 'any' is null/undefined
 * @memberof amiwo/util
 */
util.isNativeObject = function(any) {
    if (any == null) return false;
    
    var constructor = any.constructor.name;
    
    return ($nativeNodeObjectsLC.indexOf(constructor.toLowerCase()) > -1);
}

// v3.0
// -----------------------------------------------------------------------
/**
 * Improved Date::getTime() taking ms into account and accepting non Date objects
 * 
 * @returns {Number} NaN if date is null
 * @memberof amiwo/util
 */
util.getTime = function(date) {
    if (date == null) return NaN;
    
    if (date instanceof Date) {
        return date.getTime();
    } else {
        return Date.parse(date);
    }
}

// v3.1
// -----------------------------------------------------------------------
/**
 * Tests if 'str' is a valid email address
 * 
 * @returns {boolean}
 * @memberof amiwo/util
 */
util.isValidEmail = function(str) {
    if (util.isEmpty(str)) return false;
    return /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,63}$/g.test(str.toUpperCase());
}

// v4.2
// -----------------------------------------------------------------------
/**
 * Returns the items from 'elements' which are not in 'set'
 * 
 * @param {Array} set
 * @param {any} elements
 * 
 * @return {Array}
 * @memberof amiwo/util
 */
util.notInSet = function(set, elements) {
    if (util.isEmpty(set)) return (Array.isArray(elements) ? elements : [elements]);

    if (Array.isArray(elements) && (elements.length > 0)) {
        let addedElements = [];
        for (let i = 0, len = elements.length; i<len; i++) {
            addedElements = addedElements.concat(util.notInSet(set, elements[i]));
        }
        return addedElements;
    } else {
        const element = elements; // for clarity

        if (util.isObject(element)) {
            const hasId = element.hasOwnProperty("_id") || element.hasOwnProperty("id");
            for (let i=0, len = set.length; i < len; i++) {
                if (hasId) {
                    if ((set[i]._id || set[i].id) === (element._id || element.id)) return [];
                } else {
                    if (util.smartDeepEqual(set[i], element)) return [];
                }
            }
            return [element];
        } else {
            // Same as Array::includes() but works with '[]'
            for (let i = 0, len = set.length; i < len; i++) {
                if (util.smartDeepEqual(set[i], element)) return [];
            }
            return [element];
        }
    }
}

// v4.6
// -----------------------------------------------------------------------
/**
 * Set char 'chr' at index 'index' in 'str'
 * 
 * @param {String} str
 * @param {Number} index
 * @param {String} chr
 * 
 * @return String
 * @memberof amiwo/util
 */
util.setCharAt = function(str, index, chr) {
    if(index > str.length-1) return str;
    return str.substr(0,index) + chr + str.substr(index+1);
}

// v4.7
// -----------------------------------------------------------------------
/**
 * Flatten an array of arrays into an array containing (as per concat) all the elements
 * 
 * @param {Array} array
 * 
 * @return {Array}
 * @memberof amiwo/util
 */
util.flattenArray = function(array) {
    if (util.isEmpty(array)) return array;

    return array.reduce( (aggregator, current) => ( u.isEmpty(current) ? aggregator : aggregator.concat(current) ), [] );
}

// v4.8
// -----------------------------------------------------------------------
/**
 * Safe Object.keys that works with nully objects
 * 
 * @param {Object} obj
 * @param {any} def : value to return if 'obj' is nully (null by default)
 * 
 * @return {Array}, undefined if 'obj' is not an Object, null if 'obj' is nully 
 * @memberof amiwo/util
 */
util.keys = function(obj, def=null) {
    if (obj == null) return def;
    return (obj instanceof Object) ? Object.keys(obj) : undefined;
}

/**
 * Get all the values of 'obj' in an Array (as per the values matching each of key from Object.keys(obj))
 * 
 * @param {Object} obj
 * 
 * @returns {Array}, undefined if 'obj' is not an Object, null if 'obj' is null, [] if 'obj' is empty ({})
 * @memberof amiwo/util
 */
util.values = function(obj) {
    if (obj == null) return null;
    if (util.isEmpty(obj)) return [];

    return Object.keys(obj).reduce(
        function(aggreg, key) {
            aggreg.push(obj[key]);
            return aggreg;
        },
        []
    );
}

/**
 * Replace 'o1' content by 'o2's' without breaking o1 reference
 * 
 * @param {Object} o1
 * @param {Object} o2
 * 
 * @returns {Object} o1
 * @memberof amiwo/util
 */
util.overwrite = function(o1, o2) {
    if (util.isEmpty(o1) || util.isEmpty(o2)) return o1;

    for (let key in o1) {
        delete o1[key];
    }

    for (let key in o2) {
        o1[key] = o2[key];
    }

    return o1;
}

// FUNCTIONAL PROGRAMMING HELPERS
// -----------------------------------------------------------------------
/**
 * Tries to transform 'str' to lower case
 * 
 * @returns {String} lower cased 'str' or 'str' if 'str' is not a String
 * @memberof amiwo/util
 */
util.toLowerCase = function(str) {
    return (util.typeOf(str) === 'string') ? str.toLowerCase() : str;
}

/**
 * Tries to transform 'str' to upper case
 * 
 * @returns {String} upper cased 'str' or 'str' if 'str' is not a String
 * @memberof amiwo/util
 */
util.toUpperCase = function(str) {
    return (util.typeOf(str) === 'string') ? str.toUpperCase() : str;
}

/**
 * Recursive map function that go through 'obj' and apply 'callback' to each entry (for Arrays) or to each value (for Objects)
 * 
 * @param {Object|Array} obj
 * @param {function} callback : which signature is callback(element, index|key, array|obj)
 * 
 * @return {Object|Array} same type as 'obj'
 * @throws {TypeError} if 'obj' is neither an Array or an Object
 * @memberof amiwo/util
 */
util.map = function(obj, callback, key, parent) {
    if ((parent == null) && util.isEmpty(obj)) return obj;

    let $result;
    if (Array.isArray(obj)) {
        $result = [];
        for (let i = 0; i < obj.length; i++) {
            $result.push(util.map(obj[i], callback, i, obj));
        }
        return $result;
    } else if (obj instanceof Object) {
        $result = {};
        for (let key in obj) {
            $result[key] = util.map(obj[key], callback, key, obj);
        }
    } else if (parent == null) {
        throw new TypeError("Invalid parameter: expecting an Array or Object not a "+(typeof obj));
    } else {
        return callback(obj, key, parent);
    }

    return $result;
}

/**
 * Recursive filter function that go through 'obj' and returns the elements that meet the conditions specified in the callback function 'callback' to each entry (for Arrays) or to each value (for Objects)
 * 
 * @param {Object|Array} obj
 * @param {function} callback : which signature is callback(element:any, index|key: number|string, array|obj:any[]|{}) => boolean
 * 
 * @return {Object|Array} same type as 'obj'
 * @throws {TypeError} if 'obj' is neither an Array or an Object
 * @memberof amiwo/util
 */
util.filter = function(obj, callback, key, parent) {
    if ((parent == null) && util.isEmpty(obj)) return obj;

    let $result;

    if (Array.isArray(obj)) {
        $result = [];
        for (let i = 0; i < obj.length; i++) {
            let $tmp = util.filter(obj[i], callback, i, obj);
            if ($tmp !== undefined) $result.push($tmp);
        }
    } else if (obj instanceof Object) {
        $result = {};
        for (let key in obj) {
            let $tmp = util.filter(obj[key], callback, key, obj);
            if ($tmp !== undefined) $result[key] = $tmp;
        }
    } else if (parent == null) {
        throw new TypeError("Invalid parameter: expecting an Array or Object not a "+(typeof obj));
    } else {
        return callback(obj, key, parent) ? obj : undefined;
    }

    return ((parent == null) || util.isNotEmpty($result)) ? $result : undefined;
}

module.exports = util;