Source: Tracker/buffer.js

/**
 * @class
 * @name BufferManager
 * @public
 * @param tag {object} Instance of the Tag used
 * @description
 * <h2>Buffer manager (data for hits)</h2>
 * <br /><b>Option notion</b>
 * <br />An option is the way to 'typify' a parameter. Indeed it can be very useful to specify in which hits a
 * parameter must appear or how (encoding for example).
 * <br /><br /><b>Example</b>
 * <pre>
 * Here is the list of the options :
 * - encode: (boolean) if true, the parameter will be encoded
 * - permanent: (boolean) if true, the parameter won't be delete after sending. So it will be sent in all other hits (if
 * other options allow it).
 * - separator: (char) when the parameter is an array its value in the hit is the concatenation array values separated
 * by a ','. This option allow to use an other separator.
 * - hitType: (Array{string}) list the hit types in which the parameter can be sent. It's possible to use a non-existent
 * type of hit but here is the existing ones :
 * * 'page': for impression hit
 * * 'all': for all type of hit
 * </pre>
 * <b>Filter notion</b>
 * <br />'Filters' allow to select a set of parameters by their options. One filter is formed by an option name
 * and its desired value(s). The filter value MUST BE of the same type as the option value type.
 * <br /><br />The 'filter' is a tuple, an Array of two elements. The first one is the option name we want to analyse, the second
 * is the value(s) we want to filter.
 * <br />Actually there's two cases : the option value type is a list or not. In the second case it's easy : the couple
 * key/value in filter is the key/value to be retrieved. In the first case the filter will match with options which
 * contain at least one of the value of the filter.
 * <br />When a list of filters is used to retrieve some parameters, the result will be the intersection of the filters results.
 * <br /><br /><b>Example</b>
 * <pre>
 * Here is a list of parameters with their options :
 * param1 => {permanent: true, encode: true}
 * param2 => {permanent: true, hitType: ['page', 'click']}
 * param3 => {hitType: ['click']}
 * param4 => {permanent: true, hitType: ['page', 'rm']}
 * param5 => {hitType: ['page', 'rm']}
 *
 * Here is the filters results :
 * [[permanent, true]] => param1, param2, param4
 * [[hitType, ['page']]] => param2, param4, param5
 * [[hitType, ['page', 'click']]] => param1, param2, param4, param5 (hitType 'page' OR 'click')
 * [[permanent, true], [hitType, ['page']]] => param1, param2, param4 (hitType 'page' AND permanent true)
 *
 * If we want parameters with a hitType 'page' AND 'click' we must make TWO filter:
 * [[hitType, ['page']], [hitType, ['click']]] => param2
 * </pre>
 */
var BufferManager = function (tag) {

    'use strict';

    var self = this;

    /**
     * Buffer containing all hit variables with their options.
     * @memberof BufferManager#
     * @type {object}
     * @private
     */
    var _dataStore = {};

    /**
     * Set value for a hit variable (overrides if present).
     * @name set
     * @memberof BufferManager#
     * @function
     * @param name {string} Name of the hit variable
     * @param value {string|number|function|Array} value of the hit variable
     * @param options {object} Configuration of the variable, if no hitType defined, it will be "page" by default
     * @public
     */
    self.set = function (name, value, options) {
        var bufferParam = ATInternet.Utils.privacy.testBufferParam(name, value);
        if (bufferParam.toSetInBuffer) {
            options = options || {};
            options.hitType = options.hitType || ['page'];
            _dataStore[name] = {_value: bufferParam.value, _options: options};
        }
        /* @if debug */
        tag.debug('Tracker:Buffer:setParam', 'DEBUG', '', {
            name: name,
            value: value,
            options: options,
            buffer: _dataStore
        });
        /* @endif */
    };

    /**
     * Get parameter from buffer.
     * @memberof BufferManager#
     * @function
     * @param buffer {object} Data store
     * @param key {object} Name of the property to get
     * @param withOptions {boolean} If true only returns value else returns object with value and options
     * @returns {string|object}
     * @private
     */
    var _getParam = function (buffer, key, withOptions) {
        var param = ATInternet.Utils.cloneSimpleObject(buffer[key]);
        if (param && !withOptions) {
            return param._value;
        }
        return param;
    };

    /**
     * Recursive method, which filters dataStore keys which are wanted.
     * @memberof BufferManager#
     * @function
     * @param filterList {Array} List of key/value(s) in an array. (ex : [[key1,value1],[key2,[value2A,value2B]]]) Filter on variable's options
     * @param keys {Array} List of keys in the dataStore, then for each call (recursivity) this is the keys corresponding to the previous filter
     * @returns {Array} Array containing all keys of the dataStore corresponding to all filters given
     * @private
     */
    var _getFilteredParamKeys = function getFilteredParamKeys(filterList, keys) {
        if (!filterList || !(keys instanceof Array) || !(filterList instanceof Array)) {
            return [];
        } else if (filterList.length === 0) {
            return keys;
        }
        var tuple = filterList[0],
            tupleValSize,
            newKeys = [],
            newFilterList = ATInternet.Utils.cloneSimpleObject(filterList);
        newFilterList.shift();
        for (var i = 0; i < keys.length; i++) {
            if (typeof tuple[1] !== 'object') {// on ne regroupe pas les conditions afin de ne passer dans le else QUE si c'est un type object
                if (_dataStore[keys[i]] && _dataStore[keys[i]]['_options'][tuple[0]] === tuple[1]) {
                    newKeys.push(keys[i]);
                }
            } else {
                tupleValSize = tuple[1].length;
                for (var j = 0; j < tupleValSize; j++) {
                    if (_dataStore[keys[i]] && _dataStore[keys[i]]['_options'][tuple[0]] instanceof Array &&
                        ATInternet.Utils.arrayIndexOf(_dataStore[keys[i]]['_options'][tuple[0]], tuple[1][j]) >= 0) {
                        newKeys.push(keys[i]);
                        break;
                    }
                }
            }
        }
        return getFilteredParamKeys(newFilterList, newKeys);
    };

    /**
     * Get variables in the buffer using the filter given, with possibility of returning options or not.
     * @name get
     * @memberof BufferManager#
     * @function
     * @param filterList {Array} List of key/value(s) in an array. (ex : [[key1,value1],[key2,[value2A,value2B]]]) Filter on variable's options
     * @param withOptions {boolean} If true only returns value else returns object with value and options
     * @returns {string|object}
     * @public
     * @example
     * <pre><code class="javascript">// this filter will get variables with hitType 'page' OR 'all', AND with permanent true
     * var filter = [
     *  ['hitType',['page','all']],
     *  ['permanent',true]
     * ]
     *
     * var dataObj = buffer.get(filter, true); // true to get options
     * dataObj = {
     *  'variableExample' : {
     *      _value:'value',
     *      _options: {
     *          hitType:['page'],
     *          permanent:true
     *      }
     *   }
     * }
     *
     * var dataObj = buffer.get(filter); // no option in results
     * dataObj = {
     *  'variableExample':'value'
     * }
     * </code></pre>
     */
    self.get = function (filterList, withOptions) {
        var result = {};
        if (typeof filterList === 'string') {
            result = _getParam(_dataStore, filterList, withOptions);
        } else {
            // on filtre les clés qui respectent ce qu'on souhaite
            var keys = _getFilteredParamKeys(filterList, ATInternet.Utils.getObjectKeys(_dataStore));
            // on va chercher les clés filtrées dans le buffer avec ou sans leurs options
            for (var index = 0; index < keys.length; index++) {
                result[keys[index]] = _getParam(_dataStore, keys[index], withOptions);
            }
        }
        return result;
    };

    /**
     * Check if an option is handled by at least one the filters.
     * @name presentInFilters
     * @memberof BufferManager#
     * @function
     * @param filters {Array} List of buffer filters
     * @param name {string} Option name
     * @returns {boolean}
     * @public
     */
    self.presentInFilters = function (filters, name) {
        if (!filters || filters.length === 0) {
            return false;
        }
        if (filters[0][0] === name) {
            return true;
        } else {
            return self.presentInFilters(filters.slice(1), name);
        }
    };

    /**
     * @description
     * Complete the filters. In all cases, if the given option is not already handled by the filters,
     * a new filter will be added.
     * <br />Otherwise the behavior is different according to the type of the given value :
     * <ul>
     *     <li>if it's an Array, all the filter which handle the option will be completed with the value. It is assumed that these filters already contains an Array value,</li>
     *     <li>in other cases nothing happens.</li>
     * </ul>
     * @name addInFilters
     * @memberof BufferManager#
     * @function
     * @param filters {Array} List of buffer filters
     * @param name {string} Option name
     * @param val {Array|*}
     * @param found {boolean} DO NOT SPECIFY IT
     * @public
     * @example
     * <pre><code class="javascript">addInFilters(myfilters, 'permanent', true);
     * //[]                                           =>  [["permanent", true]]
     * //[["hitType", ["click"]]]                     =>  [["hitType", ["click"]], ["permanent", true]]
     * //[["hitType", ["page"]], ["permanent", true]] =>  [["hitType", ["page"]], ["permanent", true]]]
     *
     * addInFilters(myfilters, 'hitType', ['all']);
     * //[["hitType", ["page"]]                                      =>  [["hitType", ["page", "all"]]]
     * //[["permanent", true]]                                       =>  [["permanent", true], ["hitType", "all"]]
     * //[["hitType", ["click"]], ["permanent", true]]               =>  [["hitType", ["click", "all"]], ["permanent", true]]
     * //[["hitType", ["page"]], ["permanent", true]]                =>  [["hitType", ["page", "all"]], ["permanent", true]]
     * //[["hitType", ["page"]], ["hitType", ["rm"]]]                =>  [["hitType", ["page", "all"]], ["hitType", ["rm", "all"]]]
     * //[["hitType", ["page"]], ["hitType", ["rm", "all"]]]         =>  [["hitType", ["page", "all"]], ["hitType", ["rm", "all"]]]
     * //[["hitType", ["page", "all"]], ["hitType", ["rm", "all"]]]  =>  [["hitType", ["page", "all"]], ["hitType", ["rm", "all"]]]
     * </code></pre>
     */
    self.addInFilters = function (filters, name, val, found) {
        if (!filters || filters.length === 0) {
            if (found) {
                return [];
            } else {
                return [[name, val]];
            }
        }
        var fname = filters[0][0], fval = filters[0][1];
        if (fname === name) {
            if (fval instanceof Array && (ATInternet.Utils.arrayIndexOf(fval, val[0]) === -1)) {
                fval.push(val[0]);
            }
            found = true;
        }
        return [[fname, fval]].concat(self.addInFilters(filters.slice(1), name, val, found));
    };

    /**
     * Delete a hit variable in the buffer.
     * @name del
     * @memberof BufferManager#
     * @function
     * @param name {string} Name of the hit variable
     * @public
     */
    self.del = function (name) {
        _dataStore[name] = undefined;
        /* @if debug */
        tag.debug('Tracker:Buffer:delParam', 'DEBUG', '', {name: name, buffer: _dataStore});
        /* @endif */
    };

    /**
     * Empty the buffer.
     * @name clear
     * @memberof BufferManager#
     * @function
     * @public
     */
    self.clear = function () {
        _dataStore = {};
    };

    // For unit tests on private elements !!!
    /* @if test */
    self._getFilteredParamKeys = _getFilteredParamKeys;
    self._dataStore = _dataStore;
    /* @endif */
};