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

//@include lib_Class.js
//@include lib_sol.common.RepoUtils.js
//@include lib_sol.common.SordUtils.js
//@include lib_sol.common.JsonUtils.js
//@include lib_sol.common.ix.RfUtils.js
//@include lib_sol.common.WfUtils.js
//@include lib_sol.common.ObjectFormatter.js
//@include lib_sol.common.ix.ServiceBase.js
//@include lib_sol.common.Template.js

/**
 * Converts a {@link sol.common.ObjectFormatter.TemplateSord TemplateSord} to {@link sol.common.ix.functions.Set SET}-compatible
 * {@link sol.common.ix.functions.Set#entries instructions/entries} optionally using a {@link #mapping mapping} and returns them.
 *
 * Hint: target instructions in mapping can contains {@link sol.common.ix.functions.Set#entries} parameters.
 * Currently only `key`, `type`, `value`, `useKwl`, `useDynKwl`, `dynKwlCfg` and `onlyIfEmpty` will be mapped
 * to the output of the set instructions
 *
 * ### Mapping definitions
 * A mapping definition consists of source-fields and their respective target-fields.
 *
 *     { source: { id: "MY_FIELD", type: "GRP" }, target: { id: "YOUR_FIELD", type: "GRP" } }
 *
 * Resulting instruction:
 *
 *     { id: "YOUR_FIELD", type: "GRP", value: "{{sord.objKeys.MY_FIELD}}" }
 *
 * You must pass true as the `returnRendered` parameter and a valid objId (String) or templateSord (Object) as the `dataSource` parameter, if you want the value to be rendered in the result.
 *
 * It is also possible to define custom values which do not occur in the `dataSource` by including them in the mapping definition's target property:
 *
 *     { target: { id: "Y_FIELD", type: "MAP", value: "{{sord.mapKeys.MY_CUSTOM_FIELD}} fixed text" } }
 *
 * If some values which are available in the dataSource should not be added to the generated instructions, the values can be defined as a `source` without a `target`
 *
 *     { source: { id: NO_FIELD, type: "MAP" } }
 *
 * If you pass a templateSord as `dataSource`, the templateSord will be used as `sord` in any templating.
 *
 * If you pass an objid as `dataSource` and a flowId as `dataSourceFlowId`, the sord will be checked out and used in templating.
 *
 * ### Empty non rendered values
 *
 * As a default, if returnRendered is true and a value could not be rendered, the original template-String will be used as the `value` in the generated instruction.
 * If you define `renderOptions: { emptyNonRendered: true }`, templates which were not rendered, will be written to the instruction as an empty String.
 * NonRendered means, the templating result is an empty string.
 *
 * ### Only generate instructions for mapped values
 *
 * If only some values of the dataSource should be used for generating instructions, defining `returnMappingsOnly: true` will only add those values defined in the mapping
 * to the resulting instructions.
 *
 * ### Reverse Mappings
 *
 * Option `reverseMapping: true` can be used, to switch source and target mapping options. (this is e.g. useful for HR-requests.)
 *
 * ### Examples
 *
 * ### Using a TemplateSord and a mapping
 *
 * #### Arguments
 *     {
 *       dataSource: {
 *         objKeys: {
 *           MY_FIELD: "myvalue"
 *         },
 *         mapKeys: {
 *           NO_FIELD: "xyz",
 *           MY_CUSTOM_FIELD: "this is"
 *         }
 *       },
 *       mapping: [
 *         { source: { id: "MY_FIELD", type: "GRP" }, target: { id: "YOUR_FIELD", type: "GRP" } },
 *         { target: { id: "Y_FIELD", type: "MAP", value: "{{sord.mapKeys.MY_CUSTOM_FIELD}} fixed text" } },
 *         { source: { id: "NO_FIELD", type: "MAP" } },
 *         { target: { id: "OLD_DATAHISTORY", type: "FORMBLOB", value: "{{sord.formBlobs.DATAHISTORY}}" } }
 *       ],
 *       returnRendered: true,
 *       renderOptions: {
 *         emptyNonRendered: true
 *       }
 *     }
 *
 * #### Return value
 *
 *     {
 *       id: "SetInstructions",
 *       dataSource: {
 *         objKeys: {
 *           MY_FIELD: "myvalue"
 *         },
 *         mapKeys: {
 *           NO_FIELD: "xyz",
 *           MY_CUSTOM_FIELD: "this is"
 *         }
 *       },
 *       instructions: [
 *         { key: "YOUR_FIELD", type: "GRP", value: "myvalue" },
 *         { key: "Y_FIELD", type: "MAP", value: "this is fixed text" },
 *         { key: "OLD_DATAHISTORY", type: "FORMBLOB", value: "" }
 *       ],
 *       rendered: true
 *     }
 *
 * @eloix
 * @requires sol.common.RepoUtils
 * @requires sol.common.SordUtils
 * @requires sol.common.JsonUtils
 * @requires sol.common.ix.RfUtils
 * @requires sol.common.WfUtils
 * @requires sol.common.ObjectFormatter
 * @requires sol.common.ix.ServiceBase
 * @requires sol.common.Template
 */
sol.define("sol.common.ix.services.GenerateSetInstructions", {
  extend: "sol.common.ix.ServiceBase",

  /**
   * @cfg {String|Object} dataSource
   * objId or templateSord
   */

  /**
   * @cfg {String} dataSourceFlowId (optional)
   * flowId. This will only be used, if `dataSource` is an objectId
   */

  /**
   * @cfg {Object[]} mapping (optional)
   * @cfg {Object} mapping.Object
   * @cfg {Object} mapping.Object.source (optional)
   * @cfg {String} mapping.Object.source.id name of source field
   * @cfg {String|"GRP"|"MAP"|"WFMAP"|"FORMBLOB"} mapping.Object.source.type type of field
   * @cfg {Object} mapping.Object.target (optional)
   * @cfg {String} mapping.Object.target.id name of target field
   * @cfg {String|"GRP"|"MAP"|"WFMAP"|"FORMBLOB"} mapping.Object.target.type type of field
   * @cfg {String} mapping.Object.target.value (optional) fixed or template String. This is completely optional!
   */

  /**
   * @cfg {Object[]} protectedFields (optional)
   * @cfg {Object} protectedFields.Object
   * @cfg {String|"GRP"|"MAP"|"WFMAP"|"FORMBLOB"} protectedFields.Object.type
   * @cfg {String} protectedFields.Object.id name of protected field
   */

  /**
   * @cfg {Object[]} mandatoryFields (optional) (throws exception if a field is missing)
   * @cfg {Object} mandatoryFields.Object
   * @cfg {String|"GRP"|"MAP"|"WFMAP"|"FORMBLOB"} mandatoryFields.Object.type
   * @cfg {String} mandatoryFields.Object.id name of mandatory field
   */

  /**
   * @cfg {Boolean} [reverseMapping = false] (optional)
   * switches `target` and `source` in every mapping
   */

  /**
   * @cfg {Boolean} [returnMappingsOnly = false] (optional)
   * instructionset will only contain instructions resulting from a mapping
   */

  /**
   * @cfg {Boolean} [returnRendered = false] (optional)
   * decides if resulting instructions will go through handlebars
   */

  /**
   * @cfg {Boolean} [escapeHTML = true] (optional)
   * if set to false, template strings will be generated so html won't be escaped
   */

  /**
   * @cfg {Object} renderOptions (optional)
   * additional options for rendering
   *
   * @cfg {Boolean} [renderOptions.emptyNonRendered = false] (optional)
   * empty value if handlebars could not render template
   */

  optionalDontEscape: function (s) {
    var me = this;
    return (me.escapeHTML === false)
      ? "{" + s + "}"
      : s;
  },

  getTemplate: function (name, type) {
    var t = type.trim().toUpperCase(), templateProp = (
      (t === "GRP" && "objKeys")
      || (t === "MAP" && "mapKeys")
      || (t === "WFMAP" && "wfMapKeys")
      || (t === "FORMBLOB" && "formBlobs")
      || ""
      );
    return "{{sord." + (templateProp ? templateProp + "." : "") + name.trim() + "}}";
  },

  getType: function (objName) {
    return (
      (objName === "objKeys" && "GRP")
      || (objName === "mapKeys" && "MAP")
      || (objName === "wfMapKeys" && "WFMAP")
      || (objName === "formBlobs" && "FORMBLOB")
      || objName
    );
  },

  executeMapping: function (mappings) {
    var me = this;
    me.logger.debug("Generating instructions from mapping", mappings);
    return (
      mappings
      .filter(function (mapping) { // if there is no target, remove from mappings
        return (mapping.target && mapping.target !== {});
      })
      .map(function (mapping) {
        return {
          key: mapping.target.id,
          type: mapping.target.type,
          useKwl: mapping.target.useKwl || false,
          useDynKwl: mapping.target.useDynKwl || false,
          dynKwlCfg: mapping.target.dynKwlCfg || undefined,
          onlyIfEmpty: mapping.target.onlyIfEmpty || false,
          value: (
            mapping.target.value
            || (
              mapping.source
              ? me.optionalDontEscape(me.getTemplate(mapping.source.id, mapping.source.type))
              : ""
            )
          )
        };
      })
    );
  },

  buildInstructionsFromTemplateSord: function (tSord) {
    var me = this, result = [];
    me.logger.debug("Building instructions using TemplateSord", tSord);
    Object.keys(tSord).forEach(function (typeObjName) {
      var typeObj = tSord[typeObjName], t = me.getType(typeObjName);
      (typeof typeObj === "object") && Object.keys(typeObj).forEach(function (fieldName) {
        result.push({
          key: fieldName,
          type: me.getType(typeObjName),
          value: me.optionalDontEscape(me.getTemplate(fieldName, t))
        });
      });
    });
    return result;
  },

  /*
   *  returns templateSord-instructions which were not mapped
   */
  filterInstructions: function (instructions, mappings) {
    var me = this;
    me.logger.debug("Filtering mapped instructions");
    return instructions.filter(function (i) {
      return !(mappings.some(function (m) {
        return m.source && (m.source.id === i.key && m.source.type === i.type);
      }));
    });
  },

  filterProtectedFields: function (instructions, protected) {
    var me = this;
    me.logger.debug("Filtering protected fields");
    return instructions.filter(function (i) {
      return !(protected.some(function (p) {
        return (typeof p === "object") && (p.id === i.key && p.type === i.type);
      }));
    });
  },

  findMissingMandatories: function (instructions, mandatories) {
    var me = this;
    me.logger.debug("Checking if all mandatory fields have instructions");
    return mandatories.filter(function (m) {
      return !(instructions.some(function (i) {
        return (i.key === m.id && i.type === m.type);
      }));
    });
  },

  getConvertedData: function (mapping, tplObj, flowId, render, renderOptions, asAdmin, formBlobs) {
    var me = this, result = { rendered: render },
        templatingData = { sord: {} }, instructions = [];

    if (typeof tplObj === "object") {
      me.logger.debug("Using TemplateSord as dataSource");
      templatingData.sord = tplObj;
      instructions = me.buildInstructionsFromTemplateSord(tplObj);
    } else if ((typeof tplObj === "string") && tplObj) {
      me.logger.debug(["Using objId as dataSource. Retrieving TemplateSord using objId `{0}` (and flowId `{1}`)"], tplObj, flowId);
      templatingData.sord = me.getSordData(tplObj, flowId, asAdmin, formBlobs);
      instructions = me.buildInstructionsFromTemplateSord(templatingData.sord);
    }

    result.instructions = (
      me.returnMappingsOnly ? [] : me.filterInstructions(instructions, mapping)
    ).concat(me.executeMapping(mapping));

    me.logger.debug("Generated instructions", result.instructions);
    render && tplObj && sol.common.TemplateUtils.render(result.instructions, templatingData, renderOptions) && me.logger.debug("Applied Templating");



    me.logger.debug("Result", result);
    return result;
  },

  reverseMappings: function (mappings) {
    var me = this;
    me.logger.debug("Reversing Mapping ...", mappings);
    return mappings.map(function (mapping) {
      return { source: mapping.target, target: mapping.source };
    });
  },

  getSordData: function (objId, flowId, asAdmin, formBlobs) {
    try {
      return sol.common.WfUtils.getTemplateSord(
        ((asAdmin && typeof ixConnectAdmin !== "undefined") ? ixConnectAdmin : ixConnect).ix().checkoutSord(objId, SordC.mbAllIndex, LockC.NO),
        flowId,
        { asAdmin: asAdmin, formBlobs: formBlobs }
      ).sord;
    } catch (e) {
      throw "GenerateSetInstructions: Could not retrieve TemplateSord using objId `" + objId + "` " + ((flowId && "and flowId + `" + flowId + "`") || "") + ". Insufficient rights?";
    }
  },

  mappingContainsFormBlobs: function (mapping) {
    return mapping.some(function (m) {
      return typeof m === "object" && typeof m.source === "object" && m.source.type === "FORMBLOB";
    });
  },

  /**
   * @return {Object}
   * @return {Object[]} return.instructions instructions usable as SET-entries
   * @return {Object} return.instructions.Object
   * @return {String} return.instructions.Object.key target field name
   * @return {String|"GRP"|"MAP"|"WFMAP"|"FORMBLOB"} return.instructions.Object.type target field type
   * @return {String} return.instructions.Object.value target field value
   * @return {Boolean} return.rendered if the `value`s in `instructions` are rendered
   */
  process: function () {
    var me = this,
        mapping = me.mapping || [],
        tplObj = me.dataSource,
        flowId = me.dataSourceFlowId,
        render = me.returnRendered,
        renderOptions = {
          emptyNonRendered: !!(me.renderOptions && me.renderOptions.emptyNonRendered),
          stringifyResults: !!(me.renderOptions && me.renderOptions.stringifyResults)
        },
        asAdmin = me.asAdmin,
        missingMandatoryFields = [],
        result = {},
        includeFormBlobs;

    mapping = me.reverseMapping ? me.reverseMappings(mapping) : mapping;

    includeFormBlobs = me.mappingContainsFormBlobs(mapping);

    result = me.getConvertedData(mapping, tplObj, flowId, render, renderOptions, asAdmin, includeFormBlobs);

    if (me.protectedFields) {
      if (!Array.isArray(me.protectedFields)) {
        throw "protectedFields parameter only accepts an Array as an argument";
      }
      result.instructions = me.filterProtectedFields(result.instructions, me.protectedFields);
    }

    if (me.mandatoryFields) {
      missingMandatoryFields = me.findMissingMandatories(result.instructions, me.mandatoryFields);
      if (missingMandatoryFields.length > 0) {
        throw "Some mandatory fields were not found in templateSord or mapping and therefore the resulting instructions. Missing fields:" + JSON.stringify(me.mandatoryFields);
      }
    }

    return result;
  }
});

/**
 * @member sol.common.ix.services.GenerateSetInstructions
 * @method RF_sol_common_service_GenerateSetInstructions
 * @static
 * @inheritdoc sol.common.ix.ServiceBase#RF_ServiceBaseName
 */
function RF_sol_common_service_GenerateSetInstructions(iXSEContext, args) {
  var me = this, rfUtils, rfArgs, serviceProc, result,
      logger = sol.create("sol.Logger", { scope: "sol.common.ix.services.GenerateSetInstructions" });

  logger.enter("RF_sol_common_service_GenerateSetInstructions");

  rfUtils = sol.common.ix.RfUtils;
  rfArgs = rfUtils.parseAndCheckParams(iXSEContext, arguments.callee.name, args);
  if (me.asAdmin) {
    me.logger("TemplateSord will be retrieved using ixConnectAdmin");
    sol.common.ix.RfUtils.checkMainAdminRights(iXSEContext.user, rfArgs);
  }

  serviceProc = sol.create("sol.common.ix.services.GenerateSetInstructions", rfArgs);
  result = rfUtils.stringify(serviceProc.process());
  logger.exit("RF_sol_common_service_GenerateSetInstructions");
  return result;
}