﻿/*
 * 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 so it can be included using a simpler
    // (and maybe more professional) module name:
    angular.module('angular-loading-bar', ['ngLoadingBar']);


    /**
     * loadingBarInterceptor service
     *
     * Registers itself as an Angular interceptor and listens for XHR requests.
     */
    angular.module('ngLoadingBar', [])
      .config(['$httpProvider', function ($httpProvider) {

          var interceptor = ['$q', '$cacheFactory', '$timeout', '$rootScope', 'cfpLoadingBar', function ($q, $cacheFactory, $timeout, $rootScope, 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 defaults = $httpProvider.defaults;

                  if (config.method !== 'GET' || config.cache === false) {
                      config.cached = false;
                      return false;
                  }

                  if (config.cache === true && defaults.cache === undefined) {
                      cache = $cacheFactory.get('$http');
                  } else if (defaults.cache !== undefined) {
                      cache = defaults.cache;
                  } else {
                      cache = config.cache;
                  }

                  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 (!isCached(response.config)) {
                          reqsCompleted++;
                          $rootScope.$broadcast('cfpLoadingBar:loaded', { url: response.config.url });
                          if (reqsCompleted >= reqsTotal) {
                              setComplete();
                          } else {
                              cfpLoadingBar.set(reqsCompleted / reqsTotal);
                          }
                      }
                      return response;
                  },

                  'responseError': function (rejection) {
                      if (!isCached(rejection.config)) {
                          reqsCompleted++;
                          $rootScope.$broadcast('cfpLoadingBar:loaded', { url: rejection.config.url });
                          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.
       */
      .provider('cfpLoadingBar', function () {

          this.includeSpinner = true;
          this.includeBar = true;
          this.latencyThreshold = 100;
          this.parentSelector = 'body';

          this.$get = ['$document', '$timeout', '$animate', '$rootScope', function ($document, $timeout, $animate, $rootScope) {

              var $parentSelector = this.parentSelector,
                $parent = $document.find($parentSelector),
                loadingBarContainer = angular.element('<div id="loading-bar"><div class="bar"><div class="peg"></div></div></div>'),
                loadingBar = loadingBarContainer.find('div').eq(0),
                spinner = angular.element('<div id="loading-bar-spinner"><div class="spinner-icon"></div></div>');

              var incTimeout,
                completeTimeout,
                started = false,
                status = 0;

              var includeSpinner = this.includeSpinner;
              var includeBar = this.includeBar;

              /**
               * Inserts the loading bar element into the dom, and sets it to 2%
               */
              function _start() {
                  $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(0.02);
              }

              /**
               * 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 _complete() {
                  $rootScope.$broadcast('cfpLoadingBar:completed');
                  _set(1);

                  // Attempt to aggregate any start/complete calls within 500ms:
                  completeTimeout = $timeout(function () {
                      $animate.leave(loadingBarContainer, function () {
                          status = 0;
                          started = false;
                      });
                      $animate.leave(spinner);
                  }, 500);
              }

              return {
                  start: _start,
                  set: _set,
                  status: _status,
                  inc: _inc,
                  complete: _complete,
                  includeSpinner: this.includeSpinner,
                  latencyThreshold: this.latencyThreshold,
                  parentSelector: this.parentSelector
              };


          }];     //
      });       // wtf javascript. srsly
})();       //
