# adam.js

    /*
     * adam
     * https://github.com/gamtiq/adam
     *
     * Copyright (c) 2014-2020 Denis Sikuler
     * Licensed under the MIT license.
     */

    /**
     * Functions to create, process and test objects.
     *
     * @module adam
     */

    "use strict";

    /*jshint latedef:nofunc*/

    var getOwnPropertyNames, getPropertySymbols, getPrototypeOf;

    /*jshint laxbreak:true*/
    /*
     * Return own property names of given object.
     *
     * @param {Object} obj
     *      Object whose own property names should be returned.
     * @return {Array}
     *      Own property names of the given object.
     */
    getOwnPropertyNames = typeof Object.getOwnPropertyNames === "function"
                            ? Object.getOwnPropertyNames
    : function getOwnPropertyNames(obj) {
        var result = [],
            sKey;
        for (sKey in obj) {
            if (obj.hasOwnProperty(sKey)) {
                result.push(sKey);
            }
        }
        return result;
    };

    /*
     * Return prototype of given object.
     *
     * @param {Object} obj
     *      Object whose prototype should be returned.
     * @return {Object}
     *      The prototype of the given object.
     */
    getPrototypeOf = typeof Object.getPrototypeOf === "function"
                        ? Object.getPrototypeOf
    : function getPrototypeOf(obj) {
        /*jshint proto:true*/
        return obj
                ? (obj.constructor
                    ? obj.constructor.prototype
                    : (obj.__proto__ || Object.prototype)
                    )
                : null;
    };
    /*jshint laxbreak:false*/

    if (typeof Object.getOwnPropertySymbols === "function") {
        /**
         * Return list of all symbol property keys for given object including keys from prototype chain.
         *
         * This function is defined only when `Object.getOwnPropertySymbols` is available.
         *
         * @param {Object} obj
         *      Object to be processed.
         * @return {Array}
         *      List of all found symbol property keys.
         * @alias module:adam.getPropertySymbols
         */
        getPropertySymbols = function getPropertySymbols(obj) {
            var exceptList = {},
                getOwnPropertySymbols = Object.getOwnPropertySymbols,
                getPrototypeOf = Object.getPrototypeOf,
                result = [],
                nI, nL, propName, symbolList;
            if (obj && typeof obj === "object") {
                do {
                    symbolList = getOwnPropertySymbols(obj);
                    for (nI = 0, nL = symbolList.length; nI < nL; nI++) {
                        propName = symbolList[nI];
                        if (! (propName in exceptList)) {
                            result.push(propName);
                            exceptList[propName] = true;
                        }
                    }
                    obj = getPrototypeOf(obj);
                } while (obj);
            }
            return result;
        };
    }

    /**
     * Return class of given value (namely value of internal property `[[Class]]`).
     *
     * @param {Any} value
     *      Value whose class should be determined.
     * @return {String}
     *      String indicating value class.
     * @alias module:adam.getClass
     */
    function getClass(value) {
        var sClass = Object.prototype.toString.call(value);
        return sClass.substring(8, sClass.length - 1);
    }

    /**
     * Check whether given value is a `Map` or a `WeakMap`.
     *
     * @param {Any} value
     *      Value that should be checked.
     * @param {Boolean} [bWeak=false]
     *      By default the passed value is only checked for `Map`.
     *      If you specify `true` for `bWeak`, the value will be also checked for `WeakMap`.
     * @return {Boolean}
     *      `true` when the passed value is a `Map` or `WeakMap`, otherwise `false`.
     * @alias module:adam.isMap
     * @see {@link module:adam.getClass getClass}
     */
    function isMap(value, bWeak) {
        var sClass = getClass(value);
        return sClass === "Map" || (Boolean(bWeak) && sClass === "WeakMap");
    }

    /**
     * Return type of given value.
     *
     * @param {Any} value
     *      Value whose type should be determined.
     * @return {String}
     *      For `NaN` - `'nan'`, for `null` - `'null'`, otherwise - result of `typeof` operator.
     * @alias module:adam.getType
     */
    function getType(value) {
        /*jshint laxbreak:true*/
        var sType = typeof value;
        return value === null
                ? "null"
                : (sType === "number" && isNaN(value) ? "nan" : sType);
    }

    /**
     * Return number of all or filtered fields of specified object or map.
     *
     * @param {Object} obj
     *      Object or map to be processed.
     * @param {Object} [settings]
     *     Operation settings. Keys are settings names, values are corresponding settings values.
     *     The following settings are supported:
     *
     *   * `filter` - a filter specifying fields that should be counted (see {@link module:adam.checkField checkField})
     *   * `filterConnect`: `String` - a boolean connector that should be used when array of filters is specified
     *      in `filter` setting (see {@link module:adam.checkField checkField})
     * @return {Integer}
     *      Number of all or filtered fields of specified object.
     * @alias module:adam.getSize
     * @see {@link module:adam.getFields getFields}
     */
    function getSize(obj, settings) {
        return getFields(obj, settings).length;
    }

    /**
     * Check whether number of all or filtered fields of specified object/map is more than the given value.
     *
     * @param {Object} obj
     *      Object or map to be checked.
     * @param {Number} nValue
     *      Value that should be used for comparison with number of fields.
     * @param {Object} [settings]
     *     Operation settings. Keys are settings names, values are corresponding settings values.
     *     The following settings are supported:
     *
     *   * `filter` - a filter specifying fields that should be counted (see {@link module:adam.checkField checkField})
     *   * `filterConnect`: `String` - a boolean connector that should be used when array of filters is specified
     *      in `filter` setting (see {@link module:adam.checkField checkField})
     * @return {Boolean}
     *      `true`, when number of all or filtered fields is more than the given value, otherwise `false`.
     * @alias module:adam.isSizeMore
     * @see {@link module:adam.getFields getFields}
     */
    function isSizeMore(obj, nValue, settings) {
        /*jshint unused:false*/
        if (settings) {
            settings = copy(settings, {});
        }
        else {
            settings = {};
        }
        nValue++;
        settings.limit = nValue;
        return getFields(obj, settings).length === nValue;
    }

    /**
     * Check whether given value is an empty value i.e. `null`, `undefined`, `0`, empty object, empty array,
     * empty map, empty set or empty string.
     *
     * @param {Any} value
     *      Value to be checked.
     * @return {Boolean}
     *      `true` if the value is an empty value, otherwise `false`.
     * @alias module:adam.isEmpty
     * @see {@link module:adam.getClass getClass}
     * @see {@link module:adam.isSizeMore isSizeMore}
     */
    function isEmpty(value) {
        /*jshint eqeqeq:false, eqnull:true, laxbreak:true*/
        var sClass = getClass(value);
        return value == null
                || value === 0
                || value === ""
                || (typeof value === "object" && ! isSizeMore(value, 0))
                || (sClass === "Array" && value.length === 0)
                || ((sClass === "Map" || sClass === "Set") && value.size === 0);
    }

    /**
     * Check whether given value has (or does not have) specified kind (type or class).
     *
     * @param {Any} value
     *      Value to be checked.
     * @param {String} sKind
     *      Type or class for check. Can be any value that is returned by {@link module:adam.getType getType}
     *      and {@link module:adam.getClass getClass}, or one of the following:
     *
     *    * `empty` - check whether the value is empty (see {@link module:adam.isEmpty isEmpty})
     *    * `even` - check whether the value is an even integer
     *    * `false` - check whether the value is a false value
     *    * `infinity` - check whether the value is a number representing positive or negative infinity
     *    * `integer` - check whether the value is an integer number
     *    * `negative` - check whether the value is a negative number
     *    * `numeric` - check whether the value is a number or a string that can be converted to number
     *    * `odd` - check whether the value is an odd integer
     *    * `positive` - check whether the value is a positive number
     *    * `real` - check whether the value is a real number
     *    * `true` - check whether the value is a true value
     *    * `zero` - check whether the value is `0`
     *
     *      If exclamation mark (`!`) is set before the kind, it means that the check should be negated
     *      i.e. check whether given value does not have the specified kind.
     *      For example, `!real` means: check whether the value is not a real number.
     * @return {Boolean}
     *      `true` if value has the specified kind (or does not have when the check is negated), otherwise `false`.
     * @alias module:adam.isKindOf
     * @see {@link module:adam.getClass getClass}
     * @see {@link module:adam.getType getType}
     * @see {@link module:adam.isEmpty isEmpty}
     */
    function isKindOf(value, sKind) {
        /*jshint laxbreak:true*/
        var bNegate = sKind.charAt(0) === "!",
            sType = getType(value),
            bInfinity, bInteger, bResult;
        if (bNegate) {
            sKind = sKind.substring(1);
        }
        bResult= sType === sKind
                    || getClass(value) === sKind
                    || (sKind === "true" && Boolean(value))
                    || (sKind === "false" && ! value)
                    || (sKind === "empty" && isEmpty(value))
                    || (sKind === "numeric"
                        && ( sType === "number"
                                || (sType === "string" && value && ! isNaN(Number(value))) ) )
                    || (sType === "number"
                        && ((sKind === "zero" && value === 0)
                            || (sKind === "positive" && value > 0)
                            || (sKind === "negative" && value < 0)
                            || ((bInfinity = (value === Number.POSITIVE_INFINITY || value === Number.NEGATIVE_INFINITY)) && (sKind === "infinity"))
                            || (! bInfinity
                                    && ( ((bInteger = (value === Math.ceil(value))) && sKind === "integer")
                                        || (bInteger && sKind === "even" && value % 2 === 0)
                                        || (bInteger && sKind === "odd" && value % 2 !== 0)
                                        || (! bInteger && sKind === "real") )
                                )
                            )
                        );
        return bNegate ? ! bResult : bResult;
    }

    /**
     * Check whether the field of given object/map/array/set corresponds to specified condition(s) or filter(s).
     *
     * @param {Object} obj
     *      Object, map (including WeakMap), array or set (including WeakSet) to be processed.
     * @param {String | Symbol} field
     *      Field that should be checked.
     * @param {Any} filter
     *      A filter or array of filters specifying conditions that should be checked. A filter can be:
     *
     *    * a function; if the function returns a true value it means that the field corresponds to this filter;
     *      the following parameters will be passed into the function: field value, field name, reference to the object and operation settings
     *    * a regular expression; if the field value (converted to string) matches the regular expression it means
     *      that the field corresponds to this filter
     *    * a string; value can be one of the following:
     *      - `own` - if the field is own property of the object it means that the field corresponds to this filter
     *      - `!own` - if the field is not own property of the object it means that the field corresponds to this filter
     *      - any other value - is used as the check when calling {@link module:adam.isKindOf isKindOf};
     *        if {@link module:adam.isKindOf isKindOf} returns `true` for the given field value and the filter
     *        it means that the field corresponds to this filter
     *    * an object (referred below as condition);
     *      - if the object has `and` or `or` field, its value is used as subfilter and will be passed in recursive call of `checkField`
     *        as value of `filter` parameter; the field name (`and` or `or`) will be used as value of `filterConnect` setting (see below);
     *        if the result of the recursive call is `true` it means that the field corresponds to this filter
     *      - if the object has `field` field, its value is used as filter for the field name (property key) that is being checked;
     *        the filter is applied to the field name (property key) in recursive call of `checkField`
     *        as if the key is a tested field value of special object that is created for the check purposes
     *        (i.e. `checkField({field: field}, "field", filter.field, {filterConnect: settings.filterConnect})`)
     *      - if the object has `value` field, its value is used as filter; if the field value strictly equals to the filter value
     *      - if the object has `inside` field, its value is used as filter; if the filter value "contains" the field value
     *        it means that the field corresponds to this filter;
     *        the filter value can be:
     *        + an array or a string; in such case `indexOf` is used to check presence of the field value in the filter value;
     *        + a set; in such case `has` is used to check presence of the field value in the filter value;
     *        + a map or an object; when `key` field is specified in the condition, the filter value will be checked
     *           for presence of such key/value pair; if `true` is set as value for `key` field,
     *           value of `field` parameter will be used as the key;
     *           when `key` field is absent in the condition, the field value will be checked for presence
     *           in the list of values of the map/object;
     *        + any other value; in such case the field value is not inside the filter value;
     *      - in any other case if the field value strictly equals to the object it means that the field corresponds to this filter
     *    * any other value; if the field value strictly equals to the filter value it means that the field corresponds to this filter
     * @param {Object} [settings]
     *     Operation settings. Keys are settings names, values are corresponding settings values.
     *     The following settings are supported (setting's default value is specified in parentheses):
     *
     *   * `filterConnect`: `String` (`and`) - a boolean connector that should be used when array of filters is specified
     *      in `filter` parameter; valid values are the following: `and`, `or` (case-insensitive); any other value is treated as `and`
     *   * `value`: `Any` - a value that should be used for check instead of field's value
     * @return {Boolean}
     *      `true` if the field corresponds to specified filter(s), otherwise `false`.
     * @alias module:adam.checkField
     * @see {@link module:adam.getClass getClass}
     * @see {@link module:adam.isKindOf isKindOf}
     */
    function checkField(obj, field, filter, settings) {
        /*jshint laxbreak:true*/
        var sClass = getClass(obj),
            bMap = sClass === "Map" || sClass === "WeakMap",
            bSet = sClass === "Set" || sClass === "WeakSet",
            test = true,
            bAnd, data, nI, nL, sKey, sType, value;
        if (! settings) {
            settings = {};
        }
        if ("value" in settings) {
            value = settings.value;
        }
        else if (bMap) {
            value = obj.get(field);
        }
        else if (bSet) {
            value = field;
        }
        else {
            value = obj[field];
        }
        bAnd = settings.filterConnect;
        bAnd = typeof bAnd !== "string" || bAnd.toLowerCase() !== "or";
        if (getClass(filter) !== "Array") {
            filter = [filter];
        }
        for (nI = 0, nL = filter.length; nI < nL; nI++) {
            test = filter[nI];
            switch (getClass(test)) {
                case "Function":
                    test = Boolean(test(value, field, obj, settings));
                    break;
                case "String":
                    if (test === "own") {
                        test = obj[bMap || bSet ? "has" : "hasOwnProperty"](field);
                    }
                    else if (test === "!own") {
                        test = ! obj[bMap || bSet ? "has" : "hasOwnProperty"](field);
                    }
                    else {
                        test = isKindOf(value, test);
                    }
                    break;
                case "RegExp":
                    test = test.test(typeof value === "symbol" ? value.toString() : value);
                    break;
                case "Object":
                    if ("and" in test) {
                        test = checkField(obj, field, test.and, {filterConnect: "and"});
                    }
                    else if ("or" in test) {
                        test = checkField(obj, field, test.or, {filterConnect: "or"});
                    }
                    else if ("field" in test) {
                        test = checkField({field: field}, "field", test.field, {filterConnect: settings.filterConnect});
                    }
                    else if ("value" in test) {
                        test = test.value === value;
                    }
                    else if ("inside" in test) {
                        data = test.inside;
                        sType = getClass(data);
                        switch (sType) {
                            case "Array":
                            case "String":
                                test = data.indexOf(value) > -1;
                                break;
                            case "Map":
                            case "Object":
                                sKey = test.key;
                                if (sKey) {
                                    if (sKey === true) {
                                        sKey = field;
                                    }
                                    test = sType === "Map"
                                        ? (data.has(sKey) && data.get(sKey) === value)
                                        : ((sKey in data) && data[sKey] === value);
                                }
                                else {
                                    test = sType === "Map"
                                        ? (Array.from(data.values()).indexOf(value) > -1)
                                        : (getValueKey(data, value) !== null);
                                }
                                break;
                            case "Set":
                                test = data.has(value);
                                break;
                            default:
                                test = false;
                        }
                    }
                    else {
                        test = test === value;
                    }
                    break;
                default:
                    test = test === value;
            }
            if ((bAnd && ! test) || (! bAnd && test)) {
                break;
            }
        }
        return test;
    }

    /**
     * Check whether the value corresponds to specified condition(s) or filter(s).
     *
     * This function is a "wrap" for the following code:
     * ```js
     * checkField({value: value}, "value", filter, settings);
     * ```
     *
     * @param {Any} value
     *      Value that should be checked.
     * @param {Any} filter
     *      A filter or array of filters specifying conditions that should be checked.
     *      See {@link module:adam.checkField checkField} for details.
     * @param {Object} [settings]
     *     Operation settings. See {@link module:adam.checkField checkField} for details.
     * @return {Boolean}
     *      `true` if the value corresponds to specified filter(s), otherwise `false`.
     * @alias module:adam.checkValue
     * @see {@link module:adam.checkField checkField}
     */
    function checkValue(value, filter, settings) {
        return checkField({value: value}, "value", filter, settings);
    }

    /**
     * Return list of all keys of specified object/map/array/set.
     *
     * @param {Any} value
     *      Value that should be processed.
     * @return {Array | null}
     *      List of all keys of specified object/map/array/set or `null` if another value is passed.
     * @alias module:adam.getKeys
     * @see {@link module:adam.getFields getFields}
     */
    function getKeys(value) {
        var sClass = getClass(value);
        if (sClass === "Map" || sClass === "Set") {
            return Array.from( value.keys() );
        }
        else if (sClass === "Array" || getType(value) === "object") {
            return Object.keys(value);
        }
        return null;
    }

    /**
     * Return list of all or filtered fields of specified object or map.
     *
     * Fields are searched (checked) in the object itself and in its prototype chain.
     *
     * @param {Object} obj
     *      Object or map to be processed.
     * @param {Object} [settings]
     *     Operation settings. Keys are settings names, values are corresponding settings values.
     *     The following settings are supported:
     *
     *   * `filter` - a filter specifying fields that should be selected (see {@link module:adam.checkField checkField})
     *   * `filterConnect`: `String` - a boolean connector that should be used when array of filters is specified
     *      in `filter` setting (see {@link module:adam.checkField checkField})
     *   * `limit` - a maximum number of fields that should be included into result;
     *      after the specified number of fields is attained, the search will be stopped
     *   * `pairs`: `object | boolean | 'list' | 'obj'` - whether list of field-value pairs should be returned
     *      instead of list of fields; when `true` is set it is the same as `{form: 'obj'}`;
     *      when a string is set it is the same as `{form: <string>}`;
     *      an object value specifies a form of field-value pairs and can have
     *      the following fields:
     *      - `form`: `'list' | 'obj'` - whether an item of the result list should be `[field, value]` array
     *          or `{key: field, value: value}` object; default value is `'obj'`;
     *          any string that is different from `'list'` is treated as `'obj'`;
     *      - `type` - the same as `form`;
     *      - `key`: `string` - for object items of the result list specifies name of field
     *          that contains processed object's field; default value is `key`;
     *      - `value`: `string` - for object items of the result list specifies name of field
     *          that contains processed object's field value; default value is `value`;
     * @return {Array}
     *      List of all or filtered fields of specified object.
     * @alias module:adam.getFields
     * @see {@link module:adam.checkField checkField}
     * @see {@link module:adam.getKeys getKeys}
     */
    function getFields(obj, settings) {
        /*jshint latedef:false, laxbreak:true*/

        function getValue(key) {
            return bObject
                ? obj[key]
                : obj.get(key);
        }

        function isLimitReached() {
            return nLimit > 0 && result.length >= nLimit;
        }

        function processKeyList(keyList) {
            var key, nI, nL, pair;
            for (nI = 0, nL = keyList.length; nI < nL; nI++) {
                key = keyList[nI];
                if ((bMap || ! (key in addedKeyMap)) && (bAll || checkField(obj, key, filter, options))) {
                    if (sPairType === "obj") {
                        pair = {};
                        pair[sPairKey] = key;
                        pair[sPairValue] = getValue(key);
                    }
                    else if (sPairType === "list") {
                        pair = [key, getValue(key)];
                    }
                    else {
                        pair = key;
                    }
                    result.push(pair);
                    if (isLimitReached()) {
                        break;
                    }
                    if (bObject) {
                        addedKeyMap[key] = null;
                    }
                }
            }
        }

        var addedKeyMap = {},
            bAll = ! settings || ! ("filter" in settings),
            bMap = isMap(obj),
            bObject = ! bMap,
            options = settings || {},
            getOwnPropertySymbols = Object.getOwnPropertySymbols,
            bProcessSymbols = typeof getOwnPropertySymbols === "function",
            filter = bAll ? null : options.filter,
            bOwn = bAll ? false : filter === "own",
            bNotOwn = bAll ? false : filter === "!own",
            bUseFilter = bAll ? false : ! bOwn && ! bNotOwn,
            nLimit = options.limit || 0,
            pairs = options.pairs,
            result = [],
            target = obj,
            sPairKey, sPairType, sPairValue;
        if (pairs) {
            sPairType = typeof pairs;
            if (sPairType === "boolean") {
                pairs = {form: "obj"};
            }
            else if (sPairType === "string") {
                pairs = {form: pairs};
            }
            sPairType = pairs.form || pairs.type || "obj";
            if (sPairType.toLowerCase() !== "list") {
                sPairType = "obj";
            }
            sPairKey = pairs.key || "key";
            sPairValue = pairs.value || "value";
        }
        while (target) {
            if (bAll || bUseFilter || ((bOwn || bMap) && target === obj) || (bNotOwn && target !== obj)) {
                processKeyList(
                    bObject
                        ? getOwnPropertyNames(target)
                        : getKeys(target)
                );
                if (bObject && bProcessSymbols && ! isLimitReached()) {
                    processKeyList(getOwnPropertySymbols(target));
                }
                if (isLimitReached()) {
                    break;
                }
            }
            target = bObject && (bAll || bNotOwn || bUseFilter)
                        ? getPrototypeOf(target)
                        : null;
        }
        return result;
    }

    /**
     * Return the name of field (or list of names) having the specified value in the given object or map.
     *
     * @param {Object} obj
     *      Object or map to be checked.
     * @param {Any} value
     *      Value that should be searched for.
     * @param {Boolean} [bAll]
     *      Whether names of all found fields having the specified value should be returned.
     *      Default value is `false`.
     * @return {Array | String | null}
     *      Names of fields (when `bAll` is `true`) or a field name having the specified value,
     *      or `null` when the object do not contain the specified value.
     * @alias module:adam.getValueKey
     */
    function getValueKey(obj, value, bAll) {
        /*jshint laxbreak:true*/
        var result = [],
            key, list, nI, nL;
        if (isMap(obj)) {
            list = Array.from(obj.entries());
            for (nI = 0, nL = list.length; nI < nL; nI++) {
                key = list[nI];
                if (key[1] === value) {
                    if (bAll) {
                        result.push(key[0]);
                    }
                    else {
                        return key[0];
                    }
                }
            }
        }
        else {
            for (key in obj) {
                if (obj[key] === value) {
                    if (bAll) {
                        result.push(key);
                    }
                    else {
                        return key;
                    }
                }
            }
        }
        return result.length
                ? result
                : null;
    }

    /**
     * Return list of all or filtered field values of specified object or map.
     *
     * @param {Object} obj
     *      Object or map to be processed.
     * @param {Object} [settings]
     *     Operation settings. Keys are settings names, values are corresponding settings values.
     *     The following settings are supported:
     *
     *   * `filter` - a filter specifying fields that should be selected (see {@link module:adam.checkField checkField})
     *   * `filterConnect`: `String` - a boolean connector that should be used when array of filters is specified
     *      in `filter` setting (see {@link module:adam.checkField checkField})
     * @return {Array}
     *      List of all or filtered field values of specified object.
     * @alias module:adam.getValues
     * @see {@link module:adam.checkField checkField}
     */
    function getValues(obj, settings) {
        /*jshint laxbreak:true*/
        var result = [],
            bObject = ! isMap(obj),
            bAll = ! settings || ! ("filter" in settings),
            filter = bAll ? null : settings.filter,
            key, keyList, nI, nL;

        function processKey(keyValue) {
            if (bAll || checkField(obj, keyValue, filter, settings)) {
                result[result.length] = bObject
                    ? obj[keyValue]
                    : obj.get(keyValue);
            }
        }

        if (bObject) {
            for (key in obj) {
                processKey(key);
            }
        }
        else {
            keyList = getKeys(obj);
            for (nI = 0, nL = keyList.length; nI < nL; nI++) {
                processKey(keyList[nI]);
            }
        }

        return result;
    }

    /**
     * Return name of first free (absent) field/key of specified object/map, that conforms to the following pattern:
     * &lt;prefix&gt;&lt;number&gt;
     *
     * @param {Object} obj
     *      Object or map in which a free field/key should be found.
     *      If `null` (or any false value) is set, the first value that conforms to the pattern will be returned.
     * @param {Object} [settings]
     *     Operation settings. Keys are settings names, values are corresponding settings values.
     *     The following settings are supported (setting's default value is specified in parentheses):
     *
     *   * `checkPrefix`: `Boolean` (`false`) - specifies whether pattern consisting only from prefix (without number) should be checked
     *   * `prefix`: `String` (`f`) - prefix of sought field
     *   * `startNum`: `Integer` (`0`) - starting number which is used as part of pattern by search/check
     * @return {String}
     *      Name of field which is absent in the specified object and conforms to the pattern.
     * @alias module:adam.getFreeField
     */
    function getFreeField(obj, settings) {
        var bObject = ! isMap(obj),
            bCheckPrefix, nStartNum, sField, sPrefix;
        if (! settings) {
            settings = {};
        }
        sPrefix = settings.prefix;
        nStartNum = settings.startNum;
        bCheckPrefix = settings.checkPrefix;
        if (typeof sPrefix !== "string") {
            sPrefix = "f";
        }
        if (! nStartNum) {
            nStartNum = 0;
        }
        if (bCheckPrefix) {
            sField = sPrefix;
            nStartNum--;
        }
        else {
            sField = sPrefix + nStartNum;
        }
        if (obj) {
            while (bObject ? (sField in obj) : obj.has(sField)) {
                sField = sPrefix + (++nStartNum);
            }
        }
        return sField;
    }

    /**
     * Create object (map) from list of objects/maps.
     * Fields of the created object are values of specified field/key of objects/maps,
     * values of the created object are corresponding items of the list.
     *
     * @param {Array} list
     *      List of objects/values to be processed.
     * @param {Function | String} [keyField]
     *      Specifies names of fields of the created object. Can be name of field or method whose value is used
     *      as field name of the created object, or function that returns the field name.
     *      In the latter case the following parameters will be passed in the function:
     *      the source object (an item of the list), the created object, the index of the source object, operation settings.
     *      When the parameter is not set, items of the list are used as field names.
     * @param {Object} [settings]
     *     Operation settings. Keys are settings names, values are corresponding settings values.
     *     The following settings are supported (setting's default value is specified in parentheses):
     *
     *   * `deleteKeyField`: `Boolean` (`false`) - specifies whether key field (whose value is field name of the created object)
     *     should be deleted from the source object (an item of the list)
     *   * `filter` - a filter specifying objects that should be included into result (see {@link module:adam.checkField checkField});
     *     an item of the list will be used as value for check
     *   * `filterConnect`: `String` - a boolean connector that should be used when array of filters is specified
     *      in `filter` setting (see {@link module:adam.checkField checkField})
     * @return {Object}
     *      Object created from the given list.
     * @alias module:adam.fromArray
     * @see {@link module:adam.checkField checkField}
     */
    function fromArray(list, keyField, settings) {
        /*jshint laxbreak:true*/
        var nL = list.length,
            result = {},
            bAll, bMap, bDeleteKeyField, bFuncKey, filter, filterConnect, item, field, nI, sKeyName;
        if (nL) {
            if (! settings) {
                settings = {};
            }
            bAll = ! ("filter" in settings);
            filter = bAll ? null : settings.filter;
            filterConnect = settings.filterConnect;
            bFuncKey = typeof keyField === "function";
            bDeleteKeyField = Boolean(settings.deleteKeyField && keyField);
            if (! bFuncKey) {
                sKeyName = keyField;
            }
            for (nI = 0; nI < nL; nI++) {
                item = list[nI];
                bMap = isMap(item, true);
                if (bFuncKey) {
                    field = sKeyName = keyField(item, result, nI, settings);
                }
                else {
                    field = sKeyName
                        ? (bMap
                            ? item.get(sKeyName)
                            : item[sKeyName]
                        )
                        : item;
                    if (typeof field === "function") {
                        field = field.call(item);
                    }
                }
                if (bAll || checkField(result, field, filter, {value: item, filterConnect: filterConnect})) {
                    if (bDeleteKeyField) {
                        if (bMap) {
                            item.delete(sKeyName);
                        }
                        else {
                            delete item[sKeyName];
                        }
                    }
                    result[field] = item;
                }
            }
        }
        return result;
    }

    /**
     * Return the value of the first element/field in the passed array/object/map/set that satisfies the specified filter(s).
     *
     * If value passed for selection is not an array/set nor an object/map and `settings.defaultValue` is not set, the value will be returned as is.
     * If no element in the passed array/set satisfies the specified filter(s),
     * `settings.defaultValue` or the last element of the array/set (or `undefined` when the array is empty) will be returned.
     * If no field in the passed object/map satisfies the specified filter(s), `settings.defaultValue` or `undefined` will be returned.
     *
     * @param {Any} filter
     *      Filter that should be used to select a value (see {@link module:adam.checkField checkField} for details).
     * @param {Any} from
     *      An array/object/map/set from which a value should be selected.
     * @param {Object} [settings]
     *     Operation settings. Keys are settings names, values are corresponding settings values.
     *     The following settings are supported (setting's default value is specified in parentheses):
     *
     *   * `defaultValue`: `Any` - default value that should be used when no element/field in the passed array/object/map/set
     *      satisfies the specified filter(s) or when value of `from` parameter is not an array/set nor an object/map
     *   * `filterConnect`: `String` (`and`) - a boolean connector that should be used when array of filters is specified
     *      in `filter` parameter (see {@link module:adam.checkField checkField} for details)
     * @return {Any}
     *      The value of first element/field in the passed array/object/map/set that satisfies the specified filter(s),
     *      or `settings.defaultValue`, or the value of `from` parameter or `undefined`.
     * @alias module:adam.select
     * @see {@link module:adam.checkField checkField}
     */
    function select(filter, from, settings) {
        /*jshint latedef:false, laxbreak:true*/
        function getDefaultValue(value) {
            return "defaultValue" in options
                ? options.defaultValue
                : value;
        }

        var sClass = getClass(from),
            options = settings || {},
            result = getDefaultValue(from),
            key, keyList, nI, nL, nLast;
        if (sClass === "Array" || sClass === "Set") {
            if (sClass === "Set") {
                from = getKeys(from);
            }
            nL = from.length;
            if (nL) {
                nLast = nL - 1;
                for (nI = 0; nI < nL; nI++) {
                    if (checkField(from, nI, filter, options)) {
                        result = from[nI];
                        break;
                    }
                    else if (nI === nLast) {
                        result = getDefaultValue(from[nI]);
                    }
                }
            }
            else {
                result = getDefaultValue(key);
            }
        }
        else if (sClass === "Map") {
            result = getDefaultValue(key);
            keyList = getKeys(from);
            for (nI = 0, nL = keyList.length; nI < nL; nI++) {
                key = keyList[nI];
                if (checkField(from, key, filter, options)) {
                    result = from.get(key);
                    break;
                }
            }
        }
        else if (getType(from) === "object") {
            result = getDefaultValue(key);
            for (key in from) {
                if (checkField(from, key, filter, options)) {
                    result = from[key];
                    break;
                }
            }
        }
        return result;
    }

    /**
     * Divide given object or map into 2 parts: the first part includes specified fields, the second part includes all other fields.
     *
     * @param {Object} obj
     *      Object or map to be divided.
     * @param {Array | Object | null} firstObjFields
     *      List of names of fields that should be included in the first part,
     *      or an object defining those fields.
     *      If value is not specified (`null` or `undefined`), `filter` setting should be used to divide fields into parts.
     * @param {Object} [settings]
     *     Operation settings. Keys are settings names, values are corresponding settings values.
     *     The following settings are supported:
     *
     *   * `filter` - a filter that should be used to divide fields into parts (see {@link module:adam.checkField checkField});
     *      fields conforming to the filter will be included in the first part,
     *      fields that do not conform to the filter will be included in the second part
     *   * `filterConnect`: `String` - a boolean connector that should be used when array of filters is specified
     *      in `filter` setting (see {@link module:adam.checkField checkField})
     * @return {Array}
     *      Created parts: item with index 0 is the first part, item with index 1 is the second part.
     * @alias module:adam.split
     * @see {@link module:adam.checkField checkField}
     */
    function split(obj, firstObjFields, settings) {
        /*jshint laxbreak:true*/
        var bObject = ! isMap(obj),
            first = bObject ? {} : new Map(),
            second = bObject ? {} : new Map(),
            result = [first, second],
            bByName, filter, key, keyList, nI, nL;

        function getTarget() {
            return (bByName ? key in firstObjFields : checkField(obj, key, filter, settings))
                ? first
                : second;
        }

        if (! settings) {
            settings = {};
        }
        if (firstObjFields) {
            bByName = true;
            if (typeof firstObjFields.length === "number") {
                firstObjFields = fromArray(firstObjFields);
            }
        }
        else {
            bByName = false;
            filter = settings.filter;
        }
        if (bObject) {
            for (key in obj) {
                getTarget()[key] = obj[key];
            }
        }
        else {
            keyList = getKeys(obj);
            for (nI = 0, nL = keyList.length; nI < nL; nI++) {
                key = keyList[nI];
                getTarget().set(key, obj.get(key));
            }
        }
        return result;
    }

    /**
     * Remove filtered fields/elements from specified object/map/array/set.
     *
     * @param {Array | Object} obj
     *      Array, set, object or map to be processed.
     * @param {Any} filter
     *      A filter or array of filters specifying fields or elements that should be removed
     *      (see {@link module:adam.checkField checkField}).
     * @param {Object} [settings]
     *     Operation settings. Keys are settings names, values are corresponding settings values.
     *     The following settings are supported:
     *
     *   * `filterConnect`: `String` - a boolean connector that should be used when array of filters is specified
     *      in `filter` parameter (see {@link module:adam.checkField checkField})
     * @return {Array | Object}
     *      Processed array, set, object or map.
     * @alias module:adam.remove
     * @see {@link module:adam.checkField checkField}
     */
    function remove(obj, filter, settings) {
        var key, keyList, nI, nL;
        if (obj && typeof obj === "object") {
            switch (getClass(obj)) {
                case "Array":
                    key = obj.length;
                    while(key--) {
                        if (checkField(obj, key, filter, settings)) {
                            obj.splice(key, 1);
                        }
                    }
                    break;
                case "Map":
                case "Set":
                    keyList = getKeys(obj);
                    for (nI = 0, nL = keyList.length; nI < nL; nI++) {
                        key = keyList[nI];
                        if (checkField(obj, key, filter, settings)) {
                            obj.delete(key);
                        }
                    }
                    break;
                default:
                    for (key in obj) {
                        if (checkField(obj, key, filter, settings)) {
                            delete obj[key];
                        }
                    }
            }
        }
        return obj;
    }

    /**
     * Empty the given value according to the following rules:
     *
     *   * for array, `Map` or `Set`: removes all elements from the value
     *   * for object: removes all own fields from the value
     *   * for string: returns empty string
     *   * for number: returns `0`
     *   * otherwise: returns `undefined`
     *
     * @param {Any} value
     *      Value to be processed.
     * @return {Any}
     *      Processed value (for array, object, `Map` or `Set`) or empty value corresponding to the given value.
     * @alias module:adam.empty
     */
    function empty(value) {
        var sField, sType;
        switch (getType(value)) {
            case "object":
                sType = getClass(value);
                if (sType === "Array") {
                    value.length = 0;
                }
                else if (sType === "Map" || sType === "Set") {
                    value.clear();
                }
                else {
                    for (sField in value) {
                        delete value[sField];
                    }
                }
                break;
            case "string":
                value = "";
                break;
            case "number":
                value = 0;
                break;
            default:
                value = sField;
        }
        return value;
    }

    /**
     * Reverse or negate the given value according to the following rules:
     *
     *   * for array or set: creates a copy and reverses order of elements in it
     *   * for object or map: creates a copy where fields are  swapped with their values (i.e. for `{a: "b", c: "d"}` returns `{b: "a", d: "c"}`)
     *   * for string: reverses order of its characters
     *   * for number: returns negated value (i.e. `- value`)
     *   * for boolean: returns negated value (i.e. `! value`)
     *   * otherwise: returns source value without modification
     *
     * @param {Any} value
     *      Value to be processed.
     * @return {Any}
     *      Processed value.
     * @alias module:adam.reverse
     */
    function reverse(value) {
        var cache, item, nI, nL, sField;
        switch (getType(value)) {
            case "object":
                switch (getClass(value)) {
                    case "Array":
                        value = value.slice(0).reverse();
                        break;
                    case "Map":
                        cache = Array.from(value.entries());
                        value = new Map();
                        for (nI = 0, nL = cache.length; nI < nL; nI++) {
                            item = cache[nI];
                            value.set(item[1], item[0]);
                        }
                        break;
                    case "Set":
                        value = new Set( getKeys(value) );
                        break;
                    default:
                        cache = {};
                        for (sField in value) {
                            cache[ value[sField] ] = sField;
                        }
                        value = cache;
                }
                break;
            case "string":
                value = value.split("").reverse().join("");
                break;
            case "number":
                value = - value;
                break;
            case "boolean":
                value = ! value;
                break;
        }
        return value;
    }

    /**
     * Transform the given value applying the specified operation.
     *
     * @param {Any} value
     *      Value to be transformed.
     * @param {String} sAction
     *      Operation that should be applied to transform the value. Can be one of the following:
     *
     *    * `array` - convert the value to array (using `Array(value)`)
     *    * `boolean` - convert the value to boolean value (using `Boolean(value)`)
     *    * `empty` - empty the value (see {@link module:adam.empty empty})
     *    * `function` - convert the value to function (using `Function(value)`)
     *    * `integer` - try to convert the value to an integer number (using `Math.round(Number(value)`)
     *    * `map` - try to convert the value to Map (using `new Map(value)` or `new Map([[value, value]])`)
     *    * `number` - try to convert the value to number (using `Number(value)`)
     *    * `object` - convert the value to object (using `Object(value)`)
     *    * `promise` or `resolve` - convert the value to resolved promise (using `Promise.resolve(value)`)
     *    * `reject` - convert the value to rejected promise (using `Promise.reject(value)`)
     *    * `reverse` - reverse the value (see {@link module:adam.reverse reverse})
     *    * `set` - try to convert the value to Set (using `new Set(value)` or `new Set([value])`)
     *    * `string` - convert the value to string (using `String(value)`)
     *    * otherwise - source value
     * @return {Any}
     *      Transformed value.
     * @alias module:adam.transform
     * @see {@link module:adam.empty empty}
     * @see {@link module:adam.reverse reverse}
     */
    function transform(value, sAction) {
        /*jshint evil:true, -W064*/
        var result;
        switch (sAction) {
            case "array":
                result = Array(value);
                break;
            case "boolean":
                result = Boolean(value);
                break;
            case "empty":
                result = empty(value);
                break;
            case "function":
                result = Function(value);
                break;
            case "integer":
                result = Math.round(Number(value));
                break;
            case "map":
                try {
                    result = new Map(value);
                }
                catch (e) {
                    result = new Map([[value, value]]);
                }
                break;
            case "number":
                result = Number(value);
                break;
            case "object":
                result = Object(value);
                break;
            case "promise":
            case "resolve":
                result = Promise.resolve(value);
                break;
            case "reject":
                result = Promise.reject(value);
                break;
            case "reverse":
                result = reverse(value);
                break;
            case "set":
                try {
                    result = new Set(value);
                }
                catch (e) {
                    result = new Set([value]);
                }
                break;
            case "string":
                result = String(value);
                break;
            default:
                result = value;
        }
        return result;
    }

    /**
     * Copy all or filtered fields from source object/map/array/set into target object/map/array/set, applying specified transformation if necessary.
     *
     * @param {Object} source
     *      Object/map/array/set whose fields will be copied.
     * @param {Object} target
     *      Object/map/array/set into which fields should be copied.
     * @param {Object} [settings]
     *     Operation settings. Keys are settings names, values are corresponding settings values.
     *     The following settings are supported:
     *
     *   * `filter` - a filter that should be used to select fields for copying (see {@link module:adam.checkField checkField});
     *      fields conforming to the filter will be copied
     *   * `filterConnect`: `String` - a boolean connector that should be used when array of filters is specified
     *      in `filter` setting (see {@link module:adam.checkField checkField})
     *   * `transform` - an action/operation that should be applied to get field's value that will be copied
     *      instead of value from source object; can be a string specifying transformation (see {@link module:adam.transform transform})
     *      or a function whose result will be used as field's value; object with the following fields will be passed into the function:
     *      - `field` - field name
     *      - `value` field value from source object
     *      - `source` - reference to the source object
     *      - `target` - reference to the target object
     *      - `settings` - operation settings
     * @return {Object}
     *      Reference to the target object (value of `target` parameter).
     * @alias module:adam.copy
     * @see {@link module:adam.checkField checkField}
     * @see {@link module:adam.transform transform}
     */
    function copy(source, target, settings) {
        /*jshint laxbreak:true*/
        var bAll = true,
            sClass = getClass(source),
            bMap = sClass === "Map",
            bSet = sClass === "Set",
            bFuncAction, action, filter, key, keyList, nI, nL;

        function copyValue() {
            var value;
            if (bAll || checkField(source, key, filter, settings)) {
                if (bMap) {
                    value = source.get(key);
                }
                else if (bSet) {
                    value = key;
                }
                else {
                    value = source[key];
                }
                if (action) {
                    value = bFuncAction
                        ? action({source: source, target: target, field: key, value: value, settings: settings})
                        : transform(value, action);
                }
                if (bMap) {
                    target.set(key, value);
                }
                else if (bSet) {
                    target.add(value);
                }
                else {
                    target[key] = value;
                }
            }
        }

        if (! settings) {
            settings = {};
        }
        if ("filter" in settings) {
            filter = settings.filter;
            bAll = false;
        }
        action = settings.transform;
        if (typeof action === "function") {
            bFuncAction = true;
        }
        if (bMap || bSet) {
            keyList = getKeys(source);
            for (nI = 0, nL = keyList.length; nI < nL; nI++) {
                key = keyList[nI];
                copyValue();
            }
        }
        else {
            for (key in source) {
                copyValue();
            }
        }
        return target;
    }

    /**
     * Change all or filtered fields of object/map/array, applying specified action/transformation.
     *
     * @param {Object} obj
     *      Object/map/array whose fields should be changed.
     * @param {Function | String} action
     *      An action/operation that should be applied to get new field value.
     *      See description of `transform` setting of {@link module:adam.copy copy}.
     * @param {Object} [settings]
     *     Operation settings. Keys are settings names, values are corresponding settings values.
     *     The following settings are supported:
     *
     *   * `filter` - a filter that should be used to select fields for modification (see {@link module:adam.checkField checkField});
     *      fields conforming to the filter will be changed
     *   * `filterConnect`: `String` - a boolean connector that should be used when array of filters is specified
     *      in `filter` setting (see {@link module:adam.checkField checkField})
     * @return {Object}
     *      Modified object (value of `obj` parameter).
     * @alias module:adam.change
     * @see {@link module:adam.checkField checkField}
     * @see {@link module:adam.copy copy}
     */
    function change(obj, action, settings) {
        /*jshint laxbreak:true*/
        settings = settings
                    ? copy(settings, {})
                    : {};
        settings.transform = action;
        return copy(obj, obj, settings);
    }

    /**
     * Create new object containing all or filtered fields of the source object/map/array/set,
     * applying specified action/transformation for field values.
     *
     * @param {Array | Object} obj
     *      Object/map/array/set whose fields should be copied.
     * @param {Function | String} action
     *      An action/operation that should be applied to get field value that will be saved in created object.
     *      See description of `transform` setting of {@link module:adam.copy copy}.
     * @param {Object} [settings]
     *     Operation settings. Keys are settings names, values are corresponding settings values.
     *     The following settings are supported:
     *
     *   * `filter` - a filter that should be used to select fields for copying (see {@link module:adam.checkField checkField});
     *      fields conforming to the filter will be copied in created object
     *   * `filterConnect`: `String` - a boolean connector that should be used when array of filters is specified
     *      in `filter` setting (see {@link module:adam.checkField checkField})
     * @return {Object}
     *      Created object containing processed fields.
     * @alias module:adam.map
     * @see {@link module:adam.checkField checkField}
     * @see {@link module:adam.copy copy}
     */
    function map(obj, action, settings) {
        /*jshint laxbreak:true*/
        var target;
        switch (getClass(obj)) {
            case "Array":
                target = [];
                break;
            case "Map":
                target = new Map();
                break;
            case "Set":
                target = new Set();
                break;
            default:
                target = {};
        }
        settings = settings
                    ? copy(settings, {})
                    : {};
        settings.transform = action;
        return copy(obj, target, settings);
    }

    // Exports

    module.exports = {
        change: change,
        checkField: checkField,
        checkValue: checkValue,
        copy: copy,
        empty: empty,
        fromArray: fromArray,
        getClass: getClass,
        getFields: getFields,
        getFreeField: getFreeField,
        getKeys: getKeys,
        getPropertySymbols: getPropertySymbols,
        getSize: getSize,
        getType: getType,
        getValueKey: getValueKey,
        getValues: getValues,
        isEmpty: isEmpty,
        isKindOf: isKindOf,
        isMap: isMap,
        isSizeMore: isSizeMore,
        map: map,
        remove: remove,
        reverse: reverse,
        select: select,
        split: split,
        transform: transform
    };