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;
}