/**
 * Copyright 2008 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @fileoverview This file contains the core implementation for CalVis.
 * @author api.austin@google.com (Austin Chau)
 */

// Namespace to protect this library from conflicting with external
var calvis = calvis || {};

// Constant strings for month labels
calvis.MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 
    'Oct', 'Nov', 'Dec'];

// Constant strings for day labels
calvis.DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

// CSS IDs
calvis.monthViewButtonId = 'monthViewButton';
calvis.weekViewButtonId = 'weekViewButton';
calvis.todayButtonId = 'todayButton';
calvis.prevWeekButtonId = 'prevWeek';
calvis.nextWeekButtonId = 'nextWeek';
calvis.prevMonthButtonId = 'prevMonth';
calvis.nextMonthButtonId = 'nextMonth';
calvis.yearChooserId = 'yearChooser';
calvis.weekDurationId = 'weekDuration';
calvis.yearHolderId = 'yearHolder';
calvis.monthHolderId = 'monthHolder';
calvis.loginLinkId = 'loginLink';

// CSS classes
calvis.columnHeadingClass = 'columnHeading';
calvis.weekViewCellClass = 'weekViewCell';
calvis.monthViewCellClass = 'monthViewCell';
calvis.contentCellClass = 'contentCell';
calvis.eventMouseOutClass = 'eventMouseOut';
calvis.eventMouseOverClass = 'eventMouseOver';

// string labels
calvis.AppStringLabel = 'Generic-Calendar-Container-1.0';
calvis.loginLabel = 'Log in';
calvis.logoutLabel = 'Log out';
calvis.monthViewButtonLabel = 'Month';
calvis.weekViewButtonLabel = 'Week';
calvis.todayButtonLabel = 'Today';
calvis.prevWeekButtonLabel = '<<';
calvis.nextWeekButtonLabel = '>>';
calvis.prevMonthButtonLabel = '<<';
calvis.nextMonthButtonLabel = '>>';
calvis.loadingLabel = ' loading... ';
calvis.moreLabel = 'more';

/**
 * Constructor to create an instance of the Calendar Container
 */   
calvis.Calendar = function() {  

  // Fix quarks for IE
  calvis.fixIE();
  
  // Create an instance of the Google Data service object
  this.calendarService = 
      new google.gdata.calendar.CalendarService(calvis.AppStringLabel);
  
  // CSS IDs to position key container components
  this.calendarBodyId = null;
  this.navControlId = null;
  this.viewControlId = null;
  this.loginControlId = null;
  this.statusControlId = null;
  this.eventDisplayId = null;

  // The callback method to be invoked when event is triggered to be displayed
  this.eventCallback = null;

  // The calendar ID to be displayed from this container
  this.calId = null;

  // The visibility of this calendar, private or public
  this.visibility = null;
  
  // The pivot date for the current navigation view
  this.currentPivotDate = new Date();

  // The current view mode of the container
  this.currentViewMode = null;
};

/**
 * Set the CSS ID for the calendar body.  Calendar body is the grid that
 * represents where the date cells are contained.
 * @param {string} cssId The CSS ID of the calendar body.
 */   
calvis.Calendar.prototype.setCalendarBody = function(cssId) {
  this.calendarBodyId = cssId;
};

/**
 * Set the CSS ID for the navigation control.  Navgivation control allows the
 * user to navigate through the calendar
 * @param {string} cssId The CSS ID of the navigation control.
 */  
calvis.Calendar.prototype.setNavControl = function(cssId) {
  this.navControlId = cssId;
};

/**
 * Set the CSS ID for the view control.  View control allows the
 * user to switch between calendar views (month or week)
 * @param {string} cssId The CSS ID of the view control.
 */  
calvis.Calendar.prototype.setViewControl = function(cssId) {
  this.viewControlId = cssId;
};

/**
 * Set the CSS ID for the status control.  Status control is where
 * the calendar status such as "loading" or error messages are displayed
 * @param {string} cssId The CSS ID of the status control.
 */  
calvis.Calendar.prototype.setStatusControl = function(cssId) {
  this.statusControlId = cssId;
};

/**
 * Set the method that will be invoked when a trigger (click or mouseover)
 * occurs. 
 * @param {string} trigger The event that triggers the callback, can be 
 *   "click" or "mouseover"
 * @param {Function} callback This callback will be invoked when the trigger occurs. 
 *   The callback will be passed one argument of type 
 *   "google.gdata.calendar.CalendarEventEntry", documentation -
 *   http://code.google.com/apis/gdata/jsdoc/1.1/google/gdata/calendar/CalendarEventEntry.html.
 *   This callback should use this event object and access its details for display
 */  
calvis.Calendar.prototype.setEventCallback = 
    function(trigger, callback) {
  this.eventTrigger = trigger;
  this.eventCallback = callback;
};

/**
 * Set the Google Calendar ID of a public calendar.
 * @param {string} calId The ID of a public Google Calendar.
 */  
calvis.Calendar.prototype.setPublicCalendar = function(calId) {
  this.visibility = 'public';
  this.calId = calId;
};

/**
 * Set the Google Calendar ID of a private calendar.
 * @param {string} calId The ID of a private Google Calendar.
 */  
calvis.Calendar.prototype.setPrivateCalendar = function(calId) {
  this.visibility = 'private';
  this.calId = calId;
};

/**
 * Set the CSS ID for the login control.  Login control is where the 
 * login mechanism will be placed, to indicate "log in" or "log out".
 * This control is only necessary for private calendar.
 * @param {string} cssId The CSS ID of the login control.
 */  
calvis.Calendar.prototype.setLoginControl = function(cssId) {
  this.loginControlId = cssId;
};

/**
 * This method should be called after all the CSS IDs and other properties
 * are set and ready to be displayed. 
 */ 
calvis.Calendar.prototype.render = function() {
  var calendar = this;

  calendar.createFittingSpan();

  // global init

  if (calendar.visibility == 'private') {
    calendar.initLoginControl();
  }
 
  // default view is month view
  calendar.initViewControl();
  
  if (!calendar.currentViewMode) {
    calendar.setDefaultView('month');
  }

  calendar.currentViewMode();

};  

/**
 * Set the default view for CalVis initial rendering.
 * @param {string} view The View setting, either "month" or "week".
 */  
calvis.Calendar.prototype.setDefaultView = function(view) {
  switch(view) {
    case 'week':
      this.currentViewMode = this.initWeekView;
      break;
    case 'month':
      this.currentViewMode = this.initMonthView;
      break;
  }
};

/**
 * Retrieve the feed URL that will be used for token request 
 * and also for data request of your calendar.
 */ 
calvis.Calendar.prototype.getFeedUrl = function() {
  return ['http://www.google.com/calendar/feeds/', 
      this.calId, '/', this.visibility, '/full'].join('');
};

/**
 * Check to see if here is a valid token available for the 
 * private calendar.
 * @return {boolean} True/false to indicate if there is a valid token.
 */  
calvis.Calendar.prototype.hasValidToken = function(scope) {

  var success = false;

  if (google.accounts.user.checkLogin(scope)) {

    success = true;

    google.accounts.user.getInfo(function(data) {
      var target = eval(data.currentTarget.responseText);
      //console.log(target);      
      success = true;
    });
  } 

  return success;
};

/**
 * Initialize the login control.
 */  
calvis.Calendar.prototype.initLoginControl = function() {

  var calendar = this;

  var loginHtml = [];

  loginHtml.push('<a id="');
  loginHtml.push(calvis.loginLinkId);
  loginHtml.push('" href="">')
  loginHtml.push(calvis.loginLabel);
  loginHtml.push('</a>');

  var loginElement = jQuery(loginHtml.join(''));

  var scope = calendar.getFeedUrl();

  if (!calendar.hasValidToken(scope)) {
    loginElement.html(calvis.loginLabel);
  } else {
    loginElement.html(calvis.logoutLabel);
  }

  jQuery('#' + calendar.loginControlId).html(loginElement);

  loginElement.bind('click', function(event) {

    event.preventDefault();         

    if (!calendar.hasValidToken(scope)) {
      google.accounts.user.login(scope);     
    } else {
      google.accounts.user.logout();
      loginElement.html(calvis.loginLabel); 
    }
  });  

};

/**
 * Initialize the view control.
 */ 
calvis.Calendar.prototype.initViewControl = function() {

  var calendar = this;

  var viewControlHtml = [];

  viewControlHtml.push('<input type="button" id="');
  viewControlHtml.push(calvis.weekViewButtonId);
  viewControlHtml.push('" value="');
  viewControlHtml.push(calvis.weekViewButtonLabel);
  viewControlHtml.push('">');

  viewControlHtml.push('&nbsp;');
  
  viewControlHtml.push('<input type="button" id="');
  viewControlHtml.push(calvis.monthViewButtonId);
  viewControlHtml.push('" value="');
  viewControlHtml.push(calvis.monthViewButtonLabel);
  viewControlHtml.push('">');
  
  jQuery('#' + this.viewControlId).empty();
  jQuery('#' + this.viewControlId).html(viewControlHtml.join(''));

  // append click handler to view control buttons

  jQuery('#' + calvis.monthViewButtonId).click(function() {
    calendar.currentViewMode = calendar.initMonthView;
    calendar.currentViewMode();    
  });

  jQuery('#' + calvis.weekViewButtonId).click(function() {
    calendar.currentViewMode = calendar.initWeekView;
    calendar.currentViewMode();
  });

};

/**
 * Update the week duration display of the Navigation control.
 */ 
calvis.Calendar.prototype.updateWeekDuration = function() {
  var weekStart = calvis.getFirstDateOfWeek(this.currentPivotDate);
  var weekEnd = new Date(weekStart);
  weekEnd.setDate(weekEnd.getDate() + 6);

  var durationString = [];
  
  durationString.push(calvis.monthString(weekStart.getMonth()));
  durationString.push(' ');
  durationString.push(weekStart.getDate());
  durationString.push(' - ');
  durationString.push(calvis.monthString(weekEnd.getMonth()));
  durationString.push(' ');
  durationString.push(weekEnd.getDate());  
  durationString.push(' ');
  durationString.push(weekEnd.getFullYear());  
  
  jQuery('#' + calvis.weekDurationId).empty()
  jQuery('#' + calvis.weekDurationId).html(durationString.join(''));
};

/**
 * Initialize week view.
 */ 
calvis.Calendar.prototype.initWeekView = function() {      
  this.initWeekNavControl();
  this.updateWeekView();
};

/**
 * Initialize week view navigation control.
 */ 
calvis.Calendar.prototype.initWeekNavControl = function() {

  var calendar = this;

  var navControlHtml = [];

  navControlHtml.push('<input type="button" id="');
  navControlHtml.push(calvis.todayButtonId);
  navControlHtml.push('" value="');
  navControlHtml.push(calvis.todayButtonLabel);
  navControlHtml.push('"/>');

  navControlHtml.push('&nbsp;');   
  
  navControlHtml.push('<input type="button" id="');
  navControlHtml.push(calvis.prevWeekButtonId);
  navControlHtml.push('" value="');
  navControlHtml.push(calvis.prevWeekButtonLabel);
  navControlHtml.push('"/>');

  navControlHtml.push('&nbsp;');

  navControlHtml.push('<span id="');
  navControlHtml.push(calvis.weekDurationId);
  navControlHtml.push('"></span>');

  navControlHtml.push('&nbsp;');

  navControlHtml.push('<input type="button" id="');
  navControlHtml.push(calvis.nextWeekButtonId);
  navControlHtml.push('" value="');
  navControlHtml.push(calvis.nextWeekButtonLabel);
  navControlHtml.push('"/>');

  jQuery('#' + calendar.navControlId).empty();
  jQuery('#' + calendar.navControlId).html(navControlHtml.join(''));

  calendar.updateWeekDuration();

  jQuery('#' + calvis.todayButtonId).click(function() {      
    calendar.currentPivotDate = new Date();
    calendar.currentViewMode();
  });

  jQuery('#' + calvis.prevWeekButtonId).click(function() {      
    calendar.currentPivotDate.setDate(calendar.currentPivotDate.getDate() - 7);     
    calendar.updateWeekView();
  });

  jQuery('#' + calvis.nextWeekButtonId).click(function() {
    calendar.currentPivotDate.setDate(calendar.currentPivotDate.getDate() + 7);
    calendar.updateWeekView();
  });
};

/**
 * Update the current week view base on the current pivot date.
 */ 
calvis.Calendar.prototype.updateWeekView = function() {

  this.updateWeekDuration();

  var firstDate = calvis.getFirstDateOfWeek(this.currentPivotDate);

  var lastDate = new Date(firstDate);
  lastDate.setDate(firstDate.getDate() + 6);

  var weekViewHtml = [];

  weekViewHtml.push('<table style="border-collapse: separate;">');

  weekViewHtml.push('<tr>');

  var dateHolder = new Date(firstDate);

  for (var i = 0; i < 7; i++) {

    var weekDateHeading = [dateHolder.getFullYear(), '-', 
        calvis.padZero(dateHolder.getMonth() + 1), '-', 
        calvis.padZero(dateHolder.getDate())].join('');

    var weekDateHeading = [];

    weekDateHeading.push(calvis.dayString(dateHolder.getDay()));
    weekDateHeading.push(' ');
    weekDateHeading.push(dateHolder.getMonth() + 1);
    weekDateHeading.push('/');
    weekDateHeading.push(dateHolder.getDate());


    weekViewHtml.push('<td class="');
    weekViewHtml.push(calvis.columnHeadingClass);
    weekViewHtml.push('">');
    weekViewHtml.push(weekDateHeading.join(''));
    weekViewHtml.push('</td>');      

    dateHolder.setDate(dateHolder.getDate() + 1);
  }
  weekViewHtml.push('</tr>');

  var dateHolder = new Date(firstDate);

  weekViewHtml.push('<tr>');
  for (var i = 0; i < 7; i++) {
    var dateCellId = [dateHolder.getFullYear(), 
        calvis.padZero(dateHolder.getMonth() + 1), 
        calvis.padZero(dateHolder.getDate())].join('');
    var dateContentId = ['content', dateHolder.getFullYear(), 
        calvis.padZero(dateHolder.getMonth() + 1), 
        calvis.padZero(dateHolder.getDate())].join('');

    weekViewHtml.push('<td>');

    var dateHtml = [];
    dateHtml.push('<div class="');
    dateHtml.push(calvis.weekViewCellClass);
    dateHtml.push('" id=');
    dateHtml.push(dateCellId);
    dateHtml.push('>');
    //dateHtml.push(calvis.dayString(i));

    dateHtml.push('<div class="');
    dateHtml.push(calvis.contentCellClass);
    dateHtml.push('" id=');
    dateHtml.push(dateContentId);
    dateHtml.push('>');
    dateHtml.push('</div>');      
    dateHtml.push('</div>');
    
    weekViewHtml.push(dateHtml.join(''));

    weekViewHtml.push('</td>');
    
    dateHolder.setDate(dateHolder.getDate() + 1);
  }

  weekViewHtml.push('</tr>');
  weekViewHtml.push('</table>');
 
  jQuery('#' + this.calendarBodyId).empty();
  jQuery('#' + this.calendarBodyId).html(weekViewHtml.join(''));

  this.overlayGData(firstDate, lastDate);
};

calvis.Calendar.prototype.setYearChooser = function(year) {
  
  if(jQuery('#' + calvis.yearChooserId).get(0)!=null){
  var options =  jQuery('#' + calvis.yearChooserId).get(0).options;    
  
  for (var i = 0; i < options.length ; i++) {
    var option = options[i];
    
    if (option.value == year) {
      jQuery('#' + calvis.yearChooserId).get(0).selectedIndex = i;
      break;
    }
  }
  }
  else{
  	//jQuery('#' + calvis.yearChooserId).get(0).selectedIndex = 9;	
  }
};

/**
 * Populate the options for year chooser of month view.
 */ 
calvis.Calendar.prototype.createYearChooser = function() {
  
  var calendar = this;

  var year = 2000;
  var html = [];

  html.push('<select id="');
  html.push(calvis.yearChooserId);
  html.push('">');

  for (var i = 0; i < 20; i++)
  {
    var now = new Date();  
    if (year == now.getFullYear()) {
      html.push('<option selected="yes" value="');
    } else {
      html.push('<option value="');
    }

    html.push(year);
    html.push('">');
    html.push(year);
    html.push('</option>');
    year++;
  }

  html.push('</select>');

  html = html.join('');

  var element = jQuery(html).change(function() {
    calendar.currentPivotDate.setFullYear(
        $(this).get(0).options[$(this).get(0).selectedIndex].value);
    calendar.updateMonthView();
  });

  return element;
};

/**
 * Initialize the month view.
 */ 
calvis.Calendar.prototype.initMonthView = function() {
  this.initMonthNavControl();
  this.updateMonthView();
};

/**
 * Initialize the month view navigation control.
 */ 
calvis.Calendar.prototype.initMonthNavControl = function() {

  var calendar = this;

  var navControlHtml = [];

  navControlHtml.push('<input type="button" id="');
  navControlHtml.push(calvis.todayButtonId);
  navControlHtml.push('" value="');
  navControlHtml.push(calvis.todayButtonLabel);
  navControlHtml.push('">');

  navControlHtml.push('&nbsp;'); 
  
  navControlHtml.push('<input type="button" id="');
  navControlHtml.push(calvis.prevMonthButtonId);
  navControlHtml.push('" value="');
  navControlHtml.push(calvis.prevMonthButtonLabel);
  navControlHtml.push('">');
  
  navControlHtml.push('&nbsp;');
  
  navControlHtml.push('<span id="');
  navControlHtml.push(calvis.yearHolderId);
  navControlHtml.push('"/>');

  navControlHtml.push('&nbsp;');
  
  navControlHtml.push('<span id="');
  navControlHtml.push(calvis.monthHolderId);
  navControlHtml.push('">');
  navControlHtml.push(calvis.monthString(this.currentPivotDate.getMonth()));
  navControlHtml.push('</span>');
  
  navControlHtml.push('&nbsp;');

  navControlHtml.push('<input type="button" id="');
  navControlHtml.push(calvis.nextMonthButtonId);
  navControlHtml.push('" value="');
  navControlHtml.push(calvis.nextMonthButtonLabel);
  navControlHtml.push('">');

  jQuery('#' + calendar.navControlId).empty();
  jQuery('#' + calendar.navControlId).html(navControlHtml.join(''));
  jQuery('#' + calvis.yearHolderId).html(this.createYearChooser());

  jQuery('#' + calvis.todayButtonId).click(function() {      
    calendar.currentPivotDate = new Date();
    calendar.currentViewMode();
  });

  jQuery('#' + calvis.prevMonthButtonId).click(function() {

    var currentMonth = calendar.currentPivotDate.getMonth();
    calendar.currentPivotDate.setMonth(currentMonth - 1);
    calendar.currentPivotDate.setDate(1);                     

    calendar.updateMonthView();
  });

  jQuery('#' + calvis.nextMonthButtonId).click(function() {      
    
    var currentMonth = calendar.currentPivotDate.getMonth();
    calendar.currentPivotDate.setMonth(currentMonth + 1);
    calendar.currentPivotDate.setDate(1);

    calendar.updateMonthView();
  });
};

/**
 * Update the month view navigation control relative to the current
 * pivot date.
 */ 
calvis.Calendar.prototype.updateMonthNavControl = function() {  
  this.setYearChooser(this.currentPivotDate.getFullYear());
  jQuery('#' + calvis.monthHolderId).empty();
  jQuery('#' + calvis.monthHolderId).html(calvis.monthString(
      this.currentPivotDate.getMonth()));
};

/**
 * Initialize the month view grid.  The month view grid is the grid
 * rectangular boxes that represents the dates of a given month.
 */ 
calvis.Calendar.prototype.initMonthGrid = function() {
  var monthViewHtml = [];

  monthViewHtml.push('<table style="border-collapse: separate;">');    
  monthViewHtml.push('<tr>');
  for (var i=0; i<7; i++ )
  {
      monthViewHtml.push('<td class="');
      monthViewHtml.push(calvis.columnHeadingClass);
      monthViewHtml.push('">');

      monthViewHtml.push(calvis.dayString(i));
      monthViewHtml.push('</td>');      
  }
  monthViewHtml.push('</tr>');

  var index = 0;

  for (var i=0; i<6; i++ )
  {
    monthViewHtml.push('<tr>');
    for (var j=0; j<7 ;j++ )
    {
      monthViewHtml.push('<td id="date' + index + '">');
      monthViewHtml.push('&nbsp;');
      monthViewHtml.push('</td>');
      index++;
    }
    monthViewHtml.push('</tr>');
  }

  monthViewHtml.push('</table>');

  jQuery('#' + this.calendarBodyId).empty();
  jQuery('#' + this.calendarBodyId).html(monthViewHtml.join(''));
};

/**
 * Update month view relative to the current pivot date.
 */ 
calvis.Calendar.prototype.updateMonthView = function() {

  this.initMonthGrid();
  this.updateMonthNavControl();

  // fill the month dates 

  var firstDate = (new Date(this.currentPivotDate));
  firstDate.setDate(1);

  var daysInMonth = calvis.getDaysInMonth(firstDate);
  
  var dateHolder = new Date(firstDate);

  var startIndex = firstDate.getDay();      

  for (var i = 0; i < daysInMonth; i++ ) {

    var dateCellId = [dateHolder.getFullYear(), 
        calvis.padZero(dateHolder.getMonth() + 1), 
        calvis.padZero(dateHolder.getDate())].join('');
    var dateContentId = ['content', dateHolder.getFullYear(), 
        calvis.padZero(dateHolder.getMonth() + 1), 
        calvis.padZero(dateHolder.getDate())].join('');

    var dateHtml = [];

    dateHtml.push('<div class="');
    dateHtml.push(calvis.monthViewCellClass);
    dateHtml.push('" id=');
    dateHtml.push(dateCellId);
    dateHtml.push('>');

    dateHtml.push('<div class="');
    dateHtml.push(calvis.contentCellClass);
    dateHtml.push('" id=');
    dateHtml.push(dateContentId);
    dateHtml.push('>');
    
    dateHtml.push(dateHolder.getDate());

    dateHtml.push('</div>');      
    dateHtml.push('</div>');
    
    jQuery('#date' + startIndex).html(dateHtml.join(''));

    dateHolder.setDate(dateHolder.getDate() + 1);
    startIndex++;
  }  

  var lastDate = new Date(firstDate);
  lastDate.setDate(daysInMonth);

  this.overlayGData(firstDate, lastDate);
};

/**
 * This is an internal callback that is invoked when the
 * JavaScript client library returns the event data.  It will
 * then display the event title on its corresponding date cell.
 * @param {string} id The date cell.
 * @param {google.gdata.calendar.CalendarEventEntry} 
 *   A Google Data calendar event object.
 */ 
calvis.Calendar.prototype.appendEvent = function(id, event) {

  var calendar = this;

  var title = event.getTitle().getText();    
  var eventId = event.getId().getValue();

  var cell = jQuery('#' + id);

  // special case, this is a date element that doesn't exist in the current 
  // view.  This is most likely that an event that spans across 
  // two months.  This script currently doesn't handle multi-day events yet.
  if (cell.length == 0) {
    return;
  }
  
  var cellHeight = jQuery('.' + calvis.contentCellClass).parent().height();
  var cellWidth = cell.width();
  var contentFontHeight = parseInt(cell.css('font-size'));

  var currentContentHeight = cell.height();
  
  var bottomMargin = 5;

  if (cellHeight < currentContentHeight + contentFontHeight + bottomMargin) {
    // The contentCell is overflowing, too many events
    if (cell.get(0).lastChild.innerHTML == calvis.moreLabel) {
      // Can return right away, there is already a "more" link
      return;
    }
    
    jQuery(cell.get(0).lastChild).empty();
    jQuery(cell.get(0).lastChild).remove();

    var moreHtml = [];
    moreHtml.push('<div align="right" class="');
    moreHtml.push(calvis.eventMouseOutClass);
    moreHtml.push('">');
    moreHtml.push(calvis.moreLabel);
    moreHtml.push('</div>');

    var moreDiv = jQuery(moreHtml.join(''));

    moreDiv.bind('click', function() {
      calendar.currentPivotDate = event.getTimes()[0].getStartTime().getDate();
      calendar.currentControlView = calendar.initWeekView;
      calendar.initWeekView();
    });

    moreDiv.bind('mouseover', function() {
      moreDiv.removeClass(calvis.eventMouseOutClass);
      moreDiv.addClass(calvis.eventMouseOverClass); 
    });

    moreDiv.bind('mouseout', function() {
      moreDiv.removeClass(calvis.eventMouseOverClass); 
      moreDiv.addClass(calvis.eventMouseOutClass); 
    });

    cell.append(moreDiv);    

  } else {

    var eventHtml = [];
    eventHtml.push('<div id="');
    eventHtml.push(eventId);
    eventHtml.push('" class="');
    eventHtml.push(calvis.eventMouseOutClass);
    eventHtml.push('">');
    eventHtml.push(calendar.fitText('&nbsp;' + title, cellWidth));
    eventHtml.push('</div>');

    var eventDiv = jQuery(eventHtml.join(''));

    eventDiv.bind(this.eventTrigger, function() {
      if (calendar.eventCallback) {
        calendar.eventCallback(event);
      } else {
        calendar.defaultEventCallback(event);
      }      
    });

    eventDiv.bind('mouseover', function() {
      eventDiv.removeClass(calvis.eventMouseOutClass);
      eventDiv.addClass(calvis.eventMouseOverClass); 
    });

    eventDiv.bind('mouseout', function() {
      eventDiv.removeClass(calvis.eventMouseOverClass); 
      eventDiv.addClass(calvis.eventMouseOutClass); 
    });

    cell.append(eventDiv);
  }
};

/**
 * When the date cell is overflowed because of too many events for 
 * a particular date, a "more" link is created.
 * @param {string} id The date cell ID which this more link belongs to.
 */ 
calvis.Calendar.prototype.createMoreMenu = function(id) {

  var calendar = this;

  var moreHtml = [];

  moreHtml.push('<select id="');
  moreHtml.push(id);
  moreHtml.push('">');
  moreHtml.push('</select>');

  var moreDiv = jQuery(moreHtml.join(''));

  moreDiv.bind('change', function() {
    if (calendar.eventCallback) {
      calendar.eventCallback(event);
    } else {
      calendar.defaultEventCallback(event);
    }       
  });

  return moreDiv;
}

/**
 * This creates a dummy span that is used to calculate whether 
 * a date cell is overflowing.
 */ 
calvis.Calendar.prototype.createFittingSpan = function() {
  var span = jQuery('<span/>');
  span.addClass(calvis.contentCellClass);
  span.css({'visibility': 'hidden'});
  jQuery(document.body).append(span);
  this.fittingSpan = span;
}

/**
 * This method overlays the event data between two dates and append
 * these events on their appropriate date cells.
 * @param {Date} startDate The start date that will be used for data query.
 * @param {Date} endDate The end date that will be used for data query.
 */ 
calvis.Calendar.prototype.overlayGData = function(startDate, endDate) {
  
  var calendar = this;

  var feedUri = calendar.getFeedUrl();
  
  // if it is a private account and doesn't have a valid token
  if (calendar.visibility == 'private' && !calendar.hasValidToken(feedUri)) {
    jQuery('#' + this.statusControlId).html(
        'you must log in to access private calendar');
    return;
  }

  endDate.setDate(endDate.getDate() + 1);

  var startDateTime = new google.gdata.DateTime(startDate, true);
  var endDateTime = new google.gdata.DateTime(endDate, true);

  var query = new google.gdata.calendar.CalendarEventQuery(feedUri);

  query.setMinimumStartTime(startDateTime);
  query.setMaximumStartTime(endDateTime);

  query.setMaxResults(500);
  query.setOrderBy('starttime');
  query.setSortOrder('a');
  query.setSingleEvents(true);

  var callback = function(root) {
    
    var eventEntries = root.feed.getEntries();
     
      for (var i = 0; i < eventEntries.length; i++) {
        var event = eventEntries[i];
        for (var j = 0; j < event.getTimes().length; j++ ) {
          var status = event.getEventStatus();

          if (status == google.gdata.EventStatus.VALUE_CANCELED)
            continue;

          var when = event.getTimes()[j];
          var datetime = when.getStartTime();
          var date = datetime.getDate();
          
          var id = [];           
          id.push('content');
          id.push(date.getFullYear());
          id.push(calvis.padZero(date.getMonth() + 1));
          id.push(calvis.padZero(date.getDate()));
          id = id.join('');

          calendar.appendEvent(id, event);
        }
      }
      jQuery('#' + calendar.statusControlId).html('');
  }

  jQuery('#' + calendar.statusControlId).html(calvis.loadingLabel);
  calendar.calendarService.getEventsFeed(query, callback, calvis.handleError);
};

/**
 * A helper function that gets the string representation of a day of the week
 * @param {number} offset An index between 0 and 6
 */ 
calvis.dayString = function(offset) {
  return calvis.DAYS[offset];
};

/**
 * A helper function that gets the string representation of a month of the year
 * @param {number} offset An index between 0 and 11
 */
calvis.monthString = function(offset) {
  return calvis.MONTHS[offset];
};

/**
 * A helper function that derives the number of days within a month.
 * @param {Date} pivotDate The current pivot date to locate the current month.
 */
calvis.getDaysInMonth = function(pivotDate) {
  var year = pivotDate.getFullYear();
  var month = pivotDate.getMonth();
  return 32 - (new Date(year, month, 32)).getDate();
};

/**
 * A helper function that derives the first date of a week.
 * @param {Date} pivotDate The current pivot date to locate the current week.
 */
calvis.getFirstDateOfWeek = function(pivotDate) {
  var dayOffset = pivotDate.getDay();
  return new Date(pivotDate.getFullYear(), pivotDate.getMonth(), 
      pivotDate.getDate() - dayOffset);
};

/**
 * This function fits the event title to the width of a date cell, it truncates
 * the title and replaced with "...", if necessary.
 * @param {string} text The text that is examined to fit it width to date cell.
 * @param {number} limit The maximum number of characters that is permitted.
 */
calvis.Calendar.prototype.fitText = function(text, limit) {

  this.fittingSpan.html(text);

  var currentWidth = this.fittingSpan.width();

  while (currentWidth > limit) {
    text = text.replace(/[ .]+$/, '');    
    text = text.replace(/[^\n\r ]+$/, '...');
    this.fittingSpan.html(text);
    currentWidth = this.fittingSpan.width();
  }

  return text;
};

/**
 * The is the error handler that display the error message to the status control.
 * @param {Object} e The error object.
 */
calvis.handleError = function (e) {
  google.accounts.user.logout();
  jQuery('#' + calvis.loginLinkId).html(calvis.loginLabel);
  jQuery('#' + this.statusControlId).html(e.cause.statusText);
};

/**
 * Debug method that display at the token information.
 */
calvis.getTokenInfo = function() {
  google.accounts.user.getInfo(function(data) {
    console.log(data.currentTarget.responseText);
    var target = eval(data.currentTarget.responseText);
    console.log(target);
  });
};

/**
 * A helper functions that makes sure a number is represeted in two digits
 * by padding zero in front if necessary.
 * @param {number} number The number that will be padded.
 */
calvis.padZero = function(number) {
  if(number < 10) {
    number = 0 + '' + number;
  }      
  return number;
};

/**
 * This method fixes IE specific issues
 */ 
calvis.fixIE = function() {
  if (!Array.indexOf) {
    Array.prototype.indexOf = function(arg) {

      var index = -1;
      for (var i = 0; i < this.length; i++){
        var value = this[i];
        if (value == arg) {
          index = i;
          break;
        } 
      }
      return index;
    }
  }
  
  // inject "console.log" to emulate Firefox firebug.  
  // For debugging purpose
  //window.console=null;
  if (!window.console) {
    window.console = {};
    window.console.log = function(message) {
      var body = document.getElementsByTagName('body')[0];
      var messageDiv = document.createElement('div');
      messageDiv.innerHTML = message;
      body.insertBefore(messageDiv, body.lastChild);
    }
  } 

};
