'use strict';

var $timeoutMinErr = minErr('$timeout');

/** @this */
function $TimeoutProvider() {
  this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler',
       function($rootScope,   $browser,   $q,   $$q,   $exceptionHandler) {

    var deferreds = {};


    /**
     * @ngdoc service
     * @name $timeout
     *
     * @description
     * AngularJS's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch
     * block and delegates any exceptions to
     * {@link ng.$exceptionHandler $exceptionHandler} service.
     *
     * The return value of calling `$timeout` is a promise, which will be resolved when
     * the delay has passed and the timeout function, if provided, is executed.
     *
     * To cancel a timeout request, call `$timeout.cancel(promise)`.
     *
     * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to
     * synchronously flush the queue of deferred functions.
     *
     * If you only want a promise that will be resolved after some specified delay
     * then you can call `$timeout` without the `fn` function.
     *
     * @param {function()=} fn A function, whose execution should be delayed.
     * @param {number=} [delay=0] Delay in milliseconds.
     * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
     *   will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
     * @param {...*=} Pass additional parameters to the executed function.
     * @returns {Promise} Promise that will be resolved when the timeout is reached. The promise
     *   will be resolved with the return value of the `fn` function.
     *
     */
    function timeout(fn, delay, invokeApply) {
      if (!isFunction(fn)) {
        invokeApply = delay;
        delay = fn;
        fn = noop;
      }

      var args = sliceArgs(arguments, 3),
          skipApply = (isDefined(invokeApply) && !invokeApply),
          deferred = (skipApply ? $$q : $q).defer(),
          promise = deferred.promise,
          timeoutId;

      timeoutId = $browser.defer(function() {
        try {
          deferred.resolve(fn.apply(null, args));
        } catch (e) {
          deferred.reject(e);
          $exceptionHandler(e);
        } finally {
          delete deferreds[promise.$$timeoutId];
        }

        if (!skipApply) $rootScope.$apply();
      }, delay, '$timeout');

      promise.$$timeoutId = timeoutId;
      deferreds[timeoutId] = deferred;

      return promise;
    }


    /**
     * @ngdoc method
     * @name $timeout#cancel
     *
     * @description
     * Cancels a task associated with the `promise`. As a result of this, the promise will be
     * resolved with a rejection.
     *
     * @param {Promise=} promise Promise returned by the `$timeout` function.
     * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully
     *   canceled.
     */
    timeout.cancel = function(promise) {
      if (!promise) return false;

      if (!promise.hasOwnProperty('$$timeoutId')) {
        throw $timeoutMinErr('badprom',
            '`$timeout.cancel()` called with a promise that was not generated by `$timeout()`.');
      }

      if (!deferreds.hasOwnProperty(promise.$$timeoutId)) return false;

      var id = promise.$$timeoutId;
      var deferred = deferreds[id];

      // Timeout cancels should not report an unhandled promise.
      markQExceptionHandled(deferred.promise);
      deferred.reject('canceled');
      delete deferreds[id];

      return $browser.defer.cancel(id);
    };

    return timeout;
  }];
}
