/**
 * @ignore
 * DateTimeFormat for KISSY.
 * Inspired by DateTimeFormat from JDK.
 * @author yiminghe@gmail.com
 */
KISSY.add('date/format', function (S, GregorianCalendar, defaultLocale, undefined) {
    var MAX_VALUE = Number.MAX_VALUE,
        /**
         * date or time style enum
         * @enum {Number} KISSY.Date.Formatter.Style
         */
            DateTimeStyle = {
            /**
             * full style
             */
            FULL: 0,
            /**
             * long style
             */
            LONG: 1,
            /**
             * medium style
             */
            MEDIUM: 2,
            /**
             * short style
             */
            SHORT: 3
        },
        logger = S.getLogger('s/date/format');

    /*
     Letter    Date or Time Component    Presentation    Examples
     G    Era designator    Text    AD
     y    Year    Year    1996; 96
     M    Month in year    Month    July; Jul; 07
     w    Week in year    Number    27
     W    Week in month    Number    2
     D    Day in year    Number    189
     d    Day in month    Number    10
     F    Day of week in month    Number    2
     E    Day in week    Text    Tuesday; Tue
     a    Am/pm marker    Text    PM
     H    Hour in day (0-23)    Number    0
     k    Hour in day (1-24)    Number    24
     K    Hour in am/pm (0-11)    Number    0
     h    Hour in am/pm (1-12)    Number    12
     m    Minute in hour    Number    30
     s    Second in minute    Number    55
     S    Millisecond    Number    978
     x z    Time zone    General time zone    Pacific Standard Time; PST; GMT-08:00
     Z    Time zone    RFC 822 time zone    -0800
     */

    var patternChars = new Array(GregorianCalendar.DAY_OF_WEEK_IN_MONTH + 2).
        join('1');

    var ERA = 0;

    var calendarIndexMap = {};

    patternChars = patternChars.split('');
    patternChars[ERA] = 'G';
    patternChars[GregorianCalendar.YEAR] = 'y';
    patternChars[GregorianCalendar.MONTH] = 'M';
    patternChars[GregorianCalendar.DAY_OF_MONTH] = 'd';
    patternChars[GregorianCalendar.HOUR_OF_DAY] = 'H';
    patternChars[GregorianCalendar.MINUTES] = 'm';
    patternChars[GregorianCalendar.SECONDS] = 's';
    patternChars[GregorianCalendar.MILLISECONDS] = 'S';
    patternChars[GregorianCalendar.WEEK_OF_YEAR] = 'w';
    patternChars[GregorianCalendar.WEEK_OF_MONTH] = 'W';
    patternChars[GregorianCalendar.DAY_OF_YEAR] = 'D';
    patternChars[GregorianCalendar.DAY_OF_WEEK_IN_MONTH] = 'F';

    S.each(patternChars, function (v, index) {
        calendarIndexMap[v] = index;
    });

    patternChars = /**
     @ignore
     @type String
     */patternChars.join('') + 'ahkKZE';

    function encode(lastField, count, compiledPattern) {
        compiledPattern.push({
            field: lastField,
            count: count
        });
    }

    function compile(pattern) {
        var length = pattern.length;
        var inQuote = false;
        var compiledPattern = [];
        var tmpBuffer = null;
        var count = 0;
        var lastField = -1;

        for (var i = 0; i < length; i++) {
            var c = pattern.charAt(i);

            if (c == "'") {
                // '' is treated as a single quote regardless of being
                // in a quoted section.
                if ((i + 1) < length) {
                    c = pattern.charAt(i + 1);
                    if (c == '\'') {
                        i++;
                        if (count != 0) {
                            encode(lastField, count, compiledPattern);
                            lastField = -1;
                            count = 0;
                        }
                        if (inQuote) {
                            tmpBuffer += c;
                        }
                        continue;
                    }
                }
                if (!inQuote) {
                    if (count != 0) {
                        encode(lastField, count, compiledPattern);
                        lastField = -1;
                        count = 0;
                    }
                    tmpBuffer = '';
                    inQuote = true;
                } else {
                    compiledPattern.push({
                        text: tmpBuffer
                    });
                    inQuote = false;
                }
                continue;
            }
            if (inQuote) {
                tmpBuffer += c;
                continue;
            }
            if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')) {
                if (count != 0) {
                    encode(lastField, count, compiledPattern);
                    lastField = -1;
                    count = 0;
                }
                compiledPattern.push({
                    text: c
                });
                continue;
            }

            if (patternChars.indexOf(c) == -1) {
                throw new Error("Illegal pattern character " +
                    "'" + c + "'");
            }

            if (lastField == -1 || lastField == c) {
                lastField = c;
                count++;
                continue;
            }
            encode(lastField, count, compiledPattern);
            lastField = c;
            count = 1;
        }

        if (inQuote) {
            throw new Error("Unterminated quote");
        }

        if (count != 0) {
            encode(lastField, count, compiledPattern);
        }

        return compiledPattern;
    }

    var zeroDigit = '0';

    // TODO zeroDigit localization??
    function zeroPaddingNumber(value, minDigits, maxDigits, buffer) {
        // Optimization for 1, 2 and 4 digit numbers. This should
        // cover most cases of formatting date/time related items.
        // Note: This optimization code assumes that maxDigits is
        // either 2 or Integer.MAX_VALUE (maxIntCount in format()).
        buffer = buffer || [];
        maxDigits = maxDigits || MAX_VALUE;
        if (value >= 0) {
            if (value < 100 && minDigits >= 1 && minDigits <= 2) {
                if (value < 10 && minDigits == 2) {
                    buffer.push(zeroDigit);
                }
                buffer.push(value);
                return buffer.join('');
            } else if (value >= 1000 && value < 10000) {
                if (minDigits == 4) {
                    buffer.push(value);
                    return buffer.join('');
                }
                if (minDigits == 2 && maxDigits == 2) {
                    return zeroPaddingNumber(value % 100, 2, 2, buffer);
                }
            }
        }
        buffer.push(value + '');
        return buffer.join('');
    }

    /**
     *
     * date time formatter for KISSY gregorian date.
     *
     *      @example
     *      KISSY.use('date/format,date/gregorian',function(S, DateFormat, GregorianCalendar){
     *          var calendar = new GregorianCalendar(2013,9,24);
     *          // ' to escape
     *          var formatter = new DateFormat("'today is' ''yyyy/MM/dd a''");
     *          document.write(formatter.format(calendar));
     *      });
     *
     * @class KISSY.Date.Formatter
     * @param {String} pattern patter string of date formatter
     *
     * <table border="1">
     * <thead valign="bottom">
     * <tr><th class="head">Letter</th>
     * <th class="head">Date or Time Component</th>
     * <th class="head">Presentation</th>
     * <th class="head">Examples</th>
     * </tr>
     * </thead>
     * <tbody valign="top">
     * <tr><td>G</td>
     * <td>Era designator</td>
     * <td>Text</td>
     * <td>AD</td>
     * </tr>
     * <tr><td>y</td>
     * <td>Year</td>
     * <td>Year</td>
     * <td>1996; 96</td>
     * </tr>
     * <tr><td>M</td>
     * <td>Month in year</td>
     * <td>Month</td>
     * <td>July; Jul; 07</td>
     * </tr>
     * <tr><td>w</td>
     * <td>Week in year</td>
     * <td>Number</td>
     * <td>27</td>
     * </tr>
     * <tr><td>W</td>
     * <td>Week in month</td>
     * <td>Number</td>
     * <td>2</td>
     * </tr>
     * <tr><td>D</td>
     * <td>Day in year</td>
     * <td>Number</td>
     * <td>189</td>
     * </tr>
     * <tr><td>d</td>
     * <td>Day in month</td>
     * <td>Number</td>
     * <td>10</td>
     * </tr>
     * <tr><td>F</td>
     * <td>Day of week in month</td>
     * <td>Number</td>
     * <td>2</td>
     * </tr>
     * <tr><td>E</td>
     * <td>Day in week</td>
     * <td>Text</td>
     * <td>Tuesday; Tue</td>
     * </tr>
     * <tr><td>a</td>
     * <td>Am/pm marker</td>
     * <td>Text</td>
     * <td>PM</td>
     * </tr>
     * <tr><td>H</td>
     *       <td>Hour in day (0-23)</td>
     * <td>Number</td>
     * <td>0</td>
     * </tr>
     * <tr><td>k</td>
     *       <td>Hour in day (1-24)</td>
     * <td>Number</td>
     * <td>24</td>
     * </tr>
     * <tr><td>K</td>
     * <td>Hour in am/pm (0-11)</td>
     * <td>Number</td>
     * <td>0</td>
     * </tr>
     * <tr><td>h</td>
     * <td>Hour in am/pm (1-12)</td>
     * <td>Number</td>
     * <td>12</td>
     * </tr>
     * <tr><td>m</td>
     * <td>Minute in hour</td>
     * <td>Number</td>
     * <td>30</td>
     * </tr>
     * <tr><td>s</td>
     * <td>Second in minute</td>
     * <td>Number</td>
     * <td>55</td>
     * </tr>
     * <tr><td>S</td>
     * <td>Millisecond</td>
     * <td>Number</td>
     * <td>978</td>
     * </tr>
     * <tr><td>x/z</td>
     * <td>Time zone</td>
     * <td>General time zone</td>
     * <td>Pacific Standard Time; PST; GMT-08:00</td>
     * </tr>
     * <tr><td>Z</td>
     * <td>Time zone</td>
     * <td>RFC 822 time zone</td>
     * <td>-0800</td>
     * </tr>
     * </tbody>
     * </table>

     * @param {Object} locale locale object
     * @param {Number} timeZoneOffset time zone offset by minutes
     */
    function DateTimeFormat(pattern, locale, timeZoneOffset) {
        this.locale = locale || defaultLocale;
        this.pattern = compile(pattern);
        this.timezoneOffset = timeZoneOffset;
    }

    function formatField(field, count, locale, calendar) {
        var current,
            value;
        switch (field) {
            case 'G':
                value = calendar.getYear() > 0 ? 1 : 0;
                current = locale.eras[value];
                break;
            case 'y':
                value = calendar.getYear();
                if (value <= 0) {
                    value = 1 - value;
                }
                current = (zeroPaddingNumber(value, 2, count != 2 ? MAX_VALUE : 2));
                break;
            case 'M':
                value = calendar.getMonth();
                if (count >= 4) {
                    current = locale.months[value];
                } else if (count == 3) {
                    current = locale.shortMonths[value];
                } else {
                    current = zeroPaddingNumber(value + 1, count);
                }
                break;
            case 'k':
                current = zeroPaddingNumber(calendar.getHourOfDay() || 24,
                    count);
                break;
            case 'E':
                value = calendar.getDayOfWeek();
                current = count >= 4 ?
                    locale.weekdays[value] :
                    locale.shortWeekdays[value];
                break;
            case 'a':
                current = locale.ampms[calendar.getHourOfDay() >= 12 ?
                    1 :
                    0];
                break;
            case 'h':
                current = zeroPaddingNumber(calendar.
                    getHourOfDay() % 12 || 12, count);
                break;
            case 'K':
                current = zeroPaddingNumber(calendar.
                    getHourOfDay() % 12, count);
                break;
            case 'Z':
                var offset = calendar.getTimezoneOffset();
                var parts = [offset < 0 ? '-' : '+'];
                offset = Math.abs(offset);
                parts.push(zeroPaddingNumber(Math.floor(offset / 60) % 100, 2),
                    zeroPaddingNumber(offset % 60, 2));
                current = parts.join('');
                break;
            default :
                // case 'd':
                // case 'H':
                // case 'm':
                // case 's':
                // case 'S':
                // case 'D':
                // case 'F':
                // case 'w':
                // case 'W':
                var index = calendarIndexMap[field];
                value = calendar.get(index);
                current = zeroPaddingNumber(value, count);
        }
        return current;
    }

    function matchField(dateStr, startIndex, matches) {
        var matchedLen = -1,
            index = -1,
            i,
            len = matches.length;
        for (i = 0; i < len; i++) {
            var m = matches[i];
            var mLen = m.length;
            if (mLen > matchedLen &&
                matchPartString(dateStr, startIndex, m, mLen)) {
                matchedLen = mLen;
                index = i;
            }
        }
        return index >= 0 ? {
            value: index,
            startIndex: startIndex + matchedLen
        } : null;
    }

    function matchPartString(dateStr, startIndex, match, mLen) {
        for (var i = 0; i < mLen; i++) {
            if (dateStr.charAt(startIndex + i) != match.charAt(i)) {
                return false;
            }
        }
        return true;
    }

    function getLeadingNumberLen(str) {
        var i, c,
            len = str.length;
        for (i = 0; i < len; i++) {
            c = str.charAt(i);
            if (c < '0' || c > '9') {
                break;
            }
        }
        return i;
    }

    function matchNumber(dateStr, startIndex, count, obeyCount) {
        var str = dateStr , n;
        if (obeyCount) {
            if (dateStr.length <= startIndex + count) {
                return null;
            }
            str = dateStr.substring(startIndex, count);
            if (!str.match(/^\d+$/)) {
                return null;
            }
        } else {
            str = str.substring(startIndex);
        }
        n = parseInt(str, 10);
        if (isNaN(n)) {
            return null;
        }
        return {
            value: n,
            startIndex: startIndex + getLeadingNumberLen(str)
        };
    }

    function parseField(calendar, dateStr, startIndex, field, count, locale, obeyCount, tmp) {
        var match, year, hour;
        if (dateStr.length <= startIndex) {
            return startIndex;
        }
        switch (field) {
            case 'G':
                if (match = matchField(dateStr, startIndex, locale.eras)) {
                    if (calendar.isSetYear()) {
                        if (match.value == 0) {
                            year = calendar.getYear();
                            calendar.setYear(1 - year);
                        }
                    } else {
                        tmp.era = match.value;
                    }
                }
                break;
            case 'y':
                if (match = matchNumber(dateStr, startIndex, count, obeyCount)) {
                    year = match.value;
                    if ('era' in tmp) {
                        if (tmp.era === 0) {
                            year = 1 - year;
                        }
                    }
                    calendar.setYear(year);
                }
                break;
            case 'M':
                var month;
                if (count >= 3) {
                    if (match = matchField(dateStr, startIndex, locale[count == 3 ?
                        'shortMonths' : 'months'])) {
                        month = match.value;
                    }
                } else {
                    if (match = matchNumber(dateStr, startIndex, count, obeyCount)) {
                        month = match.value - 1;
                    }
                }
                if (match) {
                    calendar.setMonth(month);
                }
                break;
            case 'k':
                if (match = matchNumber(dateStr, startIndex, count, obeyCount)) {
                    calendar.setHourOfDay(match.value % 24);
                }
                break;
            case 'E':
                if (match = matchField(dateStr, startIndex, locale[count > 3 ?
                    'weekdays' :
                    'shortWeekdays'])) {
                    calendar.setDayOfWeek(match.value);
                }
                break;
            case 'a':
                if (match = matchField(dateStr, startIndex, locale.ampms)) {
                    if (calendar.isSetHourOfDay()) {
                        if (match.value) {
                            hour = calendar.getHourOfDay();
                            if (hour < 12) {
                                calendar.setHourOfDay((hour + 12) % 24);
                            }
                        }
                    } else {
                        tmp.ampm = match.value;
                    }
                }
                break;
            case 'h':
                if (match = matchNumber(dateStr, startIndex, count, obeyCount)) {
                    hour = match.value %= 12;
                    if (tmp.ampm) {
                        hour += 12;
                    }
                    calendar.setHourOfDay(hour);
                }
                break;
            case 'K':
                if (match = matchNumber(dateStr, startIndex, count, obeyCount)) {
                    hour = match.value;
                    if (tmp.ampm) {
                        hour += 12;
                    }
                    calendar.setHourOfDay(hour);
                }
                break;
            case 'Z':
                if (dateStr)
                    var sign = 1,
                        zoneChar = dateStr.charAt(startIndex);
                if (zoneChar == '-') {
                    sign = -1;
                    startIndex++;
                } else if (zoneChar == '+') {
                    startIndex++;
                } else {
                    break;
                }
                if (match = matchNumber(dateStr, startIndex, 2, true)) {
                    var zoneOffset = match.value * 60;
                    startIndex = match.startIndex;
                    if (match = matchNumber(dateStr, startIndex, 2, true)) {
                        zoneOffset += match.value
                    }
                    calendar.setTimezoneOffset(zoneOffset);
                }
                break;
            default :
                // case 'd':
                // case 'H':
                // case 'm':
                // case 's':
                // case 'S':
                // case 'D':
                // case 'F':
                // case 'w':
                // case 'W'
                if (match = matchNumber(dateStr, startIndex, count, obeyCount)) {
                    var index = calendarIndexMap[field];
                    calendar.set(index, match.value);
                }
        }
        if (match) {
            startIndex = match.startIndex;
        }
        return startIndex;
    }

    DateTimeFormat.prototype = {
        /**
         * format a GregorianDate instance according to specified pattern
         * @param {KISSY.Date.Gregorian} calendar GregorianDate instance
         * @returns {string} formatted string of GregorianDate instance
         */
        format: function (calendar) {
            var time = calendar.getTime();
            calendar = /**@type {KISSY.Date.Gregorian}
             @ignore*/new GregorianCalendar(this.timezoneOffset, this.locale);
            calendar.setTime(time);
            var i,
                ret = [],
                pattern = this.pattern,
                len = pattern.length;
            for (i = 0; i < len; i++) {
                var comp = pattern[i];
                if (comp.text) {
                    ret.push(comp.text);
                } else if ('field' in comp) {
                    ret.push(formatField(comp.field, comp.count, this.locale, calendar));
                }
            }
            return ret.join('');
        },

        /**
         * parse a formatted string of GregorianDate instance according to specified pattern
         * @param {String} dateStr formatted string of GregorianDate
         * @returns {KISSY.Date.Gregorian}
         */
        parse: function (dateStr) {
            var calendar = /**@type {KISSY.Date.Gregorian}
                 @ignore*/new GregorianCalendar(this.timezoneOffset, this.locale),
                i,
                j,
                tmp = {},
                obeyCount = false,
                dateStrLen = dateStr.length,
                errorIndex = -1,
                startIndex = 0,
                oldStartIndex = 0,
                pattern = this.pattern,
                len = pattern.length;

            loopPattern: {
                for (i = 0; errorIndex < 0 && i < len; i++) {
                    var comp = pattern[i], text, textLen;
                    oldStartIndex = startIndex;
                    if (text = comp.text) {
                        textLen = text.length;
                        if ((textLen + startIndex) > dateStrLen) {
                            errorIndex = startIndex;
                        } else {
                            for (j = 0; j < textLen; j++) {
                                if (text.charAt(j) != dateStr.charAt(j + startIndex)) {
                                    errorIndex = startIndex;
                                    break loopPattern;
                                }
                            }
                            startIndex += textLen;
                        }
                    } else if ('field' in comp) {
                        obeyCount = false;
                        var nextComp = pattern[i + 1];
                        if (nextComp) {
                            if ('field' in nextComp) {
                                obeyCount = true;
                            } else {
                                var c = nextComp.text.charAt(0);
                                if (c >= '0' && c <= '9') {
                                    obeyCount = true;
                                }
                            }
                        }
                        startIndex = parseField(calendar,
                            dateStr,
                            startIndex,
                            comp.field,
                            comp.count,
                            this.locale,
                            obeyCount,
                            tmp);
                        if (startIndex == oldStartIndex) {
                            errorIndex = startIndex;
                        }
                    }
                }
            }

            if (errorIndex >= 0) {
                logger.error('error when parsing date');
                logger.error(dateStr);
                logger.error(dateStr.substring(0, errorIndex) + '^');
                return undefined;
            }
            return calendar;
        }
    };

    S.mix(DateTimeFormat, {
        Style: DateTimeStyle,

        /**
         * get a formatter instance of short style pattern.
         * en-us: M/d/yy h:mm a
         * zh-cn: yy-M-d ah:mm
         * @param {Object} locale locale object
         * @param {Number} timeZoneOffset time zone offset by minutes
         * @returns {KISSY.Date.Gregorian}
         * @static
         */
        getInstance: function (locale, timeZoneOffset) {
            return this.getDateTimeInstance(DateTimeStyle.SHORT, DateTimeStyle.SHORT, locale, timeZoneOffset);
        },

        /**
         * get a formatter instance of specified date style.
         * @param {KISSY.Date.Formatter.Style} dateStyle date format style
         * @param {Object} locale
         * @param {Number} timeZoneOffset time zone offset by minutes
         * @returns {KISSY.Date.Gregorian}
         * @static
         */
        'getDateInstance': function (dateStyle, locale, timeZoneOffset) {
            return this.getDateTimeInstance(dateStyle, undefined, locale, timeZoneOffset);
        },

        /**
         * get a formatter instance of specified date style and time style.
         * @param {KISSY.Date.Formatter.Style} dateStyle date format style
         * @param {KISSY.Date.Formatter.Style} timeStyle time format style
         * @param {Object} locale
         * @param {Number} timeZoneOffset time zone offset by minutes
         * @returns {KISSY.Date.Gregorian}
         * @static
         */
        getDateTimeInstance: function (dateStyle, timeStyle, locale, timeZoneOffset) {
            locale = locale || defaultLocale;
            var datePattern = '';
            if (dateStyle !== undefined) {
                datePattern = locale.datePatterns[dateStyle];
            }
            var timePattern = '';
            if (timeStyle !== undefined) {
                timePattern = locale.timePatterns[timeStyle];
            }
            var pattern = datePattern;
            if (timePattern) {
                if (datePattern) {
                    pattern = S.substitute(locale.dateTimePattern, {
                        date: datePattern,
                        time: timePattern
                    });
                } else {
                    pattern = timePattern;
                }
            }
            return new DateTimeFormat(pattern, locale, timeZoneOffset);
        },

        /**
         * get a formatter instance of specified time style.
         * @param {KISSY.Date.Formatter.Style} timeStyle time format style
         * @param {Object} locale
         * @param {Number} timeZoneOffset time zone offset by minutes
         * @returns {KISSY.Date.Gregorian}
         * @static
         */
        'getTimeInstance': function (timeStyle, locale, timeZoneOffset) {
            return this.getDateTimeInstance(undefined, timeStyle, locale, timeZoneOffset);
        }
    });

    return DateTimeFormat;
}, {
    requires: [
        'date/gregorian',
        'i18n!date'
    ]
});