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

    //@include lib_Class.js
    //@include lib_sol.common.Config.js
    //@include lib_sol.common.JsonUtils.js
    //@include lib_sol.common.IxUtils.js
    //@include lib_sol.common.ix.RfUtils.js
    //@include lib_sol.common.ix.ServiceBase.js
    //@include lib_sol.common.UserUtils.js
    //@include lib_sol.common.WfUtils.js
    //@include lib_sol.common.Template.js
    //@include lib_sol.common.ObjectUtils.js

    /**
     * Retrieves templates (clips) for a redactor
     *
     * For an implementation using this service, please see sol.recruiting.ix.services.GetCommunicationRedactorTemplates.
     *
     * ### Get Redactor Templates. Use data from sords in templating.
     *
     *     {
     *       templates: [
     *         { desc: "{{translate 'sol.recruiting.form.candidate.no'}}", value: "{{CANDIDATE.objKeys.RECRUITING_CANDIDATE_NO}}" },
     *         { desc: "{{translate 'sol.recruiting.form.candidate.firstname'}}", value: "{{CANDIDATE.objKeys.RECRUITING_CANDIDATE_FIRSTNAME}}" },
     *         { desc: "{{translate 'sol.recruiting.form.candidate.lastname'}}", value: "{{REQUISITION.objKeys.RECRUITING_REQUISITION_NAME}}" },
     *         { desc: "{{translate 'sol.recruiting.form.candidate.privatephone'}}", value: "{{CANDIDATE.mapKeys.RECRUITING_CANDIDATE_PRIVATEPHONE}}" }
     *       ],
     *       render: {
     *         searchAdditionalData: [
     *           {
     *             targetProp: "REQUISITION",
     *             criteria: [
     *               { key: "RECRUITING_REQUISITION_NO", value: "R12345" },
     *               { key: "SOL_TYPE", value: ["RECRUITING_REQUISITION", "RECRUITING_POOL"] }
     *             ]
     *           },
     *           {
     *             targetProp: "CANDIDATE",
     *             doubleAsSord: true,
     *             masks: ["Recruiting Candidate"],
     *             criteria: [
     *               { key: "RECRUITING_CANDIDATE_NO", value: "C44312" },
     *               { key: "SOL_TYPE", value: "RECRUITING_CANDIDATE" }
     *             ]
     *           }
     *         ]
     *       }
     *     }
     *
     * We want 4 redactor templates to be available. Since they require templating data from a requisition and a candidate, we
     * define a search, which will find both and make their data available to handlebars.
     *
     * Since it is common to make a specific sord available as the `sord` property in handlebars, we use CANDIDATE as `sord` by defining `doubleAsSord`.
     *
     * This means, `sord.objKeys.RECRUITING_CANDIDATE_NO` === `CANDIDATE.objKeys.RECRUITING_CANDIDATE_NO`.
     *
     * Instead of supplying a `criteria` and optionally a `mask/masks` property, you can also define a `id` or `guid` property, if you already know, which
     * sords to use for templating.
     *
     *     {
     *       targetProp: "CANDIDATE",
     *       doubleAsSord: true,
     *       id: "44292"
     *     }
     *
     * #### Result
     *
     *     {
     *       templates: [
     *         ["Bewerbernummer", "0001"],
     *         ["Bezeichnung", "Initiativbewerbungen"],
     *         ["Telefon", "001211439123"]
     *         ["Vorname", "Test"]
     *       ]
     *     }
     *
     * ### Only render description text
     *
     * Any templating strings in the value part of the template definition will not be altered with this setting:
     *
     *     render: {
     *       onlyDescription: true
     *     }
     *
     * #### Result
     *
     *     {
     *       templates: [
     *         ["Bewerbernummer", "{{CANDIDATE.objKeys.RECRUITING_CANDIDATE_NO}}"],
     *         ["Bezeichnung", "{{REQUISITION.objKeys.RECRUITING_REQUISITION_NAME}}"],
     *         ["Telefon", "{{CANDIDATE.mapKeys.RECRUITING_CANDIDATE_PRIVATEPHONE}}"]
     *         ["Vorname", "{{CANDIDATE.objKeys.RECRUITING_CANDIDATE_FIRSTNAME}}"]
     *       ]
     *     }
     *
     * ### Loading templates from a configuration
     *
     * This service will most often be called from a workflow from or webapp.
     *
     * The `load` parameter enables loading the `templates` from a configuration instead of defining them in the call.
     *
     *     {
     *       load: {
     *         config: "/recruiting/Configuration/recruiting.config",
     *         jsonPath: "entities.myredactor.mytemplates"
     *       }
     *     }
     *
     * ### Details on Results
     *
     * Only descriptions having a value after templating will be added to the results.
     *
     * If the value of a template has no value after templating, itself and its description will not be added to the results.
     *
     * Results are ordered alphabetically by description.
     *
     * @author ESt, ELO Digital Office GmbH
     *
     * @eloix
     * @requires sol.common.Config
     * @requires sol.common.JsonUtils
     * @requires sol.common.IxUtils
     * @requires sol.common.ix.RfUtils
     * @requires sol.common.ix.ServiceBase
     * @requires sol.common.UserUtils
     */
    sol.define("sol.common.ix.services.GetRedactorTemplates", {
      extend: "sol.common.ix.ServiceBase",

      /**
       * @cfg {Object[]} load (optional if `templates` is defined)
       * @cfg {Object} load.Object
       * @cfg {String} load.Object.config path to config (e.g. /recruiting/Configuration/recruiting.config)
       * @cfg {String} load.Object.jsonPath json path to property in config containing the templates
       */

      /**
       * @cfg {Object[]} templates (optional if `load` is defined)
       * @cfg {Object} templates.Object
       * @cfg {String} templates.Object.desc description of the template (selectable in redactor clips)
       * @cfg {String} templates.Object.value value of the redactor template. Will be inserted if redactor clip is clicked.
       */

      /**
       * @cfg {Object[]} render (optional)
       * @cfg {Boolean} [render.onlyDesc = false] (optional) only renders the description while leaving the value unchanged
       * @cfg {Object} render.data (optional) data for templating
       * @cfg {Object[]} render.searchAdditionalData (optional) add sords to templating data
       * @cfg {Object} render.searchAdditionalData.Object
       * @cfg {String} render.searchAdditionalData.Object.targetProp sord's target name in templating
       * @cfg {Boolean} render.searchAdditionalData.Object.doubleAsSord this sord will also be available as `sord`
       * @cfg {String|String[]} render.searchAdditionalData.Object.id (optional) sord's objId
       * @cfg {String|String[]} render.searchAdditionalData.Object.guid (optional) sord's guid
       * @cfg {String|String[]} render.searchAdditionalData.Object.masks (optional) masks to include in search
       * @cfg {Object[]} render.searchAdditionalData.Object.criteria (optional) search criteria
       * @cfg {Object} render.searchAdditionalData.Object.criteria.Object.key search criterion key
       * @cfg {Object|String[]} render.searchAdditionalData.Object.criteria.Object.value search criterion value/s
       */

      _optimize: {}, // enables optimization. Will store optimization cache ID

      doublesAsSord: "", // will contain targetProp of sord whose config property `dobuleAsSord` is true

      getConfigProp: function (config, path) {
        var prop;
        prop = sol.common.ObjectUtils.getProp(
          (sol.create("sol.common.Config", { compose: config, copy: true })).config,
          path
        );

        if (!prop) {
          throw "Could not read property `" + path + "` from config `" + config + "`";
        }
        if (!Array.isArray(prop)) {
          throw "The property (" + (typeof prop) + ") read from the config must be an Array.";
        }

        return JSON.parse(JSON.stringify(prop));
      },

      isValidConfigDef: function (def) {
        var valid =
          (typeof def === "object")
            && def.config
            && sol.common.ObjectUtils.type(def.jsonPath, "string");

        if (!valid) {
          throw "`load` argument not defined, not an object or does not contain `config` and `jsonPath` properties";
        }

        return valid;
      },

      shouldRenderEverything: function (renderOpts) {
        return (typeof renderOpts === "object") && !renderOpts.onlyDescription;
      },

      getPredefinedData: function (renderOpts) {
        if (renderOpts.data && typeof renderOpts.data !== "object") {
          throw "If defined, `data` property must be an Object";
        }
        return renderOpts.data || {};
      },

      shouldSearchForAdditionalData: function (renderOpts) {
        if (renderOpts.searchAdditionalData) {
          if (!Array.isArray(renderOpts.searchAdditionalData)) {
            throw "If defined, `searchAdditionalData` property must be an Array of Objects";
          }
          return true;
        }
      },

      isValidEntitiesDef: function (entities) {
        entities.forEach(function (entity) {
          if (typeof entity !== "object") {
            throw "All elements of the `searchAdditionalData` Array must be objects.";
          }
          if (!sol.common.ObjectUtils.type(entity.targetProp, "string") || String(entity.targetProp) === "") {
            throw "All elements of the `searchAdditionalData` Array must contain a `targetProp` property.";
          }
          if (entity.criteria && !Array.isArray(entity.criteria)) {
            throw "If defined, `criteria` property must be an Array of Objects adhering to the sol.common.services.SordProvider `search` definitions.";
          }
          if (!entity.criteria && !entity.id && !entity.guid) {
            throw "You must define search criteria or an `id` (objId) or `guid`";
          }
          if (Array.isArray(entity.id) || Array.isArray(entity.guid)) { // sordprovider supports arrays of ids, this is unwanted here
            throw "If defined, `id`/`guid` property must be a String. Arrays are not allowed.";
          }
        });
        return true;
      },

      searchForEntity: function (entity) {
        var me = this, config = {
              id: entity.id | entity.guid,
              search: entity.criteria,
              mask: (entity.mask || entity.masks || [""]),
              output: [{ source: { type: "SORD", key: "id" } }],
              options: { allowEmptyMask: true, maxResults: 1 }
            };

        return sol.common.IxUtils.optimizedExecute(
            "RF_sol_common_service_SordProvider",
            config,
            me._optimize,
            "returnsId",
            ["output"]
          )
          .sords[0];
      },

      search: function (entities) {
        var me = this;
        return me.isValidEntitiesDef(entities)
          ? entities.map(me.searchForEntity.bind(me))
          : [];
      },

      merge: function (tData, results) {
        return Object.keys(results).reduce(function (data, resultName) {
          tData[resultName] = results[resultName];
          return data;
        }, tData);
      },

      collectSordData: function (entity, id) {
        return sol.common.WfUtils.getTemplateSord(
          ixConnect.ix().checkoutSord(id, SordC.mbAllIndex, LockC.NO),
          undefined,
          { formBlobs: entity.includeBlobs }
        )
        .sord;
      },

      collectDataFromIds: function (entities, entityIds) {
        var me = this;
        return entities.reduce(function (data, entity, i) {
          entityIds[i]
            && (data[entity.targetProp] = me.collectSordData(entity, entityIds[i]));

          (entity.doubleAsSord === true)
            && (me.doublesAsSord = entity.targetProp);

          return data;
        }, {});
      },

      searchAndIncludeSords: function (tData, entities) {
        var me = this;
        return me.merge(
          tData,
          me.collectDataFromIds(
            entities,
            me.search(entities)
          )
        );
      },

      collectTemplatingData: function (renderOpts) {
        var me = this, tData = {};
        if (renderOpts) {
          if (typeof renderOpts !== "object") {
            throw "If defined, `render` argument must be an Object";
          }
          tData = me.getPredefinedData(renderOpts);
          me.shouldSearchForAdditionalData(renderOpts)
            && me.searchAndIncludeSords(tData, renderOpts.searchAdditionalData);

          me.doublesAsSord
            && (tData.sord = tData[me.doublesAsSord]);
        }
        return tData;
      },

      renderAny: function (obj, tData) {
        return sol.common.TemplateUtils.render(obj, tData, { emptyNonRendered: true });
      },

      withRenderedDesc: function (tData, o) {
        var me = this;
        return (o.desc = me.renderAny(o.desc, tData)), o;
      },

      renderTemplates: function (configProp, tData, renderEverything) {
        var me = this;
        return renderEverything
          ? me.renderAny(configProp, tData)
          : configProp.map(me.withRenderedDesc.bind(me, tData));
      },

      getRawTemplates: function (templates, load) {
        var me = this;
        return (Array.isArray(templates) && templates)  // use templates, if defined as parameter
          || (me.isValidConfigDef(load) && me.getConfigProp(load.config, load.jsonPath));
      },

      /**
       * @return {Object} return
       * @return {String[][]} return.templates an array of all templates (each template is an array of [0] description and [1] value)
       */
      process: function () {
        var me = this;

        return me.renderTemplates(
          me.getRawTemplates(me.templates, me.load),
          me.collectTemplatingData(me.render),
          me.shouldRenderEverything(me.render)
        )
        .filter(function (obj) {  // only return templates having a desc and value
          return obj.desc && obj.value;
        })
        .map(function (obj) { // convert templates to redactor clip format
          return [obj.desc, obj.value];
        })
        .sort(function (a, b) {
          return (a[0]).localeCompare(b[0]);
        });
      }
    });

    /**
     * @member sol.common.ix.services.GetRedactorTemplates
     * @method RF_sol_common_service_GetRedactorTemplates
     * @static
     * @inheritdoc sol.common.ix.ServiceBase#RF_ServiceBaseName
     */
    function RF_sol_common_service_GetRedactorTemplates(iXSEContext, args) {
      var rfUtils, rfParams, serviceProc, result;

      rfUtils = sol.common.ix.RfUtils;
      rfParams = rfUtils.parseAndCheckParams(iXSEContext, arguments.callee.name, args);
      delete rfParams._optimize;

      serviceProc = sol.create("sol.common.ix.services.GetRedactorTemplates", rfParams);
      result = rfUtils.stringify({ templates: serviceProc.process() });
      return result;
    }