/**
 * Analyzes if a set of rules is applicable for an object.
 *
 * A rule should at least contain an `action`, which should be executed later if the rule applies to the analyzed object,
 * and a `date` configuration (see {@link sol.common.SordUtils#getValue}) on which all checks will be performed.
 *
 * Additionally it can specify a `shift` and a `recur` attribute (for poosible configurations see {@link sol.common_monitoring.as.MonitorUtils#evalDateUnitConfig MonitorUtils}).
 * While `shift` can be a positive or a negative value, `recure` has to be positive.
 * Also optional is the `execution` attribute, which saves the last execution eiter to a map field or an index field. In case the rule is 'reoccuring' it is mandatory,
 * because the analyzer needs determine when the next reoccurrence should be.
 *
 * `date` only: the analyzer just checks, if the date is in the past
 *
 * `shift`: the analyzer checks, if the date plus/minus a value is already exceeded.
 *
 * `recur`: the analyzer checks, if the date (or the last execution) plus a value is exceeded.
 *
 * `shift` and `recur` can be combined.
 *
 * If `date` or `shift` rules hit, now next run will be calculated, if they miss, the analyzer determines the date for the next check.
 * `recur` rules always calculate a next run date (no breaking condition so far).
 *
 * Each kind of rule returns it's specified action when it hits.
 *
 *     var analyzer = sol.create("sol.common_monitoring.as.analyzers.RuleAnalyzer", {
 *       rules: [
 *         ... rule objects ...
 *       ]
 *     });
 *
 * #Example rules:
 *
 * Starts a workflow (in the context of the user "John Doe"), if the date in the MY_DATE field is in the past:
 *
 *     {
 *       action: { type: "WORKFLOW", templateId: "MyWorkflowTemplate", user: "John Doe" },
 *       date: { type: "GRP", key: "MY_DATE" },
 *     }
 *
 * If you don't want to filter by date, instead of defining `date`, you can define `noAdditionalDateCheck: true`. This will execute the action for every sord found by the collector.
 *
 * Creates a reminder if the date in the MY_DATE field is less then two months in the future (date minus 2 month is before now) and saves the current date to a map field:
 *
 *     {
 *       action: { type: "REMINDER", user: "Administrator" },
 *       date: { type: "GRP", key: "MY_DATE" },
 *       shift: { value: -2, unit: "M" },
 *       execution: { type: "MAP", key: "MY_DATE_EXECUTION" }
 *     }
 *
 * Starts a workflow, if the date from the map field (see execution - or if not already set, the `date` itself) plus the value from the MY_REOCCURRENCE field in days ("D") is in the past,
 * and saves the date of the last sheduled execution a map field (therefor all recurring events will be executed, even if several checks have been missed).
 *
 *     {
 *       action: { type: "WORKFLOW", templateId: "MyWorkflowTemplate" },
 *       date: { type: "GRP", key: "MY_DATE" },
 *       recur: { type: "GRP", key: "MY_REOCCURRENCE", unit: "D" },
 *       execution: { type: "MAP", key: "MY_DATE_EXECUTION" }
 *     }
 *
 * By default, the action is only performed if the date is before the filing date. If the action should be performed anyway, the parameter `ignoreFilingDate` can be set.
 *
 *     {
 *       action: { type: "WORKFLOW", templateId: "MyWorkflowTemplate" },
 *       date: { type: "GRP", key: "MY_DATE" },
 *       ignoreFilingDate: true,
 *       execution: { type: "MAP", key: "MY_DATE_EXECUTION" }
 *     }
 *
 * @eloas
 *
 * @requires sol.common.JsonUtils
 * @requires sol.common.SordUtils
 * @requires sol.common.DateUtils
 * @requires sol.common_monitoring.as.MonitorUtils
 */
sol.define("sol.common_monitoring.as.analyzers.RuleAnalyzer", {

  requiredConfig: ["rules"],

  /**
   * @cfg {Object[]} rules
   */

  initialize: function (config) {
    var me = this;
    me.$super("sol.Base", "initialize", [config]);
    me._todayIso = sol.common.DateUtils.dateToIso(new Date());
  },

  /**
   * Analyzes an object utilizing the set of rules set during initialization.
   * @param {de.elo.ix.client.Sord} sord
   * @return {Object[]}
   */
  analyze: function (sord) {
    var me = this,
        results = [],
        max = me.rules.length,
        i, rule, result, hitCount, missCount;

    me.logger.enter("analyze", { objId: sord.id, name: String(sord.name), rulesCount: me.rules.length });

    for (i = 0; i < max; i++) {
      rule = me.rules[i];
      result = me.analyzeRule(sord, rule);
      if (result) {
        results.push(result);
      }
    }

    hitCount = me.countHits(results);
    missCount = max - hitCount;

    me.logger.exit("analyze", { hits: hitCount, misses: missCount, result: results });

    return results;
  },

  /**
   * @private
   * Analyze an individual rule.
   * @param {de.elo.ix.client.Sord} sord
   * @param {Object} rule
   * @return {Object}
   */
  analyzeRule: function (sord, rule) {
    var me = this,
        result = {},
        localRuleCopy = JSON.parse(JSON.stringify(rule)),
        isoDatefromSord, isoDate, sordFilingDate, lastExecution, sanitizedIsoDate;

    if (localRuleCopy.date) {
      localRuleCopy.date.fillString = "00000000000000";
      isoDatefromSord = sol.common.SordUtils.getValue(sord, localRuleCopy.date);
      isoDate = isoDatefromSord;
    } else if (localRuleCopy.noAdditionalDateCheck === true) {
      isoDate = me._todayIso;
    }

    if (isoDatefromSord && localRuleCopy.execution && (localRuleCopy.execution.type === "MAP")) {
      // enhance rule.execution to enable new reminder after the sord date, which should be checked, was altered
      localRuleCopy.execution.key = localRuleCopy.execution.key + "_" + isoDatefromSord;
    }

    if (!isoDate) {
      me.logger.debug(["skip rule: date has no value", localRuleCopy.date]);
      return null;
    }

    sordFilingDate = sol.common.DateUtils.dateToIso(sol.common.DateUtils.isoToDate(sord.IDateIso), { startOfDay: true });
    lastExecution = me.checkExecution(sord, localRuleCopy.execution);

    // An empty value or the value `0` for the recurrence of an event (e.g. `0 years`) should also to be interpreted as a one-time event.
    if (lastExecution && (!localRuleCopy.recur || !localRuleCopy.recur.value)) {
      me.logger.debug(["skip rule: was already executed (execution={0})", lastExecution]);
      return null;
    }

    me.logger.debug("analyze rule: " + sol.common.JsonUtils.stringifyAll(localRuleCopy));

    if (localRuleCopy.shift) {
      isoDate = me.shiftIso(sord, isoDate, localRuleCopy.shift);
    }

    // An empty value or the value `0` for the recurrence of an event (e.g. `0 years`) should also to be interpreted as a one-time event.
    if (localRuleCopy.recur && localRuleCopy.recur.value) {
      if (!localRuleCopy.execution) {
        throw "reoccuring rules need to define an 'execution' property";
      }
      if (lastExecution) {
        isoDate = lastExecution;
        do { // prevent useless reminders from before the element was in the repository
          isoDate = me.shiftIso(sord, isoDate, localRuleCopy.recur);
        }
        while (isoDate < sordFilingDate);
      }
      result.nextRun = me.shiftIso(sord, isoDate, localRuleCopy.recur);
      lastExecution = isoDate;
    }

    me.logger.debug(["check date: {0}", isoDate]);

    if (isoDate && (isoDate <= me._todayIso)) {
      me.updateExecution(sord, localRuleCopy.execution, lastExecution);
      sanitizedIsoDate = me.sanitizeIsoDate(isoDate, sordFilingDate);
      if (localRuleCopy.ignoreFilingDate) {
        result.action = localRuleCopy.action;
        me.logger.debug("check was successful", localRuleCopy);
      } else if (sanitizedIsoDate >= sordFilingDate) {
        result.action = localRuleCopy.action;
        me.logger.debug("check was successful", localRuleCopy);
      } else {
        me.logger.debug("skip action because date is before sord filing date");
      }
    } else {
      result.nextRun = isoDate;
      me.logger.debug("check was not successful", localRuleCopy);
    }

    if (result.action) {
      me.logger.debug(["found action: {0}", sol.common.JsonUtils.stringifyAll(result.action)]);
    }
    if (result.nextRun) {
      me.logger.debug(["next run scheduled for '{0}'", result.nextRun]);
    }

    return result;
  },

  /**
   * @private
   * Shifts an ISO date. Uses {@link sol.common.DateUtils DateUtils} and {@link sol.common_monitoring.as.MonitorUtils MonitorUtils} internally
   * @param {de.elo.ix.client.Sord} sord
   * @param {String} isoDate
   * @param {Object} params See {@link sol.common_monitoring.as.MonitorUtils#evalDateUnitConfig evalDateUnitConfig}
   * @return {String}
   */
  shiftIso: function (sord, isoDate, params) {
    var shiftCfg, date, shiftedIsoDate;

    shiftCfg = sol.common_monitoring.as.MonitorUtils.evalDateUnitConfig(sord, params);

    date = sol.common.DateUtils.isoToDate(isoDate + "");
    date = sol.common.DateUtils.shift(date, shiftCfg.value, { unit: shiftCfg.unit });

    shiftedIsoDate = sol.common.DateUtils.dateToIso(date);

    return shiftedIsoDate;
  },

  /**
   * @private
   * Retrieves the value of an earlier execution depending on a given configuration.
   * @param {de.elo.ix.client.Sord} sord
   * @param {Object} execution
   * @return {String}
   */
  checkExecution: function (sord, execution) {
    var executionDateIso, shortDateExecution;

    if (execution) {
      executionDateIso = sol.common.SordUtils.getValue(sord, execution, { fillString: "00000000000000" });
      if (!executionDateIso) {
        // check if there is an old execution entry with a short date
        shortDateExecution = sol.common.ObjectUtils.clone(execution);
        if (shortDateExecution.key.substr(shortDateExecution.key.length - 6) == "000000") {
          shortDateExecution.key = shortDateExecution.key.substring(0, shortDateExecution.key.length - 6);
          executionDateIso = sol.common.SordUtils.getValue(sord, shortDateExecution);
        }
      }
      return executionDateIso;
    }
    return null;
  },

  /**
   * @private
   * Updates the value of the current execution depending on a given configuration.
   * @param {de.elo.ix.client.Sord} sord
   * @param {Object} execution
   * @param {String} date An ISO date to set according to `execution`
   */
  updateExecution: function (sord, execution, date) {
    var me = this,
        mapitems;
    if (!execution) {
      return;
    }
    execution.value = date || me._todayIso;
    mapitems = sol.common.SordUtils.updateSord(sord, [execution], { fillString: "00000000000000" });
    if (mapitems && mapitems.length > 0) {
      ixConnect.ix().checkinMap(MapDomainC.DOMAIN_SORD, sord.id, sord.id, mapitems, LockC.NO);
    }
  },

  /**
   * @private
   * Counts the results, that where a hit. A hit is marked by a result, containing an action.
   * @param {Object[]} results
   * @return {Number}
   */
  countHits: function (results) {
    var hitCount = 0,
        i, max;
    for (i = 0, max = results.length; i < max; i++) {
      if (results[i].action) {
        hitCount++;
      }
    }
    return hitCount;
  },

  /**
   * @private
   * Fills isoDate to the correct amount of digigt to prevent the fail of a check.
   * @param {String} isoDate
   * @param {String} verificationDate
   * @return {String}
   */
  sanitizeIsoDate: function (isoDate, verificationDate) {
    if (String(isoDate).length === 8 && String(verificationDate).length === 14) {
      isoDate += "000000";
    }
    return isoDate;
  }

});