/**
 * Date Class - Extends the Date native to include more powerful parsing and formatting functions.
 *
 * Advanced by
 * Aaron Newton - aaron [dot] newton [at] cnet [dot] com
 *
 * Originally by Nicholas Barthelemy for Jester
 * https://svn.nbarthelemy.com/date-js/
 *
 * @version     beta
 *
 * @license     MIT-style license
 * @author      Harald Kirschner <mail [at] digitarald.de>
 * @copyright   Author
 */
 
new Native({name: 'Date', initialize: Date, protect: true});
 
['now', 'parse', 'UTC'].each(function(method) {
    Native.genericize(Date, method, true);
});
 
Date.$properties = {};
 
['Date', 'Day', 'FullYear', 'Hours', 'Milliseconds', 'Minutes', 'Month', 'Seconds', 'Time', 'TimezoneOffset', 
'Week', 'Timezone', 'GMTOffset', 'DayOfYear', 'LastMonth', 'UTCDate', 'UTCDay', 'UTCFullYear',
'AMPM', 'UTCHours', 'UTCMilliseconds', 'UTCMinutes', 'UTCMonth', 'UTCSeconds'].each(function(method) {
    Date.$properties[method.toLowerCase()] = method;
});
 
$extend(Date.$properties, {
    ms: 'Milliseconds',
    year: 'FullYear',
    min: 'Minutes',
    mo: 'Month',
    sec: 'Seconds',
    hr: 'Hours'
});
 
Date.implement({
    set: function(key, value) {
        key = key.toLowerCase();
        var props = Date.$properties;
        this['set' + props[key]](value);
        return this;
    },
    get: function(key) {
        key = key.toLowerCase();
        var props = Date.$properties;
        return this['get' + props[key]]();
    },
 
    clone: function() {
        return new Date(this.getTime());
    },
 
    increment: function(interval, times) {
        return this.multiply(interval, times);
    },
 
    decrement: function(interval, times) {
        return this.multiply(interval, times, false);
    },
 
    multiply: function(interval, times, increment){
        interval = interval || 'day';
        times = $pick(times, 1);
        var multiplier = (increment !== false) ? 1 : -1;
 
        var month = this.getMonth() - 1;
        var year = this.getFullYear();
        var offset = 0;
        switch (interval) {
                case 'year':
                    times.times(function(val) {
                        if (Date.isLeapYear(year + val) && month > 1 && multiplier > 0) val++;
                        if (Date.isLeapYear(year + val) && month <= 1 && multiplier < 0) val--;
                        offset += Date.$intervals.year(year + val);
                    });
                    break;
                case 'month':
                    times.times(function(val){
                        month += val * multiplier;
                        if (month < 0) {
                            year--;
                            month = 12 + month;
                        }
                        if (month > 11 || month < 0) {
                            year += (month / 12).toInt() * multiplier;
                            month = month % 12;
                        }
                        alert(1);
                        offset += Date.$intervals.month(month, year);
                    });
                    break
                default:
                    offset = Date.$intervals[interval]() * times;
        }
        this.setTime(this.getTime() + (offset * multiplier));
        return this;
    },
 
    clearTime: function() {
        this.setHours(0);
        this.setMinutes(0);
        this.setSeconds(0);
        this.setMilliseconds(0);
        return this;
    },
 
    diff: function(date, resolution) {
        resolution = resolution || 'day';
        if ($type(date) != 'date') date = Date.parse(date);
        switch (resolution) {
            case 'year':
                return date.getFullYear() - this.getFullYear();
            case 'month':
                var months = (date.getFullYear() - this.getFullYear()) * 12;
                return months + date.getMonth() - this.getMonth();
            default:
                var diff = date.getTime() - this.getTime();
                if (diff < 0 && Date.$intervals[resolution]() > (-1 * diff)) return 0;
                else if (diff >= 0 && diff < Date.$intervals[resolution]()) return 0;
                return ((date.getTime() - this.getTime()) / Date.$intervals[resolution]()).round();
        }
    },
 
    isLeapYear: function() {
        return Date.isLeapYear(this.getFullYear());
    },
 
    parse: function(str) {
        this.setTime(Date.parse(str));
        return this;
    },
 
/*  
    Property: format
    Outputs the date into a specific format.

    Arguments:
    f - (string) a string format for the output. Use the keys below with percent signs to get a desired output. See example below. Defaults to "%x %X", which renders "12/31/2007 03:45PM"

    Keys:
    a - short day ("Mon", "Tue")
    A - full day ("Monday")
    b - short month ("Jan", "Feb")
    B - full month ("Janurary")
    c - the full date to string ("Mon Dec 10 2007 14:35:42 GMT-0800 (Pacific Standard Time)"; same as .toString() method.
    d - the date to two digits (01, 05, etc)
    H - the hour to two digits in military time (24 hr mode) (01, 11, 14, etc)
    I - the hour in 12 hour time (01, 11, 2, etc)
    j - the day of the year to three digits (001 is Jan 1st)
    m - the numerical month to two digits (01 is Jan, 12 is Dec)
    M - the minuts to two digits (01, 40, 59)
    p - 'AM' or 'PM'
    S - the seconds to two digits (01, 40, 59)
    U - the week to two digits (01 is the week of Jan 1, 52 is the week of Dec 31)
    W - not yet supported
    w - the numerical day of the week, one digit (0 is Sunday, 1 is Monday)
    x - returns the format %m/%d/%Y (12/10/2007)
    X - returns %I:%M%p (02:45PM)
    y - the short year (to digits; "07")
    Y - the four digit year
    T - the GMT offset ("-0800")
    Z - the time zone ("GMT")
    % - returns % (example: %y%% = 07%)

    Shortcuts:
    These keys are NOT preceded by the percent sign.

    db - "%Y-%m-%d %H:%M:%S",
    compact - "%Y%m%dT%H%M%S",
    iso8601 - "%Y-%m-%dT%H:%M:%S%T",
    rfc822 - "%a, %d %b %Y %H:%M:%S %Z",
    short - "%d %b %H:%M",
    long - "%B %d, %Y %H:%M"

    Example:
    >new Date().format("db"); //"2007-12-10 15:01:53"
*/
    format: function(format) {
        if (!this.valueOf()) return 'invalid date';
        format = (format) ? Date.formats.get(format.toLowerCase()) || format : '%x %X';
        var d = this;
        return format.replace(/\%([aAbBcdHIjmMpSUWwxXyYTZ])/g,
            function($1, $2) {
                switch ($2) {
                    case 'a': return Date.dayNames[d.getDay()].substr(0, 3);
                    case 'A': return Date.dayNames[d.getDay()];
                    case 'b': return Date.monthNames[d.getMonth()].substr(0, 3);
                    case 'B': return Date.monthNames[d.getMonth()];
                    case 'c': return d.toString();
                    case 'd': return d.getDate().zeroise(2);
                    case 'H': return d.getHours().zeroise(2);
                    case 'I': return ((d.getHours() % 12) || 12).zeroise(2);
                    case 'j': return d.getDayOfYear().zeroise(3);
                    case 'm': return (d.getMonth() + 1).zeroise(2);
                    case 'M': return d.getMinutes().zeroise(2);
                    case 'p': return d.getHours() < 12 ? 'AM' : 'PM';
                    case 'S': return d.getSeconds().zeroise(2);
                    case 'U': return d.getWeekOfYear().zeroise(2);
                    case 'w': return d.getDay();
                    case 'x': return d.format('%m/%d/%Y');
                    case 'X': return d.format('%I:%M%p');
                    case 'y': return d.getFullYear().toString().substr(2);
                    case 'Y': return d.getFullYear();
                    case 'T': return d.getGMTOffset();
                    case 'Z': return d.getTimezone();
                    // case 'W': throw new Error('%W is not supported yet');
                }
                return $2;
            }
        );
    },
 
    getFirstDayOfMonth: function() {
        var day = (this.getDay() - (this.getDate() - 1)) % 7;
        return (day < 0) ? (day + 7) : day;
    },
 
    getLastDayOfMonth: function() {
        var ret = this.clone();
        ret.setMonth(ret.getMonth() + 1, 0);
        return ret.getDate();
    },
 
    getTimezone: function() {
        return this.toString()
            .replace(/^.*? ([A-Z]{3}).[0-9]{4}.*$/, '$1')
            .replace(/^.*?\(([A-Z])[a-z]+ ([A-Z])[a-z]+ ([A-Z])[a-z]+\)$/, '$1$2$3');
    },
 
    getGMTOffset: function() {
        var off = this.getTimezoneOffset();
        return ((off > 0) ? '-' : '+')
            + Math.floor(Math.abs(off) / 60).zeroise(2)
            + (off % 60).zeroise(2);
    },
 
    timeAgoInWords: function(){
        var params = Array.link(arguments, {time: Boolean.type, to: $defined});
        var to = Date.parse(params.to);
        var delta = parseInt((to.getTime() - this.getTime()) / 1000 / 60);
        var words = Date.agoWords;
        if (delta < 1) return words.get('sec');
        if (delta < 2) return words.get('min');
        if (delta < (45)) return ;
        if (delta < (90)) return words.get('hour');
        if (delta < (24 * 60)) return words.get('hours').substitute({delta: (delta / 3600).toInt()});
        if (delta < (48 * 60)) return words.get('day');
        delta = (delta / 86400).toInt();
        if (delta <= 30) return words.get('hours').substitute({delta: delta});
        var format = words.get((to.getYear() != this.getYear()) ? 'year' : 'years');
        if (params.time) format += words.get('time');
        return this.format(format);
    },
 
    getOrdinal: function() {
        return {'1': 'st', '2': 'nd', '3': 'rd'}[String(this.getDate()).slice(-1)] || 'th';
    },
 
    getDayOfYear: function() {
        return (Date.UTC(this.getFullYear(), this.getMonth(), this.getDate() + 1, 0, 0, 0)
            - Date.UTC(this.getFullYear(), 0, 1, 0, 0, 0)) / Date.$intervals.day();
    },
 
    getWeekOfYear: function() {
        var day = (new Date(this.getFullYear(), 0, 1)).getDate();
        return Math.round((this.getDayOfYear() + (day > 3) ? day - 4 : day + 3) / 7);
    },
 
    setAMPM: function(interval){
        var am = (/a.?m.?/i).test(interval);
        var hrs = this.getHours();
        if (am && hrs > 11) return this.decrement('hour', 12);
        return (!am && hrs < 12) ? this.increment('hour', 12) : this;
    }
 
});
 
Date.prototype.compare = Date.prototype.diff;
Date.prototype.strftime = Date.prototype.format;
 
Date.$nativeParse = Date.parse;
 
$extend(Date, {
 
    monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
 
    dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
 
    agoWords: {
        'sec': 'less than a minute ago',
        'min': 'about a minute ago',
        'mins': '{delta} minutes ago',
        'hour': 'about an hour ago',
        'hours': 'about {delta} hours ago',
        'day': '1 day ago',
        'days': '{delta} days ago',
        'year': '%B %d',
        'years': '%B %d, %Y',
        'time': '%I:%M %p'
    },
 
    isLeapYear: function(year) {
        return (!(year % 4) == 0) && ((year % 1000) != 0) || ((year % 4000) == 0);
    },
 
    $intervals: new Hash({
        ms: 1,
        second: 1000,
        minute: 60000,
        hour: 3600000,
        day: 86400000,
        week: 608400000,
        month: function(month, year) {
            if (Date.isLeapYear(year) && month == 1) return 29;
            return [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
        },
        year: function(year){
            year = year || new Date().getFullYear();
            return Date.isLeapYear(year.toInt())?31622400000:31536000000;
        }
    }).map($lambda),
 
    formats: new Hash({
        'db': '%Y-%m-%d %H:%M:%S',
        'iso8601': '%Y-%m-%dT%H:%M:%S%T',
        'rfc822': '%a, %d %b %Y %H:%M:%S %Z',
        'short': '%d %b %H:%M',
        'long': '%B %d, %Y %H:%M'
    }),
 
    parseUTC: function(value) {
      var loc = new Date(value);
      var utc = Date.UTC(loc.getFullYear(), loc.getMonth(), loc.getDate(), loc.getHours(), loc.getMinutes(), loc.getSeconds());
      return new Date(utc);
    },
 
 
    parse: function(from) {
        var type = $type(from);
        if (type == 'number') return new Date(from);
        if (type != 'string') return from;
        if (!from.length) return null;
        for (var i = 0, j = Date.parsePattern.length; i < j; i++) {
            var pattern = Date.parsePattern[i];
            var bits = from.match(pattern.re);
            if (bits) return pattern.handler(bits);
        }
        return new Date(Date.$nativeParse(from));
    },
 
    parseMonth: function(month, num) {
        var ret = -1;
        switch ($type(month)) {
            case 'object':
                ret = Date.monthNames[month.getMonth()];
                break;
            case 'number':
                ret = Date.monthNames[month - 1] || false;
                if (!ret) throw new Error('Invalid month index value must be between 1 and 12: ' + month);
                break;
            case 'string':
                var match = Date.monthNames.filter(function(name) {
                    return this.test(name);
                }, new RegExp('^' + month, 'i'));
                if (!match.length) throw new Error('Invalid month string');
                if (match.length > 1) throw new Error('Ambiguous month');
                ret = match[0];
        }
        return (num) ? Date.monthNames.indexOf(ret) : ret;
    },
 
    parseDay: function(day, num) {
        var ret = -1;
        switch ($type(day)) {
            case 'number':
                ret = Date.dayNames[day - 1] || false;
                if (!ret) throw new Error('Invalid day index value must be between 1 and 7');
                break;
            case 'string':
                var match = Date.dayNames.filter(function(name) {
                    return this.test(name);
                }, new RegExp('^' + day, 'i'));
                if (!match.length) throw new Error('Invalid day string');
                if (match.length > 1) throw new Error('Ambiguous day');
                ret = match[0];
        }
        return (num) ? Date.dayNames.indexOf(ret) : ret;
    },
 
    fixY2K: function(date) {
        if (isNaN(date)) return date;
        var newDate = new Date(date);
        return (newDate.getFullYear() < 2000 && !String(date).contains(newDate.getFullYear()))
            ? newDate.increment('y', 100)
            : newDate;
    },
 
    parsePattern: [
        {
            //"12.31.08", "12-31-08", "12/31/08", "12.31.2008", "12-31-2008", "12/31/2008"
            re: /^(\d{1,2})[\.\-\/](\d{1,2})[\.\-\/](\d{2,4})$/,
            handler: function(bits){
                var d = new Date();
                d.setYear(bits[3]);
                d.setMonth(bits[1].toInt() - 1, bits[2].toInt());
                return Date.fixY2K(d);
            }
        },
        {
            //"12.31.08", "12-31-08", "12/31/08", "12.31.2008", "12-31-2008", "12/31/2008"
            //above plus "10:45pm" ex: 12.31.08 10:45pm
            re: /^(\d{1,2})[\.\-\/](\d{1,2})[\.\-\/](\d{2,4})\s(\d{1,2}):(\d{1,2})(\w{2})$/,
            handler: function(bits){
                var d = new Date();
                d.setYear(bits[3]);
                d.setMonth(bits[1] - 1);
                d.setDate(bits[2]);
                d.setHours(bits[4]);
                d.setMinutes(bits[5]);
                d.setAMPM(bits[6]);
                return Date.fixY2K(d);
            }
        },
        {
        re: /^(\d{4})(?:-?(\d{2})(?:-?(\d{2})(?:[T ](\d{2})(?::?(\d{2})(?::?(\d{2})(?:\.(\d+))?)?)?(?:Z|(?:([-+])(\d{2})(?::?(\d{2}))?)?)?)?)?)?$/,
        handler: function(bits) {
            var offset = 0;
            var d = new Date(bits[1], 0, 1);
            if (bits[2]) d.setMonth(bits[2] - 1);
            if (bits[3]) d.setDate(bits[3]);
            if (bits[4]) d.setHours(bits[4]);
            if (bits[5]) d.setMinutes(bits[5]);
            if (bits[6]) d.setSeconds(bits[6]);
            if (bits[7]) d.setMilliseconds(('0.' + bits[7]).toInt() * 1000);
            if (bits[9]) {
                offset = (bits[9].toInt() * 60) + bits[10].toInt();
                offset *= ((bits[8] == '-') ? 1 : -1);
            }
            offset -= d.getTimezoneOffset();
            d.setTime((d * 1) + (offset * 60 * 1000).toInt())
            return d;
        }
        }, {
            re: /^tod/i,
            handler: function() {
                return new Date();
            }
        }, {
            re: /^tom/i,
            handler: function() {
                return new Date().increment();
            }
        }, {
            re: /^yes/i,
            handler: function() {
                return new Date().decrement();
            }
        }, {
            re: /^(\d{1,2})(st|nd|rd|th)?$/i,
            handler: function(bits) {
                var d = new Date();
                d.setDate(bits[1].toInt());
                return d;
            }
        }, {
            re: /^(\d{1,2})(?:st|nd|rd|th)? (\w+)$/i,
            handler: function(bits) {
                var d = new Date();
                d.setMonth(Date.parseMonth(bits[2], true), bits[1].toInt());
                return d;
            }
        }, {
            re: /^(\d{1,2})(?:st|nd|rd|th)? (\w+),? (\d{4})$/i,
            handler: function(bits) {
                var d = new Date();
                d.setMonth(Date.parseMonth(bits[2], true), bits[1].toInt());
                d.setYear(bits[3]);
                return d;
            }
        }, {
            re: /^(\w+) (\d{1,2})(?:st|nd|rd|th)?,? (\d{4})$/i,
            handler: function(bits) {
                var d = new Date();
                d.setMonth(Date.parseMonth(bits[1], true), bits[2].toInt());
                d.setYear(bits[3]);
                return d;
            }
        }, {
            re: /^next (\w+)$/i,
            handler: function(bits) {
                var d = new Date();
                var day = d.getDay();
                var newDay = Date.parseDay(bits[1], true);
                var diff = newDay - day;
                if (newDay <= day) diff += 7;
                d.setDate(d.getDate() + diff);
                return d;
            }
        }, {
            re: /^last (\w+)$/i,
            handler: function(bits) {
                var d = new Date();
                var day = d.getDay();
                var newDay = Date.parseDay(bits[1], true);
                var diff = newDay - day;
                if (newDay >= day) diff -= 7;
                d.setDate(d.getDate() - diff);
                return d;
            }
        }
    ]
});
 
Number.implement({
 
    zeroise: function(length) {
        return String(this).zeroise(length);
    }
 
});
 
String.implement({
 
    repeat: function(times) {
        var ret = [];
        for (var i = 0; i < times; i++) ret.push(this);
        return ret.join('');
    },
 
    zeroise: function(length) {
        return '0'.repeat(length - this.length) + this;
    }
 
});