'use strict';

/*@
Ox.formatArea <f> Formats a number of meters as square meters or kilometers
    > Ox.formatArea(1000)
    '1,000 m\u00B2'
    > Ox.formatArea(1000000)
    '1 km\u00B2'
@*/

Ox.formatArea = function(num, dec) {
    dec = Ox.isUndefined(dec) ? 8 : dec;
    var km = num >= 1000000;
    return Ox.formatNumber(
        (km ? num / 1000000 : num).toPrecision(dec)
    ) + ' ' + (km ? 'k' : '') + 'm\u00B2';
};

/*@
Ox.formatCurrency <f> Formats a number with a currency symbol
    > Ox.formatCurrency(1000, '$', 2)
    '$1,000.00'
@*/

Ox.formatCurrency = function(num, str, dec) {
    return str + Ox.formatNumber(num, dec);
};

/*@
Ox.formatDate <f> Formats a date according to a format string
    See 
    <a href="http://developer.apple.com/library/mac/#documentation/Darwin/Reference/ManPages/man3/strftime.3.html">strftime</a>
    and <a href="http://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a>.
    '%Q' (quarter) and '%X'/'%x' (year with 'BC'/'AD') are non-standard
    <script>
        Ox.test.date = new Date('2005-01-02 00:03:04');
    </script>
    > Ox.formatDate(Ox.test.date, '%A') // Full weekday
    'Sunday'
    > Ox.formatDate(Ox.test.date, '%a') // Abbreviated weekday
    'Sun'
    > Ox.formatDate(Ox.test.date, '%B') // Full month
    'January'
    > Ox.formatDate(Ox.test.date, '%b') // Abbreviated month
    'Jan'
    > Ox.formatDate(Ox.test.date, '%C') // Century
    '20'
    > Ox.formatDate(Ox.test.date, '%c') // US time and date
    '01/02/05 12:03:04 AM'
    > Ox.formatDate(Ox.test.date, '%D') // US date
    '01/02/05'
    > Ox.formatDate(Ox.test.date, '%d') // Zero-padded day of the month
    '02'
    > Ox.formatDate(Ox.test.date, '%e') // Space-padded day of the month
    ' 2'
    > Ox.formatDate(Ox.test.date, '%F') // Date
    '2005-01-02'
    > Ox.formatDate(Ox.test.date, '%G') // Full ISO-8601 year
    '2004'
    > Ox.formatDate(Ox.test.date, '%g') // Abbreviated ISO-8601 year
    '04'
    > Ox.formatDate(Ox.test.date, '%H') // Zero-padded hour (24-hour clock)
    '00'
    > Ox.formatDate(Ox.test.date, '%h') // Abbreviated month
    'Jan'
    > Ox.formatDate(Ox.test.date, '%I') // Zero-padded hour (12-hour clock)
    '12'
    > Ox.formatDate(Ox.test.date, '%j') // Zero-padded day of the year
    '002'
    > Ox.formatDate(Ox.test.date, '%k') // Space-padded hour (24-hour clock)
    ' 0'
    > Ox.formatDate(Ox.test.date, '%l') // Space-padded hour (12-hour clock)
    '12'
    > Ox.formatDate(Ox.test.date, '%M') // Zero-padded minute
    '03'
    > Ox.formatDate(Ox.test.date, '%m') // Zero-padded month
    '01'
    > Ox.formatDate(Ox.test.date, '%n') // Newline
    '\n'
    > Ox.formatDate(Ox.test.date, '%p') // AM or PM
    'AM'
    > Ox.formatDate(Ox.test.date, '%Q') // Quarter of the year
    '1'
    > Ox.formatDate(Ox.test.date, '%R') // Zero-padded hour and minute
    '00:03'
    > Ox.formatDate(Ox.test.date, '%r') // US time
    '12:03:04 AM'
    > Ox.formatDate(Ox.test.date, '%S') // Zero-padded second
    '04'
    > Ox.formatDate(Ox.test.date, '%s', true) // Number of seconds since the Epoch
    '1104620584'
    > Ox.formatDate(Ox.test.date, '%T') // Time
    '00:03:04'
    > Ox.formatDate(Ox.test.date, '%t') // Tab
    '\t'
    > Ox.formatDate(Ox.test.date, '%U') // Zero-padded week of the year (00-53, Sunday as first day)
    '01'
    > Ox.formatDate(Ox.test.date, '%u') // Decimal weekday (1-7, Monday as first day)
    '7'
    > Ox.formatDate(Ox.test.date, '%V') // Zero-padded ISO-8601 week of the year
    '53'
    > Ox.formatDate(Ox.test.date, '%v') // Formatted date
    ' 2-Jan-2005'
    > Ox.formatDate(Ox.test.date, '%W') // Zero-padded week of the year (00-53, Monday as first day)
    '00'
    > Ox.formatDate(Ox.test.date, '%w') // Decimal weekday (0-6, Sunday as first day)
    '0'
    > Ox.formatDate(Ox.test.date, '%X') // Full year with BC or AD
    '2005 AD'
    > Ox.formatDate(Ox.test.date, '%x') // Full year with BC or AD if year < 1000
    '2005'
    > Ox.formatDate(Ox.test.date, '%Y') // Full year
    '2005'
    > Ox.formatDate(Ox.test.date, '%y') // Abbreviated year
    '05'
    > Ox.formatDate(Ox.test.date, '%Z', true) // Time zone name
    'UTC'
    > Ox.formatDate(Ox.test.date, '%z', true) // Time zone offset
    '+0000'
    > Ox.formatDate(Ox.test.date, '%+', true) // Formatted date and time
    'Sun Jan  2 00:03:04 CET 2005'
    > Ox.formatDate(Ox.test.date, '%%')
    '%'
@*/

Ox.formatDate = function(date, str, utc) {
    // fixme: date and utc are optional, date can be date, number or string
    if (date === '') {
        return '';
    }
    date = Ox.makeDate(date);
    var format = [
        ['%', function() {return '%{%}';}],
        ['c', function() {return '%D %r';}],
        ['D', function() {return '%m/%d/%y';}],
        ['F', function() {return '%Y-%m-%d';}],
        ['h', function() {return '%b';}],
        ['R', function() {return '%H:%M';}],
        ['r', function() {return '%I:%M:%S %p';}],
        ['T', function() {return '%H:%M:%S';}],
        ['v', function() {return '%e-%b-%Y';}],
        ['\\+', function() {return '%a %b %e %H:%M:%S %Z %Y';}],
        ['A', function(d) {return Ox.WEEKDAYS[(Ox.getDay(d, utc) + 6) % 7];}],
        ['a', function(d) {return Ox.SHORT_WEEKDAYS[(Ox.getDay(d, utc) + 6) % 7];}],
        ['B', function(d) {return Ox.MONTHS[Ox.getMonth(d, utc)];}],
        ['b', function(d) {return Ox.SHORT_MONTHS[Ox.getMonth(d, utc)];}],
        ['C', function(d) {return Math.floor(Ox.getFullYear(d, utc) / 100).toString();}],
        ['d', function(d) {return Ox.pad(Ox.getDate(d, utc), 2);}],
        ['e', function(d) {return Ox.pad(Ox.getDate(d, utc), 2, ' ');}],
        ['G', function(d) {return Ox.getISOYear(d, utc);}],
        ['g', function(d) {return Ox.getISOYear(d, utc).toString().substr(-2);}],
        ['H', function(d) {return Ox.pad(Ox.getHours(d, utc), 2);}],
        ['I', function(d) {return Ox.pad((Ox.getHours(d, utc) + 11) % 12 + 1, 2);}],
        ['j', function(d) {return Ox.pad(Ox.getDayOfTheYear(d, utc), 3);}],
        ['k', function(d) {return Ox.pad(Ox.getHours(d, utc), 2, ' ');}],
        ['l', function(d) {return Ox.pad(((Ox.getHours(d, utc) + 11) % 12 + 1), 2, ' ');}],
        ['M', function(d) {return Ox.pad(Ox.getMinutes(d, utc), 2);}],
        ['m', function(d) {return Ox.pad((Ox.getMonth(d, utc) + 1), 2);}],
        ['p', function(d) {return Ox.AMPM[Math.floor(Ox.getHours(d, utc) / 12)];}],
        ['Q', function(d) {return Math.floor(Ox.getMonth(d, utc) / 4) + 1;}],
        ['S', function(d) {return Ox.pad(Ox.getSeconds(d, utc), 2);}],
        ['s', function(d) {return Math.floor(d.getTime() / 1000);}],
        ['U', function(d) {return Ox.pad(Ox.getWeek(d, utc), 2);}],
        ['u', function(d) {return Ox.getISODay(d, utc);}],
        ['V', function(d) {return Ox.pad(Ox.getISOWeek(d, utc), 2);}],
        ['W', function(d) {
            return Ox.pad(Math.floor((Ox.getDayOfTheYear(d, utc) +
            (Ox.getFirstDayOfTheYear(d, utc) || 7) - 2) / 7), 2);
        }],
        ['w', function(d) {return Ox.getDay(d, utc);}],
        ['X', function(d) {
            var y = Ox.getFullYear(d, utc);
            return Math.abs(y) + ' ' + Ox.BCAD[y < 0 ? 0 : 1];
        }],
        ['x', function(d) {
            var y = Ox.getFullYear(d, utc);
            return Math.abs(y) + (y < 1000 ? ' ' + Ox.BCAD[y < 0 ? 0 : 1] : '');
        }],
        ['Y', function(d) {return Ox.getFullYear(d, utc);}],
        ['y', function(d) {return Ox.getFullYear(d, utc).toString().substr(-2);}],
        ['Z', function(d) {return d.toString().split('(')[1].replace(')', '');}],
        ['z', function(d) {return Ox.getTimezoneOffsetString(d);}],
        ['n', function() {return '\n';}],
        ['t', function() {return '\t';}],
        ['\\{%\\}', function() {return '%';}]
    ];
    format.forEach(function(v) {
        var regexp = new RegExp('%' + v[0], 'g');
        if (regexp.test(str)) {
            str = str.replace(regexp, v[1](date));
        }
    });
    return str;
};

/*@
Ox.formatDateRange <f> Formats a date range as a string
    A date range is a pair of arbitrary-presicion date strings
    > Ox.formatDateRange('2000', '2001')
    '2000'
    > Ox.formatDateRange('2000', '2002')
    '2000 - 2002'
    > Ox.formatDateRange('2000-01', '2000-02')
    'January 2000'
    > Ox.formatDateRange('2000-01', '2000-03')
    'January - March 2000'
    > Ox.formatDateRange('2000-01-01', '2000-01-02')
    'Sat, Jan 1, 2000'
    > Ox.formatDateRange('2000-01-01', '2000-01-03')
    'Sat, Jan 1 - Mon, Jan 3, 2000'
    > Ox.formatDateRange('2000-01-01 00', '2000-01-01 01')
    'Sat, Jan 1, 2000, 00:00'
    > Ox.formatDateRange('2000-01-01 00', '2000-01-01 02')
    'Sat, Jan 1, 2000, 00:00 - 02:00'
    > Ox.formatDateRange('2000-01-01 00:00', '2000-01-01 00:01')
    'Sat, Jan 1, 2000, 00:00'
    > Ox.formatDateRange('2000-01-01 00:00', '2000-01-01 00:02')
    'Sat, Jan 1, 2000, 00:00 - 00:02'
    > Ox.formatDateRange('2000-01-01 00:00:00', '2000-01-01 00:00:01')
    'Sat, Jan 1, 2000, 00:00:00'
    > Ox.formatDateRange('2000-01-01 00:00:00', '2000-01-01 00:00:02')
    'Sat, Jan 1, 2000, 00:00:00 - 00:00:02'
    > Ox.formatDateRange('1999-12', '2000-01')
    'December 1999'
    > Ox.formatDateRange('1999-12-31', '2000-01-01')
    'Fri, Dec 31, 1999'
    > Ox.formatDateRange('1999-12-31 23:00', '2000-01-01 00:00')
    'Fri, Dec 31, 1999, 23:00'
    > Ox.formatDateRange('-50', '50')
    '50 BC - 50 AD'
    > Ox.formatDateRange('-50-01-01', '-50-12-31')
    'Sun, Jan 1 - Sun, Dec 31, 50 BC'
    > Ox.formatDateRange('-50-01-01 00:00:00', '-50-01-01 23:59:59')
    'Sun, Jan 1, 50 BC, 00:00:00 - 23:59:59'
@*/
Ox.formatDateRange = function(start, end, utc) {
    end = end || Ox.formatDate(new Date(), '%Y-%m-%d');
    var isOneUnit = false,
        range = [start, end],
        strings,
        dates = range.map(function(str){
            return Ox.parseDate(str, utc);
        }),
        parts = range.map(function(str) {
            var parts = Ox.compact(
                /(-?\d+)-?(\d+)?-?(\d+)? ?(\d+)?:?(\d+)?:?(\d+)?/.exec(str)
            );
            parts.shift();
            return parts.map(function(part) {
                return parseInt(part);
            });
        }),
        precision = parts.map(function(parts) {
            return parts.length;
        }),
        y = parts[0][0] < 0 ? '%X' : '%Y',
        formats = [
            y,
            '%B ' + y,
            '%a, %b %e, ' + y,
            '%a, %b %e, ' + y + ', %H:%M',
            '%a, %b %e, ' + y + ', %H:%M',
            '%a, %b %e, ' + y + ', %H:%M:%S', 
        ];
    if (precision[0] == precision[1]) {
        isOneUnit = true;
        Ox.loop(precision[0], function(i) {
            if (i < precision[0] - 1 && parts[0][i] != parts[1][i]) {
                isOneUnit = false;
            }
            if (i == precision[0] - 1 && parts[0][i] != parts[1][i] - 1) {
                isOneUnit = false;
            }
            return isOneUnit;
        });
    }
    if (isOneUnit) {
        strings = [Ox.formatDate(dates[0], formats[precision[0] - 1], utc)];
    } else {
        strings = [
            Ox.formatDate(dates[0], formats[precision[0] - 1], utc),
            Ox.formatDate(dates[1], formats[precision[1] - 1], utc)
        ];
        // if same year, and neither date is more precise than day,
        // then omit first year
        if (
            parts[0][0] == parts[1][0]
            && precision[0] <= 3
            && precision[1] <= 3
        ) {
            strings[0] = Ox.formatDate(
                dates[0], formats[precision[0] - 1].replace(
                    new RegExp(',? ' + y), ''
                ), utc
            );
        }
        // if same day then omit second day
        if (
            parts[0][0] == parts[1][0]
            && parts[0][1] == parts[1][1]
            && parts[0][2] == parts[1][2]
        ) {
            strings[1] = strings[1].split(', ').pop();
        }
    }
    return strings.map(function(string) {
        // %e is a space-padded day
        return string.replace('  ', ' ');
    }).join(' - ');
};

/*@
Ox.formatDateRangeDuration <f> Formats the duration of a date range as a string
    A date range is a pair of arbitrary-presicion date strings
    > Ox.formatDateRangeDuration('2000-01-01 00:00:00', '2001-03-04 04:05:06')
    '1 year 2 months 3 days 4 hours 5 minutes 6 seconds'
    > Ox.formatDateRangeDuration('2000', '2001-01-01 00:00:01')
    '1 year 1 second'
    > Ox.formatDateRangeDuration('1999', '2000', true)
    '1 year'
    > Ox.formatDateRangeDuration('2000', '2001', true)
    '1 year'
    > Ox.formatDateRangeDuration('1999-02', '1999-03', true)
    '1 month'
    > Ox.formatDateRangeDuration('2000-02', '2000-03', true)
    '1 month'
@*/
Ox.formatDateRangeDuration = function(start, end, utc) {
    end = end || Ox.formatDate(new Date(), '%Y-%m-%d');
    var date = Ox.parseDate(start, utc),
        dates = [start, end].map(function(str) {
            return Ox.parseDate(str, utc);
        }),
        keys = ['year', 'month', 'day', 'hour', 'minute', 'second'],
        parts = ['FullYear', 'Month', 'Date', 'Hours', 'Minutes', 'Seconds'],
        values = [];
    Ox.forEach(keys, function(key, i) {
        while (true) {
            if (key == 'month') {
                // set the day to the same day in the next month,
                // or to its last day if the next month is shorter
                var day = Ox.getDate(date, utc);
                Ox.setDate(date, Math.min(
                    day,
                    Ox.getDaysInMonth(
                        Ox.getFullYear(date, utc),
                        Ox.getMonth(date, utc) + 2,
                        utc
                    )
                ), utc);
            }
            // advance the date by one unit
            Ox['set' + parts[i]](date, Ox['get' + parts[i]](date, utc) + 1, utc);
            if (date <= dates[1]) {
                // still within the range, add one unit
                values[i] = (values[i] || 0) + 1;
            } else {
                // outside the range, rewind the date by one unit
                Ox['set' + parts[i]](date, Ox['get' + parts[i]](date, utc) - 1, utc);
                // and revert to original day
                key == 'month' && Ox.setDate(date, day, utc);
                break;
            }
        }
    });
    return Ox.map(values, function(value, i) {
        return value ? value + ' ' + keys[i] + (value > 1 ? 's' : '') : null;
    }).join(' ');
};

/*@
Ox.formatDuration <f> Formats a duration as a string
    > Ox.formatDuration(3599.999)
    '01:00:00'
    > Ox.formatDuration(3599.999, 2)
    '01:00:00.00'
    > Ox.formatDuration(3599.999, 3)
    '00:59:59.999'
    > Ox.formatDuration(3599.999, 'short')
    '1h'
    > Ox.formatDuration(3599.999, 3, 'short')
    '59m 59.999s'
    > Ox.formatDuration(3599.999, 'long')
    '1 hour'
    > Ox.formatDuration(3599.999, 3, 'long')
    '59 minutes 59.999 seconds'
    > Ox.formatDuration(86520, 2)
    '1:00:02:00.00'
    > Ox.formatDuration(86520, 'long')
    '1 day 2 minutes'
    > Ox.formatDuration(31543203, 2)
    '1:000:02:00:03.00'
    > Ox.formatDuration(31543203, 'long')
    '1 year 2 hours 3 seconds'
    > Ox.formatDuration(0, 2)
    '00:00:00.00'
    > Ox.formatDuration(0, 'long')
    ''
@*/
Ox.formatDuration = function(/*sec, dec, format*/) {
    var format = Ox.isString(arguments[arguments.length - 1])
            ? arguments[arguments.length - 1] : '',
        dec = Ox.isNumber(arguments[1]) ? arguments[1] : 0,
        sec = Ox.round(arguments[0], dec),
        val = [
            Math.floor(sec / 31536000),
            Math.floor(sec % 31536000 / 86400),
            Math.floor(sec % 86400 / 3600),
            Math.floor(sec % 3600 / 60),
            Ox.formatNumber(sec % 60, dec)
        ],
        str = !format ? []
            : format == 'short' ? ['y', 'd', 'h', 'm', 's']
            : ['year', 'day', 'hour', 'minute', 'second'],
        pad = [
            val[0].toString().length,
            val[0] ? 3 : 1,
            2,
            2,
            dec ? dec + 3 : 2
        ];
    while (!val[0] && val.length > (!format ? 3 : 1)) {
        val.shift();
        str.shift();
        pad.shift();
    }
    return Ox.map(val, function(v, i) {
        var ret;
        if (!format) {
            ret = Ox.pad(v, pad[i]);
        } else if (Ox.isNumber(v) ? v : parseFloat(v)) {
            ret = v + (format == 'long' ? ' ' : '') + str[i]
                + (format == 'long' && v != 1 ? 's' : '');
        } else {
            ret = null;
        }
        return ret;
    }).join(!format ? ':' : ' ');
};

/*@
Ox.formatNumber <f> Formats a number with thousands separators
    > Ox.formatNumber(123456789, 3)
    "123,456,789.000"
    > Ox.formatNumber(-2000000 / 3, 3)
    "-666,666.667"
    > Ox.formatNumber(666666.666, 0)
    "666,667"
@*/
Ox.formatNumber = function(num, dec) {
    // fixme: specify decimal and thousands separators
    var arr = [],
        abs = Math.abs(num),
        str = Ox.isUndefined(dec) ? abs.toString() : abs.toFixed(dec),
        spl = str.split('.');
    while (spl[0]) {
        arr.unshift(spl[0].substr(-3));
        spl[0] = spl[0].substr(0, spl[0].length - 3);
    }
    spl[0] = arr.join(',');
    return (num < 0 ? '-' : '') + spl.join('.');
};

/*@
Ox.formatOrdinal <f> Formats a number as an ordinal
    > Ox.formatOrdinal(1)
    "1st"
    > Ox.formatOrdinal(2)
    "2nd"
    > Ox.formatOrdinal(3)
    "3rd"
    > Ox.formatOrdinal(4)
    "4th"
    > Ox.formatOrdinal(11)
    "11th"
    > Ox.formatOrdinal(12)
    "12th"
    > Ox.formatOrdinal(13)
    "13th"
@*/
Ox.formatOrdinal = function(num) {
    var str = num.toString(),
        end = str[str.length - 1],
        ten = str.length > 1 && str[str.length - 2] == '1';
    if (end == '1' && !ten) {
        str += 'st';
    } else if (end == '2' && !ten) {
        str += 'nd';
    } else if (end == '3' && !ten) {
        str += 'rd';
    } else {
        str += 'th';
    }
    return str;
};

/*@
Ox.formatPercent <f> Formats the relation of two numbers as a percentage
    > Ox.formatPercent(1, 1000, 2)
    "0.10%"
@*/
Ox.formatPercent = function(num, total, dec) {
    return Ox.formatNumber(num / total * 100, dec) + '%'
};

/*@
Ox.formatResolution <f> Formats two values as a resolution
    > Ox.formatResolution([1920, 1080], 'px')
    "1920 x 1080 px"
@*/
// fixme: should be formatDimensions
Ox.formatResolution = function(arr, str) {
    return arr[0] + ' x ' + arr[1] + (str ? ' ' + str : '');
}

/*@ 
Ox.formatString <f> Basic string formatting
    > Ox.formatString('{0}{1}', ['foo', 'bar'])
    'foobar'
    > Ox.formatString('{a}{b}', {a: 'foo', b: 'bar'})
    'foobar'
@*/

Ox.formatString = function (str, obj) {
    return str.replace(/\{([^}]+)\}/g, function(str, match) {
        return obj[match];
    });
};

/*@
Ox.formatValue <f> Formats a numerical value
    > Ox.formatValue(0, "B")
    "0 KB"
    > Ox.formatValue(123456789, "B")
    "123.5 MB"
    > Ox.formatValue(1234567890, "B", true)
    "1.15 GiB"
@*/
// fixme: is this the best name?
Ox.formatValue = function(num, str, bin) {
    var base = bin ? 1024 : 1000,
        len = Ox.PREFIXES.length,
        val;
    Ox.forEach(Ox.PREFIXES, function(chr, i) {
        if (num < Math.pow(base, i + 2) || i == len - 1) {
            val = Ox.formatNumber(num / Math.pow(base, i + 1), i) +
                ' ' + chr + (bin ? 'i' : '') + str;
            return false;
        }
    });
    return val;
};

/*@
Ox.formatUnit <f> Formats a number with a unit
@*/
Ox.formatUnit = function(num, str, dec, factor) {
    dec = Ox.isUndefined(dec) ? 3 : dec;
    factor = Ox.isUndefined(factor) ? 1 : factor;
    return Ox.formatNumber(num * factor, dec) + (str == '%' ? '' : ' ') + str;
};

/*@
Ox.parseDuration <f> Takes a formatted duration, returns seconds
    > Ox.parseDuration('01:02:03')
    3723
    > Ox.parseDuration('3')
    3
    > Ox.parseDuration('2:')
    120
    > Ox.parseDuration('1::')
    3600
@*/
Ox.parseDuration = function(str) {
    var split = str.split(':').reverse();
    while (split.length > 3) {
        split.pop();
    }
    return split.reduce(function(prev, curr, i) {
        return prev + (parseFloat(curr) || 0) * Math.pow(60, i);
    }, 0);
}