/*!
 *  Date formatting plugin
 *
 *  Copyright (c) Eric Garside
 *  Dual licensed under:
 *      MIT: http://www.opensource.org/licenses/mit-license.php
 *      GPLv3: http://www.opensource.org/licenses/gpl-3.0.html
 */

"use strict";

/*global jQuery */

/*jslint white: true, browser: true, onevar: true, undef: true, eqeqeq: true, bitwise: true, regexp: true, strict: true, newcap: true, immed: true, maxerr: 50, indent: 4 */

(function ($) {

    //------------------------------
    //
    //  Property Declaration
    //
    //------------------------------

        /**
         *  String formatting for each month, with January at index "0" and December at "11".
         */
    var months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
    
        /**
         *  String formatting for each day, with Sunday at index "0" and Saturday at index "6"
         */
        days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
        
        /**
         *	The number of days in each month.
         */
        counts = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
        
        /**
         *  English ordinal suffix for corresponding days of the week.
         */
        suffix = [null, 'st', 'nd', 'rd'],
        
        /**
         *  Define the object which will hold reference to the actual formatting functions. By not directly prototyping these
         *  into the date function, we vastly reduce the amount of bloat adding these options causes.
         */
        _;
        
    //------------------------------
    //
    //  Internal Methods
    //
    //------------------------------
    
    /**
     *	Left-pad the string with the provided string up to the provided length.
     *
     *  @param format   The string to pad.
     *
     *  @param string   The string to pad with.
     *
     *  @param length   The length to make the string (default is "0" if undefined).
     *
     *  @return The padded string.
     */
    function pad(format, string, length)
    {
        format = format + '';
        length = length || 2;
    
        return format.length < length ? new Array(1 + length - format.length).join(string) + format : format;
    }
    
    /**
     *	Right-pad the string with the provided string up to the provided length.
     *
     *  @param format   The string to pad.
     *
     *  @param string   The string to pad with.
     *
     *  @param length   The length to make the string (default is "0" if undefined).
     *
     *  @return The padded string.
     */
    function rpad(format, string, length)
    {
        format = format + '';
        length = length || 2;
        
        return format.length < length ? format + new Array(1 + length - format.length).join(string) : format;
    }

    /**
     *  Perform a modulus calculation on a date object to extract the desired value.
     *
     *  @param date The date object to perform the calculation on.
     *
     *  @param mod1 The value to divide the date value seconds by.
     *
     *  @param mod2 The modulus value.
     *
     *  @return The computed value.
     */
    function modCalc(date, mod1, mod2)
    {
        return (Math.floor(Math.floor(date.valueOf() / 1e3) / mod1) % mod2);
    }
    
    /**
     *  Given a string, return a properly formatted date string.
     *
     *  @param date     The date object being formatted.
     *
     *  @param format   The formatting string.
     *
     *  @return The formatted date string
     */
    function formatDate(date, format)
    {
        format = format.split('');
        
        var output = '',
            
            /**
             *  The characters '{' and '}' are the start and end characters of an escape. Anything between these
             *  characters will not be treated as a formatting commands, and will merely be appended to the output
             *  string. When the buffering property here is true, we are in the midst of appending escaped characters
             *  to the output, and the formatting check should therefore be skipped.
             */
            buffering = false,
     
            char = '',
            
            index = 0;
            
        for (; index < format.length; index++)
        {
            char = format[index] + '';
            
            switch (char)
            {

            case ' ':
                output += char;
                break;
                
            case '{':
            case '}':
                buffering = char === '{';
                break;
            
            default:
                if (!buffering && _[char])
                {
                    output += _[char].apply(date);
                }
                else
                {
                    output += char;
                }
                break;

            }
        }
     
        return output;
    }
    
    //------------------------------
    //
    //  Class Definition
    //
    //------------------------------
    
    /**
     *  The formatting object holds all the actual formatting commands which should be accessible
     *  for date formatting.
     *
     *  Each method should reference the date function via its "this" context, which will be set
     *  by the formatter.
     *
     *  This function makes heavy use of the exponent notation for large numbers, to save space. In
     *  javascript, any number with a set of trailing zeros can be expressed in exponent notation.
     *
     *  Ex. 15,000,000,000 === 15e9, where the number after "e" represents the number of zeros.
     */
    _ = 
    {
        //------------------------------
        //  Timer Formatting
        //------------------------------
        
        /**
         *  This is intended to be used for delta computation when subtracting one date object from another.
         *
         *  @return The number of days since the epoch.
         */
        V: function ()
        {
            return modCalc(this, 864e2, 1e5);
        },
        
        /**
         *  This is intended to be used for delta computation when subtracting one date object from another.
         *
         *  @return The number of days since the epoch, padded to 2 digits.
         */
        v: function ()
        {
            return pad(_.V.apply(this), 0);
        },

        /**
         *  This is intended to be used for delta computation when subtracting one date object from another.
         *
         *  @return The number of days since the epoch, offset for years.
         */
        K: function ()
        {
            return _.V.apply(this) % 365;
        },
        
        /**
         *  This is intended to be used for delta computation when subtracting one date object from another.
         *
         *  @return The number of days since the epoch, offset for years, padded to 2 digits.
         */
        k: function ()
        {
            return pad(_.K.apply(this), 0);
        },
        
        /**
         *  This is intended to be used for delta computation when subtracting one date object from another.
         *
         *  @return The number of hours since the epoch.
         */
        X: function ()
        {
            return modCalc(this, 36e2, 24);
        },
        
        /**
         *  This is intended to be used for delta computation when subtracting one date object from another.
         *
         *  @return The number of hours since the epoch, padded to two digits.
         */
        x: function ()
        {
            return pad(_.X.apply(this), 0);
        },
        
        /**
         *  This is intended to be used for delta computation when subtracting one date object from another.
         *
         *  @return The number of minutes since the epoch.
         */
        p: function ()
        {
            return modCalc(this, 60, 60);
        },
        
        /**
         *  This is intended to be used for delta computation when subtracting one date object from another.
         *
         *  @return The number of minutes since the epoch, padded to two digits.
         */
        C: function ()
        {
            return pad(_.p.apply(this), 0);
        },
        
        /**
         *	This is intended to be used for delta computation when subtracting one date object from another.
         *
         *  @return The number of minutes since the epoch, uncapped. (1min 30seconds would be 90s)
         */
        E: function ()
        {
            return (_.X.apply(this) * 60) + _.p.apply(this);
        },
        
        /**
         *	This is intended to be used for delta computation when subtracting one date object from another.
         *
         *  @return The number of minutes since the epoch, uncapped and padded to two digits. (1min 30seconds would be 90s)
         */
        e: function ()
        {
            return pad(_.e.apply(this), 0);
        },
        
        //------------------------------
        //  Day Formatting
        //------------------------------
        
        /**
         *  @return The day of the month, padded to two digits.
         */
        d: function ()
        {
            return pad(this.getDate(), 0);
        },
        
        /**
         *  @return A textual representation of the day, three letters.
         */
        D: function ()
        {
            return days[this.getDay()].substring(0, 3);
        },
        
        /**
         *  @return Day of the month without leading zeros.
         */
        j: function ()
        {
            return this.getDate();
        },
        
        /**
         *  @return A full textual representation of the day of the week.
         */
        l: function ()
        {
            return days[this.getDay()];
        },
        
        /**
         *  @return ISO-8601 numeric representation of the day of the week.
         */
        N: function ()
        {
            return this.getDay() + 1;
        },
        
        /**
         *  @return English ordinal suffix for the day of the month, two characters.
         */
        S: function ()
        {
            return suffix[this.getDate()] || 'th';
        },
        
        /**
         *  @return Numeric representation of the day of the week.
         */
        w: function ()
        {
            return this.getDay();
        },
        
        /**
         *  @return The day of the year (starting from 0).
         */
        z: function ()
        {
            return Math.round((this - _.f.apply(this)) / 864e5);
        },
        
        //------------------------------
        //  Week
        //------------------------------
        
        /**
         *  @return ISO-8601 week number of year, weeks starting on Monday 
         */
        W: function ()
        {
            return Math.ceil(((((this - _.f.apply(this)) / 864e5) + _.w.apply(_.f.apply(this))) / 7));
        },
        
        //------------------------------
        //  Month
        //------------------------------
        
        /**
         *  @return A full textual representation of a month, such as January.
         */
        F: function ()
        {
            return months[this.getMonth()];
        },
        
        /**
         *  @return Numeric representation of a month, padded to two digits.
         */
        m: function ()
        {
            return pad((this.getMonth() + 1), 0);
        },
        
        /**
         *  @return A short textual representation of a month, three letters.
         */
        M: function ()
        {
            return months[this.getMonth()].substring(0, 3);
        },
        
        /**
         *  @return Numeric representation of a month, without leading zeros.
         */
        n: function ()
        {
            return this.getMonth() + 1;
        },
        
        /**
         *  @return Number of days in the given month.
         */
        t: function ()
        {
            //  For February on leap years, we must return 29.
            if (this.getMonth() === 1 && _.L.apply(this) === 1)
            {
                return 29;
            }
            
            return counts[this.getMonth()];
        },
        
        
        //------------------------------
        //  Year
        //------------------------------
        
        /**
         *  @return Whether it's a leap year. 1 if it is a leap year, 0 otherwise.
         */
        L: function ()
        {
            var Y = _.Y.apply(this);
        
            return Y % 4 ? 0 : Y % 100 ? 1 : Y % 400 ? 0 : 1;
        },
        
        /**
         *  @return A Date object representing the first day of the current year.
         */
        f: function ()
        {
            return new Date(this.getFullYear(), 0, 1);
        },
        
        /**
         *  @return A full numeric representation of the year, 4 digits.
         */
        Y: function ()
        {
            return this.getFullYear();
        },
        
        /**
         *  @return A two digit representation of the year.
         */
        y: function ()
        {
            return ('' + this.getFullYear()).substr(2);
        },
        
        //------------------------------
        //  Time
        //------------------------------
        
        /**
         *  @return Lowercase Ante/Post Meridiem values.
         */
        a: function ()
        {
            return this.getHours() < 12 ? 'am' : 'pm';
        },
        
        /**
         *  @return Uppercase Ante/Post Meridiem values.
         */
        A: function ()
        {
            return _.a.apply(this).toUpperCase();
        },
        
        /**
         *  If you ever use this for anything, email <eric@knewton.com>, cause he'd like to know how you found this nonsense useful.
         *
         *  @return Swatch internet time. 
         */
        B: function ()
        {
            return pad(Math.floor((((this.getHours()) * 36e5) + (this.getMinutes() * 6e4) + (this.getSeconds() * 1e3)) / 864e2), 0, 3);
        },
        
        /**
         *  @return 12-hour format of an hour.
         */
        g: function ()
        {
            return this.getHours() % 12 || 12;
        },
        
        /**
         *  @return 24-hour format of an hour.
         */
        G: function ()
        {
            return this.getHours();
        },
        
        /**
         *  @return 12-hour format of an hour, padded to two digits.
         */
        h: function ()
        {
            return pad(_.g.apply(this), 0);
        },
        
        /**
         *  @return 24-hour format of an hour, padded to two digits.
         */
        H: function ()
        {
            return pad(this.getHours(), 0);
        },
        
        /**
         *  @return Minutes, padded to two digits.
         */
        i: function ()
        {
            return pad(this.getMinutes(), 0);
        },
        
        /**
         *  @return Seconds, padded to two digits.
         */
        s: function ()
        {
            return pad(this.getSeconds(), 0);
        },
        
        /**
         *  @return Microseconds
         */
        u: function ()
        {
            return this.getTime() % 1e3;
        },
        
        //------------------------------
        //  Timezone
        //------------------------------
        
        /**
         *  @return Difference to GMT in hours.
         */
        O: function ()
        {
            var t = this.getTimezoneOffset() / 60;
            
            return rpad(pad((t >= 0 ? '+' : '-') + Math.abs(t), 0), 0, 4);
        },
        
        /**
         *  @return Difference to GMT in hours, with colon between hours and minutes
         */
        P: function ()
        {
            var t = _.O.apply(this);
            
            return t.subst(0, 3) + ':' + t.substr(3);
        },
        
        /**
         *  @return Timezone offset in seconds.
         */
        Z: function ()
        {
            return this.getTimezoneOffset() * 60;
        },
        
        //------------------------------
        //  Full Date/Time
        //------------------------------
        
        /**
         *  @return ISO 8601 date
         */
        c: function ()
        {
            return _.Y.apply(this) + '-' + _.m.apply(this) + '-' + _.d.apply(this) + 'T' + _.H.apply(this) + ':' + _.i.apply(this) + ':' +  _.s.apply(this) + _.P.apply(this);
        },
        
        /**
         *  @return RFC 2822 formatted date
         */
        r: function ()
        {
            return this.toString();
        },
        
        /**
         *  @return The number of seconds since the epoch.
         */
        U: function ()
        {
            return this.getTime() / 1e3;
        }
    };
    
    //------------------------------
    //
    //  Native Prototype
    //
    //------------------------------
    
    $.extend(Date.prototype, {
        
        /**
         *	Given a string of formatting commands, return the date object as a formatted string.
         *
         *  @param format   The formatting string.
         *
         *  @return The formatted date string
         */
        format: function (format)
        {
            return formatDate(this, format);
        }
    });
    
    //------------------------------
    //
    //  Expose to jQuery
    //
    //------------------------------
    
    $.dateformat = 
    {
        /**
         *	Get a reference to the formatting rules, or set custom rules.
         *
         *  @param custom   The custom rules to set for formatting.
         *
         *  @return The formatting rules.
         */
        rules: function (custom)
        {
            if (custom !== undefined)
            {
                _ = $.extend(_, custom);
            }
            
            return _;
        },
        
        /**
         *	Determine if the dateformat plugin has the requested formatting rule.
         *
         *  @param rule The formatting rule to check.
         *
         *  @return True if the rule exists, false otherwise.
         */
        hasRule: function (rule)
        {
            return _[rule] !== undefined;
        },
    
        /**
         *	Get a formatting value for a given date.
         *
         *  @param type The formatting character type to get the value of.
         *
         *  @param date The date to extract the value from. Defaults to current.
         */
        get: function (type, date)
        {
            return _[type].apply(date || new Date());
        },
        
        /**
         *	Given a string of formatting commands, return the date object as a formatted string.
         *
         *  @param format   The formatting string.
         *
         *  @param date The date to extract the value from. Defaults to current.
         *
         *  @return The formatted date string
         */
        format: function (format, date)
        {
            return formatDate(date || new Date(), format);
        },
        
        /**
         *	@inheritDoc
         */
        pad: pad,
        
        /**
         *	@inheritDoc
         */
        rpad: rpad
    };
    
}(jQuery));

