/*! 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);
