﻿/**
 * Bunch of useful filters for angularJS(with no external dependencies!)
 * @version v0.5.7 - 2015-10-04 * @link https://github.com/a8m/angular-filter
 * @author Ariel Mashraki <ariel@mashraki.co.il>
 * @license MIT License, http://www.opensource.org/licenses/MIT
 */
(function (window, angular, undefined) {
    /*jshint globalstrict:true*/
    'use strict';

    var isDefined = angular.isDefined,
        isUndefined = angular.isUndefined,
        isFunction = angular.isFunction,
        isString = angular.isString,
        isNumber = angular.isNumber,
        isObject = angular.isObject,
        isArray = angular.isArray,
        forEach = angular.forEach,
        extend = angular.extend,
        copy = angular.copy,
        equals = angular.equals;


    /**
     * @description
     * get an object and return array of values
     * @param object
     * @returns {Array}
     */
    function toArray(object) {
        return isArray(object)
          ? object
          : Object.keys(object).map(function (key) {
              return object[key];
          });
    }

    /**
     * @param value
     * @returns {boolean}
     */
    function isNull(value) {
        return value === null;
    }

    /**
     * @description
     * return if object contains partial object
     * @param partial{object}
     * @param object{object}
     * @returns {boolean}
     */
    function objectContains(partial, object) {
        var keys = Object.keys(partial);

        return keys.map(function (el) {
            return (object[el] !== undefined) && (object[el] == partial[el]);
        }).indexOf(false) == -1;

    }

    /**
     * @description
     * search for approximate pattern in string
     * @param word
     * @param pattern
     * @returns {*}
     */
    function hasApproxPattern(word, pattern) {
        if (pattern === '')
            return word;

        var index = word.indexOf(pattern.charAt(0));

        if (index === -1)
            return false;

        return hasApproxPattern(word.substr(index + 1), pattern.substr(1))
    }

    /**
     * @description
     * return the first n element of an array,
     * if expression provided, is returns as long the expression return truthy
     * @param array
     * @param n {number}
     * @param expression {$parse}
     * @return array or single object
     */
    function getFirstMatches(array, n, expression) {
        var count = 0;

        return array.filter(function (elm) {
            var rest = isDefined(expression) ? (count < n && expression(elm)) : count < n;
            count = rest ? count + 1 : count;

            return rest;
        });
    }
    /**
     * Polyfill to ECMA6 String.prototype.contains
     */
    if (!String.prototype.contains) {
        String.prototype.contains = function () {
            return String.prototype.indexOf.apply(this, arguments) !== -1;
        };
    }

    /**
     * @param num {Number}
     * @param decimal {Number}
     * @param $math
     * @returns {Number}
     */
    function convertToDecimal(num, decimal, $math) {
        return $math.round(num * $math.pow(10, decimal)) / ($math.pow(10, decimal));
    }

    /**
     * @description
     * Get an object, and return an array composed of it's properties names(nested too).
     * @param obj {Object}
     * @param stack {Array}
     * @param parent {String}
     * @returns {Array}
     * @example
     * parseKeys({ a:1, b: { c:2, d: { e: 3 } } }) ==> ["a", "b.c", "b.d.e"]
     */
    function deepKeys(obj, stack, parent) {
        stack = stack || [];
        var keys = Object.keys(obj);

        keys.forEach(function (el) {
            //if it's a nested object
            if (isObject(obj[el]) && !isArray(obj[el])) {
                //concatenate the new parent if exist
                var p = parent ? parent + '.' + el : parent;
                deepKeys(obj[el], stack, p || el);
            } else {
                //create and save the key
                var key = parent ? parent + '.' + el : el;
                stack.push(key)
            }
        });
        return stack
    }

    /**
     * @description
     * Test if given object is a Scope instance
     * @param obj
     * @returns {Boolean}
     */
    function isScope(obj) {
        return obj && obj.$evalAsync && obj.$watch;
    }

    /**
     * @ngdoc filter
     * @name a8m.angular
     * @kind function
     *
     * @description
     * reference to angular function
     */

    angular.module('a8m.angular', [])

        .filter('isUndefined', function () {
            return function (input) {
                return angular.isUndefined(input);
            }
        })
        .filter('isDefined', function () {
            return function (input) {
                return angular.isDefined(input);
            }
        })
        .filter('isFunction', function () {
            return function (input) {
                return angular.isFunction(input);
            }
        })
        .filter('isString', function () {
            return function (input) {
                return angular.isString(input)
            }
        })
        .filter('isNumber', function () {
            return function (input) {
                return angular.isNumber(input);
            }
        })
        .filter('isArray', function () {
            return function (input) {
                return angular.isArray(input);
            }
        })
        .filter('isObject', function () {
            return function (input) {
                return angular.isObject(input);
            }
        })
        .filter('isEqual', function () {
            return function (o1, o2) {
                return angular.equals(o1, o2);
            }
        });

    /**
     * @ngdoc filter
     * @name a8m.conditions
     * @kind function
     *
     * @description
     * reference to math conditions
     */
    angular.module('a8m.conditions', [])

     .filter({
         isGreaterThan: isGreaterThanFilter,
         '>': isGreaterThanFilter,

         isGreaterThanOrEqualTo: isGreaterThanOrEqualToFilter,
         '>=': isGreaterThanOrEqualToFilter,

         isLessThan: isLessThanFilter,
         '<': isLessThanFilter,

         isLessThanOrEqualTo: isLessThanOrEqualToFilter,
         '<=': isLessThanOrEqualToFilter,

         isEqualTo: isEqualToFilter,
         '==': isEqualToFilter,

         isNotEqualTo: isNotEqualToFilter,
         '!=': isNotEqualToFilter,

         isIdenticalTo: isIdenticalToFilter,
         '===': isIdenticalToFilter,

         isNotIdenticalTo: isNotIdenticalToFilter,
         '!==': isNotIdenticalToFilter
     });

    function isGreaterThanFilter() {
        return function (input, check) {
            return input > check;
        };
    }

    function isGreaterThanOrEqualToFilter() {
        return function (input, check) {
            return input >= check;
        };
    }

    function isLessThanFilter() {
        return function (input, check) {
            return input < check;
        };
    }

    function isLessThanOrEqualToFilter() {
        return function (input, check) {
            return input <= check;
        };
    }

    function isEqualToFilter() {
        return function (input, check) {
            return input == check;
        };
    }

    function isNotEqualToFilter() {
        return function (input, check) {
            return input != check;
        };
    }

    function isIdenticalToFilter() {
        return function (input, check) {
            return input === check;
        };
    }

    function isNotIdenticalToFilter() {
        return function (input, check) {
            return input !== check;
        };
    }
    /**
     * @ngdoc filter
     * @name isNull
     * @kind function
     *
     * @description
     * checks if value is null or not
     * @return Boolean
     */
    angular.module('a8m.is-null', [])
        .filter('isNull', function () {
            return function (input) {
                return isNull(input);
            }
        });

    /**
     * @ngdoc filter
     * @name after-where
     * @kind function
     *
     * @description
     * get a collection and properties object, and returns all of the items
     * in the collection after the first that found with the given properties.
     *
     */
    angular.module('a8m.after-where', [])
        .filter('afterWhere', function () {
            return function (collection, object) {

                collection = isObject(collection)
                  ? toArray(collection)
                  : collection;

                if (!isArray(collection) || isUndefined(object)) return collection;

                var index = collection.map(function (elm) {
                    return objectContains(object, elm);
                }).indexOf(true);

                return collection.slice((index === -1) ? 0 : index);
            }
        });

    /**
     * @ngdoc filter
     * @name after
     * @kind function
     *
     * @description
     * get a collection and specified count, and returns all of the items
     * in the collection after the specified count.
     *
     */

    angular.module('a8m.after', [])
        .filter('after', function () {
            return function (collection, count) {
                collection = isObject(collection)
                  ? toArray(collection)
                  : collection;

                return (isArray(collection))
                  ? collection.slice(count)
                  : collection;
            }
        });

    /**
     * @ngdoc filter
     * @name before-where
     * @kind function
     *
     * @description
     * get a collection and properties object, and returns all of the items
     * in the collection before the first that found with the given properties.
     */
    angular.module('a8m.before-where', [])
      .filter('beforeWhere', function () {
          return function (collection, object) {

              collection = isObject(collection)
                ? toArray(collection)
                : collection;

              if (!isArray(collection) || isUndefined(object)) return collection;

              var index = collection.map(function (elm) {
                  return objectContains(object, elm);
              }).indexOf(true);

              return collection.slice(0, (index === -1) ? collection.length : ++index);
          }
      });

    /**
     * @ngdoc filter
     * @name before
     * @kind function
     *
     * @description
     * get a collection and specified count, and returns all of the items
     * in the collection before the specified count.
     */
    angular.module('a8m.before', [])
        .filter('before', function () {
            return function (collection, count) {
                collection = isObject(collection)
                  ? toArray(collection)
                  : collection;

                return (isArray(collection))
                  ? collection.slice(0, (!count) ? count : --count)
                  : collection;
            }
        });

    /**
     * @ngdoc filter
     * @name chunkBy
     * @kind function
     *
     * @description
     * Collect data into fixed-length chunks or blocks
     */

    angular.module('a8m.chunk-by', ['a8m.filter-watcher'])
        .filter('chunkBy', ['filterWatcher', function (filterWatcher) {
            return function (array, n, fillVal) {

                return filterWatcher.isMemoized('chunkBy', arguments) ||
                    filterWatcher.memoize('chunkBy', arguments, this,
                        _chunkBy(array, n, fillVal));
                /**
                 * @description
                 * Get array with size `n` in `val` inside it.
                 * @param n
                 * @param val
                 * @returns {Array}
                 */
                function fill(n, val) {
                    var ret = [];
                    while (n--) ret[n] = val;
                    return ret;
                }

                function _chunkBy(array, n, fillVal) {
                    if (!isArray(array)) return array;
                    return array.map(function (el, i, self) {
                        i = i * n;
                        el = self.slice(i, i + n);
                        return !isUndefined(fillVal) && el.length < n
                            ? el.concat(fill(n - el.length, fillVal))
                            : el;
                    }).slice(0, Math.ceil(array.length / n));
                }
            }
        }]);

    /**
     * @ngdoc filter
     * @name concat
     * @kind function
     *
     * @description
     * get (array/object, object/array) and return merged collection
     */
    angular.module('a8m.concat', [])
      .filter('concat', [function () {
          return function (collection, joined) {

              if (isUndefined(joined)) return collection;

              if (isArray(collection)) {
                  return isObject(joined)
                    ? collection.concat(toArray(joined))
                    : collection.concat(joined);
              }

              if (isObject(collection)) {
                  var array = toArray(collection);
                  return (isObject(joined))
                    ? array.concat(toArray(joined))
                    : array.concat(joined);
              }
              return collection;
          };
      }
      ]);

    /**
     * @ngdoc filter
     * @name contains
     * @kind function
     *
     * @description
     * Checks if given expression is present in one or more object in the collection
     */
    angular.module('a8m.contains', [])
      .filter({
          contains: ['$parse', containsFilter],
          some: ['$parse', containsFilter]
      });

    function containsFilter($parse) {
        return function (collection, expression) {

            collection = isObject(collection) ? toArray(collection) : collection;

            if (!isArray(collection) || isUndefined(expression)) {
                return false;
            }

            return collection.some(function (elm) {
                return (isObject(elm) || isFunction(expression))
                  ? $parse(expression)(elm)
                  : elm === expression;
            });

        }
    }

    /**
     * @ngdoc filter
     * @name countBy
     * @kind function
     *
     * @description
     * Sorts a list into groups and returns a count for the number of objects in each group.
     */

    angular.module('a8m.count-by', [])

      .filter('countBy', ['$parse', function ($parse) {
          return function (collection, property) {

              var result = {},
                get = $parse(property),
                prop;

              collection = (isObject(collection)) ? toArray(collection) : collection;

              if (!isArray(collection) || isUndefined(property)) {
                  return collection;
              }

              collection.forEach(function (elm) {
                  prop = get(elm);

                  if (!result[prop]) {
                      result[prop] = 0;
                  }

                  result[prop]++;
              });

              return result;
          }
      }]);

    /**
     * @ngdoc filter
     * @name defaults
     * @kind function
     *
     * @description
     * defaultsFilter allows to specify a default fallback value for properties that resolve to undefined.
     */
    angular.module('a8m.defaults', [])
      .filter('defaults', ['$parse', function ($parse) {
          return function (collection, defaults) {

              collection = isObject(collection) ? toArray(collection) : collection;

              if (!isArray(collection) || !isObject(defaults)) {
                  return collection;
              }

              var keys = deepKeys(defaults);

              collection.forEach(function (elm) {
                  //loop through all the keys
                  keys.forEach(function (key) {
                      var getter = $parse(key);
                      var setter = getter.assign;
                      //if it's not exist
                      if (isUndefined(getter(elm))) {
                          //get from defaults, and set to the returned object
                          setter(elm, getter(defaults))
                      }
                  });
              });

              return collection;
          }
      }]);
    /**
     * @ngdoc filter
     * @name every
     * @kind function
     *
     * @description
     * Checks if given expression is present in all members in the collection
     *
     */
    angular.module('a8m.every', [])
      .filter('every', ['$parse', function ($parse) {
          return function (collection, expression) {
              collection = isObject(collection) ? toArray(collection) : collection;

              if (!isArray(collection) || isUndefined(expression)) {
                  return true;
              }

              return collection.every(function (elm) {
                  return (isObject(elm) || isFunction(expression))
                    ? $parse(expression)(elm)
                    : elm === expression;
              });
          }
      }]);

    /**
     * @ngdoc filter
     * @name filterBy
     * @kind function
     *
     * @description
     * filter by specific properties, avoid the rest
     */
    angular.module('a8m.filter-by', [])
      .filter('filterBy', ['$parse', function ($parse) {
          return function (collection, properties, search) {
              var comparator;

              search = (isString(search) || isNumber(search)) ?
                String(search).toLowerCase() : undefined;

              collection = isObject(collection) ? toArray(collection) : collection;

              if (!isArray(collection) || isUndefined(search)) {
                  return collection;
              }

              return collection.filter(function (elm) {
                  return properties.some(function (prop) {

                      /**
                       * check if there is concatenate properties
                       * example:
                       * object: { first: 'foo', last:'bar' }
                       * filterBy: ['first + last'] => search by full name(i.e 'foo bar')
                       */
                      if (!~prop.indexOf('+')) {
                          comparator = $parse(prop)(elm)
                      } else {
                          var propList = prop.replace(new RegExp('\\s', 'g'), '').split('+');
                          comparator = propList.reduce(function (prev, cur, index) {
                              return (index === 1) ? $parse(prev)(elm) + ' ' + $parse(cur)(elm) :
                                prev + ' ' + $parse(cur)(elm);
                          });
                      }

                      return (isString(comparator) || isNumber(comparator))
                        ? String(comparator).toLowerCase().contains(search)
                        : false;
                  });
              });
          }
      }]);

    /**
     * @ngdoc filter
     * @name first
     * @kind function
     *
     * @description
     * Gets the first element or first n elements of an array
     * if callback is provided, is returns as long the callback return truthy
     */
    angular.module('a8m.first', [])
      .filter('first', ['$parse', function ($parse) {
          return function (collection) {
              var n
                , getter
                , args;

              collection = isObject(collection)
                ? toArray(collection)
                : collection;

              if (!isArray(collection)) {
                  return collection;
              }

              args = Array.prototype.slice.call(arguments, 1);
              n = (isNumber(args[0])) ? args[0] : 1;
              getter = (!isNumber(args[0])) ? args[0] : (!isNumber(args[1])) ? args[1] : undefined;

              return (args.length) ? getFirstMatches(collection, n, (getter) ? $parse(getter) : getter) :
                collection[0];
          }
      }]);

    /**
     * @ngdoc filter
     * @name flatten
     * @kind function
     *
     * @description
     * Flattens a nested array (the nesting can be to any depth).
     * If you pass shallow, the array will only be flattened a single level
     */
    angular.module('a8m.flatten', [])
      .filter('flatten', function () {
          return function (collection, shallow) {

              shallow = shallow || false;
              collection = isObject(collection)
                ? toArray(collection)
                : collection;

              if (!isArray(collection)) {
                  return collection;
              }

              return !shallow
                ? flatten(collection, 0)
                : [].concat.apply([], collection);
          }
      });

    /**
     * flatten nested array (the nesting can be to any depth).
     * @param array {Array}
     * @param i {int}
     * @returns {Array}
     * @private
     */
    function flatten(array, i) {
        i = i || 0;

        if (i >= array.length)
            return array;

        if (isArray(array[i])) {
            return flatten(array.slice(0, i)
              .concat(array[i], array.slice(i + 1)), i);
        }
        return flatten(array, i + 1);
    }

    /**
     * @ngdoc filter
     * @name fuzzyByKey
     * @kind function
     *
     * @description
     * fuzzy string searching by key
     */
    angular.module('a8m.fuzzy-by', [])
      .filter('fuzzyBy', ['$parse', function ($parse) {
          return function (collection, property, search, csensitive) {

              var sensitive = csensitive || false,
                prop, getter;

              collection = isObject(collection) ? toArray(collection) : collection;

              if (!isArray(collection) || isUndefined(property)
                || isUndefined(search)) {
                  return collection;
              }

              getter = $parse(property);

              return collection.filter(function (elm) {

                  prop = getter(elm);
                  if (!isString(prop)) {
                      return false;
                  }

                  prop = (sensitive) ? prop : prop.toLowerCase();
                  search = (sensitive) ? search : search.toLowerCase();

                  return hasApproxPattern(prop, search) !== false
              })
          }

      }]);
    /**
     * @ngdoc filter
     * @name fuzzy
     * @kind function
     *
     * @description
     * fuzzy string searching for array of strings, objects
     */
    angular.module('a8m.fuzzy', [])
      .filter('fuzzy', function () {
          return function (collection, search, csensitive) {
              var sensitive = csensitive || false;
              collection = isObject(collection) ? toArray(collection) : collection;

              if (!isArray(collection) || isUndefined(search)) {
                  return collection;
              }

              search = (sensitive) ? search : search.toLowerCase();

              return collection.filter(function (elm) {
                  if (isString(elm)) {
                      elm = (sensitive) ? elm : elm.toLowerCase();
                      return hasApproxPattern(elm, search) !== false
                  }
                  return (isObject(elm)) ? _hasApproximateKey(elm, search) : false;
              });

              /**
               * checks if object has key{string} that match
               * to fuzzy search pattern
               * @param object
               * @param search
               * @returns {boolean}
               * @private
               */
              function _hasApproximateKey(object, search) {
                  var properties = Object.keys(object),
                    prop, flag;
                  return 0 < properties.filter(function (elm) {
                      prop = object[elm];

                      //avoid iteration if we found some key that equal[performance]
                      if (flag) return true;

                      if (isString(prop)) {
                          prop = (sensitive) ? prop : prop.toLowerCase();
                          return flag = (hasApproxPattern(prop, search) !== false);
                      }

                      return false;

                  }).length;
              }
          }
      });

    /**
     * @ngdoc filter
     * @name groupBy
     * @kind function
     *
     * @description
     * Create an object composed of keys generated from the result of running each element of a collection,
     * each key is an array of the elements.
     */

    angular.module('a8m.group-by', ['a8m.filter-watcher'])
      .filter('groupBy', ['$parse', 'filterWatcher', function ($parse, filterWatcher) {
          return function (collection, property) {

              if (!isObject(collection) || isUndefined(property)) {
                  return collection;
              }

              return filterWatcher.isMemoized('groupBy', arguments) ||
                filterWatcher.memoize('groupBy', arguments, this,
                  _groupBy(collection, $parse(property)));

              /**
               * groupBy function
               * @param collection
               * @param getter
               * @returns {{}}
               */
              function _groupBy(collection, getter) {
                  var result = {};
                  var prop;

                  forEach(collection, function (elm) {
                      prop = getter(elm);

                      if (!result[prop]) {
                          result[prop] = [];
                      }
                      result[prop].push(elm);
                  });
                  return result;
              }
          }
      }]);

    /**
     * @ngdoc filter
     * @name isEmpty
     * @kind function
     *
     * @description
     * get collection or string and return if it empty
     */
    angular.module('a8m.is-empty', [])
      .filter('isEmpty', function () {
          return function (collection) {
              return isObject(collection)
                ? !toArray(collection).length
                : !collection.length;
          }
      });

    /**
     * @ngdoc filter
     * @name join
     * @kind function
     *
     * @description
     * join a collection by a provided delimiter (space by default)
     */
    angular.module('a8m.join', [])
      .filter('join', function () {
          return function (input, delimiter) {
              if (isUndefined(input) || !isArray(input)) {
                  return input;
              }
              if (isUndefined(delimiter)) delimiter = ' ';

              return input.join(delimiter);
          };
      })
    ;

    /**
     * @ngdoc filter
     * @name last
     * @kind function
     *
     * @description
     * Gets the last element or last n elements of an array
     * if callback is provided, is returns as long the callback return truthy
     */
    angular.module('a8m.last', [])
      .filter('last', ['$parse', function ($parse) {
          return function (collection) {
              var n
                , getter
                , args
                //cuz reverse change our src collection
                //and we don't want side effects
                , reversed = copy(collection);

              reversed = isObject(reversed)
                ? toArray(reversed)
                : reversed;

              if (!isArray(reversed)) {
                  return reversed;
              }

              args = Array.prototype.slice.call(arguments, 1);
              n = (isNumber(args[0])) ? args[0] : 1;
              getter = (!isNumber(args[0])) ? args[0] : (!isNumber(args[1])) ? args[1] : undefined;

              return (args.length)
                //send reversed collection as arguments, and reverse it back as result
                ? getFirstMatches(reversed.reverse(), n, (getter) ? $parse(getter) : getter).reverse()
                //get the last element
                : reversed[reversed.length - 1];
          }
      }]);

    /**
     * @ngdoc filter
     * @name map
     * @kind function
     *
     * @description
     * Returns a new collection of the results of each expression execution.
     */
    angular.module('a8m.map', [])
      .filter('map', ['$parse', function ($parse) {
          return function (collection, expression) {

              collection = isObject(collection)
                ? toArray(collection)
                : collection;

              if (!isArray(collection) || isUndefined(expression)) {
                  return collection;
              }

              return collection.map(function (elm) {
                  return $parse(expression)(elm);
              });
          }
      }]);

    /**
     * @ngdoc filter
     * @name omit
     * @kind function
     *
     * @description
     * filter collection by expression
     */

    angular.module('a8m.omit', [])

      .filter('omit', ['$parse', function ($parse) {
          return function (collection, expression) {

              collection = isObject(collection)
                ? toArray(collection)
                : collection;

              if (!isArray(collection) || isUndefined(expression)) {
                  return collection;
              }

              return collection.filter(function (elm) {
                  return !($parse(expression)(elm));
              });
          }
      }]);

    /**
     * @ngdoc filter
     * @name pick
     * @kind function
     *
     * @description
     * filter collection by expression
     */

    angular.module('a8m.pick', [])

      .filter('pick', ['$parse', function ($parse) {
          return function (collection, expression) {

              collection = isObject(collection)
                ? toArray(collection)
                : collection;

              if (!isArray(collection) || isUndefined(expression)) {
                  return collection;
              }

              return collection.filter(function (elm) {
                  return $parse(expression)(elm);
              });
          }
      }]);

    /**
     * @ngdoc filter
     * @name range
     * @kind function
     *
     * @description
     * rangeFilter provides some support for a for loop using numbers
     */
    angular.module('a8m.range', [])
      .filter('range', function () {
          return function (input, total) {
              for (var i = 0; i < parseInt(total) ; i++) {
                  input.push(i);
              }
              return input;
          };
      });
    /**
     * @ngdoc filter
     * @name removeWith
     * @kind function
     *
     * @description
     * get collection and properties object, and removed elements
     * with this properties
     */

    angular.module('a8m.remove-with', [])
      .filter('removeWith', function () {
          return function (collection, object) {

              if (isUndefined(object)) {
                  return collection;
              }
              collection = isObject(collection)
                ? toArray(collection)
                : collection;

              return collection.filter(function (elm) {
                  return !objectContains(object, elm);
              });
          }
      });


    /**
     * @ngdoc filter
     * @name remove
     * @kind function
     *
     * @description
     * remove specific members from collection
     */

    angular.module('a8m.remove', [])

      .filter('remove', function () {
          return function (collection) {
              collection = isObject(collection) ? toArray(collection) : collection;
              var args = Array.prototype.slice.call(arguments, 1);

              if (!isArray(collection)) {
                  return collection;
              }

              return collection.filter(function (member) {
                  return !args.some(function (nest) {
                      return equals(nest, member);
                  })
              });
          }
      });

    /**
     * @ngdoc filter
     * @name reverse
     * @kind function
     *
     * @description
     * Reverses a string or collection
     */
    angular.module('a8m.reverse', [])
        .filter('reverse', [function () {
            return function (input) {
                input = isObject(input) ? toArray(input) : input;

                if (isString(input)) {
                    return input.split('').reverse().join('');
                }

                return isArray(input)
                  ? input.slice().reverse()
                  : input;
            }
        }]);

    /**
     * @ngdoc filter
     * @name searchField
     * @kind function
     *
     * @description
     * for each member, join several strings field and add them to
     * new field called 'searchField' (use for search filtering)
     */
    angular.module('a8m.search-field', [])
      .filter('searchField', ['$parse', function ($parse) {
          return function (collection) {

              var get, field;

              collection = isObject(collection) ? toArray(collection) : collection;

              var args = Array.prototype.slice.call(arguments, 1);

              if (!isArray(collection) || !args.length) {
                  return collection;
              }

              return collection.map(function (member) {

                  field = args.map(function (field) {
                      get = $parse(field);
                      return get(member);
                  }).join(' ');

                  return extend(member, { searchField: field });
              });
          }
      }]);

    /**
     * @ngdoc filter
     * @name toArray
     * @kind function
     *
     * @description
     * Convert objects into stable arrays.
     * if addKey set to true,the filter also attaches a new property
     * $key to the value containing the original key that was used in
     * the object we are iterating over to reference the property
     */
    angular.module('a8m.to-array', [])
      .filter('toArray', function () {
          return function (collection, addKey) {

              if (!isObject(collection)) {
                  return collection;
              }

              return !addKey
                ? toArray(collection)
                : Object.keys(collection).map(function (key) {
                    return extend(collection[key], { $key: key });
                });
          }
      });

    /**
     * @ngdoc filter
     * @name unique/uniq
     * @kind function
     *
     * @description
     * get collection and filter duplicate members
     * if uniqueFilter get a property(nested to) as argument it's
     * filter by this property as unique identifier
     */

    angular.module('a8m.unique', [])
      .filter({
          unique: ['$parse', uniqFilter],
          uniq: ['$parse', uniqFilter]
      });

    function uniqFilter($parse) {
        return function (collection, property) {

            collection = isObject(collection) ? toArray(collection) : collection;

            if (!isArray(collection)) {
                return collection;
            }

            //store all unique identifiers
            var uniqueItems = [],
                get = $parse(property);

            return (isUndefined(property))
              //if it's kind of primitive array
              ? collection.filter(function (elm, pos, self) {
                  return self.indexOf(elm) === pos;
              })
              //else compare with equals
              : collection.filter(function (elm) {
                  var prop = get(elm);
                  if (some(uniqueItems, prop)) {
                      return false;
                  }
                  uniqueItems.push(prop);
                  return true;
              });

            //checked if the unique identifier is already exist
            function some(array, member) {
                if (isUndefined(member)) {
                    return false;
                }
                return array.some(function (el) {
                    return equals(el, member);
                });
            }
        }
    }

    /**
     * @ngdoc filter
     * @name where
     * @kind function
     *
     * @description
     * of each element in a collection to the given properties object,
     * returning an array of all elements that have equivalent property values.
     *
     */
    angular.module('a8m.where', [])
      .filter('where', function () {
          return function (collection, object) {
              if (isUndefined(object)) return collection;
              collection = isObject(collection)
                ? toArray(collection)
                : collection;

              return collection.filter(function (elm) {
                  return objectContains(object, elm);
              });
          }
      });

    /**
     * @ngdoc filter
     * @name xor
     * @kind function
     *
     * @description
     * Exclusive or filter by expression
     */

    angular.module('a8m.xor', [])

      .filter('xor', ['$parse', function ($parse) {
          return function (col1, col2, expression) {

              expression = expression || false;

              col1 = isObject(col1) ? toArray(col1) : col1;
              col2 = isObject(col2) ? toArray(col2) : col2;

              if (!isArray(col1) || !isArray(col2)) return col1;

              return col1.concat(col2)
                .filter(function (elm) {
                    return !(some(elm, col1) && some(elm, col2));
                });

              function some(el, col) {
                  var getter = $parse(expression);
                  return col.some(function (dElm) {
                      return expression
                        ? equals(getter(dElm), getter(el))
                        : equals(dElm, el);
                  });
              }
          }
      }]);

    /**
     * @ngdoc filter
     * @name formatBytes
     * @kind function
     *
     * @description
     * Convert bytes into appropriate display 
     * 1024 bytes => 1 KB
     */
    angular.module('a8m.math.byteFmt', ['a8m.math'])
      .filter('byteFmt', ['$math', function ($math) {
          return function (bytes, decimal) {

              if (isNumber(decimal) && isFinite(decimal) && decimal % 1 === 0 && decimal >= 0 &&
                isNumber(bytes) && isFinite(bytes)) {
                  if (bytes < 1024) { // within 1 KB so B
                      return convertToDecimal(bytes, decimal, $math) + ' B';
                  } else if (bytes < 1048576) { // within 1 MB so KB
                      return convertToDecimal((bytes / 1024), decimal, $math) + ' KB';
                  } else if (bytes < 1073741824) { // within 1 GB so MB
                      return convertToDecimal((bytes / 1048576), decimal, $math) + ' MB';
                  } else { // GB or more
                      return convertToDecimal((bytes / 1073741824), decimal, $math) + ' GB';
                  }

              }
              return "NaN";
          }
      }]);
    /**
     * @ngdoc filter
     * @name degrees
     * @kind function
     *
     * @description
     * Convert angle from radians to degrees
     */
    angular.module('a8m.math.degrees', ['a8m.math'])
      .filter('degrees', ['$math', function ($math) {
          return function (radians, decimal) {
              // if decimal is not an integer greater than -1, we cannot do. quit with error "NaN"
              // if degrees is not a real number, we cannot do also. quit with error "NaN"
              if (isNumber(decimal) && isFinite(decimal) && decimal % 1 === 0 && decimal >= 0 &&
                isNumber(radians) && isFinite(radians)) {
                  var degrees = (radians * 180) / $math.PI;
                  return $math.round(degrees * $math.pow(10, decimal)) / ($math.pow(10, decimal));
              } else {
                  return "NaN";
              }
          }
      }]);



    /**
     * @ngdoc filter
     * @name formatBytes
     * @kind function
     *
     * @description
     * Convert bytes into appropriate display 
     * 1024 kilobytes => 1 MB
     */
    angular.module('a8m.math.kbFmt', ['a8m.math'])
      .filter('kbFmt', ['$math', function ($math) {
          return function (bytes, decimal) {

              if (isNumber(decimal) && isFinite(decimal) && decimal % 1 === 0 && decimal >= 0 &&
                isNumber(bytes) && isFinite(bytes)) {
                  if (bytes < 1024) { // within 1 MB so KB
                      return convertToDecimal(bytes, decimal, $math) + ' KB';
                  } else if (bytes < 1048576) { // within 1 GB so MB
                      return convertToDecimal((bytes / 1024), decimal, $math) + ' MB';
                  } else {
                      return convertToDecimal((bytes / 1048576), decimal, $math) + ' GB';
                  }
              }
              return "NaN";
          }
      }]);
    /**
     * @ngdoc module
     * @name math
     * @description
     * reference to global Math object
     */
    angular.module('a8m.math', [])
      .factory('$math', ['$window', function ($window) {
          return $window.Math;
      }]);

    /**
     * @ngdoc filter
     * @name max
     * @kind function
     *
     * @description
     * Math.max will get an array and return the max value. if an expression
     * is provided, will return max value by expression.
     */
    angular.module('a8m.math.max', ['a8m.math'])
      .filter('max', ['$math', '$parse', function ($math, $parse) {
          return function (input, expression) {

              if (!isArray(input)) {
                  return input;
              }
              return isUndefined(expression)
                ? $math.max.apply($math, input)
                : input[indexByMax(input, expression)];
          };

          /**
           * @private
           * @param array
           * @param exp
           * @returns {number|*|Number}
           */
          function indexByMax(array, exp) {
              var mappedArray = array.map(function (elm) {
                  return $parse(exp)(elm);
              });
              return mappedArray.indexOf($math.max.apply($math, mappedArray));
          }
      }]);
    /**
     * @ngdoc filter
     * @name min
     * @kind function
     *
     * @description
     * Math.min will get an array and return the min value. if an expression
     * is provided, will return min value by expression.
     */
    angular.module('a8m.math.min', ['a8m.math'])
      .filter('min', ['$math', '$parse', function ($math, $parse) {
          return function (input, expression) {

              if (!isArray(input)) {
                  return input;
              }
              return isUndefined(expression)
                ? $math.min.apply($math, input)
                : input[indexByMin(input, expression)];
          };

          /**
           * @private
           * @param array
           * @param exp
           * @returns {number|*|Number}
           */
          function indexByMin(array, exp) {
              var mappedArray = array.map(function (elm) {
                  return $parse(exp)(elm);
              });
              return mappedArray.indexOf($math.min.apply($math, mappedArray));
          }
      }]);
    /**
     * @ngdoc filter
     * @name Percent
     * @kind function
     *
     * @description
     * percentage between two numbers
     */
    angular.module('a8m.math.percent', ['a8m.math'])
      .filter('percent', ['$math', '$window', function ($math, $window) {
          return function (input, divided, round) {

              var divider = isString(input) ? $window.Number(input) : input;
              divided = divided || 100;
              round = round || false;

              if (!isNumber(divider) || $window.isNaN(divider)) return input;

              return round
                ? $math.round((divider / divided) * 100)
                : (divider / divided) * 100;
          }
      }]);

    /**
     * @ngdoc filter
     * @name toRadians
     * @kind function
     *
     * @description
     * Convert angle from degrees to radians
     */
    angular.module('a8m.math.radians', ['a8m.math'])
      .filter('radians', ['$math', function ($math) {
          return function (degrees, decimal) {
              // if decimal is not an integer greater than -1, we cannot do. quit with error "NaN"
              // if degrees is not a real number, we cannot do also. quit with error "NaN"
              if (isNumber(decimal) && isFinite(decimal) && decimal % 1 === 0 && decimal >= 0 &&
                isNumber(degrees) && isFinite(degrees)) {
                  var radians = (degrees * 3.14159265359) / 180;
                  return $math.round(radians * $math.pow(10, decimal)) / ($math.pow(10, decimal));
              }
              return "NaN";
          }
      }]);



    /**
     * @ngdoc filter
     * @name Radix
     * @kind function
     *
     * @description
     * converting decimal numbers to different bases(radix)
     */
    angular.module('a8m.math.radix', [])
      .filter('radix', function () {
          return function (input, radix) {
              var RANGE = /^[2-9]$|^[1-2]\d$|^3[0-6]$/;

              if (!isNumber(input) || !RANGE.test(radix)) {
                  return input;
              }

              return input.toString(radix).toUpperCase();
          }
      });

    /**
     * @ngdoc filter
     * @name formatBytes
     * @kind function
     *
     * @description
     * Convert number into abbreviations.
     * i.e: K for one thousand, M for Million, B for billion
     * e.g: number of users:235,221, decimal:1 => 235.2 K
     */
    angular.module('a8m.math.shortFmt', ['a8m.math'])
      .filter('shortFmt', ['$math', function ($math) {
          return function (number, decimal) {
              if (isNumber(decimal) && isFinite(decimal) && decimal % 1 === 0 && decimal >= 0 &&
                isNumber(number) && isFinite(number)) {
                  if (number < 1e3) {
                      return number;
                  } else if (number < 1e6) {
                      return convertToDecimal((number / 1e3), decimal, $math) + ' K';
                  } else if (number < 1e9) {
                      return convertToDecimal((number / 1e6), decimal, $math) + ' M';
                  } else {
                      return convertToDecimal((number / 1e9), decimal, $math) + ' B';
                  }

              }
              return "NaN";
          }
      }]);
    /**
     * @ngdoc filter
     * @name sum
     * @kind function
     *
     * @description
     * Sum up all values within an array
     */
    angular.module('a8m.math.sum', [])
      .filter('sum', function () {
          return function (input, initial) {
              return !isArray(input)
                ? input
                : input.reduce(function (prev, curr) {
                    return prev + curr;
                }, initial || 0);
          }
      });

    /**
     * @ngdoc filter
     * @name endsWith
     * @kind function
     *
     * @description
     * checks whether string ends with the ends parameter.
     */
    angular.module('a8m.ends-with', [])

      .filter('endsWith', function () {
          return function (input, ends, csensitive) {

              var sensitive = csensitive || false,
                position;

              if (!isString(input) || isUndefined(ends)) {
                  return input;
              }

              input = (sensitive) ? input : input.toLowerCase();
              position = input.length - ends.length;

              return input.indexOf((sensitive) ? ends : ends.toLowerCase(), position) !== -1;
          }
      });

    /**
     * @ngdoc filter
     * @name latinize
     * @kind function
     *
     * @description
     * remove accents/diacritics from a string
     */
    angular.module('a8m.latinize', [])
      .filter('latinize', [function () {
          var defaultDiacriticsRemovalap = [
            { 'base': 'A', 'letters': '\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F' },
            { 'base': 'AA', 'letters': '\uA732' },
            { 'base': 'AE', 'letters': '\u00C6\u01FC\u01E2' },
            { 'base': 'AO', 'letters': '\uA734' },
            { 'base': 'AU', 'letters': '\uA736' },
            { 'base': 'AV', 'letters': '\uA738\uA73A' },
            { 'base': 'AY', 'letters': '\uA73C' },
            { 'base': 'B', 'letters': '\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181' },
            { 'base': 'C', 'letters': '\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E' },
            { 'base': 'D', 'letters': '\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779' },
            { 'base': 'DZ', 'letters': '\u01F1\u01C4' },
            { 'base': 'Dz', 'letters': '\u01F2\u01C5' },
            { 'base': 'E', 'letters': '\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E' },
            { 'base': 'F', 'letters': '\u0046\u24BB\uFF26\u1E1E\u0191\uA77B' },
            { 'base': 'G', 'letters': '\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E' },
            { 'base': 'H', 'letters': '\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D' },
            { 'base': 'I', 'letters': '\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197' },
            { 'base': 'J', 'letters': '\u004A\u24BF\uFF2A\u0134\u0248' },
            { 'base': 'K', 'letters': '\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2' },
            { 'base': 'L', 'letters': '\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780' },
            { 'base': 'LJ', 'letters': '\u01C7' },
            { 'base': 'Lj', 'letters': '\u01C8' },
            { 'base': 'M', 'letters': '\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C' },
            { 'base': 'N', 'letters': '\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4' },
            { 'base': 'NJ', 'letters': '\u01CA' },
            { 'base': 'Nj', 'letters': '\u01CB' },
            { 'base': 'O', 'letters': '\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C' },
            { 'base': 'OI', 'letters': '\u01A2' },
            { 'base': 'OO', 'letters': '\uA74E' },
            { 'base': 'OU', 'letters': '\u0222' },
            { 'base': 'OE', 'letters': '\u008C\u0152' },
            { 'base': 'oe', 'letters': '\u009C\u0153' },
            { 'base': 'P', 'letters': '\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754' },
            { 'base': 'Q', 'letters': '\u0051\u24C6\uFF31\uA756\uA758\u024A' },
            { 'base': 'R', 'letters': '\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782' },
            { 'base': 'S', 'letters': '\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784' },
            { 'base': 'T', 'letters': '\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786' },
            { 'base': 'TZ', 'letters': '\uA728' },
            { 'base': 'U', 'letters': '\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244' },
            { 'base': 'V', 'letters': '\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245' },
            { 'base': 'VY', 'letters': '\uA760' },
            { 'base': 'W', 'letters': '\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72' },
            { 'base': 'X', 'letters': '\u0058\u24CD\uFF38\u1E8A\u1E8C' },
            { 'base': 'Y', 'letters': '\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE' },
            { 'base': 'Z', 'letters': '\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762' },
            { 'base': 'a', 'letters': '\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250' },
            { 'base': 'aa', 'letters': '\uA733' },
            { 'base': 'ae', 'letters': '\u00E6\u01FD\u01E3' },
            { 'base': 'ao', 'letters': '\uA735' },
            { 'base': 'au', 'letters': '\uA737' },
            { 'base': 'av', 'letters': '\uA739\uA73B' },
            { 'base': 'ay', 'letters': '\uA73D' },
            { 'base': 'b', 'letters': '\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253' },
            { 'base': 'c', 'letters': '\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184' },
            { 'base': 'd', 'letters': '\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A' },
            { 'base': 'dz', 'letters': '\u01F3\u01C6' },
            { 'base': 'e', 'letters': '\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD' },
            { 'base': 'f', 'letters': '\u0066\u24D5\uFF46\u1E1F\u0192\uA77C' },
            { 'base': 'g', 'letters': '\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F' },
            { 'base': 'h', 'letters': '\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265' },
            { 'base': 'hv', 'letters': '\u0195' },
            { 'base': 'i', 'letters': '\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131' },
            { 'base': 'j', 'letters': '\u006A\u24D9\uFF4A\u0135\u01F0\u0249' },
            { 'base': 'k', 'letters': '\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3' },
            { 'base': 'l', 'letters': '\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747' },
            { 'base': 'lj', 'letters': '\u01C9' },
            { 'base': 'm', 'letters': '\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F' },
            { 'base': 'n', 'letters': '\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5' },
            { 'base': 'nj', 'letters': '\u01CC' },
            { 'base': 'o', 'letters': '\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275' },
            { 'base': 'oi', 'letters': '\u01A3' },
            { 'base': 'ou', 'letters': '\u0223' },
            { 'base': 'oo', 'letters': '\uA74F' },
            { 'base': 'p', 'letters': '\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755' },
            { 'base': 'q', 'letters': '\u0071\u24E0\uFF51\u024B\uA757\uA759' },
            { 'base': 'r', 'letters': '\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783' },
            { 'base': 's', 'letters': '\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B' },
            { 'base': 't', 'letters': '\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787' },
            { 'base': 'tz', 'letters': '\uA729' },
            { 'base': 'u', 'letters': '\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289' },
            { 'base': 'v', 'letters': '\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C' },
            { 'base': 'vy', 'letters': '\uA761' },
            { 'base': 'w', 'letters': '\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73' },
            { 'base': 'x', 'letters': '\u0078\u24E7\uFF58\u1E8B\u1E8D' },
            { 'base': 'y', 'letters': '\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF' },
            { 'base': 'z', 'letters': '\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763' }
          ];

          var diacriticsMap = {};
          for (var i = 0; i < defaultDiacriticsRemovalap.length; i++) {
              var letters = defaultDiacriticsRemovalap[i].letters.split("");
              for (var j = 0; j < letters.length ; j++) {
                  diacriticsMap[letters[j]] = defaultDiacriticsRemovalap[i].base;
              }
          }

          // "what?" version ... http://jsperf.com/diacritics/12
          function removeDiacritics(str) {
              return str.replace(/[^\u0000-\u007E]/g, function (a) {
                  return diacriticsMap[a] || a;
              });
          }

          return function (input) {

              return isString(input)
                ? removeDiacritics(input)
                : input;
          }
      }]);

    /**
     * @ngdoc filter
     * @name ltrim
     * @kind function
     *
     * @description
     * Left trim. Similar to trimFilter, but only for left side.
     */
    angular.module('a8m.ltrim', [])
      .filter('ltrim', function () {
          return function (input, chars) {

              var trim = chars || '\\s';

              return isString(input)
                ? input.replace(new RegExp('^' + trim + '+'), '')
                : input;
          }
      });

    /**
     * @ngdoc filter
     * @name match
     * @kind function
     *
     * @description
     * Return the matched pattern in a string.
     */
    angular.module('a8m.match', [])
      .filter('match', function () {
          return function (input, pattern, flag) {

              var reg = new RegExp(pattern, flag);

              return isString(input)
                ? input.match(reg)
                : null;
          }
      });

    /**
     * @ngdoc filter
     * @name repeat
     * @kind function
     *
     * @description
     * Repeats a string n times
     */
    angular.module('a8m.repeat', [])
      .filter('repeat', [function () {
          return function (input, n, separator) {

              var times = ~~n;

              if (!isString(input)) {
                  return input;
              }

              return !times
                ? input
                : strRepeat(input, --n, separator || '');
          }
      }]);

    /**
     * Repeats a string n times with given separator
     * @param str string to repeat
     * @param n number of times
     * @param sep separator
     * @returns {*}
     */
    function strRepeat(str, n, sep) {
        if (!n) {
            return str;
        }
        return str + sep + strRepeat(str, --n, sep);
    }
    /**
    * @ngdoc filter
    * @name rtrim
    * @kind function
    *
    * @description
    * Right trim. Similar to trimFilter, but only for right side.
    */
    angular.module('a8m.rtrim', [])
      .filter('rtrim', function () {
          return function (input, chars) {

              var trim = chars || '\\s';

              return isString(input)
                ? input.replace(new RegExp(trim + '+$'), '')
                : input;
          }
      });

    /**
     * @ngdoc filter
     * @name slugify
     * @kind function
     *
     * @description
     * remove spaces from string, replace with "-" or given argument
     */
    angular.module('a8m.slugify', [])
      .filter('slugify', [function () {
          return function (input, sub) {

              var replace = (isUndefined(sub)) ? '-' : sub;

              return isString(input)
                ? input.toLowerCase().replace(/\s+/g, replace)
                : input;
          }
      }]);

    /**
     * @ngdoc filter
     * @name startWith
     * @kind function
     *
     * @description
     * checks whether string starts with the starts parameter.
     */
    angular.module('a8m.starts-with', [])
      .filter('startsWith', function () {
          return function (input, start, csensitive) {

              var sensitive = csensitive || false;

              if (!isString(input) || isUndefined(start)) {
                  return input;
              }

              input = (sensitive) ? input : input.toLowerCase();

              return !input.indexOf((sensitive) ? start : start.toLowerCase());
          }
      });

    /**
     * @ngdoc filter
     * @name stringular
     * @kind function
     *
     * @description
     * get string with {n} and replace match with enumeration values
     */
    angular.module('a8m.stringular', [])
      .filter('stringular', function () {
          return function (input) {

              var args = Array.prototype.slice.call(arguments, 1);

              return input.replace(/{(\d+)}/g, function (match, number) {
                  return isUndefined(args[number]) ? match : args[number];
              });
          }
      });

    /**
     * @ngdoc filter
     * @name stripTags
     * @kind function
     *
     * @description
     * strip html tags from string
     */
    angular.module('a8m.strip-tags', [])
      .filter('stripTags', function () {
          return function (input) {
              return isString(input)
                ? input.replace(/<\S[^><]*>/g, '')
                : input;
          }
      });

    /**
     * @ngdoc filter
     * @name test
     * @kind function
     *
     * @description
     * test if a string match a pattern.
     */
    angular.module('a8m.test', [])
      .filter('test', function () {
          return function (input, pattern, flag) {

              var reg = new RegExp(pattern, flag);

              return isString(input)
                ? reg.test(input)
                : input;
          }
      });

    /**
     * @ngdoc filter
     * @name trim
     * @kind function
     *
     * @description
     *  Strip whitespace (or other characters) from the beginning and end of a string
     */
    angular.module('a8m.trim', [])
      .filter('trim', function () {
          return function (input, chars) {

              var trim = chars || '\\s';

              return isString(input)
                ? input.replace(new RegExp('^' + trim + '+|' + trim + '+$', 'g'), '')
                : input;
          }
      });

    /**
     * @ngdoc filter
     * @name truncate
     * @kind function
     *
     * @description
     * truncates a string given a specified length, providing a custom string to denote an omission.
     */
    angular.module('a8m.truncate', [])
      .filter('truncate', function () {
          return function (input, length, suffix, preserve) {

              length = isUndefined(length) ? input.length : length;
              preserve = preserve || false;
              suffix = suffix || '';

              if (!isString(input) || (input.length <= length)) return input;

              return input.substring(0, (preserve)
                ? ((input.indexOf(' ', length) === -1) ? input.length : input.indexOf(' ', length))
                : length) + suffix;
          };
      });

    /**
     * @ngdoc filter
     * @name ucfirst
     * @kind function
     *
     * @description
     * ucfirst
     */
    angular.module('a8m.ucfirst', [])
      .filter('ucfirst', [function () {
          return function (input) {
              return isString(input)
                ? input
                    .split(' ')
                    .map(function (ch) {
                        return ch.charAt(0).toUpperCase() + ch.substring(1);
                    })
                    .join(' ')
                : input;
          }
      }]);

    /**
     * @ngdoc filter
     * @name uriComponentEncode
     * @kind function
     *
     * @description
     * get string as parameter and return encoded string
     */
    angular.module('a8m.uri-component-encode', [])
      .filter('uriComponentEncode', ['$window', function ($window) {
          return function (input) {
              return isString(input)
                ? $window.encodeURIComponent(input)
                : input;
          }
      }]);

    /**
     * @ngdoc filter
     * @name uriEncode
     * @kind function
     *
     * @description
     * get string as parameter and return encoded string
     */
    angular.module('a8m.uri-encode', [])
      .filter('uriEncode', ['$window', function ($window) {
          return function (input) {
              return isString(input)
                ? $window.encodeURI(input)
                : input;
          }
      }]);

    /**
     * @ngdoc filter
     * @name wrap
     * @kind function
     *
     * @description
     * Wrap a string with another string
     */
    angular.module('a8m.wrap', [])
      .filter('wrap', function () {
          return function (input, wrap, ends) {
              return isString(input) && isDefined(wrap)
                ? [wrap, input, ends || wrap].join('')
                : input;
          }
      });

    /**
     * @ngdoc provider
     * @name filterWatcher
     * @kind function
     *
     * @description
     * store specific filters result in $$cache, based on scope life time(avoid memory leak).
     * on scope.$destroy remove it's cache from $$cache container
     */

    angular.module('a8m.filter-watcher', [])
      .provider('filterWatcher', function () {

          this.$get = ['$window', '$rootScope', function ($window, $rootScope) {

              /**
               * Cache storing
               * @type {Object}
               */
              var $$cache = {};

              /**
               * Scope listeners container
               * scope.$destroy => remove all cache keys
               * bind to current scope.
               * @type {Object}
               */
              var $$listeners = {};

              /**
               * $timeout without triggering the digest cycle
               * @type {function}
               */
              var $$timeout = $window.setTimeout;

              /**
               * @description
               * get `HashKey` string based on the given arguments.
               * @param fName
               * @param args
               * @returns {string}
               */
              function getHashKey(fName, args) {
                  function replacerFactory() {
                      var cache = [];
                      return function (key, val) {
                          if (isObject(val) && !isNull(val)) {
                              if (~cache.indexOf(val)) return '[Circular]';
                              cache.push(val)
                          }
                          if ($window == val) return '$WINDOW';
                          if ($window.document == val) return '$DOCUMENT';
                          if (isScope(val)) return '$SCOPE';
                          return val;
                      }
                  }
                  return [fName, JSON.stringify(args, replacerFactory())]
                    .join('#')
                    .replace(/"/g, '');
              }

              /**
               * @description
               * fir on $scope.$destroy,
               * remove cache based scope from `$$cache`,
               * and remove itself from `$$listeners`
               * @param event
               */
              function removeCache(event) {
                  var id = event.targetScope.$id;
                  forEach($$listeners[id], function (key) {
                      delete $$cache[key];
                  });
                  delete $$listeners[id];
              }

              /**
               * @description
               * for angular version that greater than v.1.3.0
               * it clear cache when the digest cycle is end.
               */
              function cleanStateless() {
                  $$timeout(function () {
                      if (!$rootScope.$$phase)
                          $$cache = {};
                  }, 2000);
              }

              /**
               * @description
               * Store hashKeys in $$listeners container
               * on scope.$destroy, remove them all(bind an event).
               * @param scope
               * @param hashKey
               * @returns {*}
               */
              function addListener(scope, hashKey) {
                  var id = scope.$id;
                  if (isUndefined($$listeners[id])) {
                      scope.$on('$destroy', removeCache);
                      $$listeners[id] = [];
                  }
                  return $$listeners[id].push(hashKey);
              }

              /**
               * @description
               * return the `cacheKey` or undefined.
               * @param filterName
               * @param args
               * @returns {*}
               */
              function $$isMemoized(filterName, args) {
                  var hashKey = getHashKey(filterName, args);
                  return $$cache[hashKey];
              }

              /**
               * @description
               * store `result` in `$$cache` container, based on the hashKey.
               * add $destroy listener and return result
               * @param filterName
               * @param args
               * @param scope
               * @param result
               * @returns {*}
               */
              function $$memoize(filterName, args, scope, result) {
                  var hashKey = getHashKey(filterName, args);
                  //store result in `$$cache` container
                  $$cache[hashKey] = result;
                  // for angular versions that less than 1.3
                  // add to `$destroy` listener, a cleaner callback
                  if (isScope(scope)) {
                      addListener(scope, hashKey);
                  } else {
                      cleanStateless();
                  }
                  return result;
              }

              return {
                  isMemoized: $$isMemoized,
                  memoize: $$memoize
              }
          }];
      });


    /**
     * @ngdoc module
     * @name angular.filters
     * @description
     * Bunch of useful filters for angularJS
     */

    angular.module('angular.filter', [

      'a8m.ucfirst',
      'a8m.uri-encode',
      'a8m.uri-component-encode',
      'a8m.slugify',
      'a8m.latinize',
      'a8m.strip-tags',
      'a8m.stringular',
      'a8m.truncate',
      'a8m.starts-with',
      'a8m.ends-with',
      'a8m.wrap',
      'a8m.trim',
      'a8m.ltrim',
      'a8m.rtrim',
      'a8m.repeat',
      'a8m.test',
      'a8m.match',

      'a8m.to-array',
      'a8m.concat',
      'a8m.contains',
      'a8m.unique',
      'a8m.is-empty',
      'a8m.after',
      'a8m.after-where',
      'a8m.before',
      'a8m.before-where',
      'a8m.defaults',
      'a8m.where',
      'a8m.reverse',
      'a8m.remove',
      'a8m.remove-with',
      'a8m.group-by',
      'a8m.count-by',
      'a8m.chunk-by',
      'a8m.search-field',
      'a8m.fuzzy-by',
      'a8m.fuzzy',
      'a8m.omit',
      'a8m.pick',
      'a8m.every',
      'a8m.filter-by',
      'a8m.xor',
      'a8m.map',
      'a8m.first',
      'a8m.last',
      'a8m.flatten',
      'a8m.join',
      'a8m.range',

      'a8m.math',
      'a8m.math.max',
      'a8m.math.min',
      'a8m.math.percent',
      'a8m.math.radix',
      'a8m.math.sum',
      'a8m.math.degrees',
      'a8m.math.radians',
      'a8m.math.byteFmt',
      'a8m.math.kbFmt',
      'a8m.math.shortFmt',

      'a8m.angular',
      'a8m.conditions',
      'a8m.is-null',

      'a8m.filter-watcher'
    ]);
})(window, window.angular);