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

//@include lib_Class.js
//@include lib_sol.common.SordUtils.js
//@include lib_sol.common.UserProfile.js
//@include lib_sol.common.Locale.js
//@include lib_sol.common.ObjectFormatter.js
//@include lib_sol.common.ix.ServiceBase.js

var logger = sol.create("sol.Logger", { scope: "sol.common.ix.services.ChildrenDataCollector" });

/**
 * Collects all child sords for a given parent in an optimized way.
 *
 * This should be used by dashboards if a larger amount of Sords should be retrieved. In addition information of
 * required keyword forms and the user locale is returned.
 *
 * Please gather information carefully. Only the required objKeys and sordKeys should be collected by this service due
 * to performance improvements. e.g.
 *
 * Collecting 10,000 invoices for the invoice dashboard takes ~15s and creates ~80MB of traffic using the IX JS-API since
 * deserializing objects might be expensive. Since only a view information is required this service brings down
 * the execution time to ~5-6s and reduces data traffic to ~6MB. This is done by creating a minimized json-data structure
 * with the help of a string builder.
 *
 * # Example
 *
 * By default StatisticSord formatter is used for generating objects that are optimized for statistical
 * operations. Refer to sol.common.ObjectFormatter.StatisticSord for more information.
 *
 *     var result = sol.common.IxUtils.execute("RF_sol_common_services_ChildrenDataCollector", {
 *       parentId: '1213',
 *       objKeys: ["INVOICE_DATE", "INVOICE_AMOUNT"]
 *     });
 *
 * # Result
 *
 *     {
 *       version: '1',
 *       formatter: 'sol.common.ObjectFormatter.StatisticSord',
 *       locale: {
 *         language: 'de',
 *         dateFormat: 'dd.MM.yyyy',
 *         decimalSeperator: ',',
 *         groupingSeperator: '.'
 *       },
 *       sords: [{
 *        id: '5669',
 *        name: 'Invoice 123',
 *        O_INVOICE_DATE: '20151202'
 *       }, {
 *         // more sords ...
 *       }],
 *       docMasks: {
 *         'Incoming Invoice': {
 *           fields: {
 *             INVOICE_DATE: { name: 'Invoice Date', type: 'text' }
 *           }
 *         }
 *       }
 *     }
 */
sol.define("sol.common.ix.services.ChildrenDataCollector", {
  extend: "sol.common.ix.ServiceBase",

  collectorVersion: "1.00.000",

  /**
   * @cfg {String} maskName
   * Restricts search results to a given keywording mask
   */
  maskName: null,

  /**
   * @cfg {String} maskNames
   * Restricts search results to the given keywording masks
   */
  maskNames: null,

  /**
   * @cfg {String} parentId
   * id of the parent element (guid, objId or archivepath)
   */
  parentId: null,

  /**
   * @cfg {int} endLevel
   * Search child objects up to this level below parentId.
   *
   * Please note that recursively collecting child nodes is expensive and should be handled with care.
   *
   * A value of 0 or 1 means, that only the sub entries directly under the parent are included.
   * Set this value to -1, to search over all levels.
   * In this case the level is internally constrained to 32 to avoid an endless loop,
   * if the tree under the parent contains recursive references.
   */
  endLevel: 1,

  /**
   * @cfg {Boolean} [mainParent=false]
   * Return no references
   */
  mainParent: false,

  /**
   * @cfg {Class} formatter (required)
   * Sord object formatter implementation.
   *
   * e.g. `sol.common.ObjectFormatter.StatisticSord` or `sol.common.ObjectFormatter.TemplateSord`
   */
  formatter: "sol.common.ObjectFormatter.StatisticSord",

  /**
   * @cfg {Array} sordKeys
   * List of required sord keys.
   *
   * This only returns the given sord keys. This is required in order to limit traffic for not required keys.
   *
   *     sordKeys: ["id", "guid", "name", "desc"],
   */
  sordKeys: ["id", "name"],

  /**
   * @cfg {Array} objKeys
   * List of required objKeys.
   *
   * This only returns the given objKeys. This is required in order to limit traffic for not required keys.
   *
   *     objKeys: ["INVOICE_DATE", "INVOICE_AMOUNT"],
   */
  objKeys: [],

  /**
   * @cfg {Array} filter
   * List of metadata that limit search results.
   *
   * This limits search results to a given list of metadata. This list is passed as an FindByIndex ObjKey-Array while
   * collecting search results.
   *
   *     filter: [{ key: 'INVOICE_DATE', val: 'x*' }],
   *
   */
  filter: [],

  /**
   * @cfg {Boolean} [returnDataDefinition=false]
   * If `true`, the service returns the data definition for the object, which is necessary for the convertion to XML.
   */
  returnDataDefinition: false,

  /**
   * @cfg {Boolean} [addSordTypeKind=false]
   * If `true`, the service returns the kind of the sord type, e.g. "REPOSITORY", "FOLDER" or "DOCUMENT".
   */
  addSordTypeKind: false,

  /**
    * @cfg {Boolean} [onlyFolders=undefined]
    * Restricts search to include folders.
    * If true and onlyDocuments is false or unset, only folders will be returned.
    * If true and onlyDocuments is also true, folders and documents will be returned.
    * If set to false or unset no restriction will be applied.
    */
  onlyFolders: undefined,

  /**
    * @cfg {Boolean} [onlyDocuments=undefined]
    * Restricts search to include documents.
    * If true and onlyFolders is alse or unset, only documents will be returned.
    * If true and onlyFolders is also true, documents and folders will be returned.
    * If set to false or unset no restriction will be applied.
    */
  onlyDocuments: undefined,

  /**
   * @property {Object} dataDefinition for xml transformation
   * This contains the data definition for the result if json objects should be transformed to XML.
   */
  dataDefinition: {
    rootElementName: "data",
    dataProperties: ["sord", "sords"],
    arrayElementTagNames: { sords: "sord" }
  },

  /**
   * @property {Number} maxFind
   * Amount of sords that is collected in one step.
   * This is used by the FindFirstSords, FindNextSords function calls.
   */
  maxFind: 1000,

  /**
   * @property {Number} totalCount
   * The search is terminated if this number of objects is found. If the number of results should not be constrained,
   * set this value to 232-1 = 2147483647 (maximum value of a positive 32bit integer minus 1).
   *
   * Please note that collecting huge amounts of data might lead to performance issues.
   */
  totalCount: 10000,

  sordKeyMap: {
    id: { elementSelector: ObjDataC.mbId },
    maskName: { elementSelector: ObjDataC.mbMask },
    guid: { elementSelector: ObjDataC.mbGuid },
    name: { elementSelector: ObjDataC.mbName },
    IDateIso: { elementSelector: ObjDataC.mbIDate },
    XDateIso: { elementSelector: ObjDataC.mbXDate },
    desc: { elementSelector: SordC.mbDesc },
    ownerName: { elementSelector: SordC.mbOwnerName }
  },

  initialize: function (config) {
    var me = this;
    me.$super("sol.common.ix.ServiceBase", "initialize", [config]);

    me.maskName = config.maskName || me.maskName;
    me.parentId = config.parentId || me.parentId;
    me.formatter = config.formatter || me.formatter;
    me.sordKeys = config.sordKeys || me.sordKeys;
    me.objKeys = config.objKeys || me.objKeys;
    me.hiddenObjKeys = config.hiddenObjKeys || me.hiddenObjKeys;
    me.hiddenObjKeyPrefix = config.hiddenObjKeyPrefix || me.hiddenObjKeyPrefix;
    me.allObjKeys = config.allObjKeys || me.allObjKeys;
    me.ec = config.ec || me.ec;
    me.endLevel = config.endLevel || me.endLevel;
    me.returnDataDefinition = config.returnDataDefinition || me.returnDataDefinition;
    me.addSordTypeKind = config.addSordTypeKind || me.addSordTypeKind;
    me.onlyFolders = config.onlyFolders;
    me.onlyDocuments = config.onlyDocuments;
    me.filter = config.filter || me.filter;

    me.objKeyMap = {};
    me.jsonData = [];
    me.docMasks = {};
  },

  /**
   * Starts the collection of the desired data
   * @return {Array}
   */
  execute: function () {
    var me = this;
    me.rfUtils = sol.common.ix.RfUtils;
    me.computeSordElementSelector();
    me.getTypeConstants();

    // add data collector version and formatting information
    me.jsonData.push("{");
    me.jsonData.push('"version":' + JSON.stringify(me.collectorVersion) + ",");
    me.jsonData.push('"formatter":' + JSON.stringify(me.formatter) + ",");
    if (me.returnDataDefinition === true) {
      me.jsonData.push('"dataDefinition":' + JSON.stringify(me.dataDefinition) + ",");
    }

    // add locale information
    me.jsonData.push('"locale":');
    me.addLocale();

    // add current sord
    me.collectSord();

    // add child sords
    me.jsonData.push(",");
    me.jsonData.push('"sords":');
    me.collectChildren();

    // add doc masks information
    me.jsonData.push(",");
    me.jsonData.push('"docMasks":');
    me.jsonData.push(JSON.stringify(me.docMasks));

    me.jsonData.push("}");

    return me.jsonData.join("");
  },

  /**
   * @private
   *
   * Computes a SordZ selector for information that is required.
   */
  computeSordElementSelector: function () {
    var me = this,
        sordKey, elementSelector, i, objKeyName;
    me.sordZ = new SordZ(ObjDataC.mbMask);
    if (me.sordKeys) {
      me.sordKeys.forEach(function (key) {
        sordKey = me.sordKeyMap[key];
        if (sordKey) {
          elementSelector = sordKey.elementSelector;
        }
        if (elementSelector) {
          me.sordZ.add(elementSelector);
        }
      });
    }
    if (me.objKeys) {
      me.sordZ.add(SordC.mbObjKeys);
      for (i = 0; i < me.objKeys.length; i++) {
        objKeyName = me.objKeys[i];
        me.objKeyMap[objKeyName] = true;
      }
    }
  },

  /**
   * @private
   */
  collectSord: function () {
    var me = this,
        sord,
        formatter = me.getSordFormatter(),
        editInfoZ = new EditInfoZ();

    if (!me.parentId) {
      return;
    }
    editInfoZ.sordZ = me.sordZ;
    sord = ixConnect.ix().checkoutSord(me.parentId, editInfoZ, LockC.NO).sord;
    if (!me.docMasks[sord.maskName]) {
      me.addDocMaskData(sord.maskName);
    }
    me.jsonData.push(",");
    me.jsonData.push('"sord":');

    me.jsonData.push(me.getFormattedJson(formatter, sord, me.docMasks[sord.maskName]));
  },

  /**
   * @private
   */
  collectChildren: function () {
    logger.enter("collectChildren");
    var me = this,
        i, filter, objKey, idx, length,
        formatter = me.getSordFormatter(),
        jsonData = [],
        findInfo = new FindInfo(),
        objKeyFilters = [],
        findResult;

    if (me.parentId) {
      findInfo.findChildren = new FindChildren();
      findInfo.findChildren.parentId = me.parentId;
      findInfo.findChildren.endLevel = me.endLevel;
      findInfo.findChildren.mainParent = me.mainParent;
    }

    // apply optional mask filter
    if (me.maskName) {
      if (!findInfo.findByIndex) {
        findInfo.findByIndex = new FindByIndex();
      }
      findInfo.findByIndex.maskId = me.maskName;
    }

    if (me.shouldSearchOnlyFolders()) {
      me.searchOnlyFolders(findInfo);
    }

    if (me.shouldSearchOnlyDocuments()) {
      me.searchOnlyDocuments(findInfo);
    }

    if (me.maskNames) {
      if (!findInfo.findByIndex) {
        findInfo.findByIndex = new FindByIndex();
      }
      findInfo.findByIndex.maskIds = me.maskNames;
    }

    // apply filter
    if (me.filter && me.filter.length > 0) {
      if (!findInfo.findByIndex) {
        findInfo.findByIndex = new FindByIndex();
      }
      for (i = 0; i < me.filter.length; i++) {
        filter = me.filter[i];
        objKey = new ObjKey();
        objKey.name = filter.key;
        objKey.data = [filter.val];
        logger.info("applied filter:", objKey);
        objKeyFilters.push(objKey);
      }
      findInfo.findByIndex.objKeys = objKeyFilters;
    }

    // apply find options
    findInfo.findOptions = new FindOptions();
    findInfo.findOptions.totalCount = me.totalCount;

    try {
      idx = 0;
      findResult = ixConnect.ix().findFirstSords(findInfo, me.maxFind, me.sordZ);
      while (true) {
        for (i = 0, length = findResult.sords.length; i < length; i++) {
          if (!me.docMasks[findResult.sords[i].maskName]) {
            me.addDocMaskData(findResult.sords[i].maskName);
          }
          jsonData.push(
            me.getFormattedJson(
              formatter,
              findResult.sords[i],
              me.docMasks[findResult.sords[i].maskName],
              { addSordTypeKind: me.shouldIncludeSordType() }
            ));
        }
        if (!findResult.moreResults) {
          break;
        }
        idx += findResult.sords.length;
        findResult = ixConnect.ix().findNextSords(findResult.searchId, idx, me.maxFind, me.sordZ);
      }
    } finally {
      if (findResult) {
        ixConnect.ix().findClose(findResult.searchId);
      }
    }

    me.jsonData.push("[");
    me.jsonData.push(jsonData.join(","));
    me.jsonData.push("]");
    logger.exit("collectChildren");
  },

  /**
   * @private
   * @return {boolean}
   */
  shouldSearchOnlyFolders: function () {
    var me = this;

    return me.onlyFolders;
  },

  /**
   * @private
   * @param {de.elo.ix.client.FindInfo} findInfo
   */
  searchOnlyFolders: function (findInfo) {
    var me = this;

    findInfo.findByType = findInfo.findByType || new FindByType();
    findInfo.findByType.typeStructures = me.onlyFolders;
  },

  /**
   * @private
   * @return {boolean}
   */
  shouldSearchOnlyDocuments: function () {
    var me = this;

    return me.onlyDocuments;
  },

  /**
   * @private
   * @param {de.elo.ix.client.FindInfo} findInfo
   */
  searchOnlyDocuments: function (findInfo) {
    var me = this;

    findInfo.findByType = findInfo.findByType || new FindByType();
    findInfo.findByType.typeDocuments = me.onlyDocuments;
  },

  /**
   * @private
   * @param {Object} formatter
   * @param {de.elo.ix.client.Sord} sord
   * @param {String} maskName
   * @param {Object} params
   * @return {String} formatted json
   */
  getFormattedJson: function (formatter, sord, maskName, params) {
    return formatter.buildJson(
      sord,
      maskName || null,
      params || null
    );
  },

  /**
   * @private
   * @return {boolean}
   */
  shouldIncludeSordType: function () {
    var me = this;

    return me.addSordTypeKind;
  },

  /**
   * @private
   * @return {Object}
   */
  getSordFormatter: function () {
    var me = this;

    return sol.create(me.formatter, {
      config: {
        objKeys: me.objKeys,
        hiddenObjKeys: me.hiddenObjKeys,
        hiddenObjKeyPrefix: me.hiddenObjKeyPrefix,
        sordKeys: me.sordKeys
      }
    });
  },

  /**
   * @private
   */
  addLocale: function () {
    var me = this;
    me.userFormats = sol.create("sol.common.Locale", { ec: me.ec });
    me.userFormats.read();

    me.jsonData.push('{"language": "');
    me.jsonData.push(me.userFormats.language);
    me.jsonData.push('", "dateFormat": "');
    me.jsonData.push(me.userFormats.dateFormat);
    me.jsonData.push('", "decimalSeparator": "');
    me.jsonData.push(me.userFormats.decimalSeparator);
    me.jsonData.push('", "groupingSeparator": "');
    me.jsonData.push(me.userFormats.groupingSeparator);
    me.jsonData.push('"}');
  },

  /**
   * @private
   * Adds document mask data
   * @param {String} maskName
   * @return {Object}
   */
  addDocMaskData: function (maskName) {
    var me = this;
    if (!me.docMasks[maskName]) {
      me.docMasks[maskName] = me.buildDocMaskData(maskName);
    }
    return me.docMasks[maskName];
  },

  /**
   * @private
   * Builds the document mask data
   * @param {String} docMaskName Document mask name
   * @return {Object}
   */
  buildDocMaskData: function (docMaskName) {
    var me = this,
        docMask, docMaskData;

    docMaskData = { fields: {} };

    try {
      docMask = sol.common.SordUtils.getDocMask(docMaskName, me.ec.ci.language);
      docMask.lines.forEach(function (docMaskLine) {
        if (me.objKeyMap[docMaskLine.key]) {
          docMaskData.fields[docMaskLine.key] = { name: String(docMaskLine.name), type: me.docMaskLineTypes[docMaskLine.type] };
        }
      });
    } catch (ex) {
      me.logger.warn(["Can't get mask info: mask={0}, ec.ci.language={1}, exception={2}", docMaskName, me.ec.ci.language, ex]);
    }

    return docMaskData;
  },

  /**
   * @private
   */
  getTypeConstants: function () {
    var me = this,
        i, field, docMaskLineC, fields;
    me.docMaskLineTypes = {};
    docMaskLineC = new DocMaskLineC();
    fields = docMaskLineC.class.declaredFields;
    for (i = 0; i < fields.length; i++) {
      field = fields[i];
      field.accessible = true;
      if (field.name.startsWith("TYPE_")) {
        me.docMaskLineTypes[String(field.getInt(docMaskLineC))] = String(field.name.substring(5));
      }
    }
  }

});

/**
 * @member sol.common.ix.services.ChildrenDataCollector
 * @method RF_sol_common_services_ChildrenDataCollector
 * @static
 * @inheritdoc sol.common.ix.ServiceBase#RF_ServiceBaseName
 */
function RF_sol_common_services_ChildrenDataCollector(ec, configAny) {
  var config, jsonDataCollector, ecLang, ixConnectLang, result;

  ecLang = String(ec.ci.language);
  ixConnectLang = String(ixConnect.loginResult.clientInfo.language);

  config = sol.common.ix.RfUtils.parseAndCheckParams(ec, arguments.callee.name, configAny),
  config.ec = ec;

  log.info("ec.ci.language=" + ecLang);
  log.info("ixConnect.loginResult.clientInfo.language=" + ixConnectLang);

  jsonDataCollector = sol.create("sol.common.ix.services.ChildrenDataCollector", config);
  result = jsonDataCollector.execute();
  return result;
}