importPackage(Packages.de.elo.ix.client);

//@include lib_Class.js
//@include lib_sol.common.Template.js
//@include lib_sol.common.WfUtils.js
//@include lib_sol.common.JsonUtils.js
//@include lib_sol.common.RepoUtils.js
//@include lib_sol.common.SordUtils.js
//@include lib_sol.common.DateUtils.js
//@include lib_sol.common.ObjectUtils.js
//@include lib_sol.common.ObjectSortUtils.js
//@include lib_sol.common.IxUtils.js
//@include lib_sol.common.ix.RfUtils.js
//@include lib_sol.common.ix.FunctionBase.js
//@include lib_sol.common.Injection.js


/**
 * Iterates over a sord's MapTable(s) or elements provided via parameter or a service call and calls a registered function for each element.
 *
 * Each row/element is passed to the function as parameter. The parameter name must be defined
 * as options.elementArg and could be called "sordMetadata".
 *
 * Scroll down for an example using a service as data source instead of a MAPTABLE.
 *
 * ### Example: Short Intro
 *
 * Define the table structure in `columns`. Omit the "1" from the end of the field name which you
 * defined in the form-editor.
 *
 * If this function is running in a workflow, it will have access to objId and flowId automatically.
 * Otherwise, define both as a parameter.
 *
 *     {
 *       "objId": "5203", // optional, when in workflow
 *       "flowId": 59,    // optional, when in workflow
 *       "columns": {
 *         "wfMap": [
 *           "COURSE_ENROLLMENT_USER",
 *           "COURSE_ENROLLMENT_STATUS"
 *         ],
 *         "map": []
 *       },
 *       "options": {
 *         "elementArg": "sordMetadata"
 *       },
 *       "callback": {
 *         "name": "RF_sol_learning_function_CreateEnrollmentHeadless",
 *         "args": {
 *           "myadditionalargument": "{{translate 'my.locale.string'}}"
 *         }
 *       }
 *     }
 *
 * The function would now be called with a parameter called `sordMetadata` for every table row.
 * The parameter would look like this
 *
 *     {
 *       "sordMetadata": {
 *           "COURSE_ENROLLMENT_USER": "Felix Unger",
 *           "COURSE_ENROLLMENT_STATUS": "ENROLLED"
 *       },
 *       "myadditionalargument": "This is a localized text"
 *     }
 *
 * To view these parameters, define `options.dryRun:true`. The callback will not be executed then.
 * Instead you can see all values with with the function will be called. (One array entry is one call).
 *
 * The object displayed in "result" will be passed to the function defined in callback.name.
 *
 * ### Example: TemplateSord
 *
 * Your target function might require a TemplateSord instead of a flat object. Simply define
 *
 *       "options": {
 *         "elementArg": "sordMetadata",
 *         "elementAsTemplateSord": true
 *       },
 *
 * The result would look like this:
 *
 *     {
 *       "sordMetadata": {
 *         "wfMapKeys": {
 *           "COURSE_ENROLLMENT_USER": "Felix Unger",
 *           "COURSE_ENROLLMENT_STATUS": "ENROLLED"
 *         }
 *       },
 *       "myadditionalargument": "This is a localized text"
 *     }
 *
 * ### Example: Mapping field values
 *
 * Most of the time, the target function will probably require different field names from the original
 * sord's field names.
 *
 * You can use a mapping to move fields from one property to another: Please note, that the property key
 * is relative to sordMetadata (or whatever is defined as `options.elementArg`), while the property value
 * is not. This enables moving values from e.g. a templateSord to an outer scope
 *
 *       "options": {
 *         "elementArg": "sordMetadata",
 *         "elementAsTemplateSord": true,
 *         "moveValues": {
 *           "wfMapKeys.COURSE_ENROLLMENT_STATUS": "sordMetadata.objKeys.COURSE_ENROLLMENT_STATUS",
 *           "wfMapKeys.COURSE_ENROLLMENT_USER": "thisis.something.else",
 *         }
 *       }
 *
 * Of course the mapping source would not include "wfMapKeys", if the "elementAsTemplateSord" option
 * was set to false. The mapping target path can be as deeply nested as you wish.
 *
 *     {
 *       "sordMetadata": {
 *         "wfMapKeys": {},
 *         "objKeys": {
 *           "COURSE_ENROLLMENT_STATUS": "ENROLLED"
 *         }
 *       },
 *       "thisis": {
 *         "something": {
 *           "else": "ENROLLED"
 *         }
 *       },
 *       "myadditionalargument": "This is a localized text"
 *     }
 *
 * Example: Adding additional parameters
 *
 * Most functions need additional parameters. And you will also want to have access to fields of the
 * (workflow) sord, which contains the maptable.
 *
 * E.g. imagine an invoice sord containing a maptable of items. Each item will have an item number
 * and item description. However, you want to call a createInvoiceItem function which also requires
 * a field called "INVOICE_NO" which is not contained in the maptable but on the base sord.
 * You can now add this field using a handlebars helper in the additional `callback.args` object.
 * In handlebars you have access to the (invoice) sord.
 *
 * If additional "callback.args" are defined, they are deep-merged into the prepared parameter
 * which contains the sordMetadata element:
 *
 *     element: { sordMetadata: { mapKeys: { ... } } }
 *     args:    { test: true, sordMetadata: { testInner: true } }
 *     result:  { test: true, sordMetadata: { testInner: true, mapKeys: { ... } } }
 *
 * A complete example could look like this:
 *
 *     {
 *       "objId": "5203", // optional, when in workflow
 *       "flowId": 59,    // optional, when in workflow
 *       "columns": {
 *         "wfMap": [
 *           "COURSE_ENROLLMENT_USER",
 *           "COURSE_ENROLLMENT_STATUS"
 *         ],
 *         "map": []
 *       },
 *       "options": {
 *         "elementArg": "sordMetadata",
 *         "elementAsTemplateSord": true,
 *         "moveValues": {
 *           "wfMapKeys.COURSE_ENROLLMENT_STATUS": "objKeys.COURSE_ENROLLMENT_STATUS"
 *         }
 *       },
 *       "callback": {
 *         "name": "RF_sol_learning_function_CreateEnrollmentHeadless",
 *         "args": {
 *           "myadditionalargument": "{{translate 'my.locale.string'}}",
 *           "sordMetadata": {
 *             "objId": "{{sord.id}}",
 *             "flowId": "{{flowId}}",
 *             "objKeys": {
 *               "COURSE_REFERENCE": "{{{sord.objKeys.COURSE_REFERENCE}}}",
 *               "COURSE_NAME": "{{{sord.objKeys.COURSE_NAME}}}"
 *             }
 *           }
 *         }
 *       }
 *     }
 *
 * Which results in
 *
 *         {
 *           "sordMetadata": {
 *             "mapKeys": {},
 *             "wfMapKeys": {
 *               "COURSE_ENROLLMENT_USER": "Administrator"
 *             },
 *             "objKeys": {
 *               "COURSE_ENROLLMENT_STATUS": "ENROLLED",
 *               "COURSE_REFERENCE": "0007",
 *               "COURSE_NAME": "Test"
 *             },
 *             "objId": "5203",
 *             "flowId": "59"
 *           },
 *           "myadditionalargument": "my.locale.string"
 *         }
 *
 * ### Filtering
 * If you want that only specific rows would be passed to the callback, you can define filter rules in
 * the options. For each row, all of the given filter will be applied and only matching objects will
 * be kept and passed to the callback function. It doesn't matter if you're using map table or wfMap table.
 * The crucial point ist, that your filter prop path is matching a object path of the result object.
 * The output format is depending on elementAsTemplateSord option or your moveValues operations.
 *
 * The filter will be applied after the moveValue operation.
 *
 *
 * #### Applying a filter with single filter value
 *
 *     {
 *       "options": {
 *         "filter": [
 *           { "prop" : "sordMetadata.mapKeys.SOLUTION_FIELD", value: "A -*"}
 *         ]
 *       }
 *     }
 *
 * #### Applying a filter with multiple filter values
 *
 *     {
 *       "options": {
 *         "filter": [
 *           { "prop" : "sordMetadata.mapKeys.SOLUTION_FIELD", value: ["A -*", "B -*"]}
 *         ]
 *       }
 *     }
 *
 * #### Returns
 *
 * An array of all return values of the function calls or all prepared parameters which
 * would have been sent during each function call if `options.dryRun` is set to true.
 *
 *     { data: [{ objId: "12345", flowId: "33" }] }
 *
 * ### Sorting
 *
 * It is possible to specify certain search criteria after which the result is sorted.
 *
 * Important: Only the result on the service side is sorted, not on the database level.
 * This can lead to incorrect sorting when using pagination.
 *
 * Several sorting criteria can also be specified. The first element is always
 * the most important criterion. In the case of identical values, the following sorting criteria
 * are applied.
 *
 * Via `type` a sorting algorithm from {@see sol.common.ObjectSortUtils} can be passed as a string.
 * The name must be identical to the functions offered there. Otherwise,
 * the default is always selected as the standard.
 *
 * Possible types at the moment: `default`, `date`.
 *
 * The type `date` enables sorting with the help of date objects.
 *
 *     {
 *      "options": {
 *         "elementArg" : "data"
 *         "sort": [
 *            { "prop": "data.startDate", "type": "date" },
 *            { "prop": "data.reference"}
 *         ]
 *       }
 *     }
 *
 * ### Complete Example: Use service results (e.g. sords) as data source
 *
 * If your data is not contained in a MAPTABLE, you can also use an external service to provide e.g. sords.
 *
 * In this example, we want to find all courses in the archive using the RF_sol_common_service_SordProvider as `elementService`.
 * Then, we want to prepend "POPULAR" to each course's name, when the course has a 5 star rating, using the RF_sol_function_Set function.
 *
 * To enable access to the element's data when the callback is prepared for execution, we define `renderArgsWithElement": true`.
 * This enables the use of the element's data in the `callback.args` object's strings.
 *
 *     {
 *       "elementService": {
 *         "name": "RF_sol_common_service_SordProvider",
 *         "args": {
 *           "masks": ["Course"],
 *           "search": [
 *              { "key": "SOL_TYPE", "value": ["COURSE"] }
 *              { "key": "REFERENCE", "value": "{{sord.objKeys.REFERENCE}}" }
 *           ],
 *           "output": [
 *             { "source": { "type": "SORD", "key": "id" }, "target": { "prop": "id" } },
 *             { "source": { "type": "GRP", "key": "COURSE_NAME" }, "target": { "prop": "name" } },
 *             { "source": { "type": "MAP", "key": "COURSE_STAR_SCORE" }, "target": { "prop": "stars" } }
 *           ]
 *         },
 *         options: {
 *           forTemplating: true
 *         }
 *       },
 *       "options": {
 *         "elementArg": "data",
 *         "moveValues": { "id": "objId" }, // the SET-function we use in the callback needs an objId.
 *         "renderArgsWithElement": true,
 *         "filter": [ { "prop": "data.stars", "value": "5" } ], // since SordProvider supports filtering too, we could have filtered above instead of here
 *         "dryRun": true // set this to false to actually execute the SET function in the end
 *       },
 *       "callback": {
 *         "name": "RF_sol_function_Set",
 *         "args": {
 *           "entries": [ { "type": "GRP", "key": "COURSE_NAME", "value": "POPULAR: {{{element.data.name}}}" }]
 *         }
 *       }
 *     }
 *
 * Info: instead of moving the `id` to `objId` using the `moveValues` option, we also could have defined `objId: "{{element.data.id}}"`
 * in the `callback.args` object.
 *
 * If you want to convert each item to an mapTable entry you can use the variable $mapIndex from your elementArg variable (e.g. data)
 * With Handlebars you can append each trailing number to your configuration result
 *
 *     { "type": "MAP", "key": "COURSE_NAME{{element.data.$mapIndex}}", "value": "{{{element.data.course}}}" }
 *
 * In this case you should avoid to use filter from the foreach function because we can't ensure that the index is correct.
 * For example the filter function will delete the second row, so your result is that $mapIndex = 2 is missing.
 *
 *     {
 *       "args": [
 *         {
 *           "entries": [ { "type": "GRP", "key": "COURSE_NAME1", "value": "POPULAR: BS ELO Contract" } ]
 *         },
 *         {
 *           "entries": [ { "type": "GRP", "key": "COURSE_NAME3", "value": "POPULAR: BS ELO Learning" } ]
 *         }
 *       ]
 *     }
 *
 * Instead use sordprovider filter function
 *
 *      {
 *       "elementService": {
 *         "name": "RF_sol_common_service_SordProvider",
 *         "args": {
 *           "masks": ["Course"],
 *           "search": [{ "key": "SOL_TYPE", "value": ["COURSE"] }],
 *           "filter": [ { "prop": "stars", "value": "5" } ]
 *           "output": [
 *             { "source": { "type": "SORD", "key": "id" }, "target": { "prop": "id" } },
 *             { "source": { "type": "GRP", "key": "COURSE_NAME" }, "target": { "prop": "name" } },
 *             { "source": { "type": "MAP", "key": "COURSE_STAR_SCORE" }, "target": { "prop": "stars" } }
 *           ]
 *         }
 *         ...
 *       }
 *
 * Attention: Only services which return an array of objects as the `sords` or `elements` property can be used with the ForEach function.
 *
 * #### Result
 *
 *     {
 *       "args": [
 *         {
 *           "data": { "name": "BS ELO Contract", "stars": "5" } }
 *           "objId": "5204",
 *           "entries": [ { "type": "GRP", "key": "COURSE_NAME", "value": "POPULAR: BS ELO Contract" } ]
 *         },
 *         {
 *           "data": { "name": "BS ELO Learning", "stars": "5" } }
 *           "objId": "5442",
 *           "entries": [ { "type": "GRP", "key": "COURSE_NAME", "value": "POPULAR: BS ELO Learning" } ]
 *         }
 *       ],
 *       "excluded": 7  // number of elements which did not match the filter criteria
 *     }
 *
 * If `dryRun` is set to false, the SET-function will be called with each of the two objects in the `args` array.
 *
 *
 * @author ESt, ELO Digital Office GmbH
 * @version 1.0
 *
 * @eloix
 *
 * @requires sol.common.Template
 * @requires sol.common.WfUtils
 * @requires sol.common.JsonUtils
 * @requires sol.common.RepoUtils
 * @requires sol.common.SordUtils
 * @requires sol.common.IxUtils
 * @requires sol.common.ObjectUtils
 * @requires sol.common.ix.RfUtils
 * @requires sol.common.ix.FunctionBase
 *
 */
sol.define("sol.common.ix.functions.ForEach", {
  extend: "sol.common.ix.FunctionBase",

  mixins: [
    "sol.common.mixins.Inject",
    "sol.common.mixins.ObjectFilter",
    "sol.common.mixins.ObjectSort"
  ],

  requiredConfig: ["callback"],

  inject: {
    sord: { sordIdFromProp: "objId", flowIdFromProp: "flowId", optional: true },
    originalConfig: { jsonFromProp: "configStr", forTemplating: false, template: false },
    config: { jsonFromProp: "configStr", forTemplating: false, template: true },
    flowId: { prop: "flowId", forTemplating: true }
  },

  /**
  * @cfg {Object} columns min. one of wfMap or map column names must be defined
  * @cfg {String[]} columns.wfMap (optional) column names of fields which are a WFMAPTABLE column (see docs)
  * @cfg {String[]} columns.map (optional) column names of fields which are a MAPTABLE column (see docs)
  */

  /**
  * @cfg {Object} options
  * @cfg {Boolean} options.elementArg property name, at which the maptable row object will be stored in the parameter object
  * @cfg {Boolean} [options.elementAsTemplateSord = false] (optional) formats the maptable row object as a templatesord before adding it to the parameter object
  * @cfg {Boolean} [options.dryRun = false] (optional) callback will not be executed, only prepared parameters will be returned. (good for debugging)
  * @cfg {Object} options.moveValues.Object
  * @cfg {Boolean}[options.deleteAfterUse = false] (optional) if set it to true, all applied rows to the callback function will be deleted. If dryRun = true, deleteInstructions will only be returned.
  * @cfg {Object[]} options.filter filter rules which will be applied to each row
  * @cfg {String} options.protectedFields.Object.* property names are the source paths, property values are the target paths for moving values in the parame
  * @cfg {Object} elementService
  * @cfg {String} elementService.name name of RF service (e.g. "RF_sol_common_service_SordProvider")
  * @cfg {Object} elementService.args additional arguments for the service
  */

  /**
  * @cfg {Object} callback
  * @cfg {String} callback.name name of registered function (e.g. "RF_sol_function_Set")
  * @cfg {Object} callback.args additional arguments for the callback
  */

  /**
   * External dependencies
   */

  executeCallback: function (callback, args) {
    var me = this;
    me.logger.debug(["executeCallback rf={0}, args={1}", callback, JSON.stringify(args)]);
    return sol.common.IxUtils.execute(callback, args);
  },

  isObj: function (o) {
    return sol.common.ObjectUtils.type(o, "object");
  },

  validStr: function (s) {
    return sol.common.ObjectUtils.type(s, "string") && String(s) && s.trim();
  },

  moveVal: function (srcObj, tgtObj, source, target) {
    var ou = sol.common.ObjectUtils;
    ou.setProp(tgtObj, target, ou.getProp(srcObj, source));
    ou.setProp(srcObj, source, undefined, true);
  },

  /**
   * Helpers
   */
  buildTable: function (columns, sord) {
    var me = this, mapTypes = Object.keys(columns);
    function createEmptyTable() {
      return Object.keys(columns).reduce(function (table, mapType) {
        return columns[mapType].forEach(function (column) {
          table[column] = [];
        }) || table;
      }, {});
    }

    function addMapDataToTable(table, mapType) {
      var sordMapData = sord[mapType + "Keys"] || {}, cols = columns[mapType];
      function addKeyToTable(key) {
        var column, rowNo;
        function keyIsColumn() {
          return cols.some(function (col) {
            return column = (
              key.indexOf(col) == 0
              && ((rowNo = +(key.slice(col.length))) === rowNo)
            ) && col;
          });
        }

        keyIsColumn() // -1 below, because MAPTABLES index has a +1 index offset
          && (table[column][rowNo - 1] = (sordMapData[key]));
      }

      return Object.keys(sordMapData).forEach(addKeyToTable) || table;
    }

    function addColumnDefinitions(info, mapType) {
      me.logger.info("table build successfully. adding table info");
      return columns[mapType].forEach(function (col) {
        info[col] = mapType + "Keys";
      }) || info;
    }

    me.logger.info("building table in memory");
    return {
      data: mapTypes.reduce(addMapDataToTable, createEmptyTable()),
      info: mapTypes.reduce(addColumnDefinitions, {})
    };
  },

  convertToElements: function (table, opt) {
    var me = this, elementArg = opt.elementArg, asTemplateSord = opt.elementAsTemplateSord,
        data = table.data, info = table.info,
        columns = Object.keys(data), elements;

    function createBlankElementsArray() {
      function arrayOfInt(int) {
        return Array.apply(null, Array(int));
      }
      return arrayOfInt(sizeOfLargestColumn());
    }

    function sizeOfLargestColumn() {
      return columns.reduce(function (max, col) {
        return (data[col].length >= max) ? data[col].length : max;
      }, 0);
    }

    function rowToObject(_, rowNo) {
      var element = {}, notEmpty = false, tmp = columns.reduce(function (obj, col) {
        var val = data[col][rowNo];
        (val !== "") && (notEmpty = true) && (obj[col] = val);
        return obj;
      }, {});

      element[elementArg] = tmp;
      element.$rowIndex = rowNo; // we need the index later for the deleteInstructions
      return notEmpty && element;
    }

    function rowToTemplateSord(_, rowNo) {
      var element = {}, notEmpty = false, tmp = columns.reduce(function (ts, col) {
        var val = data[col][rowNo];
        (val !== "") && (notEmpty = true) && (ts[info[col]][col] = val);
        return ts;
      }, { mapKeys: {}, wfMapKeys: {} });

      element[elementArg] = tmp;
      element.$rowIndex = rowNo; // we need the index later for the deleteInstructions
      return notEmpty && element;
    }

    function truthy(o) {
      return o[elementArg];
    }

    me.logger.info("Converting table to elements. Columns: " + columns);

    elements = createBlankElementsArray()
      .map(asTemplateSord ? rowToTemplateSord : rowToObject)
      .filter(truthy);

    me.logger.info("Extracted " + elements.length + " elements from table");

    return elements;
  },

  forEachElement: function (elements, opts, cbOpts) {
    var me = this, preparedArgs, args = cbOpts.args;
    function prepareCallbackArg(element) {
      var freshArgs = args;
      function deepMerge(a, b) {
        return Object.keys(b).forEach(function (p) {
          a[p] = (me.isObj(a[p]) && me.isObj(b[p]))
            ? deepMerge(a[p], b[p])
            : b[p];
        }) || a;
      }
      if (opts.renderArgsWithElement) {
        me.$templatingData.element = element;
        try {
          freshArgs = JSON.parse(sol.common.TemplateUtils.render(freshArgs, me.$templatingData));
        } catch (error) {
          freshArgs = sol.common.TemplateUtils.render(JSON.parse(freshArgs), me.$templatingData);
        }
      }
      return deepMerge(element, freshArgs);
    }

    function executeCb(params) {
      var rfName = cbOpts.name;
      return me.executeCallback(rfName, params);
    }

    function addMapIndex(element, index) {
      if (element.data && element.data.$mapIndex) {
        me.logger.warn("Don't use $mapIndex as target prop - The prop is reserved to pass MapIndex to callback and will override your definition");
      }

      // map index always begin at 1
      element.data && (element.data.$mapIndex = index + 1);
      return element;
    }

    me.logger.info("Preparing arguments for callbacks");

    // MapIndex must be added first so we can access $mapIndex in handlebars
    preparedArgs = elements
      .map(addMapIndex)
      .map(prepareCallbackArg)
      .filter(me.matchObject.bind(null, opts.filter));

    me.logger.info("Callbacks will be executed for " + preparedArgs.length + " elements");
    return opts.dryRun
      ? (me.logger.info("Dry run: No callback executed. Returning args"), { args: preparedArgs })
      : { args: preparedArgs, results: preparedArgs.map(executeCb) };
  },

  sanitizeConfig: function (cfg, sordsProvided) {
    var me = this, result = {};
    function filterArrayStrings(arr) {
      return arr.map(me.validStr).filter(me.validStr);
    }

    function columns(opt) {
      if (!(Array.isArray(opt.map) || Array.isArray(opt.wfMap))) {
        throw "`columns` must contain an array called `map` or `wfMap` or both arrays";
      }
      opt = Object.keys(opt).reduce(function (acc, key) {
        acc[key] = filterArrayStrings(opt[key]);
        return acc;
      }, {});
      if (!((opt.map || []).length || (opt.wfMap || []).length)) {
        throw "no valid column names found in `columns` arrays `map` or `wfMap`";
      }
      me.logger.info("columns config ok");
      return opt;
    }

    function callback(opt) {
      var name = opt.name, args = opt.args || {};
      if (!((name = me.validStr(name)) && name.indexOf("RF_") === 0)) {
        throw "`callback.name` must be a string starting with 'RF_' (a valid registered function)`: " + name;
      }
      if (args && !me.isObj(args)) {
        throw "`callback.args` must be an object if it is defined. current type: " + typeof args;
      }
      me.logger.info("callback config ok");
      return { name: name, args: args };
    }

    function elementService(opt) {
      var name = opt.name, args = opt.args || {}, elementServiceOptions = opt.options || {};
      if (!((name = me.validStr(name)) && name.indexOf("RF_") === 0)) {
        throw "`elementService.name` must be a string starting with 'RF_' (a valid registered service)`: " + name;
      }
      if (args && !me.isObj(args)) {
        throw "`elementService.args` must be an object if it is defined. current type: " + typeof args;
      }
      if (elementServiceOptions && !me.isObj(elementServiceOptions)) {
        throw "`elementService.options` must be an object if it is defined. current type: " + typeof elementServiceOptions;
      }

      me.logger.info("elementService config ok");
      return { name: name, args: args, options: elementServiceOptions };
    }

    function options(opt) {
      var elArg = opt.elementArg, mvVals = opt.moveValues, filters, mvKeys = [], sorts;

      if (!(elArg = me.validStr(elArg))) {
        throw "`options.elementArg` must be a string containing min. 1 character: " + elArg;
      }

      if (mvVals && !(me.isObj(mvVals) && (mvKeys = filterArrayStrings(Object.keys(mvVals))).length)) {
        throw "`options.moveValues` must be an object containing min. 1 property with a string value if it is defined. current type: " + typeof mvVals;
      }

      mvKeys.length && (mvVals = (mvKeys || []).reduce(function (acc, key) {
        var val = me.validStr(mvVals[key]);
        if (!val) {
          throw "`options.moveValues` property values must be non-empty strings! current type: " + typeof mvVals[key] + " value: " + val;
        }
        acc[key] = val;
        return acc;
      }, {}));

      // from mixin sol.common.ObjectUtils.ObjectFilter
      filters = me.generateFilter(me.options.filter || []);

      // from mixin sol.common.ObjectSortUtils.ObjectSort
      sorts = me.generateSort(me.options.sort || []);

      me.logger.info("options ok");

      return {
        elementArg: elArg,
        elementAsTemplateSord: opt.elementAsTemplateSord,
        deleteAfterUse: opt.deleteAfterUse,
        renderArgsWithElement: opt.renderArgsWithElement,
        moveValues: mvVals,
        filter: filters,
        sort: sorts,
        dryRun: opt.dryRun
      };
    }
    me.logger.info("sanitizing config");

    me.isObj(cfg.elementService) && (result.elementService = elementService(cfg.elementService));

    if ((sordsProvided || (sordsProvided = !!result.elementService)) && cfg.options.deleteAfterUse) {
      throw "`An `elementService` or `sords` were defined. `deleteAfterUse` option can only be used on MAPTABLES`.";
    }
    result.sordsProvided = sordsProvided;

    !sordsProvided && (result.columns = columns(cfg.columns));
    result.options = options(cfg.options);
    result.callback = callback(cfg.callback);

    if (result.options.renderArgsWithElement) {
      result.callback.args = sol.common.JsonUtils.stringifyQuick(me.originalConfig.callback.args);
    }


    return result;
  },

  executeDeleteInstructions: function (deleteInstructions) {
    var me = this;
    if (deleteInstructions.length) {
      me.logger.info("used table rows will be unset");
      me.executeCallback("RF_sol_function_Set", {
        objId: me.objId,
        flowId: me.flowId,
        entries: deleteInstructions
      });
    } else {
      me.logger.info("nothing to delete. DeleteInstructions are empty");
    }
  },

  generateDeleteInstructions: function (elements, table) {
    var type, typeMapping = { mapKeys: "MAP", wfMapKeys: "WFMAP" },
        deleteInstructions = [], appliedRowIndices = [];

    elements.forEach(function (el) { // build lookup array: rows for which the callback was executed
      appliedRowIndices[el.$rowIndex] = true;
    });

    Object.keys(table.data).forEach(function (columnName) {
      type = typeMapping[table.info[columnName]]; // {table: {info: SOLUTION_FIELD: "mapKeys"}} => "MAP"

      table.data[columnName].forEach(function (_, curIndex) {
        appliedRowIndices[curIndex]
          && deleteInstructions.push({ type: type, key: columnName + (curIndex + 1), value: "" });
      });
    });

    return deleteInstructions;
  },

  mv: function (opts, element) {
    var me = this, elementArg = opts.elementArg,
        moveDef = opts.moveValues, mvKeys = Object.keys(moveDef);
    return mvKeys.forEach(function (source) {
      me.moveVal(element[elementArg], element, source, moveDef[source]);
    }) || element;
  },

  sordsToElements: function (sords, opt) {
    var me = this, elementArg = opt.elementArg, elements;

    function toElement(sord) {
      var element = {};
      element[elementArg] = sord;
      return element;
    }

    elements = sords.map(toElement);

    me.logger.info("Created " + elements.length + " elements from sords");

    return elements;
  },

  executeElementService: function (cfg) {
    var me = this, elements, args = cfg.args;

    if (cfg.options && cfg.options.forTemplating === true) {

      if (!me.sord) {
        throw Error("Templating only works if an `objId` was passed for templating");
      }

      args = sol.common.TemplateUtils.render(cfg.args, { sord: me.sord });
    }

    elements = (me.executeCallback(cfg.name, args || {}) || {});
    elements = elements.sords || elements.elements;
    if (!Array.isArray(elements)) {
      throw "The RF defined as `elementService` must return an object containing a property `sords` or `elements` which contains an array (of objects)";
    }
    return elements;
  },


  process: function () {
    var me = this, config, table, elements, data, deleteInstructions,
        sords = me.sords || me.elements, result = {};

    config = me.sanitizeConfig(me.config, Array.isArray(sords));

    if (config.sordsProvided) {
      config.elementService
        && (sords = me.executeElementService(config.elementService));
      elements = me.sordsToElements(sords, config.options);
    } else {
      table = me.buildTable(config.columns, me.sord);
      elements = me.convertToElements(table, config.options);
    }

    config.options.moveValues
      && (elements = elements.map(me.mv.bind(me, config.options)));

    if (config.options.sort && config.options.sort.length > 0) {
      me.logger.debug(["element will be sorted by {0}", JSON.stringify(config.options.sort)]);
      elements = me.sortArray(elements, config.options.sort);
    }

    data = me.forEachElement(elements, config.options, config.callback);

    if (config.options.deleteAfterUse) {
      deleteInstructions = me.generateDeleteInstructions(data.args, table);
      if (config.options.dryRun) {
        result.deleteInstructions = deleteInstructions;
      } else {
        me.executeDeleteInstructions(deleteInstructions);
      }
    }

    if (config.options.dryRun) {
      result.args = data.args;
      result.excluded = elements.length - data.args.length;
    }
    result.data = data.results;

    return result;
  }
});

/**
 * @member sol.common.ix.functions.ForEach
 * @static
 * @inheritdoc sol.common.ix.FunctionBase#onEnterNode
 */
function onEnterNode(_clInfo, _userId, wfDiagram, nodeId) {
  var params = sol.common.WfUtils.parseAndCheckParams(wfDiagram, nodeId);

  params.objId = wfDiagram.objId;
  params.flowId = wfDiagram.id;
  params.configStr = sol.common.JsonUtils.stringifyQuick(params);

  sol.create("sol.common.ix.functions.ForEach", params).process();
}

/**
 * @member sol.common.ix.functions.ForEach
 * @static
 * @inheritdoc sol.common.ix.FunctionBase#onExitNode
 */
function onExitNode(_clInfo, _userId, wfDiagram, nodeId) {
  var params = sol.common.WfUtils.parseAndCheckParams(wfDiagram, nodeId);

  params.objId = wfDiagram.objId;
  params.flowId = wfDiagram.id;
  params.configStr = sol.common.JsonUtils.stringifyQuick(params);

  sol.create("sol.common.ix.functions.ForEach", params).process();
}


/**
 * @member sol.common.ix.functions.ForEach
 * @method RF_sol_common_function_ForEach
 * @static
 * @inheritdoc sol.common.ix.FunctionBase#RF_FunctionName
 * @return {Object}
 */
function RF_sol_common_function_ForEach(iXSEContext, args) {
  var logger = sol.create("sol.Logger", { scope: "sol.common.ix.functions.ForEach" }),
      rfParams = sol.common.ix.RfUtils.parseAndCheckParams(iXSEContext, arguments.callee.name, args),
      result;

  logger.enter("RF_sol_common_function_ForEach");
  rfParams.configStr = sol.common.JsonUtils.stringifyQuick(rfParams);

  result = sol.common.JsonUtils.stringifyQuick(
    sol.create("sol.common.ix.functions.ForEach", rfParams).process()
  );

  logger.exit("RF_sol_common_function_ForEach");
  return result;
}