//@include lib_Class.js

/**
 * This class contains convenience methods for working with java script basic types and objects.
 *
 * @author PZ, ELO Digital Office GmbH
 * @version 1.1
 *
 * @eloall
 */
sol.define("sol.common.ObjectUtils", {
  singleton: true,

  /**
   * @private
   * @property {Function} _toStringFunction Reference to Object.prototype.toString
   */
  _toStringFunction: Object.prototype.toString,

  /**
   * Checks, if an Object is empty.
   * @param {Object} o
   * @return {Boolean}
   */
  isEmpty: function (o) {
    if (o === null || o === undefined) {
      return true;
    }
    if (o.length > 0) {
      return false;
    }
    if (Object.getOwnPropertyNames(o).length > 0) {
      return false;
    }
    return true;
  },

  /**
   * returns a variables real type. (typeof + null, nan, array, date, javaobject, regexp,...)
   * javastrings are reported as a normal string. Best practice: Always String() values this function reports as "string"
   *
   * e.g. type(123, "number") => true
   *      type([]) => "array"
   *
   * @param {Object} val the value to typecheck
   * @param {String} should the typename which should match
   * @returns {Boolean|String} returns Boolean if `should` is defined (true if determined type equals `should`, false if not). otherwise returns the determined type as a string
   */
  type: function (val, should) {
    var t; t = (
      (val !== val && "nan")
      || ((t = typeof val) !== "object" && t)
      || (typeof java !== "undefined" && val instanceof java.lang.String && "string")
      || ((t = Object.prototype.toString.call(val)), t.substring(8, t.length - 1).toLowerCase())
    );
    return should ? t === should : t;
  },

  /**
   * Checks, if an object is a JavaScript String
   * @param {Object} o
   * @returns {Boolean}
   */
  isString: function (o) {
    return this._toStringFunction.call(o) === "[object String]";
  },

  /**
   * Checks, if an object is a JavaScript Number
   * @param {Object} o
   * @returns {Boolean}
   */
  isNumber: function (o) {
    return this._toStringFunction.call(o) === "[object Number]";
  },

  /**
   * Checks, if an object is a JavaScript Date
   * @param {Object} o
   * @returns {Boolean}
   */
  isDate: function (o) {
    return this._toStringFunction.call(o) === "[object Date]";
  },

  /**
   * Checks, if an object is a JavaScript Array
   * @param {Object} o
   * @return {Boolean}
   */
  isArray: function (o) {
    return this._toStringFunction.call(o) === "[object Array]";
  },

  /**
   * Checks, if an object is a JavaScript Function
   * @param {Object} o
   * @return {Boolean}
   */
  isFunction: function (o) {
    return this._toStringFunction.call(o) === "[object Function]";
  },

  /**
   * Checks, if an object is a JavaScript object
   * @param {Object} o
   * @return {Boolean}
   */
  isObject: function (o) {
    return this._toStringFunction.call(o) === "[object Object]";
  },

  /**
   * Checks, if an object is a Java (Rhino) object
   * @param {Object} o
   * @return {Boolean}
   */
  isJavaObject: function (o) {
    return this._toStringFunction.call(o) === "[object JavaObject]";
  },

  /**
   * Checks, if an object is a JavaScript regular expression
   * @param {Object} o
   * @return {Boolean}
   */
  isRegExp: function (o) {
    return this._toStringFunction.call(o) === "[object RegExp]";
  },

  /**
   * Checks, if an object is blank. Works for Array (only contains ) and String, all other types always return `false`.
   * @param {Object} o
   * @return {Boolean}
   */
  isBlank: function (o) {
    if (this.isArray(o)) {
      o = o.filter(function (e) {
        return (typeof e !== "undefined");
      });
    }
    if (this.isString(o)) {
      o = o.trim();
      return o.length == 0;
    }
    return this.isEmpty(o);
  },


  /**
   * Determine whether the current object `o` is truthy
   * https://developer.mozilla.org/de/docs/Glossary/Truthy
   *
   * @param {*} o
   * @returns true when the passed obj is truthy otherwise false
   */
  isTruthy: function (o) {
    return !!o;
  },

  /**
   * Returns the passed object into an array.
   * When the object is already an array the original array
   * will return
   * @param {Object|Array} o object to wrap into an array
   * @returns capsulated object within an array
  */
  toArray: function (o) {
    return sol.common.ObjectUtils.isArray(o) ? o : [o];
  },

  /**
   * Merges a list of objects.
   *
   * The `base` object is the first object in the merging chain.
   * Properties from the `mergeList` objects will be added to the base object.
   * If there is already a property in the base object, it will only be overwritten, if the types match.
   *
   *     var o1 = { a: "hello", b: "world" };
   *     var o2 = { b: "developer", c: "foobar" };
   *     var merged = sol.common.ObjectUtils.mergeObject(o1, [o2]);  // merged => { a: "hello", b: "developer", c: "foobar" }
   *
   * @param {Object} base
   * @param {Object[]} mergeList (optional)
   * @param {Boolean} [preserveCustom=false] (optional) If `true`, all objects from `mergeList` will be cloned. Otherwise the merge process will work directly on the objects and may alter them
   * @param {String} path (optional) Startpath/objectname, used for logging
   * @param {Function} assignCallback (optional) Will be called for every property assignment (if set, this function has to take care of the assignment itself)
   * @param {Function} recursionCheck (optional) This function is called for every property and decides if the property has to be merged recursively (returns `true`) or not (returns `false`)
   * @return {Object} The merged object
   */
  mergeObjects: function (base, mergeList, preserveCustom, path, assignCallback, recursionCheck) {
    var me = this,
        log = [],
        custom = [],
        idx, curr, succ, logTemp;

    if (Array.isArray(mergeList)) {
      custom = mergeList.slice();
    } else if (!!mergeList) {
      custom.push(mergeList);
    }

    if (custom.length === 0) {
      return base;
    }

    function clone(o) {
      return !!preserveCustom ? me.clone(o) : o;
    }

    idx = custom.unshift(base) - 1;

    while (idx > 0) {
      curr = clone(custom[idx]);
      succ = clone(custom[idx - 1]);

      logTemp = [];
      custom[idx - 1] = me.merge(curr, succ, logTemp, path, assignCallback, recursionCheck);
      if (logTemp.length > 0) {
        logTemp.unshift("Custom argument id " + idx + " contains logs!");
        log = log.concat(logTemp);
      }

      idx--;
    }

    if (log.length > 0) {
      custom[0]._$mergeLog$_ = log;
    }

    return custom[0];
  },

  /**
   * Clones an object.
   * @param {Object} o
   * @returns {Object}
   */
  clone: function (o) {
    return JSON.parse(JSON.stringify(o));
  },

  /**
   * Merges all properties in base into custom. Existing properties in custom will be preserved, but only
   * if they match the type of the property in base. Otherwise the property of base will be used
   * and a log will be written to array parameter log.
   *
   * This function does not support functions. It does support Array, Object and Date and creates a clone from it.
   * @param {Object} custom The object containing all merged data (object will be altered)
   * @param {Object} base The object from which will be copied to the `custom` object
   * @param {String[]} [log=[]] (optional) Logging messages will be pushed to this array
   * @param {String} [path=''] (optional) Startpath/objectname, used for logging
   * @param {Function} assignCallback (optional) Will be called for every property assignment (if set, this function has to take care of the assignment itself)
   * @param {Function} recursionCheck (optional) This function is called for every property and decides if the property has to be merged recursively (returns `true`) or not (returns `false`)
   * @returns {Object} The merged object (`custom`)
   */
  merge: function (custom, base, log, path, assignCallback, recursionCheck) {
    var me = this,
        prop;
    log = log || [];
    path = path || "";

    recursionCheck = recursionCheck || function (custom, base, prop) { // eslint-disable-line no-shadow
      return base[prop] instanceof Object && !(base[prop] instanceof Array) && !(base[prop] instanceof Date);
    };

    assignCallback = assignCallback || function (target, source, propertyName) {
      target[propertyName] = source[propertyName];
    };

    for (prop in base) {
      if (base.hasOwnProperty(prop)) {
        //check for same type (array must be checked separately) and use default property instead
        if ((typeof custom[prop] !== "undefined") && (custom[prop] !== null) && (base[prop] !== null) && ((typeof custom[prop] !== typeof base[prop]) ||
          (Array.isArray(base[prop]) !== Array.isArray(custom[prop])))) {
          log.push("Warning: The type of custom property " + path + "." + prop + " is not the same as in the target. Custom property is ignored.");
          custom[prop] = me.clone(base[prop]);
        } else if (recursionCheck(custom, base, prop)) {
          //recursion
          custom[prop] = me.merge(custom[prop] || {}, base[prop], log, path + "." + prop, assignCallback, recursionCheck); //return empty object if p does not exist in target
        } else if (custom[prop] === undefined) {
          //copy default property only if not exist in custom
          if (base[prop] instanceof Date) {
            custom[prop] = new Date(base[prop]);
          } else if (base[prop] instanceof Array) {
            custom[prop] = me.clone(base[prop]);
          } else {
            assignCallback(custom, base, prop);
          }
        }
      }
    }
    return custom;
  },

  /**
   * Converts an array of objects to an object
   * @param {Array} arr
   * @param {keyPropName} keyPropName name of the property key for the new key
   * @return {Object}
   */
  getObjectFromArray: function (arr, keyPropName) {
    var i,
        o = {},
        entry;
    if (arr && keyPropName) {
      for (i = 0; i < arr.length; i++) {
        entry = arr[i];
        o[String(entry[keyPropName])] = entry;
      }
    }
    return o;
  },

  /**
   * Returns the values of an Object
   * @param {Object} o Object
   * @return {Array} Array
   */
  getValues: function (o) {
    var key,
        arr = [];
    if (o) {
      for (key in o) {
        if (o.hasOwnProperty(key)) {
          arr.push(o[key]);
        }
      }
    }
    return arr;
  },

  /**
   * Function ´forEach´ that works in Rhino and Nashorn
   * @param {Array} arr Array
   * @param {Function} callback Callback
   * @param {Object} context Context
   */
  forEach: function (arr, callback, context) {
    if (!callback) {
      throw "Callback is missing";
    }
    var i;
    for (i = 0; i < arr.length; i++) {
      callback(arr[i], i, context);
    }
  },

  /**
   * Function ´map´ that works in Rhino and Nashorn
   * @param {Array} arr Array
   * @param {Function} callback Callback
   * @param {Object} context Context
   * @return {Array} Array
   */
  map: function (arr, callback, context) {
    var i,
        resultArr = [];
    if (!callback) {
      throw "Callback is missing";
    }
    for (i = 0; i < arr.length; i++) {
      resultArr.push(callback(arr[i], i, context));
    }
    return resultArr;
  },

  // Polyfill for Array.prototype.find (Rhino <= 1.7R5)
  arrayFind: function (arr, cb) {
    var list = arr, length = list.length, value, i;

    for (i = 0; i < length; i++) {
      value = list[i];
      if (cb(value, i, list)) {
        return value;
      }
    }
    return undefined;
  },

  /**
  * Returns the first object of an array whose property "id" has the value val.
  * @param {Array} a array containing the objects
  * @param {Any} val value to search for
  * @param {String} customProp  custom property name, if the property is not "id"
  * @param {Function} customCallback custom callback for Array.find function
  * @return {Object} found object or undefined. false if a is not an Array
  * Rhino 1.7R5 does not implement Array.prototype.find. arrayFind takes is place in this case
  */
  findObjInArray: function (a, val, customProp, customCallback) {
    var me = this,
        cb = function (ae) {
          return ae[customProp || "id"] === val;
        };
    return (
      Array.isArray(a)
      &&
      (
        (a.find && a.find(customCallback || cb))
        || (!a.find && me.arrayFind(a, (customCallback || cb)))
      )
      || undefined
    );
  },

  /**
   * Returns an object containing only specific properties of the input object.
   * @param {Object} o  input object
   * @param {Array} include  array containing all properties to include as strings. Empty array includes all properties.
   * @param {Array} exclude  array containing all properties to exclude. Empty array means exclude nothing from include. If values are defined in include and exclude, they are excluded.
   * @return {Object}
   */
  getPropsOfObj: function (o, include, exclude) {
    var name,
        result = {};

    if (typeof o === "object" && (Array.isArray(include)) && (!exclude || Array.isArray(exclude))) {
      for (name in o) {
        name = name.trim();
        if (
          name.length > 0
          && (
            (include.length === 0 && (!exclude || exclude.indexOf(name) === -1))
            || (include.indexOf(name) > -1 && (!exclude || exclude.indexOf(name) === -1))
          )
        ) {
          result[name] = o[name];
        }
      }
    }
    return result;
  },

  /**
   * gets a property from an `object` by traversing the passed `path`.
   * if the property is inside an object which is inside an array, the array must contain
   * a property called `id` containing the corresponding part of the `path`.
   * e.g. y = { x: [ {id: "test", myniceprop: 42} ] }
   * if you want to retrieve "myniceprop", you would call getProp(y, "x.test.myniceprop")
   * note: this is also a better way to get a property from an object with probably uninitialized preceding properties
   * optional: if passed a `customPropName`, getProp will use the name as the array-object-property name
   * e.g. y = { x: [ {__ID: "test", myniceprop: 42} ] }    -->  getProp(y, "x.test.myniceprop", "__ID")
   *
   * If your target prop is an array you can use the special prop `$length` to determine the array
   * length of the element. This works only in context of array and will terminate the prop request.
   *
   * e.g. y = { x : [{ id:1 }, { id:2 }] } --> getProp(y, "x.$length")
   *
   * @param {Object} object Object
   * @param {String} path Path
   * @param {String} customPropName Custom property name
   * @returns {Object}
   */
  getProp: function (object, path, customPropName) {
    var arr = (typeof path === "string") && path.split("."), curr = undefined;
    if (arr.length > 0) {
      curr = object;
      arr.forEach(function (key) {
        if (curr && (key.length > 0)) {
          if (Array.isArray(curr)) {
            if (key === "$length") {
              // special prop to determine length of an array
              curr = curr.length;
            } else {
              curr = sol.common.ObjectUtils.findObjInArray(curr, key, customPropName);
            }
          } else if (typeof curr === "object") {
            curr = curr[key];
          }
        }
      });
    } else if (path === "" || path === undefined) {
      curr = object;
    }
    return curr;
  },

  /**
   * sets a property of an `object` by traversing the passed `path`.
   * the last part of the path is the target property name.
   * if a property is inside an object which is inside an array, the array must contain
   * a property called `id` containing the corresponding part of the `path`.
   * The function can then traverse the array and create an empty object if necessary.
   *
   * @param {Object} object Object
   * @param {String} path Path
   * @param {String} value Value to be set
   * @param {Boolean} overwrite Overwrite path property if it is not an object yet
   * @param {String} customPropName Custom property name
   * @returns {Object}
   */
  setProp: function (object, path, value, overwrite, customPropName) {
    var me = this, arr = (typeof path === "string") && path.split("."),
        targetProp = arr.pop(), curr = undefined;
    if (arr.length > 0) {
      curr = object;
      arr.forEach(function (key) {
        var val = curr[key],
            arrObj;
        if (me.type(curr[key], "object")) {
          curr = val;
        } else if (Array.isArray(curr[key])) {
          if (!(arrObj = sol.common.ObjectUtils.findObjInArray(val, customPropName.value, (customPropName || {}).key || key))) {
            arrObj = {};
            arrObj[(customPropName || {}).key || key || "id"] = customPropName.value;
            val.push(arrObj);
          }
          curr = arrObj;
        } else if (val == null || overwrite) {
          curr = (curr[key] = {});
        } else {
          throw "path part `" + key + "` was not an object. Pass true as 'overwrite' parameter to perform this action ...";
        }
      });
      curr[targetProp] = value;
    } else if (targetProp !== "") {
      object[targetProp] = value;
    } else if (path === "" || path === undefined) {
      throw "path must be defined in setProp";
    }

    return curr;
  },

  /**
   * Sorts a table by column
   * @param {Array} arr Array
   * @param {Number} columnIndex Column index
   */
  sortTableByColumn: function (arr, columnIndex) {

    if (!arr) {
      throw "Array is empty";
    }

    columnIndex = columnIndex || 0;

    arr.sort(function (a, b) {
      if (a[columnIndex] === b[columnIndex]) {
        return 0;
      } else {
        return (a[columnIndex] < b[columnIndex]) ? -1 : 1;
      }
    });

    return arr;
  },

  /**
   * Converts a JavaScript array to a Java array
   * @param {Array} jsArray JavaScript array
   * @param {Object} params Parameters
   * @param {String} [params.javaType=java.lang.String] Java type
   * @return {java.lang.Array} Java array
   */
  toJavaArray: function (jsArray, params) {
    var javaArray, clazz, i, elem;

    jsArray = jsArray || [];

    params = params || {};
    params.javaType = params.javaType || "java.lang.String";
    clazz = java.lang.Class.forName(params.javaType);

    javaArray = java.lang.reflect.Array.newInstance(clazz, jsArray.length);
    for (i = 0; i < jsArray.length; i++) {
      elem = jsArray[i];
      if (params.type == "java.lang.String") {
        elem = new java.lang.String(elem);
      }
      javaArray[i] = elem;
    }

    return javaArray;
  },


  /**
   * @param {Array|String} element
   * @param {*} wc
   * @param {*} ignoreCase
   * @returns {RegExp}
   */
  toRegExp: function (element, wc, ignoreCase) {
    return (typeof element === "string"
      ? sol.common.ObjectUtils.stringToRegExp
      : sol.common.ObjectUtils.arrayToRegExp
    )(element, wc, ignoreCase);
  },

  /**
   *
   * @param {*} arr
   * @param {*} wc
   * @returns {RegExp}
   */
  arrayToRegExp: function (arr, wc) {
    var compl, len = arr.length,
        addBitwiseOR = function (query, index, length) {
          return ((index + 1 < length) && (query += "|")), query;
        };

    compl = arr.reduce(function (acc, str, i) {
      if (typeof str !== "string") {
        throw "filter: only string elements are allowed for filter criteria arrays";
      }
      return acc + addBitwiseOR("^" + str + "$", i, len);
    }, "");

    return new RegExp(compl.replace(new RegExp("\\" + wc, "g"), "." + wc));
  },

  /**
   * @param {*} str
   * @param {*} wc
   * @param {*} ignoreCase
   * @returns {RegExp}
   */
  stringToRegExp: function (str, wc, ignoreCase) {
    return new RegExp("^" + str.replace(new RegExp("\\" + wc, "g"), "." + wc), (ignoreCase ? "i" : ""));
  },

  /**
   * Traverses all nodes of an object tree
   * @param {Object} obj Object
   * @param {Function} func Function
   */
  traverse: function (obj, func) {
    var me = this,
        key;

    for (key in obj) {
      if (obj[key] != null) {
        func.apply(this, [key, obj[key]]);
        if (typeof (obj[key]) == "object") {
          me.traverse(obj[key], func);
        }
      }
    }
  }
});

sol.define("sol.common.mixins.ObjectFilter", {
  mixin: true,

  generateFilter: function (filter) {
    if (!Array.isArray(filter)) {
      throw "filter must be an array of objects!";
    }

    function isValidCriterion(criterion) {
      if ((typeof criterion !== "object") || (typeof criterion.prop !== "string" && criterion.prop) || ((typeof criterion.value !== "string") && (!Array.isArray(criterion.value)))) {
        throw "filter criterion is no object, or prop or value not suited for filtering";
      }
      return true;
    }

    return filter
      .filter(isValidCriterion)
      // clone is important because next function mutates criterion value
      .map(sol.common.ObjectUtils.clone)
      .map(function (criterion) {
        criterion.value = sol.common.ObjectUtils.toRegExp(criterion.value, "*");
        return criterion;
      });
  },

  matchObject: function (filter, obj) {
    function testProperty(currentFilter) {
      var propertyValue = sol.common.ObjectUtils.getProp(obj, currentFilter.prop);
      return currentFilter.value.test(propertyValue);
    }

    return (filter || [])
      .reduce(function (matches, currentFilter) {
        return matches && testProperty(currentFilter);
      }, true);
  }
});

//# sourceURL=lib_sol.common.ObjectUtils.js