/* YUI 3.9.1 (build 5852) Copyright 2013 Yahoo! Inc. http://yuilibrary.com/license/ */
YUI.add('calendar-base', function (Y, NAME) {

/**
 * The CalendarBase submodule is a basic UI calendar view that displays
 * a range of dates in a two-dimensional month grid, with one or more
 * months visible at a single time. CalendarBase supports custom date
 * rendering, multiple calendar panes, and selection.
 * @module calendar
 * @submodule calendar-base
 */

var getCN                 = Y.ClassNameManager.getClassName,
    CALENDAR              = 'calendar',
    CAL_GRID              = getCN(CALENDAR, 'grid'),
    CAL_LEFT_GRID         = getCN(CALENDAR, 'left-grid'),
    CAL_RIGHT_GRID        = getCN(CALENDAR, 'right-grid'),
    CAL_BODY              = getCN(CALENDAR, 'body'),
    CAL_HD                = getCN(CALENDAR, 'header'),
    CAL_HD_LABEL          = getCN(CALENDAR, 'header-label'),
    CAL_WDAYROW           = getCN(CALENDAR, 'weekdayrow'),
    CAL_WDAY              = getCN(CALENDAR, 'weekday'),
    CAL_COL_HIDDEN        = getCN(CALENDAR, 'column-hidden'),
    CAL_DAY_SELECTED      = getCN(CALENDAR, 'day-selected'),
    SELECTION_DISABLED    = getCN(CALENDAR, 'selection-disabled'),
    CAL_ROW               = getCN(CALENDAR, 'row'),
    CAL_DAY               = getCN(CALENDAR, 'day'),
    CAL_PREVMONTH_DAY     = getCN(CALENDAR, 'prevmonth-day'),
    CAL_NEXTMONTH_DAY     = getCN(CALENDAR, 'nextmonth-day'),
    CAL_ANCHOR            = getCN(CALENDAR, 'anchor'),
    CAL_PANE              = getCN(CALENDAR, 'pane'),
    CAL_STATUS            = getCN(CALENDAR, 'status'),
    L           = Y.Lang,
    substitute  = L.sub,
    arrayEach   = Y.Array.each,
    objEach     = Y.Object.each,
    iOf         = Y.Array.indexOf,
    hasKey      = Y.Object.hasKey,
    setVal      = Y.Object.setValue,
    isEmpty     = Y.Object.isEmpty,
    ydate       = Y.DataType.Date;

/** Create a calendar view to represent a single or multiple
    * month range of dates, rendered as a grid with date and
    * weekday labels.
    *
    * @class CalendarBase
    * @extends Widget
    * @param config {Object} Configuration object (see Configuration
    * attributes)
    * @constructor
    */
function CalendarBase() {
    CalendarBase.superclass.constructor.apply ( this, arguments );
}



Y.CalendarBase = Y.extend( CalendarBase, Y.Widget, {

    /**
     * A storage for various properties of individual month
     * panes.
     *
     * @property _paneProperties
     * @type Object
     * @private
     */
    _paneProperties : {},

    /**
     * The number of month panes in the calendar, deduced
     * from the CONTENT_TEMPLATE's number of {calendar_grid}
     * tokens.
     *
     * @property _paneNumber
     * @type Number
     * @private
     */
    _paneNumber : 1,

    /**
     * The unique id used to prefix various elements of this
     * calendar instance.
     *
     * @property _calendarId
     * @type String
     * @private
     */
    _calendarId : null,

    /**
     * The hash map of selected dates, populated with
     * selectDates() and deselectDates() methods
     *
     * @property _selectedDates
     * @type Object
     * @private
     */
    _selectedDates : {},

    /**
     * A private copy of the rules object, populated
     * by setting the customRenderer attribute.
     *
     * @property _rules
     * @type Object
     * @private
     */
    _rules : {},

    /**
     * A private copy of the filterFunction, populated
     * by setting the customRenderer attribute.
     *
     * @property _filterFunction
     * @type Function
     * @private
     */
    _filterFunction : null,

    /**
     * Storage for calendar cells modified by any custom
     * formatting. The storage is cleared, used to restore
     * cells to the original state, and repopulated accordingly
     * when the calendar is rerendered.
     *
     * @property _storedDateCells
     * @type Object
     * @private
     */
    _storedDateCells : {},

    /**
     * Designated initializer
     * Initializes instance-level properties of
     * calendar.
     *
     * @method initializer
     */
    initializer : function () {
        this._paneProperties = {};
        this._calendarId = Y.guid('calendar');
        this._selectedDates = {};
        if (isEmpty(this._rules)) {
             this._rules = {};
        }
        this._storedDateCells = {};
    },

    /**
     * renderUI implementation
     *
     * Creates a visual representation of the calendar based on existing parameters.
     * @method renderUI
     */
    renderUI : function () {

        var contentBox = this.get('contentBox');
        contentBox.appendChild(this._initCalendarHTML(this.get('date')));

        if (this.get('showPrevMonth')) {
                this._afterShowPrevMonthChange();
        }
        if (this.get('showNextMonth')) {
                this._afterShowNextMonthChange();
        }

        this._renderCustomRules();
        this._renderSelectedDates();

        this.get("boundingBox").setAttribute("aria-labelledby", this._calendarId + "_header");

    },

    /**
     * bindUI implementation
     *
     * Assigns listeners to relevant events that change the state
     * of the calendar.
     * @method bindUI
     */
    bindUI : function () {
        this.after('dateChange', this._afterDateChange);
        this.after('showPrevMonthChange', this._afterShowPrevMonthChange);
        this.after('showNextMonthChange', this._afterShowNextMonthChange);
        this.after('headerRendererChange', this._afterHeaderRendererChange);
        this.after('customRendererChange', this._afterCustomRendererChange);
        this.after('enabledDatesRuleChange', this._afterCustomRendererChange);
        this.after('disabledDatesRuleChange', this._afterCustomRendererChange);
        this.after('focusedChange', this._afterFocusedChange);
        this.after('selectionChange', this._renderSelectedDates);
        this._bindCalendarEvents();
    },


    /**
     * An internal utility method that generates a list of selected dates
     * from the hash storage.
     *
     * @method _getSelectedDatesList
     * @protected
     * @return {Array} The array of `Date`s that are currently selected.
     */
    _getSelectedDatesList : function () {
        var output = [];

        objEach (this._selectedDates, function (year) {
            objEach (year, function (month) {
                objEach (month, function (day) {
                    output.push (day);
                }, this);
            }, this);
        }, this);

        return output;
    },

    /**
     * A utility method that returns all dates selected in a specific month.
     *
     * @method _getSelectedDatesInMonth
     * @param {Date} oDate corresponding to the month for which selected dates
     * are requested.
     * @protected
     * @return {Array} The array of `Date`s in a given month that are currently selected.
     */
    _getSelectedDatesInMonth : function (oDate) {
        var year = oDate.getFullYear(),
            month = oDate.getMonth();

        if (hasKey(this._selectedDates, year) && hasKey(this._selectedDates[year], month)) {
            return Y.Object.values(this._selectedDates[year][month]);
        } else {
            return [];
        }
    },


    /**
     * An internal parsing method that receives a String list of numbers
     * and number ranges (of the form "1,2,3,4-6,7-9,10,11" etc.) and checks
     * whether a specific number is included in this list. Used for looking
     * up dates in the customRenderer rule set.
     *
     * @method _isNumInList
     * @param {Number} num The number to look for in a list.
     * @param {String} strList The list of numbers of the form "1,2,3,4-6,7-8,9", etc.
     * @private
     * @return {boolean} Returns true if the given number is in the given list.
     */
    _isNumInList : function (num, strList) {
        if (strList === "all") {
            return true;
        } else {
            var elements = strList.split(","),
                i = elements.length,
                range;

            while (i--) {
                range = elements[i].split("-");
                if (range.length === 2 && num >= parseInt(range[0], 10) && num <= parseInt(range[1], 10)) {
                    return true;
                }
                else if (range.length === 1 && (parseInt(elements[i], 10) === num)) {
                    return true;
                }
            }
            return false;
        }
    },

    /**
     * Given a specific date, returns an array of rules (from the customRenderer rule set)
     * that the given date matches.
     *
     * @method _getRulesForDate
     * @param {Date} oDate The date for which an array of rules is needed
     * @private
     * @return {Array} Returns an array of `String`s, each containg the name of
     * a rule that the given date matches.
     */
    _getRulesForDate : function (oDate) {
        var year = oDate.getFullYear(),
                month = oDate.getMonth(),
                date = oDate.getDate(),
                wday = oDate.getDay(),
                rules = this._rules,
                outputRules = [],
                years, months, dates, days;

        for (years in rules) {
            if (this._isNumInList(year, years)) {
                if (L.isString(rules[years])) {
                        outputRules.push(rules[years]);
                }
                else {
                    for (months in rules[years]) {
                        if (this._isNumInList(month, months)) {
                            if (L.isString(rules[years][months])) {
                                    outputRules.push(rules[years][months]);
                            }
                            else {
                                for (dates in rules[years][months]) {
                                    if (this._isNumInList(date, dates)) {
                                        if (L.isString(rules[years][months][dates])) {
                                                outputRules.push(rules[years][months][dates]);
                                        }
                                        else {
                                            for (days in rules[years][months][dates]) {
                                                if (this._isNumInList(wday, days)) {
                                                    if (L.isString(rules[years][months][dates][days])) {
                                                        outputRules.push(rules[years][months][dates][days]);
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return outputRules;
    },

    /**
     * A utility method which, given a specific date and a name of the rule,
     * checks whether the date matches the given rule.
     *
     * @method _matchesRule
     * @param {Date} oDate The date to check
     * @param {String} rule The name of the rule that the date should match.
     * @private
     * @return {boolean} Returns true if the date matches the given rule.
     *
     */
    _matchesRule : function (oDate, rule) {
        return (iOf(this._getRulesForDate(oDate), rule) >= 0);
    },

    /**
     * A utility method which checks whether a given date matches the `enabledDatesRule`
     * or does not match the `disabledDatesRule` and therefore whether it can be selected.
     * @method _canBeSelected
     * @param {Date} oDate The date to check
     * @private
     * @return {boolean} Returns true if the date can be selected; false otherwise.
     */
    _canBeSelected : function (oDate) {

        var enabledDatesRule = this.get("enabledDatesRule"),
            disabledDatesRule = this.get("disabledDatesRule");

        if (enabledDatesRule) {
            return this._matchesRule(oDate, enabledDatesRule);
        } else if (disabledDatesRule) {
            return !this._matchesRule(oDate, disabledDatesRule);
        } else {
            return true;
        }
    },

    /**
     * Selects a given date or array of dates.
     * @method selectDates
     * @param {Date|Array} dates A `Date` or `Array` of `Date`s.
     * @return {CalendarBase} A reference to this object
     * @chainable
     */
    selectDates : function (dates) {
        if (ydate.isValidDate(dates)) {
            this._addDateToSelection(dates);
        }
        else if (L.isArray(dates)) {
            this._addDatesToSelection(dates);
        }
        return this;
    },

    /**
     * Deselects a given date or array of dates, or deselects
     * all dates if no argument is specified.
     * @method deselectDates
     * @param {Date|Array} [dates] A `Date` or `Array` of `Date`s, or no
     * argument if all dates should be deselected.
     * @return {CalendarBase} A reference to this object
     * @chainable
     */
    deselectDates : function (dates) {
        if (!dates) {
            this._clearSelection();
        }
        else if (ydate.isValidDate(dates)) {
            this._removeDateFromSelection(dates);
        }
        else if (L.isArray(dates)) {
            this._removeDatesFromSelection(dates);
        }
        return this;
    },

    /**
     * A utility method that adds a given date to selection..
     * @method _addDateToSelection
     * @param {Date} oDate The date to add to selection.
     * @param {Number} [index] An optional parameter that is used
     * to differentiate between individual date selections and multiple
     * date selections.
     * @private
     */
    _addDateToSelection : function (oDate, index) {

        if (this._canBeSelected(oDate)) {

            var year = oDate.getFullYear(),
                month = oDate.getMonth(),
                day = oDate.getDate();

            if (hasKey(this._selectedDates, year)) {
                if (hasKey(this._selectedDates[year], month)) {
                    this._selectedDates[year][month][day] = oDate;
                } else {
                    this._selectedDates[year][month] = {};
                    this._selectedDates[year][month][day] = oDate;
                }
            } else {
                this._selectedDates[year] = {};
                this._selectedDates[year][month] = {};
                this._selectedDates[year][month][day] = oDate;
            }

            this._selectedDates = setVal(this._selectedDates, [year, month, day], oDate);

            if (!index) {
                this._fireSelectionChange();
            }
        }
    },

    /**
     * A utility method that adds a given list of dates to selection.
     * @method _addDatesToSelection
     * @param {Array} datesArray The list of dates to add to selection.
     * @private
     */
    _addDatesToSelection : function (datesArray) {
        arrayEach(datesArray, this._addDateToSelection, this);
        this._fireSelectionChange();
    },

    /**
     * A utility method that adds a given range of dates to selection.
     * @method _addDateRangeToSelection
     * @param {Date} startDate The first date of the given range.
     * @param {Date} endDate The last date of the given range.
     * @private
     */
    _addDateRangeToSelection : function (startDate, endDate) {

        var timezoneDifference = (endDate.getTimezoneOffset() - startDate.getTimezoneOffset())*60000,
            startTime = startDate.getTime(),
            endTime   = endDate.getTime(),
            tempTime,
            time,
            addedDate;

        if (startTime > endTime) {
            tempTime = startTime;
            startTime = endTime;
            endTime = tempTime + timezoneDifference;
        } else {
            endTime = endTime - timezoneDifference;
        }


        for (time = startTime; time <= endTime; time += 86400000) {
            addedDate = new Date(time);
            addedDate.setHours(12);
            this._addDateToSelection(addedDate, time);
        }
        this._fireSelectionChange();
    },

    /**
     * A utility method that removes a given date from selection..
     * @method _removeDateFromSelection
     * @param {Date} oDate The date to remove from selection.
     * @param {Number} [index] An optional parameter that is used
     * to differentiate between individual date selections and multiple
     * date selections.
     * @private
     */
    _removeDateFromSelection : function (oDate, index) {
        var year = oDate.getFullYear(),
            month = oDate.getMonth(),
            day = oDate.getDate();

        if (hasKey(this._selectedDates, year) &&
            hasKey(this._selectedDates[year], month) &&
            hasKey(this._selectedDates[year][month], day)
        ) {
            delete this._selectedDates[year][month][day];
            if (!index) {
                this._fireSelectionChange();
            }
        }
    },

    /**
     * A utility method that removes a given list of dates from selection.
     * @method _removeDatesFromSelection
     * @param {Array} datesArray The list of dates to remove from selection.
     * @private
     */
    _removeDatesFromSelection : function (datesArray) {
        arrayEach(datesArray, this._removeDateFromSelection, this);
        this._fireSelectionChange();
    },

    /**
     * A utility method that removes a given range of dates from selection.
     * @method _removeDateRangeFromSelection
     * @param {Date} startDate The first date of the given range.
     * @param {Date} endDate The last date of the given range.
     * @private
     */
    _removeDateRangeFromSelection : function (startDate, endDate) {
        var startTime = startDate.getTime(),
            endTime   = endDate.getTime(),
            time;

        for (time = startTime; time <= endTime; time += 86400000) {
            this._removeDateFromSelection(new Date(time), time);
        }

        this._fireSelectionChange();
    },

    /**
     * A utility method that removes all dates from selection.
     * @method _clearSelection
     * @param {boolean} noevent A Boolean specifying whether a selectionChange
     * event should be fired. If true, the event is not fired.
     * @private
     */
    _clearSelection : function (noevent) {
        this._selectedDates = {};
        this.get("contentBox").all("." + CAL_DAY_SELECTED).removeClass(CAL_DAY_SELECTED).setAttribute("aria-selected", false);
        if (!noevent) {
            this._fireSelectionChange();
        }
    },

    /**
     * A utility method that fires a selectionChange event.
     * @method _fireSelectionChange
     * @private
     */
    _fireSelectionChange : function () {

        /**
        * Fired when the set of selected dates changes. Contains a payload with
        * a `newSelection` property with an array of selected dates.
        *
        * @event selectionChange
        */
        this.fire("selectionChange", {newSelection: this._getSelectedDatesList()});
    },

    /**
     * A utility method that restores cells modified by custom formatting.
     * @method _restoreModifiedCells
     * @private
     */
    _restoreModifiedCells : function () {
        var contentbox = this.get("contentBox"),
            id;
        for (id in this._storedDateCells) {
            contentbox.one("#" + id).replace(this._storedDateCells[id]);
            delete this._storedDateCells[id];
        }
    },

    /**
     * A rendering assist method that renders all cells modified by the customRenderer
     * rules, as well as the enabledDatesRule and disabledDatesRule.
     * @method _renderCustomRules
     * @private
     */
    _renderCustomRules : function () {

        this.get("contentBox").all("." + CAL_DAY + ",." + CAL_NEXTMONTH_DAY).removeClass(SELECTION_DISABLED).setAttribute("aria-disabled", false);

        if (!isEmpty(this._rules)) {
            var paneNum,
                paneDate,
                dateArray;

            for (paneNum = 0; paneNum < this._paneNumber; paneNum++) {
                paneDate = ydate.addMonths(this.get("date"), paneNum);
                dateArray = ydate.listOfDatesInMonth(paneDate);
                arrayEach(dateArray, Y.bind(this._renderCustomRulesHelper, this));
            }
        }
    },

    /**
    * A handler for a date selection event (either a click or a keyboard
    *   selection) that adds the appropriate CSS class to a specific DOM
    *   node corresponding to the date and sets its aria-selected
    *   attribute to true.
    *
    * @method _renderCustomRulesHelper
    * @private
    */
    _renderCustomRulesHelper: function (date) {
        var enRule = this.get("enabledDatesRule"),
            disRule = this.get("disabledDatesRule"),
            matchingRules,
            dateNode;

        matchingRules = this._getRulesForDate(date);
        if (matchingRules.length > 0) {
            dateNode = this._dateToNode(date);
            if ((enRule && iOf(matchingRules, enRule) < 0) || (!enRule && disRule && iOf(matchingRules, disRule) >= 0)) {
                dateNode.addClass(SELECTION_DISABLED).setAttribute("aria-disabled", true);
            }

            if (L.isFunction(this._filterFunction)) {
                this._storedDateCells[dateNode.get("id")] = dateNode.cloneNode(true);
                this._filterFunction (date, dateNode, matchingRules);
            }
        } else if (enRule) {
             dateNode = this._dateToNode(date);
             dateNode.addClass(SELECTION_DISABLED).setAttribute("aria-disabled", true);
        }
    },

    /**
     * A rendering assist method that renders all cells that are currently selected.
     * @method _renderSelectedDates
     * @private
     */
    _renderSelectedDates : function () {
        this.get("contentBox").all("." + CAL_DAY_SELECTED).removeClass(CAL_DAY_SELECTED).setAttribute("aria-selected", false);

        var paneNum,
            paneDate,
            dateArray;

        for (paneNum = 0; paneNum < this._paneNumber; paneNum++) {
            paneDate = ydate.addMonths(this.get("date"), paneNum);
            dateArray = this._getSelectedDatesInMonth(paneDate);

            arrayEach(dateArray, Y.bind(this._renderSelectedDatesHelper, this));
        }
    },

    /**
    * Takes in a date and determines whether that date has any rules
    *   matching it in the customRenderer; then calls the specified
    *   filterFunction if that's the case and/or disables the date
    *   if the rule is specified as a disabledDatesRule.
    *
    * @method _renderSelectedDatesHelper
    * @private
    */
    _renderSelectedDatesHelper: function (date) {
        this._dateToNode(date).addClass(CAL_DAY_SELECTED).setAttribute("aria-selected", true);
    },

    /**
     * A utility method that converts a date to the node wrapping the calendar cell
     * the date corresponds to..
     * @method _dateToNode
     * @param {Date} oDate The date to convert to Node
     * @protected
     * @return {Node} The node wrapping the DOM element of the cell the date
     * corresponds to.
     */
    _dateToNode : function (oDate) {
        var day = oDate.getDate(),
            col = 0,
            daymod = day%7,
            paneNum = (12 + oDate.getMonth() - this.get("date").getMonth()) % 12,
            paneId = this._calendarId + "_pane_" + paneNum,
            cutoffCol = this._paneProperties[paneId].cutoffCol;

        switch (daymod) {
            case (0):
                if (cutoffCol >= 6) {
                    col = 12;
                } else {
                    col = 5;
                }
                break;
            case (1):
                    col = 6;
                break;
            case (2):
                if (cutoffCol > 0) {
                    col = 7;
                } else {
                    col = 0;
                }
                break;
            case (3):
                if (cutoffCol > 1) {
                    col = 8;
                } else {
                    col = 1;
                }
                break;
            case (4):
                if (cutoffCol > 2) {
                    col = 9;
                } else {
                    col = 2;
                }
                break;
            case (5):
                if (cutoffCol > 3) {
                    col = 10;
                } else {
                    col = 3;
                }
                break;
            case (6):
                if (cutoffCol > 4) {
                    col = 11;
                } else {
                    col = 4;
                }
                break;
        }
        return(this.get("contentBox").one("#" + this._calendarId + "_pane_" + paneNum + "_" + col + "_" + day));

    },

    /**
     * A utility method that converts a node corresponding to the DOM element of
     * the cell for a particular date to that date.
     * @method _nodeToDate
     * @param {Node} oNode The Node wrapping the DOM element of a particular date cell.
     * @protected
     * @return {Date} The date corresponding to the DOM element that the given node wraps.
     */
    _nodeToDate : function (oNode) {

        var idParts = oNode.get("id").split("_").reverse(),
            paneNum = parseInt(idParts[2], 10),
            day  = parseInt(idParts[0], 10),
            shiftedDate = ydate.addMonths(this.get("date"), paneNum),
            year = shiftedDate.getFullYear(),
            month = shiftedDate.getMonth();

        return new Date(year, month, day, 12, 0, 0, 0);
    },

    /**
     * A placeholder method, called from bindUI, to bind the Calendar events.
     * @method _bindCalendarEvents
     * @protected
     */
    _bindCalendarEvents : function () {},

    /**
     * A utility method that normalizes a given date by converting it to the 1st
     * day of the month the date is in, with the time set to noon.
     * @method _normalizeDate
     * @param {Date} oDate The date to normalize
     * @protected
     * @return {Date} The normalized date, set to the first of the month, with time
     * set to noon.
     */
    _normalizeDate : function (date) {
        if (date) {
            return new Date(date.getFullYear(), date.getMonth(), 1, 12, 0, 0, 0);
        } else {
            return null;
        }
    },


    /**
     * A render assist utility method that computes the cutoff column for the calendar
     * rendering mask.
     * @method _getCutoffColumn
     * @param {Date} date The date of the month grid to compute the cutoff column for.
     * @param {Number} firstday The first day of the week (modified by internationalized calendars)
     * @private
     * @return {Number} The number of the cutoff column.
     */
    _getCutoffColumn : function (date, firstday) {
        var distance = this._normalizeDate(date).getDay() - firstday,
            cutOffColumn = 6 - (distance + 7) % 7;
        return cutOffColumn;
    },

    /**
     * A render assist method that turns on the view of the previous month's dates
     * in a given calendar pane.
     * @method _turnPrevMonthOn
     * @param {Node} pane The calendar pane that needs its previous month's dates view
     * modified.
     * @protected
     */
    _turnPrevMonthOn : function (pane) {
        var pane_id = pane.get("id"),
            pane_date = this._paneProperties[pane_id].paneDate,
            daysInPrevMonth = ydate.daysInMonth(ydate.addMonths(pane_date, -1)),
            cell;

        if (!this._paneProperties[pane_id].hasOwnProperty("daysInPrevMonth")) {
            this._paneProperties[pane_id].daysInPrevMonth = 0;
        }

        if (daysInPrevMonth !== this._paneProperties[pane_id].daysInPrevMonth) {

            this._paneProperties[pane_id].daysInPrevMonth = daysInPrevMonth;

            for (cell = 5; cell >= 0; cell--) {
                pane.one("#" + pane_id + "_" + cell + "_" + (cell-5)).set('text', daysInPrevMonth--);
            }
        }
    },

    /**
     * A render assist method that turns off the view of the previous month's dates
     * in a given calendar pane.
     * @method _turnPrevMonthOff
     * @param {Node} pane The calendar pane that needs its previous month's dates view
     * modified.
     * @protected
     */
    _turnPrevMonthOff : function (pane) {
        var pane_id = pane.get("id"),
            cell;

        this._paneProperties[pane_id].daysInPrevMonth = 0;

        for (cell = 5; cell >= 0; cell--) {
            pane.one("#" + pane_id + "_" + cell + "_" + (cell-5)).setContent("&nbsp;");
        }
    },

    /**
     * A render assist method that cleans up the last few cells in the month grid
     * when the number of days in the month changes.
     * @method _cleanUpNextMonthCells
     * @param {Node} pane The calendar pane that needs the last date cells cleaned up.
     * @private
     */
    _cleanUpNextMonthCells : function (pane) {
        var pane_id = pane.get("id");
            pane.one("#" + pane_id + "_6_29").removeClass(CAL_NEXTMONTH_DAY);
            pane.one("#" + pane_id + "_7_30").removeClass(CAL_NEXTMONTH_DAY);
            pane.one("#" + pane_id + "_8_31").removeClass(CAL_NEXTMONTH_DAY);
            pane.one("#" + pane_id + "_0_30").removeClass(CAL_NEXTMONTH_DAY);
            pane.one("#" + pane_id + "_1_31").removeClass(CAL_NEXTMONTH_DAY);
    },

    /**
     * A render assist method that turns on the view of the next month's dates
     * in a given calendar pane.
     * @method _turnNextMonthOn
     * @param {Node} pane The calendar pane that needs its next month's dates view
     * modified.
     * @protected
     */
    _turnNextMonthOn : function (pane) {
        var dayCounter = 1,
            pane_id = pane.get("id"),
            daysInMonth = this._paneProperties[pane_id].daysInMonth,
            cutoffCol = this._paneProperties[pane_id].cutoffCol,
            cell,
            startingCell;

        for (cell = daysInMonth - 22; cell < cutoffCol + 7; cell++) {
            pane.one("#" + pane_id + "_" + cell + "_" + (cell+23)).set("text", dayCounter++).addClass(CAL_NEXTMONTH_DAY);
        }

        startingCell = cutoffCol;

        if (daysInMonth === 31 && (cutoffCol <= 1)) {
            startingCell = 2;
        } else if (daysInMonth === 30 && cutoffCol === 0) {
            startingCell = 1;
        }

        for (cell = startingCell ; cell < cutoffCol + 7; cell++) {
            pane.one("#" + pane_id + "_" + cell + "_" + (cell+30)).set("text", dayCounter++).addClass(CAL_NEXTMONTH_DAY);
        }
    },

    /**
     * A render assist method that turns off the view of the next month's dates
     * in a given calendar pane.
     * @method _turnNextMonthOff
     * @param {Node} pane The calendar pane that needs its next month's dates view
     * modified.
     * @protected
     */
    _turnNextMonthOff : function (pane) {
            var pane_id = pane.get("id"),
                daysInMonth = this._paneProperties[pane_id].daysInMonth,
                cutoffCol = this._paneProperties[pane_id].cutoffCol,
                cell,
                startingCell;

            for (cell = daysInMonth - 22; cell <= 12; cell++) {
                pane.one("#" + pane_id + "_" + cell + "_" + (cell+23)).setContent("&nbsp;").addClass(CAL_NEXTMONTH_DAY);
            }

            startingCell = 0;

            if (daysInMonth === 31 && (cutoffCol <= 1)) {
                startingCell = 2;
            } else if (daysInMonth === 30 && cutoffCol === 0) {
                startingCell = 1;
            }

            for (cell = startingCell ; cell <= 12; cell++) {
                pane.one("#" + pane_id + "_" + cell + "_" + (cell+30)).setContent("&nbsp;").addClass(CAL_NEXTMONTH_DAY);
            }
    },

    /**
     * The handler for the change in the showNextMonth attribute.
     * @method _afterShowNextMonthChange
     * @private
     */
    _afterShowNextMonthChange : function () {

        var contentBox = this.get('contentBox'),
            lastPane = contentBox.one("#" + this._calendarId + "_pane_" + (this._paneNumber - 1));

        this._cleanUpNextMonthCells(lastPane);

        if (this.get('showNextMonth')) {
            this._turnNextMonthOn(lastPane);
        } else {
            this._turnNextMonthOff(lastPane);
        }

    },

    /**
     * The handler for the change in the showPrevMonth attribute.
     * @method _afterShowPrevMonthChange
     * @private
     */
    _afterShowPrevMonthChange : function () {
        var contentBox = this.get('contentBox'),
            firstPane = contentBox.one("#" + this._calendarId + "_pane_" + 0);

        if (this.get('showPrevMonth')) {
            this._turnPrevMonthOn(firstPane);
        } else {
            this._turnPrevMonthOff(firstPane);
        }

    },

     /**
     * The handler for the change in the headerRenderer attribute.
     * @method _afterHeaderRendererChange
     * @private
     */
    _afterHeaderRendererChange : function () {
        var headerCell = this.get("contentBox").one("." + CAL_HD_LABEL);
        headerCell.setContent(this._updateCalendarHeader(this.get('date')));
    },

     /**
     * The handler for the change in the customRenderer attribute.
     * @method _afterCustomRendererChange
     * @private
     */
    _afterCustomRendererChange : function () {
        this._restoreModifiedCells();
        this._renderCustomRules();
    },

     /**
     * The handler for the change in the date attribute. Modifies the calendar
     * view by shifting the calendar grid mask and running custom rendering and
     * selection rendering as necessary.
     * @method _afterDateChange
     * @private
     */
    _afterDateChange : function () {

        var contentBox = this.get('contentBox'),
            headerCell = contentBox.one("." + CAL_HD).one("." + CAL_HD_LABEL),
            calendarPanes = contentBox.all("." + CAL_GRID),
            currentDate = this.get("date"),
            counter = 0;

        contentBox.setStyle("visibility", "hidden");
        headerCell.setContent(this._updateCalendarHeader(currentDate));

        this._restoreModifiedCells();

        calendarPanes.each(function (curNode) {
            this._rerenderCalendarPane(ydate.addMonths(currentDate, counter++), curNode);
        }, this);

        this._afterShowPrevMonthChange();
        this._afterShowNextMonthChange();

        this._renderCustomRules();
        this._renderSelectedDates();

        contentBox.setStyle("visibility", "visible");
    },


     /**
     * A rendering assist method that initializes the HTML for a single
     * calendar pane.
     * @method _initCalendarPane
     * @param {Date} baseDate The date corresponding to the month of the given
     * calendar pane.
     * @param {String} pane_id The id of the pane, to be used as a prefix for
     * element ids in the given pane.
     * @private
     */
    _initCalendarPane : function (baseDate, pane_id) {
        // Get a list of short weekdays from the internationalization package, or else use default English ones.
        var weekdays = this.get('strings.very_short_weekdays') || ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"],
            fullweekdays = this.get('strings.weekdays') || ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
            // Get the first day of the week from the internationalization package, or else use Sunday as default.
            firstday = this.get('strings.first_weekday') || 0,
            // Compute the cutoff column of the masked calendar table, based on the start date and the first day of week.
            cutoffCol = this._getCutoffColumn(baseDate, firstday),
            // Compute the number of days in the month based on starting date
            daysInMonth = ydate.daysInMonth(baseDate),
            // Initialize the array of individual row HTML strings
            row_array = ['','','','','',''],
            // Initialize the partial templates object
            partials = {},

            day,
            row,
            column,
            date,
            id_date,
            calendar_day_class,
            column_visibility,
            output;

            // Initialize the partial template for the weekday row cells.
            partials.weekday_row = '';

        // Populate the partial template for the weekday row cells with weekday names
        for (day = firstday; day <= firstday + 6; day++) {
            partials.weekday_row +=
                substitute(CalendarBase.WEEKDAY_TEMPLATE, {
                    weekdayname: weekdays[day%7],
                    full_weekdayname: fullweekdays[day%7]
                });
        }

        // Populate the partial template for the weekday row container with the weekday row cells
        partials.weekday_row_template = substitute(CalendarBase.WEEKDAY_ROW_TEMPLATE, partials);

        // Populate the array of individual row HTML strings
        for (row = 0; row <= 5; row++) {

            for (column = 0; column <= 12; column++) {

                // Compute the value of the date that needs to populate the cell
                date = 7*row - 5 + column;

                // Compose the value of the unique id of the current calendar cell
                id_date = pane_id + "_" + column + "_" + date;

                // Set the calendar day class to one of three possible values
                calendar_day_class = CAL_DAY;

                if (date < 1) {
                    calendar_day_class = CAL_PREVMONTH_DAY;
                } else if (date > daysInMonth) {
                    calendar_day_class = CAL_NEXTMONTH_DAY;
                }

                // Cut off dates that fall before the first and after the last date of the month
                if (date < 1 || date > daysInMonth) {
                    date = "&nbsp;";
                }

                // Decide on whether a column in the masked table is visible or not based on the value of the cutoff column.
                column_visibility = (column >= cutoffCol && column < (cutoffCol + 7)) ? '' : CAL_COL_HIDDEN;

                // Substitute the values into the partial calendar day template and add it to the current row HTML string
                row_array[row] += substitute (CalendarBase.CALDAY_TEMPLATE, {
                    day_content: date,
                    calendar_col_class: "calendar_col" + column,
                    calendar_col_visibility_class: column_visibility,
                    calendar_day_class: calendar_day_class,
                    calendar_day_id: id_date
                });
            }
        }

        // Instantiate the partial calendar pane body template
        partials.body_template = '';

        // Populate the body template with the rows templates
        arrayEach (row_array, function (v) {
             partials.body_template += substitute(CalendarBase.CALDAY_ROW_TEMPLATE, {calday_row: v});
        });

        // Populate the calendar grid id
        partials.calendar_pane_id = pane_id;

        // Populate the calendar pane tabindex
        partials.calendar_pane_tabindex = this.get("tabIndex");
        partials.pane_arialabel = ydate.format(baseDate, { format: "%B %Y" });


        // Generate final output by substituting class names.
        output = substitute(substitute (CalendarBase.CALENDAR_GRID_TEMPLATE, partials),
                                                        CalendarBase.CALENDAR_STRINGS);

        // Store the initialized pane information
        this._paneProperties[pane_id] = {cutoffCol: cutoffCol, daysInMonth: daysInMonth, paneDate: baseDate};

        return output;
    },

     /**
     * A rendering assist method that rerenders a specified calendar pane, based
     * on a new Date.
     * @method _rerenderCalendarPane
     * @param {Date} newDate The date corresponding to the month of the given
     * calendar pane.
     * @param {Node} pane The node corresponding to the calendar pane to be rerenders.
     * @private
     */
    _rerenderCalendarPane : function (newDate, pane) {

        // Get the first day of the week from the internationalization package, or else use Sunday as default.
        var firstday = this.get('strings.first_weekday') || 0,
            // Compute the cutoff column of the masked calendar table, based on the start date and the first day of week.
            cutoffCol = this._getCutoffColumn(newDate, firstday),
            // Compute the number of days in the month based on starting date
            daysInMonth = ydate.daysInMonth(newDate),
            // Get pane id for easier reference
            paneId = pane.get("id"),
            column,
            currentColumn,
            curCell;

        // Hide the pane before making DOM changes to speed them up
        pane.setStyle("visibility", "hidden");
        pane.setAttribute("aria-label", ydate.format(newDate, {format:"%B %Y"}));

        // Go through all columns, and flip their visibility setting based on whether they are within the unmasked range.
        for (column = 0; column <= 12; column++) {
            currentColumn = pane.all("." + "calendar_col" + column);
            currentColumn.removeClass(CAL_COL_HIDDEN);

            if (column < cutoffCol || column >= (cutoffCol + 7)) {
                currentColumn.addClass(CAL_COL_HIDDEN);
            } else {
                // Clean up dates in visible columns to account for the correct number of days in a month
                switch(column) {
                    case 0:
                        curCell = pane.one("#" + paneId + "_0_30");
                        if (daysInMonth >= 30) {
                            curCell.set("text", "30");
                            curCell.removeClass(CAL_NEXTMONTH_DAY).addClass(CAL_DAY);
                        } else {
                            curCell.setContent("&nbsp;");
                            curCell.addClass(CAL_NEXTMONTH_DAY).addClass(CAL_DAY);
                        }
                        break;
                    case 1:
                        curCell = pane.one("#" + paneId + "_1_31");
                        if (daysInMonth >= 31) {
                            curCell.set("text", "31");
                            curCell.removeClass(CAL_NEXTMONTH_DAY).addClass(CAL_DAY);
                        } else {
                            curCell.setContent("&nbsp;");
                            curCell.removeClass(CAL_DAY).addClass(CAL_NEXTMONTH_DAY);
                        }
                        break;
                    case 6:
                        curCell = pane.one("#" + paneId + "_6_29");
                        if (daysInMonth >= 29) {
                            curCell.set("text", "29");
                            curCell.removeClass(CAL_NEXTMONTH_DAY).addClass(CAL_DAY);
                        } else {
                            curCell.setContent("&nbsp;");
                            curCell.removeClass(CAL_DAY).addClass(CAL_NEXTMONTH_DAY);
                        }
                        break;
                    case 7:
                        curCell = pane.one("#" + paneId + "_7_30");
                        if (daysInMonth >= 30) {
                            curCell.set("text", "30");
                            curCell.removeClass(CAL_NEXTMONTH_DAY).addClass(CAL_DAY);
                        } else {
                            curCell.setContent("&nbsp;");
                            curCell.removeClass(CAL_DAY).addClass(CAL_NEXTMONTH_DAY);
                        }
                        break;
                    case 8:
                        curCell = pane.one("#" + paneId + "_8_31");
                        if (daysInMonth >= 31) {
                            curCell.set("text", "31");
                            curCell.removeClass(CAL_NEXTMONTH_DAY).addClass(CAL_DAY);
                        } else {
                            curCell.setContent("&nbsp;");
                            curCell.removeClass(CAL_DAY).addClass(CAL_NEXTMONTH_DAY);
                        }
                        break;
                }
            }
        }

        // Update stored pane properties
        this._paneProperties[paneId].cutoffCol = cutoffCol;
        this._paneProperties[paneId].daysInMonth = daysInMonth;
        this._paneProperties[paneId].paneDate = newDate;

        // Bring the pane visibility back after all DOM changes are done
        pane.setStyle("visibility", "visible");

    },

     /**
     * A rendering assist method that updates the calendar header based
     * on a given date and potentially the provided headerRenderer.
     * @method _updateCalendarHeader
     * @param {Date} baseDate The date with which to update the calendar header.
     * @private
     */
    _updateCalendarHeader : function (baseDate) {
        var headerString = "",
            headerRenderer = this.get("headerRenderer");

        if (Y.Lang.isString(headerRenderer)) {
            headerString = ydate.format(baseDate, {format:headerRenderer});
        } else if (headerRenderer instanceof Function) {
            headerString = headerRenderer.call(this, baseDate);
        }

        return headerString;
    },

     /**
     * A rendering assist method that initializes the calendar header HTML
     * based on a given date and potentially the provided headerRenderer.
     * @method _updateCalendarHeader
     * @param {Date} baseDate The date with which to initialize the calendar header.
     * @private
     */
    _initCalendarHeader : function (baseDate) {
        return substitute(substitute(CalendarBase.HEADER_TEMPLATE, {
                calheader: this._updateCalendarHeader(baseDate),
                calendar_id: this._calendarId
            }), CalendarBase.CALENDAR_STRINGS );
    },

     /**
     * A rendering assist method that initializes the calendar HTML
     * based on a given date.
     * @method _initCalendarHTML
     * @param {Date} baseDate The date with which to initialize the calendar.
     * @private
     */
    _initCalendarHTML : function (baseDate) {
        // Instantiate the partials holder
        var partials = {},
            // Counter for iterative template replacement.
            counter = 0,
            singlePane,
            output;

        // Generate the template for the header
        partials.header_template =  this._initCalendarHeader(baseDate);
        partials.calendar_id = this._calendarId;

        partials.body_template = substitute(substitute (CalendarBase.CONTENT_TEMPLATE, partials),
                                                                                 CalendarBase.CALENDAR_STRINGS);

        // Instantiate the iterative template replacer function
        function paneReplacer () {
            singlePane = this._initCalendarPane(ydate.addMonths(baseDate, counter), partials.calendar_id + "_pane_" + counter);
            counter++;
            return singlePane;
        }

        // Go through all occurrences of the calendar_grid_template token and replace it with an appropriate calendar grid.
        output = partials.body_template.replace(/\{calendar_grid_template\}/g, Y.bind(paneReplacer, this));

        // Update the paneNumber count
        this._paneNumber = counter;

        return output;
    }
}, {

     /**
        * The CSS classnames for the calendar templates.
        * @property CALENDAR_STRINGS
        * @type Object
        * @readOnly
        * @protected
        * @static
        */
    CALENDAR_STRINGS: {
        calendar_grid_class       : CAL_GRID,
        calendar_body_class       : CAL_BODY,
        calendar_hd_class         : CAL_HD,
        calendar_hd_label_class   : CAL_HD_LABEL,
        calendar_weekdayrow_class : CAL_WDAYROW,
        calendar_weekday_class    : CAL_WDAY,
        calendar_row_class        : CAL_ROW,
        calendar_day_class        : CAL_DAY,
        calendar_dayanchor_class  : CAL_ANCHOR,
        calendar_pane_class       : CAL_PANE,
        calendar_right_grid_class : CAL_RIGHT_GRID,
        calendar_left_grid_class  : CAL_LEFT_GRID,
        calendar_status_class     : CAL_STATUS
    },

    /*

    ARIA_STATUS_TEMPLATE: '<div role="status" aria-atomic="true" class="{calendar_status_class}"></div>',

    AriaStatus : null,

    updateStatus : function (statusString) {

        if (!CalendarBase.AriaStatus) {
            CalendarBase.AriaStatus = create(
                                                         substitute (CalendarBase.ARIA_STATUS_TEMPLATE,
                                                                                 CalendarBase.CALENDAR_STRINGS));
            Y.one("body").append(CalendarBase.AriaStatus);
        }

            CalendarBase.AriaStatus.set("text", statusString);
    },

    */

     /**
        * The main content template for calendar.
        * @property CONTENT_TEMPLATE
        * @type String
        * @protected
        * @static
        */
    CONTENT_TEMPLATE:  '<div class="yui3-g {calendar_pane_class}" id="{calendar_id}">' +
                        '{header_template}' +
                        '<div class="yui3-u-1">' +
                        '{calendar_grid_template}' +
                        '</div>' +
                        '</div>',

     /**
        * A single pane template for calendar (same as default CONTENT_TEMPLATE)
        * @property ONE_PANE_TEMPLATE
        * @type String
        * @protected
        * @readOnly
        * @static
        */
    ONE_PANE_TEMPLATE: '<div class="yui3-g {calendar_pane_class}" id="{calendar_id}">' +
                            '{header_template}' +
                            '<div class="yui3-u-1">' +
                                '{calendar_grid_template}' +
                            '</div>' +
                        '</div>',

     /**
        * A two pane template for calendar.
        * @property TWO_PANE_TEMPLATE
        * @type String
        * @protected
        * @readOnly
        * @static
        */
    TWO_PANE_TEMPLATE: '<div class="yui3-g {calendar_pane_class}" id="{calendar_id}">' +
                            '{header_template}' +
                            '<div class="yui3-u-1-2">'+
                                '<div class = "{calendar_left_grid_class}">' +
                                    '{calendar_grid_template}' +
                                '</div>' +
                            '</div>' +
                            '<div class="yui3-u-1-2">' +
                                '<div class = "{calendar_right_grid_class}">' +
                                    '{calendar_grid_template}' +
                                '</div>' +
                            '</div>' +
                        '</div>',
     /**
        * A three pane template for calendar.
        * @property THREE_PANE_TEMPLATE
        * @type String
        * @protected
        * @readOnly
        * @static
        */
    THREE_PANE_TEMPLATE: '<div class="yui3-g {calendar_pane_class}" id="{calendar_id}">' +
                            '{header_template}' +
                            '<div class="yui3-u-1-3">' +
                                '<div class="{calendar_left_grid_class}">' +
                                    '{calendar_grid_template}' +
                                '</div>' +
                            '</div>' +
                            '<div class="yui3-u-1-3">' +
                                '{calendar_grid_template}' +
                            '</div>' +
                            '<div class="yui3-u-1-3">' +
                                '<div class="{calendar_right_grid_class}">' +
                                    '{calendar_grid_template}' +
                                '</div>' +
                            '</div>' +
                        '</div>',
     /**
        * A template for the calendar grid.
        * @property CALENDAR_GRID_TEMPLATE
        * @type String
        * @protected
        * @static
        */
    CALENDAR_GRID_TEMPLATE: '<table class="{calendar_grid_class}" id="{calendar_pane_id}" role="grid" aria-readonly="true" ' +
                                'aria-label="{pane_arialabel}" tabindex="{calendar_pane_tabindex}">' +
                                '<thead>' +
                                    '{weekday_row_template}' +
                                '</thead>' +
                                '<tbody>' +
                                    '{body_template}' +
                                '</tbody>' +
                            '</table>',

     /**
        * A template for the calendar header.
        * @property HEADER_TEMPLATE
        * @type String
        * @protected
        * @static
        */
    HEADER_TEMPLATE: '<div class="yui3-g {calendar_hd_class}">' +
                        '<div class="yui3-u {calendar_hd_label_class}" id="{calendar_id}_header" aria-role="heading">' +
                            '{calheader}' +
                        '</div>' +
                    '</div>',

     /**
        * A template for the row of weekday names.
        * @property WEEKDAY_ROW_TEMPLATE
        * @type String
        * @protected
        * @static
        */
    WEEKDAY_ROW_TEMPLATE: '<tr class="{calendar_weekdayrow_class}" role="row">' +
                            '{weekday_row}' +
                        '</tr>',

     /**
        * A template for a single row of calendar days.
        * @property CALDAY_ROW_TEMPLATE
        * @type String
        * @protected
        * @static
        */
    CALDAY_ROW_TEMPLATE: '<tr class="{calendar_row_class}" role="row">' +
                            '{calday_row}' +
                        '</tr>',

     /**
        * A template for a single cell with a weekday name.
        * @property CALDAY_ROW_TEMPLATE
        * @type String
        * @protected
        * @static
        */
    WEEKDAY_TEMPLATE: '<th class="{calendar_weekday_class}" role="columnheader" aria-label="{full_weekdayname}">{weekdayname}</th>',

     /**
        * A template for a single cell with a calendar day.
        * @property CALDAY_TEMPLATE
        * @type String
        * @protected
        * @static
        */
    CALDAY_TEMPLATE: '<td class="{calendar_col_class} {calendar_day_class} {calendar_col_visibility_class}" id="{calendar_day_id}" ' +
                        'role="gridcell" tabindex="-1">' +
                        '{day_content}' +
                    '</td>',

     /**
        * The identity of the widget.
        *
        * @property NAME
        * @type String
        * @default 'calendarBase'
        * @readOnly
        * @protected
        * @static
        */
    NAME: 'calendarBase',

     /**
        * Static property used to define the default attribute configuration of
        * the Widget.
        *
        * @property ATTRS
        * @type {Object}
        * @protected
        * @static
        */
    ATTRS: {
        tabIndex: {
            value: 1
        },
        /**
         * The date corresponding to the current calendar view. Always
         * normalized to the first of the month that contains the date
         * at assignment time. Used as the first date visible in the
         * calendar.
         *
         * @attribute date
         * @type Date
         * @default The first of the month containing today's date, as
         * set on the end user's system.
         */
        date: {
            value: new Date(),
            setter: function (val) {
                var newDate = this._normalizeDate(val);
                if (ydate.areEqual(newDate, this.get('date'))) {
                        return this.get('date');
                } else {
                        return newDate;
                }
            }
        },

        /**
         * A setting specifying whether to shows days from the previous
         * month in the visible month's grid, if there are empty preceding
         * cells available.
         *
         * @attribute showPrevMonth
         * @type boolean
         * @default false
         */
        showPrevMonth: {
            value: false
        },

        /**
         * A setting specifying whether to shows days from the next
         * month in the visible month's grid, if there are empty
         * cells available at the end.
         *
         * @attribute showNextMonth
         * @type boolean
         * @default false
         */
        showNextMonth: {
            value: false
        },

        /**
         * Strings and properties derived from the internationalization packages
         * for the calendar.
         *
         * @attribute strings
         * @type Object
         * @protected
         */
        strings : {
            valueFn: function() { return Y.Intl.get("calendar-base"); }
        },

        /**
         * Custom header renderer for the calendar.
         *
         * @attribute headerRenderer
         * @type String | Function
         */
        headerRenderer: {
            value: "%B %Y"
        },

        /**
         * The name of the rule which all enabled dates should match.
         * Either disabledDatesRule or enabledDatesRule should be specified,
         * or neither, but not both.
         *
         * @attribute enabledDatesRule
         * @type String
         * @default null
         */
        enabledDatesRule: {
            value: null
        },

        /**
         * The name of the rule which all disabled dates should match.
         * Either disabledDatesRule or enabledDatesRule should be specified,
         * or neither, but not both.
         *
         * @attribute disabledDatesRule
         * @type String
         * @default null
         */
        disabledDatesRule: {
            value: null
        },

        /**
         * A read-only attribute providing a list of currently selected dates.
         *
         * @attribute selectedDates
         * @readOnly
         * @type Array
         */
        selectedDates : {
            readOnly: true,
            getter: function () {
                return (this._getSelectedDatesList());
            }
        },

        /**
         * An object of the form {rules:Object, filterFunction:Function},
         * providing  set of rules and a custom rendering function for
         * customizing specific calendar cells.
         *
         * @attribute customRenderer
         * @readOnly
         * @type Object
         * @default {}
         */
        customRenderer : {
            lazyAdd: false,
            value: {},
            setter: function (val) {
                this._rules = val.rules;
                this._filterFunction = val.filterFunction;
            }
        }
    }

});


}, '3.9.1', {
    "requires": [
        "widget",
        "datatype-date",
        "datatype-date-math",
        "cssgrids"
    ],
    "lang": [
        "de",
        "en",
        "es",
        "es-AR",
        "fr",
        "it",
        "ja",
        "nb-NO",
        "nl",
        "pt-BR",
        "ru",
        "zh-HANT-TW"
    ],
    "skinnable": true
});
