# Source: src/main/js/common/GenUtils.js

    const assert = require("assert");
    const BigInteger = require("./biginteger").BigInteger;

    /**
     * MIT License
     *
     * Permission is hereby granted, free of charge, to any person obtaining a copy
     * of this software and associated documentation files (the "Software"), to deal
     * in the Software without restriction, including without limitation the rights
     * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     * copies of the Software, and to permit persons to whom the Software is
     * furnished to do so, subject to the following conditions:
     *
     * The above copyright notice and this permission notice shall be included in all
     * copies or substantial portions of the Software.
     *
     * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     * SOFTWARE.
     */

    /**
     * Collection of general purpose utilities.
     *
     * TODO: could pull in assert and remove these asserts
     * TODO: needs cleanup as ES6+ utility class
     */
    class GenUtils {

      /**
       * Indicates if the given argument is defined.
       *
       * @param arg is the arg to test
       * @returns true if the given arg is defined, false otherwise
       */
      static isDefined(arg) {
        return typeof arg !== 'undefined';
      }

      /**
       * Indicates if the given argument is undefined.
       *
       * @param arg is the arg to test
       * @returns true if the given arg is undefined, false otherwise
       */
      static isUndefined(arg) {
        return typeof arg === 'undefined';
      }

      /**
       * Indicates if the given arg is initialized.
       *
       * @param arg is the arg to test
       * @returns true if the given arg is initialized, false otherwise
       */
      static isInitialized(arg) {
        return arg !== undefined && arg !== null;
      }

      /**
       * Indicates if the given arg is uninitialized.
       *
       * @param arg is the arg to test
       * @returns true if the given arg is uninitialized, false otherwise
       */
      static isUninitialized(arg) {
        if (!arg) return true;
        return false;
      }

      /**
       * Indicates if the given argument is a number.
       *
       * @param arg is the argument to test
       * @returns true if the argument is a number, false otherwise
       */
      static isNumber(arg) {
        return !isNaN(parseFloat(arg)) && isFinite(arg);
      }

      /**
       * Indicates if the given argument is an integer.
       *
       * @param arg is the argument to test
       * @returns true if the given argument is an integer, false otherwise
       */
      static isInt(arg) {
        return arg === parseInt(Number(arg)) && !isNaN(arg) && !isNaN(parseInt(arg, 10));
      }

      /**
       * Indicates if the given argument is an array.
       *
       * @param arg is the argument to test as being an array
       * @returns true if the argument is an array, false otherwise
       */
      static isArray(arg) {
        return arg instanceof Array && Array.isArray(arg);
      }

      /**
       * Indicates if the given argument is a string.
       *
       * @param arg is the argument to test as being a string
       * @returns true if the argument is a string, false otherwise
       */
      static isString(arg) {
        return typeof arg === 'string';
      }

      /**
       * Determines if the given argument is a boolean.
       *
       * @param arg is the argument to test as being a boolean
       * @returns true if the argument is a boolean, false otherwise
       */
      static isBoolean(arg) {
        return typeof(arg) == typeof(true);
      }

      /**
       * Determines if the given argument is a static.
       *
       * @param arg is the argument to test as being a static
       * @returns true if the argument is a static, false otherwise
       */
      static isFunction(arg) {
        return typeof arg === "static";
      }

      /**
       * Indicates if the given argument is an object and optionally if it has the given constructor name.
       *
       * @param arg is the argument to test
       * @param obj is an object to test arg instanceof obj (optional)
       * @returns true if the given argument is an object and optionally has the given constructor name
       */
      static isObject(arg, obj) {
        if (!arg) return false;
        if (typeof arg !== 'object') return false;
        if (obj && !(arg instanceof obj)) return false;
        return true;
      }

      /**
       * Determines if all alphabet characters in the given string are upper case.
       *
       * @param str is the string to test
       * @returns true if the string is upper case, false otherwise
       */
      static isUpperCase(str) {
        return str.toUpperCase() === str;
      }

      /**
       * Determines if all alphabet characters in the given string are lower case.
       *
       * @param str is the string to test
       * @param true if the string is lower case, false otherwise
       */
      static isLowerCase(str) {
        return str.toLowerCase() === str;
      }

      /**
       * Asserts that the given argument is hex.
       *
       * @param arg is the argument to assert as hex
       * @param msg is the message to throw if the argument is not hex
       */
      static assertHex(str, msg) {
        GenUtils.assertTrue(isHex(str), msg ? msg : "Argument asserted as hex but is not hex");
      }

      /**
       * Indicates if the given argument is a hexidemal string.
       *
       * Credit: https://github.com/roryrjb/is-hex/blob/master/is-hex.js.
       *
       * @param str is the string to test
       * @returns true if the given string is hexidecimal, false otherwise
       */
      static isHex(arg) {
        if (typeof arg !== 'string') return false;
        if (arg.length === 0) return false;
        return (arg.match(/([0-9]|[a-f])/gim) || []).length === arg.length;
      }

      /**
       * Determines if the given string is base32.
       */
      static isBase32(str) {
        if (typeof str !== 'string') return false;
        GenUtils.assertTrue(str.length > 0, "Cannot determine if empty string is base32");
        return /^[ABCDEFGHIJKLMNOPQRSTUVWXYZ234567]+$/.test(str);
      }

      /**
       * Asserts that the given argument is base58.
       *
       * @param arg is the argument to assert as base58
       * @param msg is the message to throw if the argument is not base58
       */
      static assertBase58(str, msg) {
        GenUtils.assertTrue(isBase58(str), msg ? msg : "Argument asserted as base58 but is not base58");
      }

      /**
       * Determines if the given string is base58.
       */
      static isBase58(str) {
        if (typeof str !== 'string') return false;
        GenUtils.assertTrue(str.length > 0, "Cannot determine if empty string is base58");
        return /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/.test(str);
      }

      /**
       * Asserts that the given argument is base64.
       *
       * @param arg is the argument to assert as base64
       * @param msg is the message to throw if the argument is not base64
       */
      static assertBase64(str, msg) {
        GenUtils.assertTrue(isBase64(str), msg ? msg : "Argument asserted as base64 but is not base64");
      }

      /**
       * Determines if the given string is base64.
       */
      static isBase64(str) {
        if (typeof str !== 'string') return false;
        GenUtils.assertTrue(str.length > 0, "Cannot determine if empty string is base64");
        try {
          return btoa(atob(str)) == str;
        } catch (err) {
          return false;
        }
      }

      /**
       * Throws an exception with the given message.
       *
       * @param msg defines the message to throw the exception with (optional)
       */
      static fail(msg) {
        throw new Error(msg ? msg : "Failure (no message)");
      }

      /**
       * Asserts that the given boolean is true.  Throws an exception if not a boolean or false.
       *
       * @param bool is the boolean to assert true
       * @param msg is the message to throw if bool is false (optional)
       */
      static assertTrue(bool, msg) {
        if (typeof bool !== 'boolean') throw new Error("Argument is not a boolean");
        if (!bool) throw new Error(msg ? msg : "Boolean asserted as true but was false");
      }

      /**
       * Asserts that the given boolean is false.  Throws an exception if not a boolean or true.
       *
       * @param bool is the boolean to assert false
       * @param msg is the message to throw if bool is true (optional)
       */
      static assertFalse(bool, msg) {
        if (typeof bool !== 'boolean') throw new Error("Argument is not a boolean");
        if (bool) throw new Error(msg ? msg : "Boolean asserted as false but was true");
      }

      /**
       * Asserts that the given argument is null.  Throws an exception if not null.
       *
       * @param arg is the argument to assert null
       * @param msg is the message to throw if arg is not null (optional)
       */
      static assertNull(arg, msg) {
        if (arg !== null) throw new Error(msg ? msg : "Argument asserted as null but was not null: " + arg);
      }

      /**
       * Asserts that the given argument is not null.  Throws an exception if null.
       *
       * @param arg is the argument to assert not null
       * @param msg is the message to throw if arg is null (optional)
       */
      static assertNotNull(arg, msg) {
        if (arg === null) throw new Error(msg ? msg : "Argument asserted as not null but was null");
      }

      /**
       * Asserts that the given argument is defined.  Throws an exception if undefined.
       *
       * @param arg is the argument to assert defined
       * @param msg is the message to throw if arg is undefined (optional)
       */
      static assertDefined(arg, msg) {
        if (GenUtils.isUndefined(arg)) throw new Error(msg ? msg : "Argument asserted as defined but was undefined");
      }

      /**
       * Asserts that the given argument is undefined.  Throws an exception if defined.
       *
       * @param arg is the argument to assert undefined
       * @param msg is the message to throw if arg is defined (optional)
       */
      static assertUndefined(arg, msg) {
        if (GenUtils.isDefined(arg)) throw new Error(msg ? msg : "Argument asserted as undefined but was defined: " + arg);
      }

      /**
       * Asserts that the given argument is initialized.  Throws an exception if not initialized.
       *
       * @param arg is the argument to assert as initialized
       * @param msg is the message to throw if arg is not initialized (optional)
       */
      static assertInitialized(arg, msg) {
        if (GenUtils.isUninitialized(arg)) {
          throw new Error(msg ? msg : "Argument asserted as initialized but was " + arg);
        }
      }

      /**
       * Asserts that the given argument is uninitialized.  Throws an exception if initialized.
       *
       * @param arg is the argument to assert as uninitialized
       * @param msg is the message to throw if arg is initialized (optional)
       */
      static assertUninitialized(arg, msg) {
        if (GenUtils.isInitialized(arg)) throw new Error(msg ? msg : "Argument asserted as uninitialized but was initialized");
      }

      /**
       * Asserts that the given arguments are equal.  Throws an exception if not equal.
       *
       * @param arg1 is an argument to assert as equal
       * @param arg2 is an argument to assert as equal
       * @param msg is the message to throw if the arguments are not equal
       */
      static assertEquals(arg1, arg2, msg) {
        GenUtils.assertTrue(GenUtils.equals(arg1, arg2), msg ? msg : "Arguments asserted as equal but are not equal: " + arg1 + " vs " + arg2);
      }

      /**
       * Asserts that the given arguments are not equal.  Throws an exception if equal.
       *
       * @param arg1 is an argument to assert as not equal
       * @param arg2 is an argument to assert as not equal
       * @param msg is the message to throw if the arguments are equal
       */
      static assertNotEquals(arg1, arg2, msg) {
        if (arg1 === arg2) throw new Error(msg ? msg : "Arguments asserted as not equal but are equal: " + arg1 + " vs " + arg2);
      }

      /**
       * Asserts that the given argument is an integer.
       *
       * @param arg is the argument to assert as an integer
       * @param msg is the message to throw if the argument is not an integer
       */
      static assertInt(arg, msg) {
        if (!GenUtils.isInt(arg)) throw new Error(msg ? msg : "Argument asserted as an integer but is not an integer");
      }

      /**
       * Asserts that the given argument is a number.
       *
       * @param arg is the argument to assert as a number
       * @param msg is the message to throw if the argument is not a number
       */
      static assertNumber(arg, msg) {
        if (!GenUtils.isNumber(arg)) throw new Error(msg ? msg : "Argument asserted as a number but is not a number");
      }

      /**
       * Asserts that the given argument is a boolean.
       *
       * @param arg is the argument to assert as a boolean
       * @param msg is the message to throw if the argument is not a boolean
       */
      static assertBoolean(arg, msg) {
        if (!GenUtils.isBoolean(arg)) throw new Error(msg ? msg : "Argument asserted as a boolean but is not a boolean");
      }

      /**
       * Asserts that the given argument is a string.
       *
       * @param arg is the argument to assert as a string
       * @param msg is the message to throw if the argument is not a string
       */
      static assertString(arg, msg) {
        if (!GenUtils.isString(arg)) throw new Error(msg ? msg : "Argument asserted as a string but is not a string: " + arg);
      }

      /**
       * Asserts that the given argument is an array.
       *
       * @param arg is the argument to assert as an array
       * @param msg is the message to throw if the argument is not an array
       */
      static assertArray(arg, msg) {
        if (!GenUtils.isArray(arg)) throw new Error(msg ? msg : "Argument asserted as an array but is not an array");
      }

      /**
       * Asserts that the given argument is a static.
       *
       * @param arg is the argument to assert as a static
       * @param msg is the message to throw if the argument is not a static
       */
      static assertFunction(arg, msg) {
        if (!GenUtils.isFunction(arg)) throw new Error(msg ? msg : "Argument asserted as a static but is not a static");
      }

      /**
       * Asserts that the given argument is an object with the given name.
       *
       * @param arg is the argument to test
       * @param obj is an object to assert arg instanceof obj (optional)
       * @param msg is the message to throw if the argument is not the specified object
       */
      static assertObject(arg, obj, msg) {
        GenUtils.assertInitialized(arg, msg);
        if (obj) {
          if (!isObject(arg, obj)) throw new Error(msg ? msg : "Argument asserted as object '" + obj.name + "' but was not");
        } else {
          if (!isObject(arg)) throw new Error(msg ? msg : "Argument asserted as object but was not");
        }
      }

      /**
       * Sets the child's prototype to the parent's prototype.
       *
       * @param child is the child class
       * @param parent is the parent class
       */
      static inheritsFrom(child, parent) {
        child.prototype = Object.create(parent.prototype);
        child.prototype.constructor = child;
      }

      /**
       * Invokes functions with arguments.
       *
       * arguments[0] is assumed to be an array of functions to invoke
       * arguments[1...n] are args to invoke the functions with
       */
      static invoke() {
        let fns = arguments[0];
        let args = [];
        for (let i = 1; i < arguments.length; i++) args.push(arguments[i]);
        for (let i = 0; i < fns.length; i++) {
          assertFunction(fns[i], "Functions[" + i + "] is not a static");
          fns[i].apply(null, args);
        }
      }

      /**
       * Returns the power set of the given array.
       *
       * @param arr is the array to get the power set of
       * @returns [][] is the power set of the given array
       */
      static getPowerSet(arr) {
        let fn = function(n, src, got, all) {
          if (n == 0) {
            if (got.length > 0) {
              all[all.length] = got;
            }
            return;
          }
          for (let j = 0; j < src.length; j++) {
            fn(n - 1, src.slice(j + 1), got.concat([ src[j] ]), all);
          }
          return;
        }
        let all = [];
        all.push([]);
        for (let i = 0; i < arr.length; i++) {
          fn(i, arr, [], all);
        }
        all.push(arr);
        return all;
      }

      /**
       * Returns the power set of the given array whose elements are the given size.
       *
       * @param arr is the array to get the power set of
       * @param size is the required size of the elements within the power set
       * returns [][] is the power set of the given array whose elements are the given size
       */
      static getPowerSetOfLength(arr, size) {
        assertInitialized(arr);
        assertInitialized(size);
        GenUtils.assertTrue(size >= 1);
        let powerSet = getPowerSet(arr);
        let powerSetOfLength = [];
        for (let i = 0; i < powerSet.length; i++) {
          if (powerSet[i].length === size) {
            powerSetOfLength.push(powerSet[i]);
          }
        }
        return powerSetOfLength;
      }

      /**
       * Returns an array of indices of the given size.
       *
       * @param size specifies the size to get indices for
       * @returns array of the given size with indices starting at 0
       */
      static getIndices(size) {
        let indices = [];
        for (let i = 0; i < size; i++) {
          indices.push(i);
        }
        return indices;
      }

      /**
       * Returns a new array containing unique elements of the given array.
       *
       * @param arr is the array to return unique elements from
       * @returns a new array with the given array's unique elements
       */
      static toUniqueArray(arr) {
        return arr.filter(function(value, index, self) {
          return self.indexOf(value) === index;
        });
      }

      /**
       * Copies the given array.
       *
       * @param arr is the array to copy
       * @returns a copy of the given array
       */
      static copyArray(arr) {
        GenUtils.assertArray(arr);
        let copy = [];
        for (let i = 0; i < arr.length; i++) copy.push(arr[i]);
        return copy;
      }

      /**
       * Removes every instance of the given value from the given array.
       *
       * @param arr is the array to remove the value from
       * @param val is the value to remove from the array
       * @returns true if the value is found and removed, false otherwise
       */
      static remove(arr, val) {
        let found = false;
        for (let i = arr.length - 1; i >= 0; i--) {
          if (arr[i] === val) {
            arr.splice(i, 1);
            found = true;
            i--;
          }
        }
        return found;
      }

      /**
       * Returns a copy of the given array where each element is lowercase.
       *
       * @param arr is the array to convert to lowercase
       * @returns a copy of the given array where each element is lowercase
       */
      static toLowerCaseArray(arr) {
        let arr2 = [];
        for (let i = 0; i < arr.length; i++) {
          arr2.push(arr[i].toLowerCase());
        }
        return arr2;
      }

      /**
       * Listifies the given argument.
       *
       * @param arrOrElem is an array or an element in the array
       * @returns an array which is the given arg if it's an array or an array with the given arg as an element
       */
      static listify(arrOrElem) {
        return GenUtils.isArray(arrOrElem) ? arrOrElem : [arrOrElem];
      }

      /**
       * Indicates if the given array contains the given object.
       *
       * @param {object[]} arr - array that may or may not contain the object
       * @param {object} obj - object to check for inclusion in the array
       * @param {boolean} compareByReference - compare strictly by reference, forgoing deep equality check
       * @returns true if the array contains the object, false otherwise
       */
      static arrayContains(arr, obj, compareByReference) {
        GenUtils.assertTrue(GenUtils.isArray(arr));
        for (let i = 0; i < arr.length; i++) {
          if (arr[i] === obj) return true;
          if (!compareByReference && GenUtils.equals(arr[i], obj)) return true;
        }
        return false;
      }

      /**
       * Indicates if the given string contains the given substring.
       *
       * @param str is the string to search for a substring
       * @param substring is the substring to searchin within the string
       * @returns true if the substring is within the string, false otherwise
       */
      static strContains(str, substring) {
        return str.indexOf(substring) > -1;
      }

      /**
       * Determines if two arrays are equal.
       *
       * @param arr1 is an array to compare
       * @param arr2 is an array to compare
       * @returns true if the arrays are equal, false otherwise
       */
      static arraysEqual(arr1, arr2) {
        if (arr1 === arr2) return true;
        if (arr1 == null && arr2 == null) return true;
        if (arr1 == null || arr2 == null) return false;
        if (typeof arr1 === 'undefined' && typeof arr2 === 'undefined') return true;
        if (typeof arr1 === 'undefined' || typeof arr2 === 'undefined') return false;
        if (!GenUtils.isArray(arr1)) throw new Error("First argument is not an array");
        if (!GenUtils.isArray(arr2)) throw new Error("Second argument is not an array");
        if (arr1.length != arr2.length) return false;
        for (let i = 0; i < arr1.length; ++i) {
          if (!GenUtils.equals(arr1[i], arr2[i])) return false;
        }
        return true;
      }

      /**
       * Determines if two arguments are deep equal.
       *
       * @param arg1 is an argument to compare
       * @param arg2 is an argument to compare
       * @returns true if the arguments are deep equals, false otherwise
       */
      static equals(arg1, arg2) {
        if (GenUtils.isArray(arg1) && GenUtils.isArray(arg2)) return GenUtils.arraysEqual(arg1, arg2);
        if (GenUtils.isObject(arg1) && GenUtils.isObject(arg2)) return GenUtils.objectsEqual(arg1, arg2);
        return arg1 === arg2;
      }

      /**
       * Determines if two objects are deep equal.
       *
       * Undefined values are considered equal to non-existent keys.
       *
       * @param map1 is a map to compare
       * @param map2 is a map to compare
       * @returns true if the maps have identical keys and values, false otherwise
       */
      static objectsEqual(map1, map2) {
        let keys1 = Object.keys(map1);
        let keys2 = Object.keys(map2);

        // compare each key1 to keys2
        for (let key1 of keys1) {
          let found = false;
          for (let key2 of keys2) {
            if (key1 === key2) {
              if (!GenUtils.equals(map1[key1], map2[key2])) return false;
              found = true;
              break;
            }
          }
          if (!found && map1[key1] !== undefined) return false; // allows undefined values to equal non-existent keys
        }

        // compare each key2 to keys1
        for (let key2 of keys2) {
          let found = false;
          for (let key1 of keys1) {
            if (key1 === key2) {
              found = true; // no need to re-compare which was done earlier
              break;
            }
          }
          if (!found && map2[key2] !== undefined) return false; // allows undefined values to equal non-existent keys
        }
        return true;

        // TODO: support strict option?
    //    if (strict) {
    //      let keys1 = Object.keys(map1);
    //      if (keys1.length !== Object.keys(map2).length) return false;
    //      for (let i = 0; i < keys1.length; i++) {
    //        let key = Object.keys(map1)[i];
    //        if (!GenUtils.equals(map1[key], map2[key])) return false;
    //      }
    //    }
      }

      /**
       * Deletes properties from the object that are undefined.
       *
       * @param obj is the object to delete undefined keys from
       */
      static deleteUndefinedKeys(obj) {
        for (let key of Object.keys(obj)) {
          if (obj[key] === undefined) delete obj[key];
        }
      }

      /**
       * Returns combinations of the given array of the given size.
       *
       * @param arr is the array to get combinations from
       * @param combinationSize specifies the size of each combination
       */
      static getCombinations(arr, combinationSize) {

        // validate input
        assertInitialized(arr);
        assertInitialized(combinationSize);
        GenUtils.assertTrue(combinationSize >= 1);

        // get combinations of array indices of the given size
        let indexCombinations = getPowerSetOfLength(getIndices(arr.length), combinationSize);

        // collect combinations from each combination of array indices
        let combinations = [];
        for (let indexCombinationsIdx = 0; indexCombinationsIdx < indexCombinations.length; indexCombinationsIdx++) {

          // get combination of array indices
          let indexCombination = indexCombinations[indexCombinationsIdx];

          // build combination from array
          let combination = [];
          for (let indexCombinationIdx = 0; indexCombinationIdx < indexCombination.length; indexCombinationIdx++) {
            combination.push(arr[indexCombination[indexCombinationIdx]]);
          }

          // add to combinations
          combinations.push(combination);
        }

        return combinations;
      }

      /**
       * Gets an 'a' element that is downloadable when clicked.
       *
       * @param name is the name of the file to download
       * @param contents are the string contents of the file to download
       * @returns 'a' dom element with downloadable file
       */
      static getDownloadableA(name, contents) {
        let a = window.document.createElement('a');
        a.href = window.URL.createObjectURL(new Blob([contents], {type: 'text/plain'}));
        a.download = name;
        a.target="_blank";
        a.innerHTML = name;
        return a;
      }

      /**
       * Returns the given node's outer HTML.
       *
       * @param node is the node to get outer HTML for
       * @returns the outer HTML of the given node
       */
      static getOuterHtml(node) {
        return $('<div>').append($(node).clone()).html();
      }

      /**
       * Copies properties in the given object to a new object.
       *
       * @param obj is object to copy properties for
       * @returns a new object with properties copied from the given object
       */
      static copyProperties(obj) {
        return JSON.parse(JSON.stringify(obj))
      }

      /**
       * Deletes all properties in the given object.
       *
       * @param obj is the object to delete properties from
       */
      static deleteProperties(obj) {
        let props = [];
        for (let prop in obj) props.push(prop); // TODO: if (obj.hasOwnProperty(prop)) { ...
        for (i = 0; i < props.length; i++) delete obj[props[i].toString()];
      }

      /**
       * Converts a CSV string to a 2-dimensional array of strings.
       *
       * @param csv is the CSV string to convert
       * @returns a 2-dimensional array of strings
       */
      static csvToArr(csv) {
        return $.csv.toArrays(csv);
      }

      /**
       * Converts the given array to a CSV string.
       *
       * @param arr is a 2-dimensional array of strings
       * @returns the CSV string
       */
      static arrToCsv(arr) {
        return $.csv.fromObjects(arr, {headers: false});
      }

      /**
       * Indicates if the given string contains whitespace.
       *
       * @param str is the string to test
       * @returns true if the string contains whitespace, false otherwise
       */
      static hasWhitespace(str) {
        return /\s/g.test(str);
      }

      /**
       * Indicates if the given character is whitespace.
       *
       * @param char is the character to test
       * @returns true if the given character is whitespace, false otherwise
       */
      static isWhitespace(char) {
        return /\s/.test(char);
      }

      /**
       * Indicates if the given character is a newline.
       *
       * @param char is the character to test
       * @returns true if the given character is a newline, false otherwise
       */
      static isNewline(char) {
        return char === '\n' || char === '\r';
      }

      /**
       * Counts the number of non-whitespace characters in the given string.
       *
       * @param str is the string to count the number of non-whitespace characters in
       * @returns int is the number of non-whitespace characters in the given string
       */
      static countNonWhitespaceCharacters(str) {
        let count = 0;
        for (let i = 0; i < str.length; i++) {
          if (!isWhitespace(str.charAt(i))) count++;
        }
        return count;
      }

      /**
       * Returns tokens separated by whitespace from the given string.
       *
       * @param str is the string to get tokens from
       * @returns string[] are the tokens separated by whitespace within the string
       */
      static getWhitespaceTokens(str) {
        return str.match(/\S+/g);
      }

      /**
       * Returns lines separated by newlines from the given string.
       *
       * @param str is the string to get lines from
       * @param string[] are the lines separated by newlines within the string
       */
      static getLines(str) {
        return str.match(/[^\r\n]+/g);
      }

      /**
       * Returns the document's first stylesheet which has no href.
       *
       * @returns StyleSheet is the internal stylesheet
       */
      static getInternalStyleSheet() {
        for (let i = 0; i < document.styleSheets.length; i++) {
          let styleSheet = document.styleSheets[i];
          if (!styleSheet.href) return styleSheet;
        }
        return null;
      }

      /**
       * Returns the document's internal stylesheet as text.
       *
       * @returns str is the document's internal stylesheet
       */
      static getInternalStyleSheetText() {
        let internalCss = "";
        let internalStyleSheet = getInternalStyleSheet();
        if (!internalStyleSheet) return null;
        for (let i = 0; i < internalStyleSheet.cssRules.length; i++) {
          internalCss += internalStyleSheet.cssRules[i].cssText + "\n";
        }
        return internalCss;
      }

      /**
       * Manually builds an HTML document string.
       *
       * @param content specifies optional document content
       *        content.div is a pre-existing div to stringify and add to the body
       *        content.title is the title of the new tab
       *        content.dependencyPaths specifies paths to js, css, or img paths
       *        content.internalCss is css to embed in the html document
       *        content.metas are meta elements with keys/values to include
       * @returns str is the document string
       */
      static buildHtmlDocument(content) {
        let str = "<!DOCTYPE HTML>";
        str += "<html><head>";

        // add metas
        if (content.metas) {
          let metas = listify(content.metas);
          for (let i = 0; i < metas.length; i++) {
            let meta = metas[i];
            let elem = document.createElement("meta");
            for (let prop in meta) {
              if (meta.hasOwnProperty(prop)) {
                elem.setAttribute(prop.toString(), meta[prop.toString()]);
              }
            }
            str += elem.outerHTML;
          }
        }

        // add title and internal css
        str += content.title ? "<title>" + content.title + "</title>" : "";
        str += content.internalCss ? "<style>" + content.internalCss + "</style>" : "";

        // add dependency paths
        if (content.dependencyPaths) {
          let dependencyPaths = listify(content.dependencyPaths);
          for (let i = 0; i < dependencyPaths.length; i++) {
            let dependencyPath = dependencyPaths[i];
            if (dependencyPath.endsWith(".js")) str += "<script src='" + dependencyPath + "'></script>";
            else if (dependencyPath.endsWith(".css")) str += "<link rel='stylesheet' type='text/css' href='" + dependencyPath + "'/>";
            else if (dependencyPath.endsWith(".png") || dependencyPath.endsWith(".img"))  str += "<img src='" + dependencyPath + "'>";
            else throw new Error("Unrecognized dependency path extension: " + dependencyPath);
          }
        }
        str += "</head><body>";
        if (content.div) str += $("<div>").append(content.div.clone()).html();  // add cloned div as string
        str += "</body></html>";
        return str;
      }

      /**
       * Opens the given div in a new window.
       *
       * @param content specifies optional window content
       *        content.div is a pre-existing div to stringify and add to the body
       *        content.title is the title of the new tab
       *        content.dependencyPaths specifies paths to js, css, or img paths
       *        content.internalCss is css to embed in the html document
       *        content.metas are meta elements with keys/values to include
       * @param onLoad(err, window) is invoked with a reference to the window when available
       */
      static newWindow(content, onLoad) {
        let onLoadCalled = false;
        let w = window.open();
        if (!isInitialized(w) || !isInitialized(w.document)) {
          onLoadOnce(new Error("Could not get window reference"));
          return;
        }
        w.opener = null;
        w.document.write(buildHtmlDocument(content));
        w.addEventListener('load', function() {
          onLoadOnce(null, w);
        });
        w.document.close();

        // prevents onLoad() from being called multiple times
        function onLoadOnce(err, window) {
          if (onLoadCalled) return;
          onLoadCalled = true;
          if (onLoad) onLoad(err, window);
        }
      }

      /**
       * Converts the given image to a base64 encoded data url.
       *
       * @param img is the image to convert
       * @param quality is a number between 0 and 1 specifying the image quality
       */
      static imgToDataUrl(img, quality) {
        let canvas = document.createElement('canvas');
        canvas.height = img.naturalHeight;
        canvas.width = img.naturalWidth;
        let context = canvas.getContext('2d');
        context.drawImage(img, 0, 0);
        return canvas.toDataURL(quality);
      }

      /**
       * Determines if the image at the given URL is accessible.
       *
       * @param url is the url to an image
       * @param timeout is the maximum time to wait
       * @param onDone(bool) when the image is determined to be accessible or not
       */
      static isImageAccessible(url, timeout, onDone) {

        // track return so it only executes once
        let returned = false;

        // attempt to load favicon
        let img = new Image();
        img.onload = onResponse;
        img.onerror = onResponse;
        img.src = url + "?" + (+new Date()); // trigger image load with cache buster

        // nest failure timeouts to give response a chance when browser is under load
        setTimeout(function() {
          setImmediate(function() {
            setImmediate(function() {
              setImmediate(function() {
                if (!returned) {
                  returned = true;
                  onDone(false);
                }
              });
            });
          });
        }, timeout);

        function onResponse(e) {
          if (returned) return;
          returned = true;
          if (typeof e === 'undefined' || e.type === "error") onDone(false);
          else onDone(true);
        }
      }

      /**
       * Determines if the given file is a zip file.
       *
       * @param file is a file
       * @returns true if the given file is a zip file, false otherwise
       */
      static isZipFile(file) {
        return file.name.endsWith(".zip") || file.type === 'application/zip';
      }

      /**
       * Determines if the given file is a json file.
       *
       * @param file is a file
       * @returns true if the given file is a json file, false otherwise
       */
      static isJsonFile(file) {
        return file.name.endsWith(".json") || file.type === 'application/json';
      }

      /**
       * Determines if the given file is a csv file.
       *
       * @param file is a file
       * @returns true if the given file is a csv file, false otherwise
       */
      static isCsvFile(file) {
        return file.name.endsWith(".csv") || file.type === 'text/csv';
      }

      /**
       * Determines if the given file is a txt file.
       *
       * @param file is a file
       * @returns true if the given file is a txt file, false otherwise
       */
      static isTxtFile(file) {
        return file.name.endsWith(".txt") || file.type === 'text/plain';
      }

      /**
       * Fetches the given list of images.
       *
       * Prerequisite: async.js.
       *
       * @param paths are the paths to the images to fetch
       * @param onDone(err, images) is called when done
       */
      static getImages(paths, onDone) {

        // listify paths
        if (!GenUtils.isArray(paths)) {
          GenUtils.assertTrue(isString(paths));
          paths = [paths];
        }

        // collect functions to fetch images
        let funcs = [];
        for (let i = 0; i < paths.length; i++) {
          funcs.push(loadFunc(paths[i]));
        }

        // fetch in parallel
        async.parallel(funcs, onDone);

        // callback static to fetch a single image
        function loadFunc(path) {
          return function(onDone) {
            let img = new Image();
            img.onload = function() { onDone(null, img); }
            img.onerror = function() { onDone(new Error("Cannot load image: " + path)); }
            img.src = path;
          }
        }
      }

      /**
       * Returns a string indentation of the given length;
       *
       * @param length is the length of the indentation
       * @returns {string} is an indentation string of the given length
       */
      static getIndent(length) {
        let str = "";
        for (let i = 0; i < length; i++) str += '  '; // two spaces
        return str;
      }

      static initPolyfills() {

        // Polyfill Object.assign()
        // Credit: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
        if (typeof Object.assign != 'static') {
          // Must be writable: true, enumerable: false, configurable: true
          Object.defineProperty(Object, "assign", {
            value: function assign(target, varArgs) { // .length of static is 2
              'use strict';
              if (target == null) { // TypeError if undefined or null
                throw new TypeError('Cannot convert undefined or null to object');
              }

              let to = Object(target);

              for (let index = 1; index < arguments.length; index++) {
                let nextSource = arguments[index];

                if (nextSource != null) { // Skip over if undefined or null
                  for (let nextKey in nextSource) {
                    // Avoid bugs when hasOwnProperty is shadowed
                    if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
                      to[nextKey] = nextSource[nextKey];
                    }
                  }
                }
              }
              return to;
            },
            writable: true,
            configurable: true
          });
        }

        /**
         * Polyfill str.replaceAt(idx, replacement).
         */
        String.prototype.replaceAt=function(idx, replacement) {
          return this.substr(0, idx) + replacement + this.substr(idx + replacement.length);
        }

        /**
         * Polyfill str.startsWith(searchString, position).
         *
         * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith#Polyfill
         */
        String.prototype.startsWith = function(searchString, position) {
          return this.substr(position || 0, searchString.length) === searchString;
        };

        /**
         * Polyfill str.endsWith(searchString, position).
         *
         * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith#Polyfill
         */
        String.prototype.endsWith = function(searchString, position) {
          if (!(position < this.length)) position = this.length;  // works better than >= because it compensates for NaN
          else position |= 0; // round position
          return this.substr(position - searchString.length, searchString.length) === searchString;
        }

        /**
         * Removes the given value from the array.
         *
         * @returns true if the value was found and removed, false otherwise
         */
        Array.prototype.removeVal = function(val) {
          var found = false;
          for (var i = 0; i < this.length; i++) {
            if (this[i] == val) {
              found = true;
              this.splice(i, 1);
              i--;
            }
          }
          return found;;
        };
      }

      /**
       * Generates a v4 UUID.
       *
       * Source: https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
       */
      static getUUID() {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
          let r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
          return v.toString(16);
        });
      }

      /**
       * Indicates if the current environment is a browser.
       *
       * @return {boolean} true if the environment is a browser, false otherwise
       */
      static isBrowser() {
        let isWorker = typeof importScripts === 'function';
        let isBrowserMain = new Function("try {return this===window;}catch(e){return false;}")();
        let isJsDom = isBrowserMain ? new Function("try {return window.navigator.userAgent.includes('jsdom');}catch(e){return false;}")() : false;
        return isWorker || (isBrowserMain && !isJsDom);
      }

      /**
       * Indicates if the current environment is a firefox-based browser.
       *
       * @return {boolean} true if the environment is a firefox-based browser, false otherwise
       */
      static isFirefox() {
        return this.isBrowser() && navigator.userAgent.indexOf("Firefox") > 0;
      }

      /**
       * Gets the IE version number.
       *
       * Credit: https://stackoverflow.com/questions/19999388/check-if-user-is-using-ie-with-jquery/21712356#21712356
       *
       * @returns the IE version number or null if not IE
       */
      static getIEVersion() {
        let ua = window.navigator.userAgent;

        let msie = ua.indexOf('MSIE ');
        if (msie > 0) {
            // IE 10 or older => return version number
            return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);
        }

        let trident = ua.indexOf('Trident/');
        if (trident > 0) {
            // IE 11 => return version number
            let rv = ua.indexOf('rv:');
            return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);
        }

        let edge = ua.indexOf('Edge/');
        if (edge > 0) {
           // Edge (IE 12+) => return version number
           return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10);
        }

        // other browser
        return null;
      }

      /**
       * Gets a parameter value.
       *
       * Credit: https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
       *
       * @param name is the name of the parameter to get the value of
       * @param url is a URL to get the parameter from, uses the window's current href if not given
       * @returns the parameter's value
       */
      static getParameterByName(name, url) {
        if (!url) url = window.location.href;
        name = name.replace(/[\[\]]/g, "\\$&");
        let regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), results = regex.exec(url);
        if (!results) return null;
        if (!results[2]) return '';
        return decodeURIComponent(results[2].replace(/\+/g, " "));
      }

      /**
       * Gets a non-cryptographically secure random number within a given range.
       *
       * @param min is the minimum range of the int to generate, inclusive
       * @param max is the maximum range of the int to generate, inclusive
       *
       * Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
       */
      static getRandomInt(min, max) {
        min = Math.ceil(min);
        max = Math.floor(max);
        return Math.floor(Math.random() * (max - min + 1)) + min;
      }

      /**
       * Gets random ints.
       *
       * @param min is the minimum range of the ints to generate, inclusive
       * @param max is the maximum range of the ints to generate, inclusive
       * @param count is the number of random ints to get
       */
      static getRandomInts(min, max, count) {
        GenUtils.assertTrue(typeof count === "number");
        let ints = [];
        for (let i = 0; i < count; i++) ints.push(GenUtils.getRandomInt(min, max));
        return ints;
      }

      /**
       * Gets a given number of unique random ints within a range.
       *
       * @param min is the minimum range of the ints to generate, inclusive
       * @param max is the maximum range of the ints to generate, inclusive
       * @param count is the number of unique random ints to get
       */
      static getUniqueRandomInts(min, max, count) {
        let ints = [];
        GenUtils.assertTrue(count >= 0);
        GenUtils.assertTrue(max - min + 1 >= count);
        while (ints.length < count) {
          let randomInt = GenUtils.getRandomInt(min, max);
          if (!ints.includes(randomInt)) ints.push(randomInt);
        }
        return ints;
      }

      /**
       * Randomize array element order in-place using Durstenfeld shuffle algorithm.
       *
       * Credit: https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
       */
      static shuffle(array) {
        for (var i = array.length - 1; i > 0; i--) {
          var j = Math.floor(Math.random() * (i + 1));
          var temp = array[i];
          array[i] = array[j];
          array[j] = temp;
        }
      }

      /**
       * Sorts an array by natural ordering.
       *
       * @param the array to sort
       */
      static sort(array) {
        array.sort((a, b) => a === b ? 0 : a > b ? 1 : -1);
      }

      /**
       * Sets the given value ensuring a previous value is not overwritten.
       *
       * TODO: remove for portability because function passing not supported in other languages, use reconcile only
       *
       * @param obj is the object to invoke the getter and setter on
       * @param getFn gets the current value
       * @param setFn sets the current value
       * @param val is the value to set iff it does not overwrite a previous value
       * @param config specifies reconciliation configuration
       *        config.resolveDefined uses defined value if true or undefined, undefined if false
       *        config.resolveTrue uses true over false if true, false over true if false, must be equal if undefined
       *        config.resolveMax uses max over min if true, min over max if false, must be equal if undefined
       * @param errMsg is the error message to throw if the values cannot be reconciled (optional)
       */
      static safeSet(obj, getFn, setFn, val, config, errMsg) {
        let curVal = getFn.call(obj);
        let reconciledVal = GenUtils.reconcile(curVal, val, config, errMsg);
        if (curVal !== reconciledVal) setFn.call(obj, reconciledVal);
      }

      /**
       * Reconciles two values.
       *
       * TODO: remove custom error message
       *
       * @param val1 is a value to reconcile
       * @param val2 is a value to reconcile
       * @param config specifies reconciliation configuration
       *        config.resolveDefined uses defined value if true or undefined, undefined if false
       *        config.resolveTrue uses true over false if true, false over true if false, must be equal if undefined
       *        config.resolveMax uses max over min if true, min over max if false, must be equal if undefined
       * @param errMsg is the error message to throw if the values cannot be reconciled (optional)
       * @returns the reconciled value if reconcilable, throws error otherwise
       */
      static reconcile(val1, val2, config, errMsg) {

        // check for equality
        if (val1 === val2) return val1;

        // check for BigInteger equality
        let comparison; // save comparison for later if applicable
        if (val1 instanceof BigInteger && val2 instanceof BigInteger) {
          comparison = val1.compare(val2);
          if (comparison === 0) return val1;
        }

        // resolve one value defined
        if (val1 === undefined || val2 === undefined) {
          if (config && config.resolveDefined === false) return undefined;  // use undefined
          else return val1 === undefined ? val2 : val1;  // use defined value
        }

        // resolve different booleans
        if (config && config.resolveTrue !== undefined && typeof val1 === "boolean" && typeof val2 === "boolean") {
          assert.equal(typeof config.resolveTrue, "boolean");
          return config.resolveTrue;
        }

        // resolve different numbers
        if (config && config.resolveMax !== undefined) {
          assert.equal(typeof config.resolveMax, "boolean");

          // resolve js numbers
          if (typeof val1 === "number" && typeof val2 === "number") {
            return config.resolveMax ? Math.max(val1, val2) : Math.min(val1, val2);
          }

          // resolve BigIntegers
          if (val1 instanceof BigInteger && val2 instanceof BigInteger) {
            return config.resolveMax ? (comparison < 0 ? val2 : val1) : (comparison < 0 ? val1 : val2);
          }
        }

        // assert deep equality
        assert.deepEqual(val1, val2, errMsg ? errMsg : "Cannot reconcile values " + val1 + " and " + val2 + " with config: " + JSON.stringify(config));
        return val1;
      }

      /**
       * Returns a human-friendly key value line.
       *
       * @param key is the key
       * @param value is the value
       * @param indent indents the line
       * @param newline specifies if the string should be terminated with a newline or not
       * @param ignoreUndefined specifies if undefined values should return an empty string
       * @returns {string} is the human-friendly key value line
       */
      static kvLine(key, value, indent = 0, newline = true, ignoreUndefined = true) {
        if (value === undefined && ignoreUndefined) return "";
        return GenUtils.getIndent(indent) + key + ": " + value + (newline ? '\n' : "");
      }

      /**
       * Replace big integers (16 or more consecutive digits) with strings in order
       * to preserve numeric precision.
       *
       * @param {string} str is the string to be modified
       * @return {string} the modified string with big numbers converted to strings
       */
      static stringifyBIs(str) {
        return str.replace(/("[^"]*"\s*:\s*)(\d{16,})/g, '$1"$2"');
      }

      /**
       * Print the current stack trace.
       *
       * @param {string} msg - optional message to print with the trace
       */
      static printStackTrace(msg) {
        try { throw new Error(msg); }
        catch (err) { console.error(err.stack); }
      }

      /**
       * Wait for the duration.
       *
       * @param {number} durationMs - the duration to wait for in milliseconds
       */
      static async waitFor(durationMs) {
        return new Promise(function(resolve) { setTimeout(resolve, durationMs); });
      }

      /**
       * Kill the given nodejs child process.
       *
       * @param {process} process - the nodejs child process to kill
       * @param {string|undefined} signal - the kill signal, e.g. SIGTERM, SIGKILL, SIGINT (default)
       * @return {Promise<number|undefined>} the exit code from killing the process
       */
      static async killProcess(process, signal) {
        return new Promise((resolve, reject) => {
          process.on("exit", function(code, signal) { resolve(code); });
          process.on("error", function(err) { reject(err); });
          try {
            if (!process.kill(signal ? signal : "SIGINT")) resolve(); // resolve immediately if not running
          } catch (err) {
            reject(err);
          }
        });
      }

      /**
       * Normalize a URI.
       *
       * @param {string} uri - the URI to normalize
       * @return {string} the normalized URI
       */
      static normalizeUri(uri) {
        if (!uri) throw Error("Must provide URI to normalize");
        uri = uri.replace(/\/$/, ""); // strip trailing slash
        if (!new RegExp("^\\w+://.+").test(uri)) uri= "http://" + uri; // assume http if protocol not given
        return uri;
      }
    }

    module.exports = GenUtils;