/**
 * Progress.js v0.1.0
 * https://github.com/usablica/progress.js
 * MIT licensed
 *
 * Copyright (C) 2013 usabli.ca - Afshin Mehrabani (@afshinmeh)
 */

(function (root, factory) {
  if (typeof exports === 'object') {
    // CommonJS
    factory(exports);
  } else if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module.
    define(['exports'], factory);
  } else {
    // Browser globals
    factory(root);
  }
}(this, function (exports) {
  //Default config/variables
  var VERSION = '0.1.0';

  /**
   * ProgressJs main class
   *
   * @class ProgressJs
   */
  function ProgressJs(obj) {

    if (typeof obj.length != 'undefined') {
      this._targetElement = obj;
    } else {
      this._targetElement = [obj];
    }

    if (typeof window._progressjsId === 'undefined')
      window._progressjsId = 1;

    if (typeof window._progressjsIntervals === 'undefined')
      window._progressjsIntervals = {};

    this._options = {
      //progress bar theme
      theme: 'blue',
      //overlay mode makes an overlay layer in the target element
      overlayMode: false,
      //to consider CSS3 transitions in events
      considerTransition: true
    };
  }

  /**
   * Start progress for specific element(s)
   *
   * @api private
   * @method _createContainer
   */
  function _startProgress() {

    //call onBeforeStart callback
    if (typeof this._onBeforeStartCallback != 'undefined') {
      this._onBeforeStartCallback.call(this);
    }

    //create the container for progress bar
    _createContainer.call(this);

    for (var i = 0, elmsLength = this._targetElement.length; i < elmsLength; i++) {
      _setProgress.call(this, this._targetElement[i]);
    }
  }

  /**
   * Set progress bar for specific element
   *
   * @api private
   * @method _setProgress
   * @param {Object} targetElement
   */
  function _setProgress(targetElement) {

    //if the target element already as `data-progressjs`, ignore the init
    if (targetElement.hasAttribute("data-progressjs"))
      return;

    //get target element position
    var targetElementOffset = _getOffset.call(this, targetElement);

    targetElement.setAttribute("data-progressjs", window._progressjsId);

    var progressElementContainer = document.createElement('div');
    progressElementContainer.className = 'progressjs-progress progressjs-theme-' + this._options.theme;


    //set the position percent elements, it depends on targetElement tag
    if (targetElement.tagName.toLowerCase() === 'body') {
      progressElementContainer.style.position = 'fixed';
    } else {
      progressElementContainer.style.position = 'absolute';
    }

    progressElementContainer.setAttribute("data-progressjs", window._progressjsId);
    var progressElement = document.createElement("div");
    progressElement.className = "progressjs-inner";

    //create an element for current percent of progress bar
    var progressPercentElement = document.createElement('div');
    progressPercentElement.className = "progressjs-percent";
    progressPercentElement.innerHTML = "1%";

    progressElement.appendChild(progressPercentElement);

    if (this._options.overlayMode && targetElement.tagName.toLowerCase() === 'body') {
      //if we have `body` for target element and also overlay mode is enable, we should use a different
      //position for progress bar container element
      progressElementContainer.style.left = 0;
      progressElementContainer.style.right = 0;
      progressElementContainer.style.top = 0;
      progressElementContainer.style.bottom = 0;
    } else {
      //set progress bar container size and offset
      progressElementContainer.style.left = targetElementOffset.left + 'px';
      progressElementContainer.style.top = targetElementOffset.top + 'px';
      progressElementContainer.style.width = targetElementOffset.width + 'px';

      if (this._options.overlayMode) {
        progressElementContainer.style.height = targetElementOffset.height + 'px';
      }
    }

    progressElementContainer.appendChild(progressElement);

    //append the element to container
    var container = document.querySelector('.progressjs-container');
    container.appendChild(progressElementContainer);

    _setPercentFor(targetElement, 1);

    //and increase the progressId
    ++window._progressjsId;
  }

  /**
   * Set percent for all elements
   *
   * @api private
   * @method _setPercent
   * @param {Number} percent
   */
  function _setPercent(percent) {
    for (var i = 0, elmsLength = this._targetElement.length; i < elmsLength; i++) {
      _setPercentFor.call(this, this._targetElement[i], percent);
    }
  }

  /**
   * Set percent for specific element
   *
   * @api private
   * @method _setPercentFor
   * @param {Object} targetElement
   * @param {Number} percent
   */
  function _setPercentFor(targetElement, percent) {
    var self = this;

    //prevent overflow!
    if (percent >= 100)
      percent = 100;

    if (targetElement.hasAttribute("data-progressjs")) {
      //setTimeout for better CSS3 animation applying in some cases
      setTimeout(function () {

        //call the onprogress callback
        if (typeof self._onProgressCallback != 'undefined') {
          self._onProgressCallback.call(self, targetElement, percent);
        }

        var percentElement = _getPercentElement(targetElement);
        percentElement.style.width = parseInt(percent) + '%';

        var percentElement = percentElement.querySelector(".progressjs-percent");
        var existingPercent = parseInt(percentElement.innerHTML.replace('%', ''));

        //start increase/decrease the percent element with animation
        (function (percentElement, existingPercent, currentPercent) {

          var increasement = true;
          if (existingPercent > currentPercent) {
            increasement = false;
          }

          var intervalIn = 10;
          function changePercentTimer(percentElement, existingPercent, currentPercent) {
            //calculate the distance between two percents
            var distance = Math.abs(existingPercent - currentPercent);
            if (distance < 3) {
              intervalIn = 30;
            } else if (distance < 20) {
              intervalIn = 20;
            } else {
              intervanIn = 1;
            }

            if ((existingPercent - currentPercent) != 0) {
              //set the percent
              percentElement.innerHTML = (increasement ? (++existingPercent) : (--existingPercent)) + '%';
              setTimeout(function () { changePercentTimer(percentElement, existingPercent, currentPercent); }, intervalIn);
            }
          }

          changePercentTimer(percentElement, existingPercent, currentPercent);

        })(percentElement, existingPercent, parseInt(percent));

      }, 50);
    }
  }

  /**
   * Get the progress bar element
   *
   * @api private
   * @method _getPercentElement
   * @param {Object} targetElement
   */
  function _getPercentElement(targetElement) {
    var progressjsId = parseInt(targetElement.getAttribute('data-progressjs'));
    return document.querySelector('.progressjs-container > .progressjs-progress[data-progressjs="' + progressjsId + '"] > .progressjs-inner');
  }

  /**
   * Auto increase the progress bar every X milliseconds
   *
   * @api private
   * @method _autoIncrease
   * @param {Number} size
   * @param {Number} millisecond
   */
  function _autoIncrease(size, millisecond) {
    var self = this;

    var progressjsId = parseInt(this._targetElement[0].getAttribute('data-progressjs'));

    if (typeof window._progressjsIntervals[progressjsId] != 'undefined') {
      clearInterval(window._progressjsIntervals[progressjsId]);
    }
    window._progressjsIntervals[progressjsId] = setInterval(function () {
      _increasePercent.call(self, size);
    }, millisecond);
  }

  /**
   * Increase the size of progress bar
   *
   * @api private
   * @method _increasePercent
   * @param {Number} size
   */
  function _increasePercent(size) {
    for (var i = 0, elmsLength = this._targetElement.length; i < elmsLength; i++) {
      var currentElement = this._targetElement[i];
      if (currentElement.hasAttribute('data-progressjs')) {
        var percentElement = _getPercentElement(currentElement);
        var existingPercent = parseInt(percentElement.style.width.replace('%', ''));
        if (existingPercent) {
          _setPercentFor.call(this, currentElement, existingPercent + (size || 1));
        }
      }
    }
  }

  /**
   * Close and remove progress bar
   *
   * @api private
   * @method _end
   */
  function _end() {

    //call onBeforeEnd callback
    if (typeof this._onBeforeEndCallback != 'undefined') {
      if (this._options.considerTransition === true) {
        //we can safety assume that all layers would be the same, so `this._targetElement[0]` is the same as `this._targetElement[1]`
        _getPercentElement(this._targetElement[0]).addEventListener(whichTransitionEvent(), this._onBeforeEndCallback, false);
      } else {
        this._onBeforeEndCallback.call(this);
      }
    }

    var progressjsId = parseInt(this._targetElement[0].getAttribute('data-progressjs'));

    for (var i = 0, elmsLength = this._targetElement.length; i < elmsLength; i++) {
      var currentElement = this._targetElement[i];
      var percentElement = _getPercentElement(currentElement);

      if (!percentElement)
        return;

      var existingPercent = parseInt(percentElement.style.width.replace('%', ''));

      var timeoutSec = 1;
      if (existingPercent < 100) {
        _setPercentFor.call(this, currentElement, 100);
        timeoutSec = 500;
      }

      //I believe I should handle this situation with eventListener and `transitionend` event but I'm not sure
      //about compatibility with IEs. Should be fixed in further versions.
      (function (percentElement, currentElement) {
        setTimeout(function () {
          percentElement.parentNode.className += " progressjs-end";

          setTimeout(function () {
            //remove the percent element from page
            percentElement.parentNode.parentNode.removeChild(percentElement.parentNode);
            //and remove the attribute
            currentElement.removeAttribute("data-progressjs");
          }, 1000);
        }, timeoutSec);
      })(percentElement, currentElement);
    }

    //clean the setInterval for autoIncrease function
    if (window._progressjsIntervals[progressjsId]) {
      //`delete` keyword has some problems in IE
      try {
        clearInterval(window._progressjsIntervals[progressjsId]);
        window._progressjsIntervals[progressjsId] = null;
        delete window._progressjsIntervals[progressjsId];
      } catch (ex) { }
    }
  }

  /**
   * Create the progress bar container
   *
   * @api private
   * @method _createContainer
   */
  function _createContainer() {
    //first check if we have an container already, we don't need to create it again
    if (!document.querySelector(".progressjs-container")) {
      var containerElement = document.createElement("div");
      containerElement.className = "progressjs-container";
      document.body.appendChild(containerElement);
    }
  }

  /**
   * Get an element position on the page
   * Thanks to `meouw`: http://stackoverflow.com/a/442474/375966
   *
   * @api private
   * @method _getOffset
   * @param {Object} element
   * @returns Element's position info
   */
  function _getOffset(element) {
    var elementPosition = {};

    if (element.tagName.toLowerCase() === 'body') {
      //set width
      elementPosition.width = element.clientWidth;
      //set height
      elementPosition.height = element.clientHeight;
    } else {
      //set width
      elementPosition.width = element.offsetWidth;
      //set height
      elementPosition.height = element.offsetHeight;
    }

    //calculate element top and left
    var _x = 0;
    var _y = 0;
    while (element && !isNaN(element.offsetLeft) && !isNaN(element.offsetTop)) {
      _x += element.offsetLeft;
      _y += element.offsetTop;
      element = element.offsetParent;
    }
    //set top
    elementPosition.top = _y;
    //set left
    elementPosition.left = _x;

    return elementPosition;
  }

  /**
   * Overwrites obj1's values with obj2's and adds obj2's if non existent in obj1
   * via: http://stackoverflow.com/questions/171251/how-can-i-merge-properties-of-two-javascript-objects-dynamically
   *
   * @param obj1
   * @param obj2
   * @returns obj3 a new object based on obj1 and obj2
   */
  function _mergeOptions(obj1, obj2) {
    var obj3 = {};
    for (var attrname in obj1) { obj3[attrname] = obj1[attrname]; }
    for (var attrname in obj2) { obj3[attrname] = obj2[attrname]; }
    return obj3;
  }

  var progressJs = function (targetElm) {
    if (typeof (targetElm) === 'object') {
      //Ok, create a new instance
      return new ProgressJs(targetElm);

    } else if (typeof (targetElm) === 'string') {
      //select the target element with query selector
      var targetElement = document.querySelectorAll(targetElm);

      if (targetElement) {
        return new ProgressJs(targetElement);
      } else {
        throw new Error('There is no element with given selector.');
      }
    } else {
      return new ProgressJs(document.body);
    }
  };

  /**
   * Get correct transition callback
   * Thanks @webinista: http://stackoverflow.com/a/9090128/375966
   *
   * @returns transition name
   */
  function whichTransitionEvent() {
    var t;
    var el = document.createElement('fakeelement');
    var transitions = {
      'transition': 'transitionend',
      'OTransition': 'oTransitionEnd',
      'MozTransition': 'transitionend',
      'WebkitTransition': 'webkitTransitionEnd'
    }

    for (t in transitions) {
      if (el.style[t] !== undefined) {
        return transitions[t];
      }
    }
  }

  /**
   * Current ProgressJs version
   *
   * @property version
   * @type String
   */
  progressJs.version = VERSION;

  //Prototype
  progressJs.fn = ProgressJs.prototype = {
    clone: function () {
      return new ProgressJs(this);
    },
    setOption: function (option, value) {
      this._options[option] = value;
      return this;
    },
    setOptions: function (options) {
      this._options = _mergeOptions(this._options, options);
      return this;
    },
    start: function () {
      _startProgress.call(this);
      return this;
    },
    set: function (percent) {
      _setPercent.call(this, percent);
      return this;
    },
    increase: function (size) {
      _increasePercent.call(this, size);
      return this;
    },
    autoIncrease: function (size, millisecond) {
      _autoIncrease.call(this, size, millisecond);
      return this;
    },
    end: function () {
      _end.call(this);
      return this;
    },
    onbeforeend: function (providedCallback) {
      if (typeof (providedCallback) === 'function') {
        this._onBeforeEndCallback = providedCallback;
      } else {
        throw new Error('Provided callback for onbeforeend was not a function');
      }
      return this;
    },
    onbeforestart: function (providedCallback) {
      if (typeof (providedCallback) === 'function') {
        this._onBeforeStartCallback = providedCallback;
      } else {
        throw new Error('Provided callback for onbeforestart was not a function');
      }
      return this;
    },
    onprogress: function (providedCallback) {
      if (typeof (providedCallback) === 'function') {
        this._onProgressCallback = providedCallback;
      } else {
        throw new Error('Provided callback for onprogress was not a function');
      }
      return this;
    }
  };

  exports.progressJs = progressJs;
  return progressJs;
}));
