/*global setImmediate: false, setTimeout: false, console: false */
(function () {

  var async = {};

  // global on the server, window in the browser
  var root, previous_async;

  root = this;
  if (root != null) {
    previous_async = root.async;
  }

  async.noConflict = function () {
    root.async = previous_async;
    return async;
  };

  function only_once(fn) {
    var called = false;
    return function () {
      if (called) {
        throw new Error("Callback was already called.");
      }
      called = true;
      fn.apply(root, arguments);
    }
  }

  //// cross-browser compatiblity functions ////

  var _each = function (arr, iterator) {
    if (arr.forEach) {
      return arr.forEach(iterator);
    }
    for (var i = 0; i < arr.length; i += 1) {
      iterator(arr[i], i, arr);
    }
  };

  var _map = function (arr, iterator) {
    if (arr.map) {
      return arr.map(iterator);
    }
    var results = [];
    _each(arr, function (x, i, a) {
      results.push(iterator(x, i, a));
    });
    return results;
  };

  var _reduce = function (arr, iterator, memo) {
    if (arr.reduce) {
      return arr.reduce(iterator, memo);
    }
    _each(arr, function (x, i, a) {
      memo = iterator(memo, x, i, a);
    });
    return memo;
  };

  var _keys = function (obj) {
    if (Object.keys) {
      return Object.keys(obj);
    }
    var keys = [];
    for (var k in obj) {
      if (obj.hasOwnProperty(k)) {
        keys.push(k);
      }
    }
    return keys;
  };

  //// exported async module functions ////

  //// nextTick implementation with browser-compatible fallback ////
  if (typeof process === 'undefined' || !(process.nextTick)) {
    if (typeof setImmediate === 'function') {
      async.nextTick = function (fn) {
        // not a direct alias for IE10 compatibility
        setImmediate(fn);
      };
      async.setImmediate = async.nextTick;
    }
    else {
      async.nextTick = function (fn) {
        setTimeout(fn, 0);
      };
      async.setImmediate = async.nextTick;
    }
  }
  else {
    async.nextTick = process.nextTick;
    if (typeof setImmediate !== 'undefined') {
      async.setImmediate = setImmediate;
    }
    else {
      async.setImmediate = async.nextTick;
    }
  }

  async.each = function (arr, iterator, callback) {
    callback = callback || function () {
      };
    if (!arr.length) {
      return callback();
    }
    var completed = 0;
    _each(arr, function (x) {
      iterator(x, only_once(function (err) {
        if (err) {
          callback(err);
          callback = function () {
          };
        }
        else {
          completed += 1;
          if (completed >= arr.length) {
            callback(null);
          }
        }
      }));
    });
  };
  async.forEach = async.each;

  async.eachSeries = function (arr, iterator, callback) {
    callback = callback || function () {
      };
    if (!arr.length) {
      return callback();
    }
    var completed = 0;
    var iterate = function () {
      iterator(arr[completed], function (err) {
        if (err) {
          callback(err);
          callback = function () {
          };
        }
        else {
          completed += 1;
          if (completed >= arr.length) {
            callback(null);
          }
          else {
            iterate();
          }
        }
      });
    };
    iterate();
  };
  async.forEachSeries = async.eachSeries;

  async.eachLimit = function (arr, limit, iterator, callback) {
    var fn = _eachLimit(limit);
    fn.apply(null, [arr, iterator, callback]);
  };
  async.forEachLimit = async.eachLimit;

  var _eachLimit = function (limit) {

    return function (arr, iterator, callback) {
      callback = callback || function () {
        };
      if (!arr.length || limit <= 0) {
        return callback();
      }
      var completed = 0;
      var started = 0;
      var running = 0;

      (function replenish() {
        if (completed >= arr.length) {
          return callback();
        }

        while (running < limit && started < arr.length) {
          started += 1;
          running += 1;
          iterator(arr[started - 1], function (err) {
            if (err) {
              callback(err);
              callback = function () {
              };
            }
            else {
              completed += 1;
              running -= 1;
              if (completed >= arr.length) {
                callback();
              }
              else {
                replenish();
              }
            }
          });
        }
      })();
    };
  };

  var doParallel = function (fn) {
    return function () {
      var args = Array.prototype.slice.call(arguments);
      return fn.apply(null, [async.each].concat(args));
    };
  };
  var doParallelLimit = function (limit, fn) {
    return function () {
      var args = Array.prototype.slice.call(arguments);
      return fn.apply(null, [_eachLimit(limit)].concat(args));
    };
  };
  var doSeries = function (fn) {
    return function () {
      var args = Array.prototype.slice.call(arguments);
      return fn.apply(null, [async.eachSeries].concat(args));
    };
  };

  var _asyncMap = function (eachfn, arr, iterator, callback) {
    var results = [];
    arr = _map(arr, function (x, i) {
      return {index: i, value: x};
    });
    eachfn(arr, function (x, callback) {
      iterator(x.value, function (err, v) {
        results[x.index] = v;
        callback(err);
      });
    }, function (err) {
      callback(err, results);
    });
  };
  async.map = doParallel(_asyncMap);
  async.mapSeries = doSeries(_asyncMap);
  async.mapLimit = function (arr, limit, iterator, callback) {
    return _mapLimit(limit)(arr, iterator, callback);
  };

  var _mapLimit = function (limit) {
    return doParallelLimit(limit, _asyncMap);
  };

  // reduce only has a series version, as doing reduce in parallel won't
  // work in many situations.
  async.reduce = function (arr, memo, iterator, callback) {
    async.eachSeries(arr, function (x, callback) {
      iterator(memo, x, function (err, v) {
        memo = v;
        callback(err);
      });
    }, function (err) {
      callback(err, memo);
    });
  };
  // inject alias
  async.inject = async.reduce;
  // foldl alias
  async.foldl = async.reduce;

  async.reduceRight = function (arr, memo, iterator, callback) {
    var reversed = _map(arr, function (x) {
      return x;
    }).reverse();
    async.reduce(reversed, memo, iterator, callback);
  };
  // foldr alias
  async.foldr = async.reduceRight;

  var _filter = function (eachfn, arr, iterator, callback) {
    var results = [];
    arr = _map(arr, function (x, i) {
      return {index: i, value: x};
    });
    eachfn(arr, function (x, callback) {
      iterator(x.value, function (v) {
        if (v) {
          results.push(x);
        }
        callback();
      });
    }, function (err) {
      callback(_map(results.sort(function (a, b) {
        return a.index - b.index;
      }), function (x) {
        return x.value;
      }));
    });
  };
  async.filter = doParallel(_filter);
  async.filterSeries = doSeries(_filter);
  // select alias
  async.select = async.filter;
  async.selectSeries = async.filterSeries;

  var _reject = function (eachfn, arr, iterator, callback) {
    var results = [];
    arr = _map(arr, function (x, i) {
      return {index: i, value: x};
    });
    eachfn(arr, function (x, callback) {
      iterator(x.value, function (v) {
        if (!v) {
          results.push(x);
        }
        callback();
      });
    }, function (err) {
      callback(_map(results.sort(function (a, b) {
        return a.index - b.index;
      }), function (x) {
        return x.value;
      }));
    });
  };
  async.reject = doParallel(_reject);
  async.rejectSeries = doSeries(_reject);

  var _detect = function (eachfn, arr, iterator, main_callback) {
    eachfn(arr, function (x, callback) {
      iterator(x, function (result) {
        if (result) {
          main_callback(x);
          main_callback = function () {
          };
        }
        else {
          callback();
        }
      });
    }, function (err) {
      main_callback();
    });
  };
  async.detect = doParallel(_detect);
  async.detectSeries = doSeries(_detect);

  async.some = function (arr, iterator, main_callback) {
    async.each(arr, function (x, callback) {
      iterator(x, function (v) {
        if (v) {
          main_callback(true);
          main_callback = function () {
          };
        }
        callback();
      });
    }, function (err) {
      main_callback(false);
    });
  };
  // any alias
  async.any = async.some;

  async.every = function (arr, iterator, main_callback) {
    async.each(arr, function (x, callback) {
      iterator(x, function (v) {
        if (!v) {
          main_callback(false);
          main_callback = function () {
          };
        }
        callback();
      });
    }, function (err) {
      main_callback(true);
    });
  };
  // all alias
  async.all = async.every;

  async.sortBy = function (arr, iterator, callback) {
    async.map(arr, function (x, callback) {
      iterator(x, function (err, criteria) {
        if (err) {
          callback(err);
        }
        else {
          callback(null, {value: x, criteria: criteria});
        }
      });
    }, function (err, results) {
      if (err) {
        return callback(err);
      }
      else {
        var fn = function (left, right) {
          var a = left.criteria, b = right.criteria;
          return a < b ? -1 : a > b ? 1 : 0;
        };
        callback(null, _map(results.sort(fn), function (x) {
          return x.value;
        }));
      }
    });
  };

  async.auto = function (tasks, callback) {
    callback = callback || function () {
      };
    var keys = _keys(tasks);
    if (!keys.length) {
      return callback(null);
    }

    var results = {};

    var listeners = [];
    var addListener = function (fn) {
      listeners.unshift(fn);
    };
    var removeListener = function (fn) {
      for (var i = 0; i < listeners.length; i += 1) {
        if (listeners[i] === fn) {
          listeners.splice(i, 1);
          return;
        }
      }
    };
    var taskComplete = function () {
      _each(listeners.slice(0), function (fn) {
        fn();
      });
    };

    addListener(function () {
      if (_keys(results).length === keys.length) {
        callback(null, results);
        callback = function () {
        };
      }
    });

    _each(keys, function (k) {
      var task = (tasks[k] instanceof Function) ? [tasks[k]] : tasks[k];
      var taskCallback = function (err) {
        var args = Array.prototype.slice.call(arguments, 1);
        if (args.length <= 1) {
          args = args[0];
        }
        if (err) {
          var safeResults = {};
          _each(_keys(results), function (rkey) {
            safeResults[rkey] = results[rkey];
          });
          safeResults[k] = args;
          callback(err, safeResults);
          // stop subsequent errors hitting callback multiple times
          callback = function () {
          };
        }
        else {
          results[k] = args;
          async.setImmediate(taskComplete);
        }
      };
      var requires = task.slice(0, Math.abs(task.length - 1)) || [];
      var ready = function () {
        return _reduce(requires, function (a, x) {
            return (a && results.hasOwnProperty(x));
          }, true) && !results.hasOwnProperty(k);
      };
      if (ready()) {
        task[task.length - 1](taskCallback, results);
      }
      else {
        var listener = function () {
          if (ready()) {
            removeListener(listener);
            task[task.length - 1](taskCallback, results);
          }
        };
        addListener(listener);
      }
    });
  };

  async.waterfall = function (tasks, callback) {
    callback = callback || function () {
      };
    if (tasks.constructor !== Array) {
      var err = new Error('First argument to waterfall must be an array of functions');
      return callback(err);
    }
    if (!tasks.length) {
      return callback();
    }
    var wrapIterator = function (iterator) {
      return function (err) {
        if (err) {
          callback.apply(null, arguments);
          callback = function () {
          };
        }
        else {
          var args = Array.prototype.slice.call(arguments, 1);
          var next = iterator.next();
          if (next) {
            args.push(wrapIterator(next));
          }
          else {
            args.push(callback);
          }
          async.setImmediate(function () {
            iterator.apply(null, args);
          });
        }
      };
    };
    wrapIterator(async.iterator(tasks))();
  };

  var _parallel = function (eachfn, tasks, callback) {
    callback = callback || function () {
      };
    if (tasks.constructor === Array) {
      eachfn.map(tasks, function (fn, callback) {
        if (fn) {
          fn(function (err) {
            var args = Array.prototype.slice.call(arguments, 1);
            if (args.length <= 1) {
              args = args[0];
            }
            callback.call(null, err, args);
          });
        }
      }, callback);
    }
    else {
      var results = {};
      eachfn.each(_keys(tasks), function (k, callback) {
        tasks[k](function (err) {
          var args = Array.prototype.slice.call(arguments, 1);
          if (args.length <= 1) {
            args = args[0];
          }
          results[k] = args;
          callback(err);
        });
      }, function (err) {
        callback(err, results);
      });
    }
  };

  async.parallel = function (tasks, callback) {
    _parallel({map: async.map, each: async.each}, tasks, callback);
  };

  async.parallelLimit = function (tasks, limit, callback) {
    _parallel({map: _mapLimit(limit), each: _eachLimit(limit)}, tasks, callback);
  };

  async.series = function (tasks, callback) {
    callback = callback || function () {
      };
    if (tasks.constructor === Array) {
      async.mapSeries(tasks, function (fn, callback) {
        if (fn) {
          fn(function (err) {
            var args = Array.prototype.slice.call(arguments, 1);
            if (args.length <= 1) {
              args = args[0];
            }
            callback.call(null, err, args);
          });
        }
      }, callback);
    }
    else {
      var results = {};
      async.eachSeries(_keys(tasks), function (k, callback) {
        tasks[k](function (err) {
          var args = Array.prototype.slice.call(arguments, 1);
          if (args.length <= 1) {
            args = args[0];
          }
          results[k] = args;
          callback(err);
        });
      }, function (err) {
        callback(err, results);
      });
    }
  };

  async.iterator = function (tasks) {
    var makeCallback = function (index) {
      var fn = function () {
        if (tasks.length) {
          tasks[index].apply(null, arguments);
        }
        return fn.next();
      };
      fn.next = function () {
        return (index < tasks.length - 1) ? makeCallback(index + 1) : null;
      };
      return fn;
    };
    return makeCallback(0);
  };

  async.apply = function (fn) {
    var args = Array.prototype.slice.call(arguments, 1);
    return function () {
      return fn.apply(
        null, args.concat(Array.prototype.slice.call(arguments))
      );
    };
  };

  var _concat = function (eachfn, arr, fn, callback) {
    var r = [];
    eachfn(arr, function (x, cb) {
      fn(x, function (err, y) {
        r = r.concat(y || []);
        cb(err);
      });
    }, function (err) {
      callback(err, r);
    });
  };
  async.concat = doParallel(_concat);
  async.concatSeries = doSeries(_concat);

  async.whilst = function (test, iterator, callback) {
    if (test()) {
      iterator(function (err) {
        if (err) {
          return callback(err);
        }
        async.whilst(test, iterator, callback);
      });
    }
    else {
      callback();
    }
  };

  async.doWhilst = function (iterator, test, callback) {
    iterator(function (err) {
      if (err) {
        return callback(err);
      }
      if (test()) {
        async.doWhilst(iterator, test, callback);
      }
      else {
        callback();
      }
    });
  };

  async.until = function (test, iterator, callback) {
    if (!test()) {
      iterator(function (err) {
        if (err) {
          return callback(err);
        }
        async.until(test, iterator, callback);
      });
    }
    else {
      callback();
    }
  };

  async.doUntil = function (iterator, test, callback) {
    iterator(function (err) {
      if (err) {
        return callback(err);
      }
      if (!test()) {
        async.doUntil(iterator, test, callback);
      }
      else {
        callback();
      }
    });
  };

  async.queue = function (worker, concurrency) {
    if (concurrency === undefined) {
      concurrency = 1;
    }
    function _insert(q, data, pos, callback) {
      if (data.constructor !== Array) {
        data = [data];
      }
      _each(data, function (task) {
        var item = {
          data: task,
          callback: typeof callback === 'function' ? callback : null
        };

        if (pos) {
          q.tasks.unshift(item);
        } else {
          q.tasks.push(item);
        }

        if (q.saturated && q.tasks.length === concurrency) {
          q.saturated();
        }
        async.setImmediate(q.process);
      });
    }

    var workers = 0;
    var q = {
      tasks: [],
      concurrency: concurrency,
      saturated: null,
      empty: null,
      drain: null,
      push: function (data, callback) {
        _insert(q, data, false, callback);
      },
      unshift: function (data, callback) {
        _insert(q, data, true, callback);
      },
      process: function () {
        if (workers < q.concurrency && q.tasks.length) {
          var task = q.tasks.shift();
          if (q.empty && q.tasks.length === 0) {
            q.empty();
          }
          workers += 1;
          var next = function () {
            workers -= 1;
            if (task.callback) {
              task.callback.apply(task, arguments);
            }
            if (q.drain && q.tasks.length + workers === 0) {
              q.drain();
            }
            q.process();
          };
          var cb = only_once(next);
          worker(task.data, cb);
        }
      },
      length: function () {
        return q.tasks.length;
      },
      running: function () {
        return workers;
      }
    };
    return q;
  };

  async.cargo = function (worker, payload) {
    var working = false,
      tasks = [];

    var cargo = {
      tasks: tasks,
      payload: payload,
      saturated: null,
      empty: null,
      drain: null,
      push: function (data, callback) {
        if (data.constructor !== Array) {
          data = [data];
        }
        _each(data, function (task) {
          tasks.push({
            data: task,
            callback: typeof callback === 'function' ? callback : null
          });
          if (cargo.saturated && tasks.length === payload) {
            cargo.saturated();
          }
        });
        async.setImmediate(cargo.process);
      },
      process: function process() {
        if (working) {
          return;
        }
        if (tasks.length === 0) {
          if (cargo.drain) {
            cargo.drain();
          }
          return;
        }

        var ts = typeof payload === 'number'
          ? tasks.splice(0, payload)
          : tasks.splice(0);

        var ds = _map(ts, function (task) {
          return task.data;
        });

        if (cargo.empty) {
          cargo.empty();
        }
        working = true;
        worker(ds, function () {
          working = false;

          var args = arguments;
          _each(ts, function (data) {
            if (data.callback) {
              data.callback.apply(null, args);
            }
          });

          process();
        });
      },
      length: function () {
        return tasks.length;
      },
      running: function () {
        return working;
      }
    };
    return cargo;
  };

  var _console_fn = function (name) {
    return function (fn) {
      var args = Array.prototype.slice.call(arguments, 1);
      fn.apply(null, args.concat([function (err) {
        var args = Array.prototype.slice.call(arguments, 1);
        if (typeof console !== 'undefined') {
          if (err) {
            if (console.error) {
              console.error(err);
            }
          }
          else if (console[name]) {
            _each(args, function (x) {
              console[name](x);
            });
          }
        }
      }]));
    };
  };
  async.log = _console_fn('log');
  async.dir = _console_fn('dir');
  /*async.info = _console_fn('info');
   async.warn = _console_fn('warn');
   async.error = _console_fn('error');*/

  async.memoize = function (fn, hasher) {
    var memo = {};
    var queues = {};
    hasher = hasher || function (x) {
        return x;
      };
    var memoized = function () {
      var args = Array.prototype.slice.call(arguments);
      var callback = args.pop();
      var key = hasher.apply(null, args);
      if (key in memo) {
        callback.apply(null, memo[key]);
      }
      else if (key in queues) {
        queues[key].push(callback);
      }
      else {
        queues[key] = [callback];
        fn.apply(null, args.concat([function () {
          memo[key] = arguments;
          var q = queues[key];
          delete queues[key];
          for (var i = 0, l = q.length; i < l; i++) {
            q[i].apply(null, arguments);
          }
        }]));
      }
    };
    memoized.memo = memo;
    memoized.unmemoized = fn;
    return memoized;
  };

  async.unmemoize = function (fn) {
    return function () {
      return (fn.unmemoized || fn).apply(null, arguments);
    };
  };

  async.times = function (count, iterator, callback) {
    var counter = [];
    for (var i = 0; i < count; i++) {
      counter.push(i);
    }
    return async.map(counter, iterator, callback);
  };

  async.timesSeries = function (count, iterator, callback) {
    var counter = [];
    for (var i = 0; i < count; i++) {
      counter.push(i);
    }
    return async.mapSeries(counter, iterator, callback);
  };

  async.compose = function (/* functions... */) {
    var fns = Array.prototype.reverse.call(arguments);
    return function () {
      var that = this;
      var args = Array.prototype.slice.call(arguments);
      var callback = args.pop();
      async.reduce(fns, args, function (newargs, fn, cb) {
          fn.apply(that, newargs.concat([function () {
            var err = arguments[0];
            var nextargs = Array.prototype.slice.call(arguments, 1);
            cb(err, nextargs);
          }]))
        },
        function (err, results) {
          callback.apply(that, [err].concat(results));
        });
    };
  };

  var _applyEach = function (eachfn, fns /*args...*/) {
    var go = function () {
      var that = this;
      var args = Array.prototype.slice.call(arguments);
      var callback = args.pop();
      return eachfn(fns, function (fn, cb) {
          fn.apply(that, args.concat([cb]));
        },
        callback);
    };
    if (arguments.length > 2) {
      var args = Array.prototype.slice.call(arguments, 2);
      return go.apply(this, args);
    }
    else {
      return go;
    }
  };
  async.applyEach = doParallel(_applyEach);
  async.applyEachSeries = doSeries(_applyEach);

  async.forever = function (fn, callback) {
    function next(err) {
      if (err) {
        if (callback) {
          return callback(err);
        }
        throw err;
      }
      fn(next);
    }

    next();
  };

  // AMD / RequireJS
  if (typeof define !== 'undefined' && define.amd) {
    define([], function () {
      return async;
    });
  }
  // Node.js
  else if (typeof module !== 'undefined' && module.exports) {
    module.exports = async;
  }
  // included directly via <script> tag
  else {
    root.async = async;
  }

}());
