//@include lib_Class.js
//@include lib_sol.common.Config.js
//@include lib_sol.common.TranslateTerms.js
//@include lib_sol.common.ObjectUtils.js
//@include lib_sol.common.SordUtils.js
//@include lib_sol.common.Template.js
//@include lib_sol.common.WfUtils.js

/*
 * @author ESt, ELO Digital Office GmbH
 * @version 1.03.000
 *
 * @elojc
 * @eloas
 * @eloix
 *
 * Injection Mixin
 *
 * You can use this mixin in business solution classes to load configurations
 * and apply templating to them using sord- or other data
 *
 * As this mixin calls the including classes' inject method, you have to define an inject
 * method in the including class.
 *
 * Usually, this mixin is accompanied by a solution-specific `configuration`-mixin, which
 * is responsible for describing the relationship between a config-name and the config repository path.
 * (see sol.hr.mixins.Configuration for an example configuration-description file)
 *
 * A typical usage of this mixin would be e.g. an indexserver-service.
 * ## Possible Injections
 * Injections have to be defined in the classes `inject`-property.
 *
 * ### Config Injection
 *
 * You can inject configs or a property of a config by using the `config` and `prop` keywords:
 *
 *     { config: "hr", prop: "entities.file.valueA", template: true }
 *
 * This would initialize the hr.config, read the property `valueA` and apply templating to it.
 * The `template` keyword is optional. You can enable it, if the config property contains a handlebars template to render it.
 * You may also define `emptyNonRendered: true`, which will set the handlebars template to an empty string, if it could not be rendered.
 * (Otherwise the handlebars template will still be the value of the injected string)
 *
 * Every injected config property injection which results in an object or array will be deep-copied.
 *
 * ### Sord Injection
 *
 * Injecting sords is possible by using the `sordId` and `flowId` keywords:
 *
 *     { sordId: "12345", flowId: "442381" }
 *
 * Usually the objId and flowId are values one does not know at compile time and must therefore be determined dynamically.
 * You can use
 *
 *     { sordIdFromProp: "objId", flowIdFromProp: "flowId" }
 *
 * You can use the optional setting to skip injecting a sord if no objId was found:
 *
 *     { sordIdFromProp: "objId", flowIdFromProp: "flowId", optional: true }
 *
 * to access a property stored in `me`/`this` (see extensive example below)
 *
 * If you define the property `includeBlobs`, the sord's FormBlobs will also we read.
 *
 * All sords will automatically be added to the data used for handlebars templating.
 * If you don't want to add it to the templating data, you can define the property `forTemplating` as false.
 *
 * ### JSON Injection
 *
 * You can inject a json-string, which will then be parsed, by using the `json` property.
 *
 *     { json: '{ "myProp": 12345123 }' }
 *
 * As with sordIdFromProp, you can use `jsonFromProp` to read the JSON from a variable.
 *
 * All parsed JSON will be added to the data used for handlebars templating.
 * If you don't want to add it to templating, set the `forTemplating` property to false.
 *
 * ### Property Injection (Inject into templating data)
 *
 * You can add any data local to the class (`me`/`this`) to the handlebars templating by defining
 *
 *     { prop: "params" }
 *
 * Usually, the injection mechanism would overwrite the original params if you define
 *
 *     params: { prop: "params" }
 *
 * ### Add to templating but don't inject
 *
 * You can disable injection by defining the property `dontInject` as true.
 * This way, the property will be added to templating but won't be injected.
 * This is only possible in Sord-, JSON- and Property-Injection.
 *
 * ### Disable logging of an injected value
 *
 * You can disable the logging of specific injections by defining the property
 * `log` as false. This may be useful for sensitive data. Instead of the value,
 * `N/A` will be shown in the logs.
 * Always doublecheck if the actual logging outputs reflect your wishes.
 *
 * ### Usage of `sol.common.mixins.Injector.SordToken`
 *
 * Use this token mixin to inject automatically sord injectorId by objId or source prop.
 * The mixin function will append either `{sord: { "sordIdFromProp": "objId"}}` or `{sord: { "prop": "source"}}`
 * to your `inject: {}` variable. It depends which params you have defined in your function context. me.objId will always
 * win when it is set. Otherwise it will use me.source to create the sordToken.
 * The token will only append when token (injectorId) is not already set.
 *
 *  sol.define("sol.hr.ix.actions.GetMyServiceResult", {
 *      extend: "sol.common.ix.ServiceBase",
 *
 *      mixins: ["sol.hr.mixins.Configuration",  "sol.common.mixins.Injector.SordToken", "sol.common.mixins.Injector.SordToken"],
 *  }
 *
 *
 * It is important that `sol.common.mixins.Injector.SordToken` is included first before `sol.common.mixins.Injector.SordToken` mechanismus is triggered! 
 * It does not matter if the inject mechanism is called manually.
 *
 * ## Example
 *
 * This simple example reads a parameter from the config-file, applies a sord, a parameter and a translation to the template
 * and returns the value. Note: sords are retrieved with a normal user connection. If you need access to a sord via ixConnectAdmin,
 * you must checkout the sord manually in the initialize function and add it to the templating using the `prop`-option
 *
 *    /hr/Configuration/hr.config:
 *    {
 *      entities: { file: { valueA: "{{translate 'sol.hr.descr'}}{{PERSONNELFILE.objKeys.LASTNAME}}, {{params.name}}" } }
 *    }
 *
 *    /hr/All Rhino/lib_sol.hr.mixins.Configuration:
 *      //include lib_Class.js
 *
 *      sol.define("sol.hr.mixins.Configuration", {
 *        mixin: true,
 *
 *        $configRelation: {
 *          hr: "/hr/Configuration/hr.config",
 *          myOtherConfig: "/hr/Configuration/something.config",
 *          // load recruiting.config. if it is not found, load recruiting.fallback.config
 *          recruiting_in_hr: ["/recruiting/Configuration/recruiting.config", "/hr/Configuration/recruiting.fallback.config"]
 *        }
 *      });
 *
 *    note: this example implies that the parameters
 *      { objId: "12345", flowId: "441234", name: "Vertragsanpassung" }
 *    are passed to the service.
 *    //include lib_sol.hr.mixins.Configuration.js
 *    //include lib_sol.common.Injection.js
 *
 *    sol.define("sol.hr.ix.actions.GetMyServiceResult", {
 *      extend: "sol.common.ix.ServiceBase",
 *
 *      mixins: ["sol.hr.mixins.Configuration", "sol.common.mixins.Inject"],
 *
 *      inject: {
 *        myConfigValue: { config: "hr", prop: "entities.file.valueA", template: true }, // ""
 *        // templating data
 *        PERSONNELFILE: { sordIdFromProp: "objId", flowIdFromProp: "flowId" },
 *        params: { prop: "params" }
 *        // mySpecialSord: { prop: "myAdminAccessSord" }
 *      },
 *
 *      initialize: function (params) {
 *        var me = this;
 *        this.$super("sol.common.ix.ActionBase", "initialize", [params]);
 *        me.params = params;
 *        // me.myAdminAccessSord = me.getMySordViaAdminConnection();
 *      },
 *
 *      process: function () {
 *        var me = this;
 *        return me.myConfigValue;  // "Titel: Mayer, Vertragsanpassung"
 *      }
 *
 *  ## initializing config before initialize of calling class
 *
 *  If you need access to config values during initialize(), you can omit "sol.common.mixins.Inject" from the mixins array
 *  and call its constructor manually:
 *
 *      mixins: ["sol.hr.mixins.Configuration"],
 *      initialize: function (params) {
 *        var me = this;
 *        sol.create("sol.common.Injection").inject(me);  // sets up config including templating ...
 *
 *        me.tableTitle = me.dynkwl.tableTitle; // this is a config value we need before calling $super.initialize();
 *        me.$super("sol.hr.ix.dynkwl.MyIterator", "initialize", [config]);
 *      }
 *
 */
sol.define("sol.common.mixins.Inject", {
  mixin: true,

  initialize: function () {
    var me = this;
    me.logger && me.logger.debug(["Initializing injection mixin."]);
    sol.create("sol.common.Injection").inject(me);
  }
});

sol.define("sol.common.mixins.Injection.SordToken", {
  mixin: true,
  initialize: function (config) {
    var me = this, sordInjector;

    sordInjector = config.objId
      ? { sord: { sordIdFromProp: "objId" } }
      : { sord: { prop: "source" } };

    // We can pass source as templateSord directly to the function
    // in this case we dont want to checkout the sord again
    // so we provide the templateSord object as sord to handlebar.
    // That means we can use either objId or templateSord object to inject the
    // sord object to the handlebar context and resolve templates in configs
    sol.common.InjectionUtils.addInjections(me, sordInjector);
  }
});

sol.define("sol.common.Injection", {
  /**
   * @private
   * used in logging messages if a string if masked with ifLog() (injection.log===false)
   */
  noLogTxt: "N/A",

  /**
   * returns `any` if `log` is not false. Otherwise returns `me.noLogTxt`.
   */
  ifLog: function (log, any) {
    var me = this;
    return (log !== false) ? any : me.noLogTxt;
  },

   /**
   * retrieves template sord of objId/guid & flowId
   * make sure the objId/guid exists first!
   * @param {String} objId GUID/ObjId of sord to perform checkout on
   * @param {String} flowId used by getTemplateSord to retrieve wfMapKeys and formblobs
   * @return {TemplateSord} returns the requested templateSord
   */
  getSordData: function (objId, flowId, asAdmin, formBlobs) {
    var me = this;
    me.logger.debug(["Retrieving sord-data"]);
    return sol.common.WfUtils.getTemplateSord(
      ((asAdmin && typeof ixConnectAdmin !== "undefined") ? ixConnectAdmin : ixConnect).ix().checkoutSord(objId, SordC.mbAllIndex, LockC.NO),
      flowId || me.flowId,
      { asAdmin: asAdmin, formBlobs: formBlobs }
    ).sord;
  },


  initConfig: function (configName, classContext) {
    var me = this, configPaths, configLoaded;
    me.logger.debug(["Reading configuration because injection requires access to configuration `{0}`. See further log for the actual config-property injection.", configName]);
    if (me.typeOf(classContext.$configRelation, "object")) {
      configPaths = classContext.$configRelation[configName];
      if (configPaths && (Array.isArray(configPaths) || (configPaths = [configPaths]))) {
          configLoaded = configPaths.some(function (path) {
            try {
              classContext.$configs[configName] = (sol.create("sol.common.Config", { compose: path, copy: true })).config;
            } catch (_) {}
            return !!classContext.$configs[configName];
          });
          if (!configLoaded) {
            throw "Config at path " + configPaths[0] + "was not found!";
          }
        me.logger.debug(["Configuration `{0}` loaded.", configName]);
      } else {
        throw "$configRelation of configuration-mixin did not contain a configuration-path for config `" + configName + "`.";
      }
    } else {
      throw "$configRelation is not defined or not a javascript object. Please make sure to include a configuration mixin in the calling class.";
    }
  },

  injectConfig: function (config, injectId, classContext) {
    var me = this, configName = String(config.config);
    me.logger.debug(["Setup configuration `{0}` for injection `{1}`.", configName, injectId]);
    classContext.$configs = classContext.$configs || {};
    classContext.$configs[configName] || me.initConfig(configName, classContext);
    me.logger.debug("Adding config-property injection object to backlog for subsequent injection", me.ifLog(config.log, config));
    me.$configInjections.push(config);
  },

  injectSordById: function (sordConfig, injectId, classContext) {
    var me = this, sordData,
        sordId = sordConfig.sordId || sol.common.ObjectUtils.getProp(classContext, sordConfig.sordIdFromProp),
        flowId = sordConfig.flowId || sol.common.ObjectUtils.getProp(classContext, sordConfig.flowIdFromProp);

    if (!sordId && sordConfig.optional) {
      me.logger.debug("Optional sord was not found and therefore not injected. Continuing ...");
      return;
    }
    me.logger.debug(["Injecting sord-data of injection `{0}` by id `{1}` using flowId `{2}`.", injectId, me.ifLog(sordConfig.log, sordId), me.ifLog(sordConfig.log, flowId || me.flowId)]);
    sordData = me.getSordData(sordId, flowId, false, (sordConfig.includeBlobs === true));
    if (sordConfig.forTemplating !== false) {  // is added to templating as default
      me.logger.debug("Adding retrieved sord-data to templating-data.");
      classContext.$templatingData = classContext.$templatingData || {};
      classContext.$templatingData[injectId] = sordData;  // also add sord to templating
    } else {
      me.logger.debug("Sord-data has been retrieved but has not been added to templating-data.");
    }
    return sordConfig.dontInject ? undefined : sordData;
  },

  injectJSON: function (json, injectId, classContext) {
    var me = this, result;
    me.logger.debug(["Parsing JSON of injection `{0}`.", injectId]);
    result = JSON.parse(me.typeOf(json.json, "string") ? String(json.json) : sol.common.ObjectUtils.getProp(classContext, json.jsonFromProp));
    if (json.forTemplating !== false) {  // is added to templating as default
      me.logger.debug("Adding parsed JSON to templating-data.");
      classContext.$templatingData = classContext.$templatingData || {};
      classContext.$templatingData[injectId] = result;  // also add data to templating
    } else {
      me.logger.debug("JSON has been parsed but has not been added to templating-data.");
    }
    return json.dontInject ? undefined : result;
  },

  injectFromThis: function (prop, injectId, classContext) {
    var me = this, result, propType;
    me.logger.debug(["Reading property of class-context as defined in injection `{0}`.", injectId]);
    result = sol.common.ObjectUtils.getProp(classContext, String(prop.prop));
    propType = me.typeOf(result);
    me.logger.debug("Property value read", me.ifLog(prop.log, result));
    if ((propType === "object") || (propType === "array")) {
      me.logger.debug("The value is an object or an array. It will be cloned to minimize sideeffects");
      result = me.copyConfig(result);
    }
    if (prop.forTemplating !== false) {  // is added to templating as default
      me.logger.debug("Adding value to templating-data");
      classContext.$templatingData = classContext.$templatingData || {};
      classContext.$templatingData[injectId] = result;  // also add data to templating
    } else {
      me.logger.debug("Property has been read but has not been added to templating-data.");
    }
    return prop.dontInject ? undefined : result;
  },

  injectConfigProperty: function (config, classContext) {
    var me = this, prop, propType;
    me.logger.debug(["Reading config-property `{0}` from configuration `{1}`.", config.prop, config.config]);
    prop = sol.common.ObjectUtils.getProp(classContext.$configs[config.config], config.prop);
    propType = me.typeOf(prop);
    me.logger.debug("Configuration value read", me.ifLog(config.log, prop));
    if ((propType === "object") || (propType === "array")) {
      me.logger.debug("The value is an object or an array. It will be cloned to minimize sideeffects");
      prop = me.copyConfig(prop);
    }
    return prop;
  },

  copyConfig: function (obj) {
    return JSON.parse(JSON.stringify(obj, function (_, val) {
      return (val && val.getClass) ? String(val) : val;
    }));
  },

  performInjection: function (injection, injectionId, classContext) {
    var me = this, injectionFct;
    me.logger.debug(["Deciding on injection mechanism for injection `{0}`", injectionId]);
    if ((me.typeOf(injection.config, "string") && String(injection.config)) && (me.typeOf(injection.prop, "string"))) {
      injectionFct = me.injectConfig; // setup config
    } else if ((injection.sordId || injection.sordIdFromProp) && (injection.sordId + "")) {
      injectionFct = me.injectSordById; // injects a sord by passed Id
    } else if (me.typeOf(injection.json, "string") || (me.typeOf(injection.jsonFromProp, "string") && String(injection.jsonFromProp))) {
      injectionFct = me.injectJSON; // injects data parsed from JSON
    } else if (me.typeOf(injection.prop, "string") && String(injection.prop)) {
      injectionFct = me.injectFromThis; // injects data from this to this (basically only used to expose data to templating)
    } else {
      throw "Injection `" + injectionId + "` did not match any injection mechanism or  the mechanism configuration is incomplete. (possible mechanisms: config, sordId, json, prop, ...)";
    }

    injection.injectId = injectionId;
    injection.template && me.$injectionsToTemplate.push(injection);

    return injectionFct && injectionFct.call(me, injection, injectionId, classContext);
  },

  /**
   * injects data into a class `classContext`
   * @param {InitializedClassContext} classContext me/this of the class to inject data into.
   */
  inject: function (classContext) {
    var me = this, injections = classContext.inject, injectionResult;
    me.$configInjections = [];
    me.$injectionsToTemplate = [];
    me.typeOf = sol.common.ObjectUtils.type;
    me.logger.enter("sol.common.Injection.inject");
    me.logger.debug("Running inject on class-context");
    if (me.typeOf(injections, "object")) {
      me.logger.debug("Analyzing injections", injections);
      Object.keys(injections).forEach(function (injectionId) {
        var injection = injections[injectionId];
        me.logger.debug(["Testing injection `{0}`", injectionId]);
        if (me.typeOf(injection, "object")) {
          me.logger.debug(["Acting on injection `{0}`", injectionId]);
          injectionResult = me.performInjection(injection, injectionId, classContext);
          // every function will return its result which will then be injected. only config properties don't return & will be injected later
          if (injectionResult !== undefined) {
            classContext[injectionId] = injectionResult;
            me.logger.debug(["Injection `{0}` has been injected into the class context. Value", injectionId], me.ifLog(injection.log, injectionResult));
          } else {
            me.logger.debug(["Injection value of injection `{0}` is undefined. This may be ok, if configuration was read or you defined `dontInject`", injectionId]);
          }
        } else {
          throw "Injecting `" + injectionId + "` failed. The property value is not a javascript object.";
        }
      });

      me.logger.debug("Inject config properties from backlog");
      me.$configInjections.forEach(function (injection) {
        classContext[injection.injectId] = me.injectConfigProperty(injection, classContext);
      });

      me.$injectionsToTemplate.forEach(function (injection) {
        if (injection.template) {
          me.logger.debug("Applying template to injected value. Value before templating", me.ifLog(injection.log, classContext[injection.injectId]));
          classContext[injection.injectId] = sol.common.TemplateUtils.render(classContext[injection.injectId], classContext.$templatingData, { emptyNonRendered: !!injection.emptyNonRendered });
          me.logger.debug("Value after templating", me.ifLog(injection.log, classContext[injection.injectId]));
        }
      });
      me.logger.debug("All injections have been performed.");
    } else {
      throw "No injections defined. `inject` property value must be a javascript object. Type was: `" + me.typeOf(injections) + "`";
    }
    me.logger.exit("sol.common.Injection.inject");
  }
});

sol.define("sol.common.InjectionUtils", {
  singleton: true,

  /**
   * Append all injection keys to `context.inject` when the key does not already exist
   * @param {*} context
   * @param {*} injections
   */
  addInjections: function (context, injections) {
    Object.keys(injections)
    .filter(function (key) {
      return !context.inject[key];
    })
    .forEach(function (key) {
      context.inject[key] = injections[key];
    });
  }

});