/*! Moment Duration Format v1.3.0
 *  https://github.com/jsmreese/moment-duration-format
 *  Date: 2014-07-15
 *
 *  Duration format plugin function for the Moment.js library
 *  http://momentjs.com/
 *
 *  Copyright 2014 John Madhavan-Reese
 *  Released under the MIT license
 */

(function (root, undefined) {
  // repeatZero(qty)
  // returns "0" repeated qty times
  function repeatZero(qty) {
    var result = '';

    // exit early
    // if qty is 0 or a negative number
    // or doesn't coerce to an integer
    qty = parseInt(qty, 10);
    if (!qty || qty < 1) {
      return result;
    }

    while (qty) {
      result += '0';
      qty -= 1;
    }

    return result;
  }

  // padZero(str, len [, isRight])
  // pads a string with zeros up to a specified length
  // will not pad a string if its length is aready
  // greater than or equal to the specified length
  // default output pads with zeros on the left
  // set isRight to `true` to pad with zeros on the right
  function padZero(str, len, isRight) {
    if (str == null) {
      str = '';
    }
    str = '' + str;

    return (isRight ? str : '') + repeatZero(len - str.length) + (isRight ? '' : str);
  }

  // isArray
  function isArray(array) {
    return Object.prototype.toString.call(array) === '[object Array]';
  }

  // isObject
  function isObject(obj) {
    return Object.prototype.toString.call(obj) === '[object Object]';
  }

  // findLast
  function findLast(array, callback) {
    var index = array.length;

    while ((index -= 1)) {
      if (callback(array[index])) {
        return array[index];
      }
    }
  }

  // find
  function find(array, callback) {
    var index = 0,
      max = array.length,
      match;

    if (typeof callback !== 'function') {
      match = callback;
      callback = function (item) {
        return item === match;
      };
    }

    while (index < max) {
      if (callback(array[index])) {
        return array[index];
      }
      index += 1;
    }
  }

  // each
  function each(array, callback) {
    var index = 0,
      max = array.length;

    if (!array || !max) {
      return;
    }

    while (index < max) {
      if (callback(array[index], index) === false) {
        return;
      }
      index += 1;
    }
  }

  // map
  function map(array, callback) {
    var index = 0,
      max = array.length,
      ret = [];

    if (!array || !max) {
      return ret;
    }

    while (index < max) {
      ret[index] = callback(array[index], index);
      index += 1;
    }

    return ret;
  }

  // pluck
  function pluck(array, prop) {
    return map(array, function (item) {
      return item[prop];
    });
  }

  // compact
  function compact(array) {
    var ret = [];

    each(array, function (item) {
      if (item) {
        ret.push(item);
      }
    });

    return ret;
  }

  // unique
  function unique(array) {
    var ret = [];

    each(array, function (_a) {
      if (!find(ret, _a)) {
        ret.push(_a);
      }
    });

    return ret;
  }

  // intersection
  function intersection(a, b) {
    var ret = [];

    each(a, function (_a) {
      each(b, function (_b) {
        if (_a === _b) {
          ret.push(_a);
        }
      });
    });

    return unique(ret);
  }

  // rest
  function rest(array, callback) {
    var ret = [];

    each(array, function (item, index) {
      if (!callback(item)) {
        ret = array.slice(index);
        return false;
      }
    });

    return ret;
  }

  // initial
  function initial(array, callback) {
    var reversed = array.slice().reverse();

    return rest(reversed, callback).reverse();
  }

  // extend
  function extend(a, b) {
    for (var key in b) {
      if (b.hasOwnProperty(key)) {
        a[key] = b[key];
      }
    }

    return a;
  }

  // define internal moment reference
  var moment;

  if (typeof require === 'function') {
    try {
      moment = require('moment');
    } catch (e) {}
  }

  if (!moment && root.moment) {
    moment = root.moment;
  }

  if (!moment) {
    throw 'Moment Duration Format cannot find Moment.js';
  }

  // moment.duration.format([template] [, precision] [, settings])
  moment.duration.fn.format = function () {
    var tokenizer,
      tokens,
      types,
      typeMap,
      momentTypes,
      foundFirst,
      trimIndex,
      args = [].slice.call(arguments),
      settings = extend({}, this.format.defaults),
      // keep a shadow copy of this moment for calculating remainders
      remainder = moment.duration(this);

    // add a reference to this duration object to the settings for use
    // in a template function
    settings.duration = this;

    // parse arguments
    each(args, function (arg) {
      if (typeof arg === 'string' || typeof arg === 'function') {
        settings.template = arg;
        return;
      }

      if (typeof arg === 'number') {
        settings.precision = arg;
        return;
      }

      if (isObject(arg)) {
        extend(settings, arg);
      }
    });

    // types
    types = settings.types = isArray(settings.types) ? settings.types : settings.types.split(' ');

    // template
    if (typeof settings.template === 'function') {
      settings.template = settings.template.apply(settings);
    }

    // tokenizer regexp
    tokenizer = new RegExp(
      map(types, function (type) {
        return settings[type].source;
      }).join('|'),
      'g'
    );

    // token type map function
    typeMap = function (token) {
      return find(types, function (type) {
        return settings[type].test(token);
      });
    };

    // tokens array
    tokens = map(
      settings.template.match(tokenizer),
      function (token, index) {
        var type = typeMap(token),
          length = token.length;

        return {
          index: index,
          length: length,

          // replace escaped tokens with the non-escaped token text
          token: type === 'escape' ? token.replace(settings.escape, '$1') : token,

          // ignore type on non-moment tokens
          type: type === 'escape' || type === 'general' ? null : type,

          // calculate base value for all moment tokens
          //baseValue: ((type === "escape" || type === "general") ? null : this.as(type))
        };
      },
      this
    );

    // unique moment token types in the template (in order of descending magnitude)
    momentTypes = intersection(types, unique(compact(pluck(tokens, 'type'))));

    // exit early if there are no momentTypes
    if (!momentTypes.length) {
      return pluck(tokens, 'token').join('');
    }

    // calculate values for each token type in the template
    each(momentTypes, function (momentType, index) {
      var value, wholeValue, decimalValue, isLeast, isMost;

      // calculate integer and decimal value portions
      value = remainder.as(momentType);
      wholeValue = value > 0 ? Math.floor(value) : Math.ceil(value);
      decimalValue = value - wholeValue;

      // is this the least-significant moment token found?
      isLeast = index + 1 === momentTypes.length;

      // is this the most-significant moment token found?
      isMost = !index;

      // update tokens array
      // using this algorithm to not assume anything about
      // the order or frequency of any tokens
      each(tokens, function (token) {
        if (token.type === momentType) {
          extend(token, {
            value: value,
            wholeValue: wholeValue,
            decimalValue: decimalValue,
            isLeast: isLeast,
            isMost: isMost,
          });

          if (isMost) {
            // note the length of the most-significant moment token:
            // if it is greater than one and forceLength is not set, default forceLength to `true`
            if (settings.forceLength == null && token.length > 1) {
              settings.forceLength = true;
            }

            // rationale is this:
            // if the template is "h:mm:ss" and the moment value is 5 minutes, the user-friendly output is "5:00", not "05:00"
            // shouldn't pad the `minutes` token even though it has length of two
            // if the template is "hh:mm:ss", the user clearly wanted everything padded so we should output "05:00"
            // if the user wanted the full padded output, they can set `{ trim: false }` to get "00:05:00"
          }
        }
      });

      // update remainder
      remainder.subtract(wholeValue, momentType);
    });

    // trim tokens array
    if (settings.trim) {
      tokens = (settings.trim === 'left' ? rest : initial)(tokens, function (token) {
        // return `true` if:
        // the token is not the least moment token (don't trim the least moment token)
        // the token is a moment token that does not have a value (don't trim moment tokens that have a whole value)
        return !(token.isLeast || (token.type != null && token.wholeValue));
      });
    }

    // build output

    // the first moment token can have special handling
    foundFirst = false;

    // run the map in reverse order if trimming from the right
    if (settings.trim === 'right') {
      tokens.reverse();
    }

    tokens = map(tokens, function (token) {
      var val, decVal;

      if (!token.type) {
        // if it is not a moment token, use the token as its own value
        return token.token;
      }

      // apply negative precision formatting to the least-significant moment token
      if (token.isLeast && settings.precision < 0) {
        val = (Math.floor(token.wholeValue * Math.pow(10, settings.precision)) * Math.pow(10, -settings.precision)).toString();
      } else {
        val = token.wholeValue.toString();
      }

      // remove negative sign from the beginning
      val = val.replace(/^\-/, '');

      // apply token length formatting
      // special handling for the first moment token that is not the most significant in a trimmed template
      if (token.length > 1 && (foundFirst || token.isMost || settings.forceLength)) {
        val = padZero(val, token.length);
      }

      // add decimal value if precision > 0
      if (token.isLeast && settings.precision > 0) {
        decVal = token.decimalValue
          .toString()
          .replace(/^\-/, '')
          .split(/\.|e\-/);
        switch (decVal.length) {
          case 1:
            val += '.' + padZero(decVal[0], settings.precision, true).slice(0, settings.precision);
            break;

          case 2:
            val += '.' + padZero(decVal[1], settings.precision, true).slice(0, settings.precision);
            break;

          case 3:
            val += '.' + padZero(repeatZero(+decVal[2] - 1) + (decVal[0] || '0') + decVal[1], settings.precision, true).slice(0, settings.precision);
            break;

          default:
            throw 'Moment Duration Format: unable to parse token decimal value.';
        }
      }

      // add a negative sign if the value is negative and token is most significant
      if (token.isMost && token.value < 0) {
        val = '-' + val;
      }

      foundFirst = true;

      return val;
    });

    // undo the reverse if trimming from the right
    if (settings.trim === 'right') {
      tokens.reverse();
    }

    return tokens.join('');
  };

  moment.duration.fn.format.defaults = {
    // token definitions
    escape: /\[(.+?)\]/,
    years: /[Yy]+/,
    months: /M+/,
    weeks: /[Ww]+/,
    days: /[Dd]+/,
    hours: /[Hh]+/,
    minutes: /m+/,
    seconds: /s+/,
    milliseconds: /S+/,
    general: /.+?/,

    // token type names
    // in order of descending magnitude
    // can be a space-separated token name list or an array of token names
    types: 'escape years months weeks days hours minutes seconds milliseconds general',

    // format options

    // trim
    // "left" - template tokens are trimmed from the left until the first moment token that has a value >= 1
    // "right" - template tokens are trimmed from the right until the first moment token that has a value >= 1
    // (the final moment token is not trimmed, regardless of value)
    // `false` - template tokens are not trimmed
    trim: 'left',

    // precision
    // number of decimal digits to include after (to the right of) the decimal point (positive integer)
    // or the number of digits to truncate to 0 before (to the left of) the decimal point (negative integer)
    precision: 0,

    // force first moment token with a value to render at full length even when template is trimmed and first moment token has length of 1
    forceLength: null,

    // template used to format duration
    // may be a function or a string
    // template functions are executed with the `this` binding of the settings object
    // so that template strings may be dynamically generated based on the duration object
    // (accessible via `this.duration`)
    // or any of the other settings
    template: function () {
      var types = this.types,
        dur = this.duration,
        lastType = findLast(types, function (type) {
          return dur._data[type];
        });

      // default template strings for each duration dimension type
      switch (lastType) {
        case 'seconds':
          return 'h:mm:ss';
        case 'minutes':
          return 'd[d] h:mm';
        case 'hours':
          return 'd[d] h[h]';
        case 'days':
          return 'M[m] d[d]';
        case 'weeks':
          return 'y[y] w[w]';
        case 'months':
          return 'y[y] M[m]';
        case 'years':
          return 'y[y]';
        default:
          return 'y[y] M[m] d[d] h:mm:ss';
      }
    },
  };
})(this);
