define(function (require) {
  return function (_) {
    _.mixin(_, {

      /**
       * move an obj either up or down in the collection by
       * injecting it either before/after the prev/next obj that
       * satisfied the qualifier
       *
       * or, just from one index to another...
       *
       * @param  {array} objs - the list to move the object within
       * @param  {number|any} obj - the object that should be moved, or the index that the object is currently at
       * @param  {number|boolean} below - the index to move the object to, or whether it should be moved up or down
       * @param  {function} qualifier - a lodash-y callback, object = _.where, string = _.pluck
       * @return {array} - the objs argument
       */
      move: function (objs, obj, below, qualifier) {
        var origI = _.isNumber(obj) ? obj : objs.indexOf(obj);
        if (origI === -1) return objs;

        if (_.isNumber(below)) {
          // move to a specific index
          objs.splice(below, 0, objs.splice(origI, 1)[0]);
          return objs;
        }

        below = !!below;
        qualifier = _.callback(qualifier);

        var above = !below;
        var finder = below ? _.findIndex : _.findLastIndex;

        // find the index of the next/previous obj that meets the qualifications
        var targetI = finder(objs, function (otherAgg, otherI) {
          if (below && otherI <= origI) return;
          if (above && otherI >= origI) return;
          return !!qualifier(otherAgg, otherI);
        });

        if (targetI === -1) return objs;

        // place the obj at it's new index
        objs.splice(targetI, 0, objs.splice(origI, 1)[0]);
      },

      /**
       * Like _.groupBy, but allows specifying multiple groups for a
       * single object.
       *
       * _.organizeBy([{ a: [1, 2, 3] }, { b: true, a: [1, 4] }], 'a')
       * // Object {1: Array[2], 2: Array[1], 3: Array[1], 4: Array[1]}
       *
       * _.groupBy([{ a: [1, 2, 3] }, { b: true, a: [1, 4] }], 'a')
       * // Object {'1,2,3': Array[1], '1,4': Array[1]}
       *
       * @param  {array} collection - the list of values to organize
       * @param  {Function} callback - either a property name, or a callback.
       * @return {object}
       */
      organizeBy: function (collection, callback) {
        var buckets = {};
        var prop = typeof callback === 'function' ? false : callback;

        function add(key, obj) {
          if (!buckets[key]) buckets[key] = [];
          buckets[key].push(obj);
        }

        _.each(collection, function (obj) {
          var keys = prop === false ? callback(obj) : obj[prop];

          if (!_.isArray(keys)) {
            add(keys, obj);
            return;
          }

          var length = keys.length;
          while (length-- > 0) {
            add(keys[length], obj);
          }
        });

        return buckets;
      },

      /**
       * Remove or add a value to an array based on it's presense in the
       * array initially.
       *
       * @param  {array} arr
       * @param  {any} value - the value to toggle
       * @return {array} arr
       */
      toggleInOut: function (arr, value) {
        if (_.contains(arr, value)) {
          arr.splice(arr.indexOf(value), 1);
        } else {
          arr.push(value);
        }
        return arr;
      },

      /**
       * Efficient and safe version of [].push(dest, source);
       *
       * @param  {Array} source - the array to pull values from
       * @param  {Array} dest   - the array to push values into
       * @return {Array} dest
       */
      pushAll: function (source, dest) {
        var start = dest.length;
        var adding = source.length;

        // allocate - http://goo.gl/e2i0S0
        dest.length = start + adding;

        // fill sparse positions
        var i = -1;
        while (++i < adding) dest[start + i] = source[i];

        return dest;
      },


    });
  };
});
