﻿/*
 * angular-loading-bar
 *
 * intercepts XHR requests and creates a loading bar.
 * Based on the excellent nprogress work by rstacruz (more info in readme)
 *
 * (c) 2013 Wes Cruver
 * License: MIT
 */


(function () {

    'use strict';

    // Alias the loading bar for various backwards compatibilities since the project has matured:
    angular.module('angular-loading-bar', ['cfp.loadingBarInterceptor']);
    angular.module('chieffancypants.loadingBar', ['cfp.loadingBarInterceptor']);


    /**
     * loadingBarInterceptor service
     *
     * Registers itself as an Angular interceptor and listens for XHR requests.
     */
    angular.module('cfp.loadingBarInterceptor', ['cfp.loadingBar'])
      .config(['$httpProvider', function ($httpProvider) {

          var interceptor = ['$q', '$cacheFactory', '$timeout', '$rootScope', '$log', 'cfpLoadingBar', function ($q, $cacheFactory, $timeout, $rootScope, $log, cfpLoadingBar) {

              /**
               * The total number of requests made
               */
              var reqsTotal = 0;

              /**
               * The number of requests completed (either successfully or not)
               */
              var reqsCompleted = 0;

              /**
               * The amount of time spent fetching before showing the loading bar
               */
              var latencyThreshold = cfpLoadingBar.latencyThreshold;

              /**
               * $timeout handle for latencyThreshold
               */
              var startTimeout;


              /**
               * calls cfpLoadingBar.complete() which removes the
               * loading bar from the DOM.
               */
              function setComplete() {
                  $timeout.cancel(startTimeout);
                  cfpLoadingBar.complete();
                  reqsCompleted = 0;
                  reqsTotal = 0;
              }

              /**
               * Determine if the response has already been cached
               * @param  {Object}  config the config option from the request
               * @return {Boolean} retrns true if cached, otherwise false
               */
              function isCached(config) {
                  var cache;
                  var defaultCache = $cacheFactory.get('$http');
                  var defaults = $httpProvider.defaults;

                  // Choose the proper cache source. Borrowed from angular: $http service
                  if ((config.cache || defaults.cache) && config.cache !== false &&
                    (config.method === 'GET' || config.method === 'JSONP')) {
                      cache = angular.isObject(config.cache) ? config.cache
                        : angular.isObject(defaults.cache) ? defaults.cache
                        : defaultCache;
                  }

                  var cached = cache !== undefined ?
                    cache.get(config.url) !== undefined : false;

                  if (config.cached !== undefined && cached !== config.cached) {
                      return config.cached;
                  }
                  config.cached = cached;
                  return cached;
              }


              return {
                  'request': function (config) {
                      // Check to make sure this request hasn't already been cached and that
                      // the requester didn't explicitly ask us to ignore this request:
                      if (!config.ignoreLoadingBar && !isCached(config)) {
                          $rootScope.$broadcast('cfpLoadingBar:loading', { url: config.url });
                          if (reqsTotal === 0) {
                              startTimeout = $timeout(function () {
                                  cfpLoadingBar.start();
                              }, latencyThreshold);
                          }
                          reqsTotal++;
                          cfpLoadingBar.set(reqsCompleted / reqsTotal);
                      }
                      return config;
                  },

                  'response': function (response) {
                      if (!response || !response.config) {
                          $log.error('Broken interceptor detected: Config object not supplied in response:\n https://github.com/chieffancypants/angular-loading-bar/pull/50');
                          return response;
                      }

                      if (!response.config.ignoreLoadingBar && !isCached(response.config)) {
                          reqsCompleted++;
                          $rootScope.$broadcast('cfpLoadingBar:loaded', { url: response.config.url, result: response });
                          if (reqsCompleted >= reqsTotal) {
                              setComplete();
                          } else {
                              cfpLoadingBar.set(reqsCompleted / reqsTotal);
                          }
                      }
                      return response;
                  },

                  'responseError': function (rejection) {
                      if (!rejection || !rejection.config) {
                          $log.error('Broken interceptor detected: Config object not supplied in rejection:\n https://github.com/chieffancypants/angular-loading-bar/pull/50');
                          return $q.reject(rejection);
                      }

                      if (!rejection.config.ignoreLoadingBar && !isCached(rejection.config)) {
                          reqsCompleted++;
                          $rootScope.$broadcast('cfpLoadingBar:loaded', { url: rejection.config.url, result: rejection });
                          if (reqsCompleted >= reqsTotal) {
                              setComplete();
                          } else {
                              cfpLoadingBar.set(reqsCompleted / reqsTotal);
                          }
                      }
                      return $q.reject(rejection);
                  }
              };
          }];

          $httpProvider.interceptors.push(interceptor);
      }]);


    /**
     * Loading Bar
     *
     * This service handles adding and removing the actual element in the DOM.
     * Generally, best practices for DOM manipulation is to take place in a
     * directive, but because the element itself is injected in the DOM only upon
     * XHR requests, and it's likely needed on every view, the best option is to
     * use a service.
     */
    angular.module('cfp.loadingBar', [])
      .provider('cfpLoadingBar', function () {

          this.includeSpinner = false;
          this.includeBar = true;
          this.latencyThreshold = 150;
          this.startSize = 0.02;
          this.parentSelector = '#view';
          this.spinnerTemplate = '<div id="loading-bar-spinner"><div class="spinner-icon"></div></div>';
          this.loadingBarTemplate = '<div id="loading-bar"><div class="bar"><div class="peg"></div></div></div>';

          this.$get = ['$injector', '$document', '$timeout', '$rootScope', function ($injector, $document, $timeout, $rootScope) {
              var $animate;
              var $parentSelector = this.parentSelector,
                loadingBarContainer = angular.element(this.loadingBarTemplate),
                loadingBar = loadingBarContainer.find('div').eq(0),
                spinner = angular.element(this.spinnerTemplate);

              var incTimeout,
                completeTimeout,
                started = false,
                status = 0;

              var includeSpinner = this.includeSpinner;
              var includeBar = this.includeBar;
              var startSize = this.startSize;

              /**
               * Inserts the loading bar element into the dom, and sets it to 2%
               */
              function _start() {
                  if (!$animate) {
                      $animate = $injector.get('$animate');
                  }

                  var $parent = $document.find($parentSelector).eq(0);
                  $timeout.cancel(completeTimeout);

                  // do not continually broadcast the started event:
                  if (started) {
                      return;
                  }

                  $rootScope.$broadcast('cfpLoadingBar:started');
                  started = true;

                  if (includeBar) {
                      $animate.enter(loadingBarContainer, $parent);
                  }

                  if (includeSpinner) {
                      $animate.enter(spinner, $parent);
                  }

                  _set(startSize);
              }

              /**
               * Set the loading bar's width to a certain percent.
               *
               * @param n any value between 0 and 1
               */
              function _set(n) {
                  if (!started) {
                      return;
                  }
                  var pct = (n * 100) + '%';
                  loadingBar.css('width', pct);
                  status = n;

                  // increment loadingbar to give the illusion that there is always
                  // progress but make sure to cancel the previous timeouts so we don't
                  // have multiple incs running at the same time.
                  $timeout.cancel(incTimeout);
                  incTimeout = $timeout(function () {
                      _inc();
                  }, 250);
              }

              /**
               * Increments the loading bar by a random amount
               * but slows down as it progresses
               */
              function _inc() {
                  if (_status() >= 1) {
                      return;
                  }

                  var rnd = 0;

                  // TODO: do this mathmatically instead of through conditions

                  var stat = _status();
                  if (stat >= 0 && stat < 0.25) {
                      // Start out between 3 - 6% increments
                      rnd = (Math.random() * (5 - 3 + 1) + 3) / 100;
                  } else if (stat >= 0.25 && stat < 0.65) {
                      // increment between 0 - 3%
                      rnd = (Math.random() * 3) / 100;
                  } else if (stat >= 0.65 && stat < 0.9) {
                      // increment between 0 - 2%
                      rnd = (Math.random() * 2) / 100;
                  } else if (stat >= 0.9 && stat < 0.99) {
                      // finally, increment it .5 %
                      rnd = 0.005;
                  } else {
                      // after 99%, don't increment:
                      rnd = 0;
                  }

                  var pct = _status() + rnd;
                  _set(pct);
              }

              function _status() {
                  return status;
              }

              function _completeAnimation() {
                  status = 0;
                  started = false;
              }

              function _complete() {
                  if (!$animate) {
                      $animate = $injector.get('$animate');
                  }

                  $rootScope.$broadcast('cfpLoadingBar:completed');
                  _set(1);

                  $timeout.cancel(completeTimeout);

                  // Attempt to aggregate any start/complete calls within 500ms:
                  completeTimeout = $timeout(function () {
                      var promise = $animate.leave(loadingBarContainer, _completeAnimation);
                      if (promise && promise.then) {
                          promise.then(_completeAnimation);
                      }
                      $animate.leave(spinner);
                  }, 500);
              }

              return {
                  start: _start,
                  set: _set,
                  status: _status,
                  inc: _inc,
                  complete: _complete,
                  includeSpinner: this.includeSpinner,
                  latencyThreshold: this.latencyThreshold,
                  parentSelector: this.parentSelector,
                  startSize: this.startSize
              };


          }];     //
      });       // wtf javascript. srsly
})();       //
