/*
YUI 3.4.1 (build 4118)
Copyright 2011 Yahoo! Inc. All rights reserved.
Licensed under the BSD License.
http://yuilibrary.com/license/
*/
YUI.add('calendar-base', function(Y) {

/**
 * 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'),
    L           = Y.Lang,
    node        = Y.Node,
    create      = node.create,
    substitute  = Y.substitute,
    each        = Y.each,
    hasVal      = Y.Array.hasValue,
    iOf         = Y.Array.indexOf,
    hasKey      = Y.Object.hasKey,
    setVal      = Y.Object.setValue,
    owns        = Y.Object.owns,
    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(config) {
  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 = {};
    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();
  },
  /**
   * 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._bindCalendarEvents();
  },

  /**
    * syncUI implementation
    *
    * Update the scroll position, based on the current value of scrollY
    * @method syncUI
    */  
  syncUI : function () {
      if (this.get('showPrevMonth')) {
           this._afterShowPrevMonthChange();

      }

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

    /**
     * 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 = [];
      each (this._selectedDates, function (year) {
        each (year, function (month) {
          each (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 rendering method that modifies a date cell to have the
     * selected CSS class if the date cell is visible.
     *
     * @method _renderSelectedDate
     * @param {Date} oDate The date corresponding to a specific date cell.
     * @private
     */
    _renderSelectedDate : function (oDate) {
        if (this._isDateVisible(oDate)) {
            this._dateToNode(oDate).addClass(CAL_DAY_SELECTED);
        }
    },

    /**
     * An internal rendering method that modifies a date cell to remove the
     * selected CSS class if the date cell is visible.
     *
     * @method _renderUnelectedDate
     * @param {Date} oDate The date corresponding to a specific date cell.
     * @private
     */
    _renderUnselectedDate : function (oDate) {
        if (this._isDateVisible(oDate)) {
            this._dateToNode(oDate).removeClass(CAL_DAY_SELECTED);
        }
    },

    /**
     * An internal utility method that checks whether a particular date
     * is in the current view of the calendar.
     *
     * @method _isDateVisible
     * @param {Date} oDate The date corresponding to a specific date cell.
     * @private
     * @return {boolean} Returns true if the given date is in the current 
     * view of the calendar.
     */
    _isDateVisible : function (oDate) {
      var minDate = this.get("date"),
          maxDate = ydate.addMonths(minDate, this._paneNumber - 1),
          oDateTime = this._normalizeDate(oDate).getTime();
          
      if (minDate.getTime() <= oDateTime && oDateTime <= maxDate) {
          return true;
      }
      else {
          return false;
      }
    },

    /**
     * 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(","),
            val;
        for (val in elements) {
            var range = elements[val].split("-");
            if (range.length == 2 && num >= parseInt(range[0], 10) && num <= parseInt(range[1], 10)) {
                return true;
            }
            else if (range.length == 1 && (parseInt(elements[val], 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.
     */
    selectDates : function (dates) {
      if (ydate.isValidDate(dates)) {
         this._addDateToSelection(dates);
      }
      else if (L.isArray(dates)) {
         this._addDatesToSelection(dates);
      }
    },

    /**
     * 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.
     */
    deselectDates : function (dates) {
      if (dates == null) {
         this._clearSelection();
      }
      else if (ydate.isValidDate(dates)) {
         this._removeDateFromSelection(dates);
      }
      else if (L.isArray(dates)) {
         this._removeDatesFromSelection(dates);
      }
    },

    /**
     * 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);
        this._renderSelectedDate(oDate);

        if (index == null) {
        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) {
        each(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();
            
            if (startTime > endTime) {
                var tempTime = startTime;
                startTime = endTime;
                endTime = tempTime + timezoneDifference;
            }
            else {
                endTime = endTime - timezoneDifference;
            }


        for (var time = startTime; time <= endTime; time += 86400000) {
            var 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];
               this._renderUnselectedDate(oDate);
               if (index == null) {
                 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) {
        each(datesArray, this._removeDateDromSelection);
        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();
        
        for (var 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.
     * @private
     */
    _clearSelection : function (noevent) {
        this._selectedDates = {};
        this.get("contentBox").all("." + CAL_DAY_SELECTED).removeClass(CAL_DAY_SELECTED);
        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);
        
        if (!isEmpty(this._rules)) {
        var enRule = this.get("enabledDatesRule"),
            disRule = this.get("disabledDatesRule");

           for (var paneNum = 0; paneNum < this._paneNumber; paneNum++) {
             var paneDate = ydate.addMonths(this.get("date"), paneNum);
             var dateArray = ydate.listOfDatesInMonth(paneDate);
             each(dateArray, function (date) {
                var matchingRules = this._getRulesForDate(date);
                if (matchingRules.length > 0) {
                    var dateNode = this._dateToNode(date);
                    if ((enRule && !(iOf(matchingRules, enRule) >= 0)) ||
                        (disRule && (iOf(matchingRules, disRule) >= 0))) {
                            dateNode.addClass(SELECTION_DISABLED);
                        }
                        
                    if (L.isFunction(this._filterFunction)) {
                        this._storedDateCells[dateNode.get("id")] = dateNode.cloneNode(true);
                        this._filterFunction (date, dateNode, matchingRules);
                    }
                }
                },
             this);
          }
       }         
    },

    /**
     * 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);
    
        for (var paneNum = 0; paneNum < this._paneNumber; paneNum++) {
        var paneDate = ydate.addMonths(this.get("date"), paneNum);
        var dateArray = this._getSelectedDatesInMonth(paneDate);
        each(dateArray, function (date) {
            this._dateToNode(date).addClass(CAL_DAY_SELECTED);
                        },
             this);
      }
  },

    /**
     * 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);

        var 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) {
       return new Date(date.getFullYear(), date.getMonth(), 1, 12, 0, 0, 0);  
    },


    /**
     * 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;
   var 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));

        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 (var cell = 5; cell >= 0; cell--) 
           {
            pane.one("#" + pane_id + "_" + cell + "_" + (cell-5)).setContent(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");
        this._paneProperties[pane_id].daysInPrevMonth = 0;

        for (var 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;

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

        var startingCell = cutoffCol;
        if (daysInMonth == 31 && (cutoffCol <= 1)) {
          startingCell = 2;
        }
        else if (daysInMonth== 30 && cutoffCol == 0) {
          startingCell = 1;
        }
  
        for (var cell = startingCell ; cell < cutoffCol + 7; cell++) {
            pane.one("#" + pane_id + "_" + cell + "_" + (cell+30)).setContent(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;

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

        var startingCell = 0;
        if (daysInMonth == 31 && (cutoffCol <= 1)) {
          startingCell = 2;
        }
        else if (daysInMonth == 30 && cutoffCol == 0) {
          startingCell = 1;
        }
  
        for (var 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).one("h4");
    headerCell.setContent(this._updateCalendarHeader(this.get('date')));
  },

     /**
     * The handler for the change in the customRenderer attribute.
     * @method _afterCustomRendererChange
     * @private
     */ 
    _afterCustomRendererChange : function () {
        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("h4"),
        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) {
        // Initialize final output HTML string
    var calString = '',
        // Get a list of short weekdays from the internationalization package, or else use default English ones.
        weekdays = this.get('strings.very_short_weekdays') || ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"],
        // 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 = {};
        
            // 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 (var day = firstday; day <= firstday + 6; day++) {
               partials["weekday_row"] += 
                  substitute(CalendarBase.WEEKDAY_TEMPLATE,
                       {weekdayname: weekdays[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 (var row = 0; row <= 5; row++) {
          
              for (var column = 0; column <= 12; column++) {  
             
             // Compute the value of the date that needs to populate the cell
             var date = 7*row - 5 + column;

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

             // Set the calendar day class to one of three possible values
             var 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.
             var 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
      each (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;

      // Generate final output by substituting class names.
          var 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");
  
       // Hide the pane before making DOM changes to speed them up
         pane.setStyle("visibility", "hidden");
  
       // Go through all columns, and flip their visibility setting based on whether they are within the unmasked range.
         for (var column = 0; column <= 12; column++) {
        var 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:
          var curCell = pane.one("#" + paneId + "_0_30");
          if (daysInMonth >= 30) {
            curCell.setContent("30");
            curCell.removeClass(CAL_NEXTMONTH_DAY).addClass(CAL_DAY);
          }
          else {
            curCell.setContent("&nbsp;");
            curCell.addClass(CAL_NEXTMONTH_DAY).addClass(CAL_DAY);
          }
          break;
         case 1:
          var curCell = pane.one("#" + paneId + "_1_31");
          if (daysInMonth >= 31) {
            curCell.setContent("31");
            curCell.removeClass(CAL_NEXTMONTH_DAY).addClass(CAL_DAY);
          }
          else {
            curCell.setContent("&nbsp;");
            curCell.removeClass(CAL_DAY).addClass(CAL_NEXTMONTH_DAY);
          }
          break;
         case 6:
          var curCell = pane.one("#" + paneId + "_6_29");
          if (daysInMonth >= 29) {
            curCell.setContent("29");
            curCell.removeClass(CAL_NEXTMONTH_DAY).addClass(CAL_DAY);
          }
          else {
            curCell.setContent("&nbsp;");
            curCell.removeClass(CAL_DAY).addClass(CAL_NEXTMONTH_DAY);
          }
          break;
         case 7:
          var curCell = pane.one("#" + paneId + "_7_30");
          if (daysInMonth >= 30) {
            curCell.setContent("30");
            curCell.removeClass(CAL_NEXTMONTH_DAY).addClass(CAL_DAY);
          }
          else {
            curCell.setContent("&nbsp;");
            curCell.removeClass(CAL_DAY).addClass(CAL_NEXTMONTH_DAY);
          }
          break;
         case 8:
          var curCell = pane.one("#" + paneId + "_8_31");
          if (daysInMonth >= 31) {
            curCell.setContent("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)}), 
                      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;
        
        // 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 () {
          var 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.
        var 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
  },

   /**
    * 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 yui3-calendar-main-grid">' +
                              '{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}">' + 
                           '<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}">' + 
                          '<h4>' +
                              '{calheader}' +
                          '</h4>' +
                         '</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}">' + 
                           '{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}">' + 
                 '{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}">{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}">' +
                       '{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: {
    /**
     * 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');
        }
      }
      },

    /**
     * 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 (val) {
            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 : {
            value: {},
            setter: function (val) {
                this._rules = val.rules;
                this._filterFunction = val.filterFunction;
            }
        }
  }
  
});


}, '3.4.1' ,{requires:['widget', 'substitute', 'datatype-date', 'datatype-date-math', 'cssgrids'], lang:['en', 'ja', 'ru']});
