(function () {
    // constants
    var monthsAbbr = [
        'Jan',
        'Feb',
        'Mar',
        'Apr',
        'May',
        'Jun',
        'Jul',
        'Aug',
        'Sep',
        'Oct',
        'Nov',
        'Dec'
    ];

    var monthsFull = [
        'January',
        'February',
        'March',
        'April',
        'May',
        'June',
        'July',
        'August',
        'September',
        'October',
        'November',
        'December'
    ];

    var daysAbbr = [
        'Sun',
        'Mon',
        'Tue',
        'Wed',
        'Thu',
        'Fri',
        'Sat'
    ];

    var daysFull = [
        'Sunday',
        'Monday',
        'Tuesday',
        'Wednesday',
        'Thursday',
        'Friday',
        'Saturday'
    ];

    var dayNames = {
        'su':         0,
        'sun':        0,
        'sunday':     0,
        'mo':         1,
        'mon':        1,
        'monday':     1,
        'tu':         2,
        'tue':        2,
        'tuesday':    2,
        'we':         3,
        'wed':        3,
        'wednesday':  3,
        'th':         4,
        'thu':        4,
        'thursday':   4,
        'fr':         5,
        'fri':        5,
        'friday':     5,
        'sa':         6,
        'sat':        6,
        'saturday':   6
    };
    var monthsAll = monthsFull.concat(monthsAbbr);
    var daysAll = [
        'su',
        'sun',
        'sunday',
        'mo',
        'mon',
        'monday',
        'tu',
        'tue',
        'tuesday',
        'we',
        'wed',
        'wednesday',
        'th',
        'thu',
        'thursday',
        'fr',
        'fri',
        'friday',
        'sa',
        'sat',
        'saturday'
    ];

    var monthNames = {
        'jan':        0,
        'january':    0,
        'feb':        1,
        'february':   1,
        'mar':        2,
        'march':      2,
        'apr':        3,
        'april':      3,
        'may':        4,
        'jun':        5,
        'june':       5,
        'jul':        6,
        'july':       6,
        'aug':        7,
        'august':     7,
        'sep':        8,
        'september':  8,
        'oct':        9,
        'october':    9,
        'nov':        10,
        'november':   10,
        'dec':        11,
        'december':   11
    };

    var daysInMonth = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];


    // private helper functions
    /** @ignore */
    function pad(str, length) {
        str = String(str);
        while (str.length < length) {
            str = '0' + str;
        }
        return str;
    }

    var isInteger = function (str) {
        if (str.match(/^(\d+)$/)) {
            return true;
        }
        return false;
    };
    var getInt = function (str, i, minlength, maxlength) {
        for (var x = maxlength; x >= minlength; x--) {
            var token = str.substring(i, i + x);
            if (token.length < minlength) {
                return null;
            }
            if (isInteger(token)) {
                return token;
            }
        }
        return null;
    };

    // static class methods
    var origParse = Date.parse;
    // ------------------------------------------------------------------
    // getDateFromFormat( date_string , format_string )
    //
    // This function takes a date string and a format string. It matches
    // If the date string matches the format string, it returns the
    // getTime() of the date. If it does not match, it returns NaN.
    // Original Author: Matt Kruse <matt@mattkruse.com>
    // WWW: http://www.mattkruse.com/
    // Adapted from: http://www.mattkruse.com/javascript/date/source.html
    // ------------------------------------------------------------------


    var getDateFromFormat = function (val, format) {
        val = val + "";
        format = format + "";
        var iVal = 0;
        var iFormat = 0;
        var c = "";
        var token = "";
        var token2 = "";
        var x, y;
        var now = new Date();
        var year = now.getYear();
        var month = now.getMonth() + 1;
        var date = 1;
        var hh = 0;
        var mm = 0;
        var ss = 0;
        var ampm = "";



        while (iFormat < format.length) {
            // Get next token from format string
            c = format.charAt(iFormat);
            token = "";
            while ((format.charAt(iFormat) === c) && (iFormat < format.length)) {
                token += format.charAt(iFormat++);
            }
            // Extract contents of value based on format token
            if (token === "yyyy" || token === "yy" || token === "y") {
                if (token === "yyyy") {
                    x = 4;
                    y = 4;
                }
                if (token === "yy") {
                    x = 2;
                    y = 2;
                }
                if (token === "y") {
                    x = 2;
                    y = 4;
                }
                year = getInt(val, iVal, x, y);
                if (year === null) {
                    return NaN;
                }
                iVal += year.length;
                if (year.length === 2) {
                    if (year > 70) {
                        year = 1900 + (year - 0);
                    } else {
                        year = 2000 + (year - 0);
                    }
                }
            } else if (token === "MMM" || token === "NNN") {
                month = 0;
                for (var i = 0; i < monthsAll.length; i++) {
                    var monthName = monthsAll[i];
                    if (val.substring(iVal, iVal + monthName.length).toLowerCase() === monthName.toLowerCase()) {
                        if (token === "MMM" || (token === "NNN" && i > 11)) {
                            month = i + 1;
                            if (month > 12) {
                                month -= 12;
                            }
                            iVal += monthName.length;
                            break;
                        }
                    }
                }
                if ((month < 1) || (month > 12)) {
                    return NaN;
                }
            } else if (token === "EE" || token === "E") {
                for (var n = 0; n < daysAll.length; n++) {
                    var dayName = daysAll[n];
                    if (val.substring(iVal, iVal + dayName.length).toLowerCase() === dayName.toLowerCase()) {
                        iVal += dayName.length;
                        break;
                    }
                }
            } else if (token === "MM" || token === "M") {
                month = getInt(val, iVal, token.length, 2);
                if (month === null || (month < 1) || (month > 12)) {
                    return NaN;
                }
                iVal += month.length;
            } else if (token === "dd" || token === "d") {
                date = getInt(val, iVal, token.length, 2);
                if (date === null || (date < 1) || (date > 31)) {
                    return NaN;
                }
                iVal += date.length;
            } else if (token === "hh" || token === "h") {
                hh = getInt(val, iVal, token.length, 2);
                if (hh === null || (hh < 1) || (hh > 12)) {
                    return NaN;
                }
                iVal += hh.length;
            } else if (token === "HH" || token === "H") {
                hh = getInt(val, iVal, token.length, 2);
                if (hh === null || (hh < 0) || (hh > 23)) {
                    return NaN;
                }
                iVal += hh.length;
            } else if (token === "KK" || token === "K") {
                hh = getInt(val, iVal, token.length, 2);
                if (hh === null || (hh < 0) || (hh > 11)) {
                    return NaN;
                }
                iVal += hh.length;
            } else if (token === "kk" || token === "k") {
                hh = getInt(val, iVal, token.length, 2);
                if (hh === null || (hh < 1) || (hh > 24)) {
                    return NaN;
                }
                iVal += hh.length;
                hh--;
            } else if (token === "mm" || token === "m") {
                mm = getInt(val, iVal, token.length, 2);
                if (mm === null || (mm < 0) || (mm > 59)) {
                    return NaN;
                }
                iVal += mm.length;
            } else if (token === "ss" || token === "s") {
                ss = getInt(val, iVal, token.length, 2);
                if (ss === null || (ss < 0) || (ss > 59)) {
                    return NaN;
                }
                iVal += ss.length;
            } else if (token === "a") {
                if (val.substring(iVal, iVal + 2).toLowerCase() === "am") {
                    ampm = "AM";
                } else if (val.substring(iVal, iVal + 2).toLowerCase() === "pm") {
                    ampm = "PM";
                } else {
                    return NaN;
                }
                iVal += 2;
            } else {
                if (val.substring(iVal, iVal + token.length) !== token) {
                    return NaN;
                } else {
                    iVal += token.length;
                }
            }
        }
        // If there are any trailing characters left in the value, it doesn't match
        if (iVal !== val.length) {
            return NaN;
        }
        // Is date valid for month?
        if (month === 2) {
            // Check for leap year
            if (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0)) { // leap year
                if (date > 29) {
                    return NaN;
                }
            } else {
                if (date > 28) {
                    return NaN;
                }
            }
        }
        if ((month === 4) || (month === 6) || (month === 9) || (month === 11)) {
            if (date > 30) {
                return NaN;
            }
        }
        // Correct hours value
        if (hh < 12 && ampm === "PM") {
            hh = hh - 0 + 12;
        } else if (hh > 11 && ampm === "AM") {
            hh -= 12;
        }
        var newdate = new Date(year, month - 1, date, hh, mm, ss);
        return newdate.getTime();
    };


    /** @ignore */
    Date.parse = function (date, format) {
        if (format) {
            return getDateFromFormat(date, format);
        }
        var timestamp = origParse(date), minutesOffset = 0, match;
        if (isNaN(timestamp) && (match = /^(\d{4}|[+\-]\d{6})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3,}))?)?(?:(Z)|([+\-])(\d{2})(?::?(\d{2}))?))?/.exec(date))) {
            if (match[8] !== 'Z') {
                minutesOffset = +match[10] * 60 + (+match[11]);

                if (match[9] === '+') {
                    minutesOffset = 0 - minutesOffset;
                }
            }

            match[7] = match[7] || '000';

            timestamp = Date.UTC(+match[1], +match[2] - 1, +match[3], +match[4], +match[5] + minutesOffset, +match[6], +match[7].substr(0, 3));
        }

        return timestamp;
    };

    function polyfill(name, func) {
        if (Date.prototype[name] === undefined) {
            Date.prototype[name] = func;
        }
    }

    /**
        Returns new instance of Date object with the date set to today and
        the time set to midnight
        @returns {Date} Today's Date
        @function
     */
    Date.today = function () {
        return new Date().clearTime();
    };

    /**
        Returns new instance of Date object with the date set to today and
        the time set to midnight in UTC
        @returns {Date} Today's Date in UTC
        @function
     */
    Date.UTCtoday = function () {
        return new Date().clearUTCTime();
    };

    /**
        Returns new instance of Date object with the date set to tomorrow and
        the time set to midnight
        @returns {Date} Tomorrow's Date
        @function
     */
    Date.tomorrow = function () {
        return Date.today().add({days: 1});
    };

    /**
        Returns new instance of Date object with the date set to tomorrow and
        the time set to midnight in UTC
        @returns {Date} Tomorrow's Date in UTC
        @function
     */
    Date.UTCtomorrow = function () {
        return Date.UTCtoday().add({days: 1});
    };

    /**
        Returns new instance of Date object with the date set to yesterday and
        the time set to midnight
        @returns {Date} Yesterday's Date
        @function
     */
    Date.yesterday = function () {
        return Date.today().add({days: -1});
    };

    /**
        Returns new instance of Date object with the date set to yesterday and
        the time set to midnight in UTC
        @returns {Date} Yesterday's Date in UTC
        @function
     */
    Date.UTCyesterday = function () {
        return Date.UTCtoday().add({days: -1});
    };

    Date.validateDay = function (day, year, month) {
        var date = new Date(year, month, day);
        return (date.getFullYear() === year &&
            date.getMonth() === month &&
            date.getDate() === day);
    };

    Date.validateYear = function (year) {
        return (year >= 0 && year <= 9999);
    };

    Date.validateSecond = function (second) {
        return (second >= 0 && second < 60);
    };

    Date.validateMonth = function (month) {
        return (month >= 0 && month < 12);
    };

    Date.validateMinute = function (minute) {
        return (minute >= 0 && minute < 60);
    };

    Date.validateMillisecond = function (milli) {
        return (milli >= 0 && milli < 1000);
    };

    Date.validateHour = function (hour) {
        return (hour >= 0 && hour < 24);
    };

    Date.compare = function (date1, date2) {
        if (date1.valueOf() < date2.valueOf()) {
            return -1;
        } else if (date1.valueOf() > date2.valueOf()) {
            return 1;
        }
        return 0;
    };

    Date.equals = function (date1, date2) {
        return date1.valueOf() === date2.valueOf();
    };


    Date.getDayNumberFromName = function (name) {
        return dayNames[name.toLowerCase()];
    };


    Date.getMonthNumberFromName = function (name) {
        return monthNames[name.toLowerCase()];
    };

    Date.isLeapYear = function (year) {
        return (new Date(year, 1, 29).getDate() === 29);
    };

    Date.getDaysInMonth = function (year, month) {
        if (month === 1) {
            return Date.isLeapYear(year) ? 29 : 28;
        }
        return daysInMonth[month];
    };

    polyfill('getMonthAbbr', function () {
        return monthsAbbr[this.getMonth()];
    });

    polyfill('getMonthName', function () {
        return monthsFull[this.getMonth()];
    });

    polyfill('getUTCOffset', function () {
        var tz = pad(Math.abs(this.getTimezoneOffset() / 0.6), 4);
        if (this.getTimezoneOffset() > 0) {
            tz = '-' + tz;
        }
        return tz;
    });

    polyfill('toCLFString',  function () {
        return pad(this.getDate(), 2) + '/' + this.getMonthAbbr() + '/' +
               this.getFullYear() + ':' + pad(this.getHours(), 2) + ':' +
               pad(this.getMinutes(), 2) + ':' + pad(this.getSeconds(), 2) +
               ' ' + this.getUTCOffset();
    });

    polyfill('toYMD', function (separator) {
        separator = typeof separator === 'undefined' ? '-' : separator;
        return this.getFullYear() + separator + pad(this.getMonth() + 1, 2) +
            separator + pad(this.getDate(), 2);
    });

    polyfill('toDBString', function () {
        return this.getUTCFullYear() + '-' +  pad(this.getUTCMonth() + 1, 2) +
               '-' + pad(this.getUTCDate(), 2) + ' ' + pad(this.getUTCHours(), 2) +
               ':' + pad(this.getUTCMinutes(), 2) + ':' + pad(this.getUTCSeconds(), 2);
    });

    polyfill('clearTime', function () {
        this.setHours(0);
        this.setMinutes(0);
        this.setSeconds(0);
        this.setMilliseconds(0);

        return this;
    });

    polyfill('clearUTCTime', function () {
        this.setUTCHours(0);
        this.setUTCMinutes(0);
        this.setUTCSeconds(0);
        this.setUTCMilliseconds(0);

        return this;
    });

    polyfill('add', function (obj) {
        if (obj.milliseconds !== undefined) {
            this.setMilliseconds(this.getMilliseconds() + obj.milliseconds);
        }
        if (obj.seconds !== undefined) {
            this.setSeconds(this.getSeconds() + obj.seconds);
        }
        if (obj.minutes !== undefined) {
            this.setMinutes(this.getMinutes() + obj.minutes);
        }
        if (obj.hours !== undefined) {
            this.setHours(this.getHours() + obj.hours);
        }
        if (obj.days !== undefined) {
            this.setDate(this.getDate() + obj.days);
        }
        if (obj.weeks !== undefined) {
            this.setDate(this.getDate() + (obj.weeks * 7));
        }
        if (obj.months !== undefined) {
            this.setMonth(this.getMonth() + obj.months);
        }
        if (obj.years !== undefined) {
            this.setFullYear(this.getFullYear() + obj.years);
        }
        return this;
    });

    polyfill('addMilliseconds', function (milliseconds) {
        return this.add({ milliseconds: milliseconds });
    });

    polyfill('addSeconds', function (seconds) {
        return this.add({ seconds: seconds });
    });

    polyfill('addMinutes', function (minutes) {
        return this.add({ minutes: minutes });
    });

    polyfill('addHours', function (hours) {
        return this.add({ hours: hours });
    });

    polyfill('addDays', function (days) {
        return this.add({ days: days });
    });

    polyfill('addWeeks', function (weeks) {
        return this.add({ days: (weeks * 7) });
    });

    polyfill('addMonths', function (months) {
        return this.add({ months: months });
    });

    polyfill('addYears', function (years) {
        return this.add({ years: years });
    });

    polyfill('setTimeToNow', function () {
        var n = new Date();
        this.setMilliseconds(n.getMilliseconds());
        this.setSeconds(n.getSeconds());
        this.setMinutes(n.getMinutes());
        this.setHours(n.getHours());
    });

    polyfill('clone', function () {
        return new Date(this.valueOf());
    });

    polyfill('between', function (start, end) {
        return (this.valueOf() >= start.valueOf() &&
                this.valueOf() <= end.valueOf());
    });

    polyfill('compareTo', function (date) {
        return Date.compare(this, date);
    });

    polyfill('equals', function (date) {
        return Date.equals(this, date);
    });

    polyfill('isAfter', function (date) {
        date = date ? date : new Date();
        return (this.compareTo(date) > 0);
    });

    polyfill('isBefore', function (date) {
        date = date ? date : new Date();
        return (this.compareTo(date) < 0);
    });

    polyfill('getDaysBetween', function (date) {
        return ((date.clone().valueOf() - this.valueOf()) / 86400000) | 0;
    });

    polyfill('getHoursBetween', function (date) {
        return ((date.clone().valueOf() - this.valueOf()) / 3600000) | 0;
    });

    polyfill('getMinutesBetween', function (date) {
        return ((date.clone().valueOf() - this.valueOf()) / 60000) | 0;
    });

    polyfill('getSecondsBetween', function (date) {
        return ((date.clone().valueOf() - this.valueOf()) / 1000) | 0;
    });

    polyfill('getMillisecondsBetween', function (date) {
        return ((date.clone().valueOf() - this.valueOf())) | 0;
    });

    polyfill('getOrdinalNumber', function () {
        return Math.ceil((this.clone().clearTime() - new Date(this.getFullYear(), 0, 1)) / 86400000) + 1;
    });

    polyfill('toFormat', function (format) {
        return toFormat(format, getReplaceMap(this));
    });

    polyfill('toUTCFormat', function (format) {
        return toFormat(format, getUTCReplaceMap(this));
    });

    var toFormat = function(format, replaceMap) {
        var f = [ format ], i, l, s;
        var replace = function (str, rep) {
            var i = 0, l = f.length, j, ll, t, n = [];
            for (; i < l; i++) {
                if (typeof f[i] == 'string') {
                    t = f[i].split(str);
                    for (j = 0, ll = t.length - 1; j < ll; j++) {
                        n.push(t[j]);
                        n.push([rep]); // replacement pushed as non-string
                    }
                    n.push(t[ll]);
                } else {
                    // must be a replacement, don't process, just push
                    n.push(f[i]);
                }
            }
            f = n;
        };

        for (i in replaceMap) {
            replace(i, replaceMap[i]);
        }

        s = '';
        for (i = 0, l = f.length; i < l; i++)
          s += typeof f[i] == 'string' ? f[i] : f[i][0];
        return f.join('');
    };

    var getReplaceMap = function(date) {
        var hours = (date.getHours() % 12) ? date.getHours() % 12 : 12;
        return {
            'YYYY': date.getFullYear(),
            'YY': String(date.getFullYear()).slice(-2),
            'MMMM': monthsFull[date.getMonth()],
            'MMM': monthsAbbr[date.getMonth()],
            'MM': pad(date.getMonth() + 1, 2),
            'MI': pad(date.getMinutes(), 2),
            'M': date.getMonth() + 1,
            'DDDD': daysFull[date.getDay()],
            'DDD': daysAbbr[date.getDay()],
            'DD': pad(date.getDate(), 2),
            'D': date.getDate(),
            'HH24': pad(date.getHours(), 2),
            'HH': pad(hours, 2),
            'H': hours,
            'SS': pad(date.getSeconds(), 2),
            'PP': (date.getHours() >= 12) ? 'PM' : 'AM',
            'P': (date.getHours() >= 12) ? 'pm' : 'am',
            'LL': pad(date.getMilliseconds(), 3)
        };
    };

    var getUTCReplaceMap = function(date) {
        var hours = (date.getUTCHours() % 12) ? date.getUTCHours() % 12 : 12;
        return {
            'YYYY': date.getUTCFullYear(),
            'YY': String(date.getUTCFullYear()).slice(-2),
            'MMMM': monthsFull[date.getUTCMonth()],
            'MMM': monthsAbbr[date.getUTCMonth()],
            'MM': pad(date.getUTCMonth() + 1, 2),
            'MI': pad(date.getUTCMinutes(), 2),
            'M': date.getUTCMonth() + 1,
            'DDDD': daysFull[date.getUTCDay()],
            'DDD': daysAbbr[date.getUTCDay()],
            'DD': pad(date.getUTCDate(), 2),
            'D': date.getUTCDate(),
            'HH24': pad(date.getUTCHours(), 2),
            'HH': pad(hours, 2),
            'H': hours,
            'SS': pad(date.getUTCSeconds(), 2),
            'PP': (date.getUTCHours() >= 12) ? 'PM' : 'AM',
            'P': (date.getUTCHours() >= 12) ? 'pm' : 'am',
            'LL': pad(date.getUTCMilliseconds(), 3)
        };
    };
}());