/*!
 * FullCalendar v1.4
 * http://arshaw.com/fullcalendar/
 *
 * Use fullcalendar.css for basic styling.
 * For event drag & drop, required jQuery UI draggable.
 * For event resizing, requires jQuery UI resizable.
 *
 * Copyright (c) 2009 Adam Shaw
 * Dual licensed under the MIT and GPL licenses:
 *   http://www.opensource.org/licenses/mit-license.php
 *   http://www.gnu.org/licenses/gpl.html
 *
 */
 
(function($) {


var fc = $.fullCalendar = {};
var views = fc.views = {};


/* Defaults
-----------------------------------------------------------------------------*/

var defaults = {

  // display
  defaultView: 'month',
  aspectRatio: 1.35,
  header: {
    left: 'title',
    center: '',
    right: 'today prev,next'
  },
  
  // editing
  //editable: false,
  //disableDragging: false,
  //disableResizing: false,
  
  allDayDefault: true,
  
  // event ajax
  startParam: 'start',
  endParam: 'end',
  cacheParam: '_',
  
  // time formats
  titleFormat: {
    month: 'MMMM yyyy',
    week: "MMM d[ yyyy]{ '&#8212;'[ MMM] d yyyy}",
    day: 'dddd, MMM d, yyyy'
  },
  columnFormat: {
    month: 'ddd',
    week: 'ddd M/d',
    day: 'dddd M/d'
  },
  timeFormat: { // for event elements
    '': 'h(:mm)t' // default
  },
  
  // locale
  isRTL: false,
  firstDay: 0,
  monthNames: ['January','February','March','April','May','June','July','August','September','October','November','December'],
  monthNamesShort: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
  dayNames: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
  dayNamesShort: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'],
  buttonText: {
    prev: '&nbsp;&#9668;&nbsp;',
    next: '&nbsp;&#9658;&nbsp;',
    prevYear: '&nbsp;&lt;&lt;&nbsp;',
    nextYear: '&nbsp;&gt;&gt;&nbsp;',
    today: 'today',
    month: 'month',
    week: 'week',
    day: 'day'
  },
  
  // jquery-ui theming
  theme: false,
  buttonIcons: {
    prev: 'circle-triangle-w',
    next: 'circle-triangle-e'
  },
  
  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  // print link to a page to display a printable calendar
  printLink: '',
  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  // class name for the tooltip, just in case you have more than one cal per page
  tooltip: 'tooltip'
  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
};

// right-to-left defaults
var rtlDefaults = {
  header: {
    left: 'next,prev today',
    center: '',
    right: 'title'
  },
  buttonText: {
    prev: '&nbsp;&#9658;&nbsp;',
    next: '&nbsp;&#9668;&nbsp;',
    prevYear: '&nbsp;&gt;&gt;&nbsp;',
    nextYear: '&nbsp;&lt;&lt;&nbsp;'
  },
  buttonIcons: {
    prev: 'circle-triangle-e',
    next: 'circle-triangle-w'
  }
};

// function for adding/overriding defaults
var setDefaults = fc.setDefaults = function(d) {
  $.extend(true, defaults, d);
}



/* .fullCalendar jQuery function
-----------------------------------------------------------------------------*/

$.fn.fullCalendar = function(options) {

  // method calling
  if (typeof options == 'string') {
    var args = Array.prototype.slice.call(arguments, 1),
      res;
    this.each(function() {
      var r = $.data(this, 'fullCalendar')[options].apply(this, args);
      if (res == undefined) {
        res = r;
      }
    });
    if (res != undefined) {
      return res;
    }
    return this;
  }

  // pluck the 'events' and 'eventSources' options
  var eventSources = options.eventSources || [];
  delete options.eventSources;
  if (options.events) {
    eventSources.push(options.events);
    delete options.events;
  }
  
  // first event source reserved for 'sticky' events
  eventSources.unshift([]);
  
  // initialize options
  options = $.extend(true, {},
    defaults,
    (options.isRTL || options.isRTL==undefined && defaults.isRTL) ? rtlDefaults : {},
    options
  );
  var tm = options.theme ? 'ui' : 'fc'; // for making theme classes
  
  
  this.each(function() {
  
  
    /* Instance Initialization
    -----------------------------------------------------------------------------*/
    
    // element
    var _element = this,
      element = $(this).addClass('fc'),
      content = $("<div class='fc-content " + tm + "-widget-content' style='position:relative'/>").appendTo(this); // relative for ie6
    if (options.isRTL) {
      element.addClass('fc-rtl');
    }
    if (options.theme) {
      element.addClass('ui-widget');
    }
    
    // view managing
    var date = new Date(),
      viewName, view, // the current view
      viewInstances = {};
    if (options.year != undefined) {
      date.setYear(options.year);
    }
    if (options.month != undefined) {
      date.setMonth(options.month);
    }
    if (options.date != undefined) {
      date.setDate(options.date);
    }
    
    /* View Rendering
    -----------------------------------------------------------------------------*/
    
    function changeView(v) {
      if (v != viewName) {
        fixContentSize();
        if (view) {
          if (view.eventsChanged) {
            eventsDirtyExcept(view);
            view.eventsChanged = false;
          }
          view.element.hide();
        }
        if (viewInstances[v]) {
          (view = viewInstances[v]).element.show();
          if (view.shown) {
            view.shown();
          }
        }else{
          view = viewInstances[v] = $.fullCalendar.views[v](
            $("<div class='fc-view fc-view-" + v + "'/>").appendTo(content),
            options);
        }
        if (header) {
          // update 'active' view button
          header.find('div.fc-button-' + viewName).removeClass(tm + '-state-active');
          header.find('div.fc-button-' + v).addClass(tm + '-state-active');
        }
        view.name = viewName = v;
        render();
        unfixContentSize();
      }
    }
    
    function render(inc) {
      if (_element.offsetWidth !== 0) { // visible on the screen
        if (inc || !view.date || +view.date != +date) { // !view.date means it hasn't been rendered yet
          fixContentSize();
          view.render(date, inc || 0, function(callback) {
            // dont refetch if new view contains the same events (or a subset)
            if (!eventStart || view.visStart < eventStart || view.visEnd > eventEnd) {
              fetchEvents(callback);
            }else{
              callback(events); // no refetching
            }
          });
          unfixContentSize();
          view.date = cloneDate(date);
        }
        else if (view.sizeDirty) {
          view.updateSize();
          view.rerenderEvents();
        }
        else if (view.eventsDirty) {
          // ensure events are rerendered if another view messed with them
          // pass in 'events' b/c event might have been added/removed
          view.clearEvents();
          view.renderEvents(events);
        }
        if (header) {
          // update title text
          header.find('h2.fc-header-title').html(view.title);
          
          // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          // update the print link date range
          var print_start = formatDate(view.start,'yyyyMMdd');
          var print_end = formatDate(view.end,'yyyyMMdd');
          var print_dates = '&dates='+print_start+'%2F'+print_end;
          header.find('a.fc-header-print').attr('href', options.printLink+print_dates+'&iframe');
          // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          
          // enable/disable 'today' button
          var today = new Date();
          if (today >= view.start && today < view.end) {
            header.find('div.fc-button-today').addClass(tm + '-state-disabled');
          }else{
            header.find('div.fc-button-today').removeClass(tm + '-state-disabled');
          }
        }
        view.sizeDirty = false;
        view.eventsDirty = false;
        view.trigger('viewDisplay', _element);
      }
    }
    
    // marks other views' events as dirty
    function eventsDirtyExcept(exceptView) {
      $.each(viewInstances, function() {
        if (this != exceptView) {
          this.eventsDirty = true;
        }
      });
    }
    
    // marks other views' sizes as dirty
    function sizesDirtyExcept(exceptView) {
      $.each(viewInstances, function() {
        if (this != exceptView) {
          this.sizeDirty = true;
        }
      });
    }
    
    // called when any event objects have been added/removed/changed, rerenders
    function eventsChanged() {
      view.clearEvents();
      view.renderEvents(events);
      eventsDirtyExcept(view);
    }
    
    
    
    /* Event Sources and Fetching
    -----------------------------------------------------------------------------*/
    
    var events = [],
      eventStart, eventEnd;
    
    // Fetch from ALL sources. Clear 'events' array and populate
    function fetchEvents(callback) {
      events = [];
      eventStart = cloneDate(view.visStart);
      eventEnd = cloneDate(view.visEnd);
      var queued = eventSources.length,
        sourceDone = function() {
          if (--queued == 0) {
            if (callback) {
              callback(events);
            }
          }
        }, i=0;
      for (; i<eventSources.length; i++) {
        fetchEventSource(eventSources[i], sourceDone);
      }
    }
    
    // Fetch from a particular source. Append to the 'events' array
    function fetchEventSource(src, callback) {
      var prevViewName = view.name,
        prevDate = cloneDate(date),
        reportEvents = function(a) {
          if (prevViewName == view.name && +prevDate == +date) { // protects from fast switching
            for (var i=0; i<a.length; i++) {
              normalizeEvent(a[i], options);
              a[i].source = src;
            }
            events = events.concat(a);
            if (callback) {
              callback(a);
            }
          }
        },
        reportEventsAndPop = function(a) {
          reportEvents(a);
          popLoading();
        };
      if (typeof src == 'string') {
        var params = {};
        params[options.startParam] = Math.round(eventStart.getTime() / 1000);
        params[options.endParam] = Math.round(eventEnd.getTime() / 1000);
        params[options.cacheParam] = (new Date()).getTime();
        pushLoading();
        $.getJSON(src, params, reportEventsAndPop);
      }
      else if ($.isFunction(src)) {
        pushLoading();
        src(cloneDate(eventStart), cloneDate(eventEnd), reportEventsAndPop);
      }
      else {
        reportEvents(src); // src is an array
      }
    }
    
    
    
    /* Loading State
    -----------------------------------------------------------------------------*/
    
    var loadingLevel = 0;
    
    function pushLoading() {
      if (!loadingLevel++) {
        view.trigger('loading', _element, true);
      }
    }
    
    function popLoading() {
      if (!--loadingLevel) {
        view.trigger('loading', _element, false);
      }
    }
    
    
    
    /* Public Methods
    -----------------------------------------------------------------------------*/
    
    var publicMethods = {
    
      render: render,
      changeView: changeView,
      
      //
      // Navigation
      //
      
      prev: function() {
        render(-1);
      },
      
      next: function() {
        render(1);
      },
      
      prevYear: function() {
        addYears(date, -1);
        render();
      },
      
      nextYear: function() {
        addYears(date, 1);
        render();
      },
      
      today: function() {
        date = new Date();
        render();
      },
      
      gotoDate: function(year, month, dateNum) {
        if (typeof year == 'object') {
          date = cloneDate(year); // provided 1 argument, a Date
        }else{
          if (year != undefined) {
            date.setYear(year);
          }
          if (month != undefined) {
            date.setMonth(month);
          }
          if (dateNum != undefined) {
            date.setDate(dateNum);
          }
        }
        render();
      },
      
      incrementDate: function(years, months, days) {
        if (years != undefined) {
          addYears(date, years);
        }
        if (months != undefined) {
          addMonths(date, months);
        }
        if (days != undefined) {
          addDays(date, days);
        }
        render();
      },
      
      //
      // Event Manipulation
      //
      
      updateEvent: function(event) { // update an existing event
        var i, len = events.length, e,
          startDelta = event.start - event._start,
          endDelta = event.end ?
            (event.end - (event._end || view.defaultEventEnd(event))) // event._end would be null if event.end
            : 0;                                                      // was null and event was just resized
        for (i=0; i<len; i++) {
          e = events[i];
          if (e._id == event._id && e != event) {
            e.start = new Date(+e.start + startDelta);
            if (event.end) {
              if (e.end) {
                e.end = new Date(+e.end + endDelta);
              }else{
                e.end = new Date(+view.defaultEventEnd(e) + endDelta);
              }
            }else{
              e.end = null;
            }
            e.title = event.title;
            e.url = event.url;
            e.allDay = event.allDay;
            e.className = event.className;
            e.editable = event.editable;
            normalizeEvent(e, options);
          }
        }
        normalizeEvent(event, options);
        eventsChanged();
      },
      
      renderEvent: function(event, stick) { // render a new event
        normalizeEvent(event, options);
        if (!event.source) {
          if (stick) {
            (event.source = eventSources[0]).push(event);
          }
          events.push(event);
        }
        eventsChanged();
      },
      
      removeEvents: function(filter) {
        if (!filter) { // remove all
          events = [];
          // clear all array sources
          for (var i=0; i<eventSources.length; i++) {
            if (typeof eventSources[i] == 'object') {
              eventSources[i] = [];
            }
          }
        }else{
          if (!$.isFunction(filter)) { // an event ID
            var id = filter + '';
            filter = function(e) {
              return e._id == id;
            };
          }
          events = $.grep(events, filter, true);
          // remove events from array sources
          for (var i=0; i<eventSources.length; i++) {
            if (typeof eventSources[i] == 'object') {
              eventSources[i] = $.grep(eventSources[i], filter, true);
            }
          }
        }
        eventsChanged();
      },
      
      clientEvents: function(filter) {
        if ($.isFunction(filter)) {
          return $.grep(events, filter);
        }
        else if (filter) { // an event ID
          filter += '';
          return $.grep(events, function(e) {
            return e._id == filter;
          });
        }
        return events; // else, return all
      },
      
      rerenderEvents: function() {
        view.rerenderEvents(); 
      },
      
      //
      // Event Source
      //
    
      addEventSource: function(source) {
        eventSources.push(source);
        fetchEventSource(source, function() {
          eventsChanged();
        });
      },
    
      removeEventSource: function(source) {
        eventSources = $.grep(eventSources, function(src) {
          return src != source;
        });
        // remove all client events from that source
        events = $.grep(events, function(e) {
          return e.source != source;
        });
        eventsChanged();
      },
      
      refetchEvents: function() {
        fetchEvents(eventsChanged);
      }
      
    };
    
    $.data(this, 'fullCalendar', publicMethods);
    
    
    
    /* Header
    -----------------------------------------------------------------------------*/
    
    var header,
      sections = options.header;
    if (sections) {
      header = $("<table class='fc-header'/>")
        .append($("<tr/>")
          .append($("<td class='fc-header-left'/>").append(buildSection(sections.left)))
          .append($("<td class='fc-header-center'/>").append(buildSection(sections.center)))
          .append($("<td class='fc-header-right'/>").append(buildSection(sections.right))))
        .prependTo(element);
    }
    function buildSection(buttonStr) {
      if (buttonStr) {
        var tr = $("<tr/>");
        $.each(buttonStr.split(' '), function(i) {
          if (i > 0) {
            tr.append("<td><span class='fc-header-space'/></td>");
          }
          var prevButton;
          $.each(this.split(','), function(j, buttonName) {
            if (buttonName == 'title') {
              tr.append("<td><h2 class='fc-header-title'/></td>");
              if (prevButton) {
                prevButton.addClass(tm + '-corner-right');
              }
              prevButton = null;
              
            // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            // print button/link
            }else if (buttonName == 'print') {
              // set the mode to print
              var mode = 'MONTH';
              switch(options.defaultView){
                case 'month':
                  mode = 'MONTH';
                  break;
                case 'basicWeek':
                  mode = 'WEEK';
                  break;
                case 'basicDay':
                  mode = 'DAY';
                  break;
                case 'agendaWeek':
                  mode = 'WEEK';
                  break;
                case 'agendaDay':
                  mode = 'DAY';
                  break;
              }
              // build the link
              options.printLink = options.printLink+"&mode="+mode;
              // use new window to display the print options
              var new_win = ' onclick="NewWindow(this.href,\'gcalprint\',\'800\',\'600\',\'yes\',\'yes\',\'no\',\'no\',\'no\',\'no\',\'no\',\'no\');return false;"';
              // add the link
              tr.append("<td><a class='fc-header-print' href='"+options.printLink+"' title='Print This Calendar'"+new_win+">Print</a></td>");
              // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
              
            }else{
              var buttonClick;
              if (publicMethods[buttonName]) {
                buttonClick = publicMethods[buttonName];
              }
              else if (views[buttonName]) {
                buttonClick = function() {
                  button.removeClass(tm + '-state-hover');
                  changeView(buttonName)
                };
              }
              if (buttonClick) {
                if (prevButton) {
                  prevButton.addClass(tm + '-no-right');
                }
                var button,
                  icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null,
                  text = smartProperty(options.buttonText, buttonName);
                if (icon) {
                  button = $("<div class='fc-button-" + buttonName + " ui-state-default'>" +
                    "<a><span class='ui-icon ui-icon-" + icon + "'/></a></div>");
                }
                else if (text) {
                  button = $("<div class='fc-button-" + buttonName + " " + tm + "-state-default'>" +
                    "<a><span>" + text + "</span></a></div>");
                }
                if (button) {
                  button
                    .click(function() {
                      if (!button.hasClass(tm + '-state-disabled')) {
                        buttonClick();
                      }
                    })
                    .mousedown(function() {
                      button
                        .not('.' + tm + '-state-active')
                        .not('.' + tm + '-state-disabled')
                        .addClass(tm + '-state-down');
                    })
                    .mouseup(function() {
                      button.removeClass(tm + '-state-down');
                    })
                    .hover(
                      function() {
                        button
                          .not('.' + tm + '-state-active')
                          .not('.' + tm + '-state-disabled')
                          .addClass(tm + '-state-hover');
                      },
                      function() {
                        button
                          .removeClass(tm + '-state-hover')
                          .removeClass(tm + '-state-down');
                      }
                    )
                    .appendTo($("<td/>").appendTo(tr));
                  if (prevButton) {
                    prevButton.addClass(tm + '-no-right');
                  }else{
                    button.addClass(tm + '-corner-left');
                  }
                  prevButton = button;
                }
              }
            }
          });
          if (prevButton) {
            prevButton.addClass(tm + '-corner-right');
          }
        });
        return $("<table/>").append(tr);
      }
    }
    
    
    
    /* Resizing
    -----------------------------------------------------------------------------*/
    
    var elementWidth,
      contentSizeFixed = false,
      resizeCnt = 0;
    
    function fixContentSize() {
      if (!contentSizeFixed) {
        contentSizeFixed = true;
        content.css({
          overflow: 'hidden',
          height: Math.round(content.width() / options.aspectRatio)
        });
        // TODO: previous action might have caused scrollbars
        // which will make the window width more narrow, possibly changing the aspect ratio
      }
    }
    
    function unfixContentSize() {
      if (contentSizeFixed) {
        content.css({
          overflow: 'visible',
          height: ''
        });
        if ($.browser.msie && ($.browser.version=='6.0' || $.browser.version=='7.0')) {
          // in IE6/7 the inside of the content div was invisible
          // bizarre hack to get this work... need both lines
          content[0].clientHeight;
          content.hide().show();
        }
        contentSizeFixed = false;
      }
    }
    
    $(window).resize(function() {
      if (!contentSizeFixed && view.date) { // view.date means the view has been rendered
        var rcnt = ++resizeCnt; // add a delay
        setTimeout(function() {
          if (rcnt == resizeCnt && !contentSizeFixed) {
            var newWidth = element.width();
            if (newWidth != elementWidth) {
              elementWidth = newWidth;
              fixContentSize();
              view.updateSize();
              unfixContentSize();
              view.rerenderEvents(true);
              sizesDirtyExcept(view);
              view.trigger('windowResize', _element);
            }
          }
        }, 200);
      }
    });
    
    
    // let's begin...
    changeView(options.defaultView);
    elementWidth = element.width();
  
  });
  
  return this;
  
};



/* Important Event Utilities
-----------------------------------------------------------------------------*/

var fakeID = 0;

function normalizeEvent(event, options) {
  event._id = event._id || (event.id == undefined ? '_fc' + fakeID++ : event.id + '');
  if (event.date) {
    if (!event.start) {
      event.start = event.date;
    }
    delete event.date;
  }
  event._start = cloneDate(event.start = parseDate(event.start));
  event.end = parseDate(event.end);
  if (event.end && event.end <= event.start) {
    event.end = null;
  }
  event._end = event.end ? cloneDate(event.end) : null;
  if (event.allDay == undefined) {
    event.allDay = options.allDayDefault;
  }
  if (event.className) {
    if (typeof event.className == 'string') {
      event.className = event.className.split(/\s+/);
    }
  }else{
    event.className = [];
  }
}


/* Grid-based Views: month, basicWeek, basicDay
-----------------------------------------------------------------------------*/

setDefaults({
  weekMode: 'fixed'
});

views.month = function(element, options) {
  return new Grid(element, options, {
    render: function(date, delta, fetchEvents) {
      if (delta) {
        addMonths(date, delta);
        date.setDate(1);
      }
      var start = this.start = cloneDate(date, true);
      start.setDate(1);
      this.title = formatDates(
        start,
        addDays(cloneDate(this.end = addMonths(cloneDate(start), 1)), -1),
        this.option('titleFormat'),
        options
      );
      addDays(this.visStart = cloneDate(start), -((start.getDay() - options.firstDay + 7) % 7));
      addDays(this.visEnd = cloneDate(this.end), (7 - this.visEnd.getDay() + options.firstDay) % 7);
      var rowCnt = Math.round((this.visEnd - this.visStart) / (DAY_MS * 7));
      if (options.weekMode == 'fixed') {
        addDays(this.visEnd, (6 - rowCnt) * 7);
        rowCnt = 6;
      }
      this.renderGrid(rowCnt, 7, this.option('columnFormat'), true, fetchEvents);
    }
  });
}

views.basicWeek = function(element, options) {
  return new Grid(element, options, {
    render: function(date, delta, fetchEvents) {
      if (delta) {
        addDays(date, delta * 7);
      }
      this.title = formatDates(
        this.start = this.visStart = addDays(cloneDate(date), -((date.getDay() - options.firstDay + 7) % 7)),
        addDays(cloneDate(this.end = this.visEnd = addDays(cloneDate(this.start), 7)), -1),
        this.option('titleFormat'),
        options
      );
      this.renderGrid(1, 7, this.option('columnFormat'), false, fetchEvents);
    }
  });
};

views.basicDay = function(element, options) {
  return new Grid(element, options, {
    render: function(date, delta, fetchEvents) {
      if (delta) {
        addDays(date, delta);
      }
      this.title = formatDate(date, this.option('titleFormat'), options);
      this.start = this.visStart = cloneDate(date, true);
      this.end = this.visEnd = addDays(cloneDate(this.start), 1);
      this.renderGrid(1, 1, this.option('columnFormat'), false, fetchEvents);
    }
  });
}


// rendering bugs

var tdHeightBug, rtlLeftDiff;


function Grid(element, options, methods) {
  
  var tm, firstDay,
    rtl, dis, dit,  // day index sign / translate
    rowCnt, colCnt,
    colWidth,
    thead, tbody,
    cachedSegs, //...
    
  // initialize superclass
  view = $.extend(this, viewMethods, methods, {
    renderGrid: renderGrid,
    renderEvents: renderEvents,
    rerenderEvents: rerenderEvents,
    updateSize: updateSize,
    defaultEventEnd: function(event) { // calculates an end if event doesnt have one, mostly for resizing
      return cloneDate(event.start);
    },
    visEventEnd: function(event) { // returns exclusive 'visible' end, for rendering
      if (event.end) {
        var end = cloneDate(event.end);
        return (event.allDay || end.getHours() || end.getMinutes()) ? addDays(end, 1) : end;
      }else{
        return addDays(cloneDate(event.start), 1);
      }
    }
  });
  view.init(element, options);
  
  
  
  /* Grid Rendering
  -----------------------------------------------------------------------------*/
  
  
  element.addClass('fc-grid').css('position', 'relative');
  if (element.disableSelection) {
    element.disableSelection();
  }

  function renderGrid(r, c, colFormat, showNumbers, fetchEvents) {
    rowCnt = r;
    colCnt = c;
    
    // update option-derived variables
    tm = options.theme ? 'ui' : 'fc'; 
    firstDay = options.firstDay;
    if (rtl = options.isRTL) {
      dis = -1;
      dit = colCnt - 1;
    }else{
      dis = 1;
      dit = 0;
    }
    
    var month = view.start.getMonth(),
      today = clearTime(new Date()),
      s, i, j, d = cloneDate(view.visStart);
    
    if (!tbody) { // first time, build all cells from scratch
    
      var table = $("<table/>").appendTo(element);
      
      s = "<thead><tr>";
      for (i=0; i<colCnt; i++) {
        s += "<th class='fc-" +
          dayIDs[d.getDay()] + ' ' + // needs to be first
          tm + '-state-default' +
          (i==dit ? ' fc-leftmost' : '') +
          "'>" + formatDate(d, colFormat, options) + "</th>";
        addDays(d, 1);
      }
      thead = $(s + "</tr></thead>").appendTo(table);
      
      s = "<tbody>";
      d = cloneDate(view.visStart);
      for (i=0; i<rowCnt; i++) {
        s += "<tr class='fc-week" + i + "'>";
        for (j=0; j<colCnt; j++) {
          s += "<td class='fc-" +
            dayIDs[d.getDay()] + ' ' + // needs to be first
            tm + '-state-default fc-day' + (i*colCnt+j) +
            (j==dit ? ' fc-leftmost' : '') +
            (rowCnt>1 && d.getMonth() != month ? ' fc-other-month' : '') +
            (+d == +today ?
            ' fc-today '+tm+'-state-highlight' :
            ' fc-not-today') + "'>" +
            (showNumbers ? "<div class='fc-day-number'>" + d.getDate() + "</div>" : '') +
            "<div class='fc-day-content'><div>&nbsp;</div></div></td>";
          addDays(d, 1);
        }
        s += "</tr>";
      }
      tbody = $(s + "</tbody>").appendTo(table);
      tbody.find('td').click(dayClick);
    
    }else{ // NOT first time, reuse as many cells as possible
    
      view.clearEvents();
    
      var prevRowCnt = tbody.find('tr').length;
      if (rowCnt < prevRowCnt) {
        tbody.find('tr:gt(' + (rowCnt-1) + ')').remove(); // remove extra rows
      }
      else if (rowCnt > prevRowCnt) { // needs to create new rows...
        s = '';
        for (i=prevRowCnt; i<rowCnt; i++) {
          s += "<tr class='fc-week" + i + "'>";
          for (j=0; j<colCnt; j++) {
            s += "<td class='fc-" +
              dayIDs[d.getDay()] + ' ' + // needs to be first
              tm + '-state-default fc-new fc-day' + (i*colCnt+j) +
              (j==dit ? ' fc-leftmost' : '') + "'>" +
              (showNumbers ? "<div class='fc-day-number'></div>" : '') +
              "<div class='fc-day-content'><div>&nbsp;</div></div>" +
              "</td>";
            addDays(d, 1);
          }
          s += "</tr>";
        }
        tbody.append(s);
      }
      tbody.find('td.fc-new').removeClass('fc-new').click(dayClick);
      
      // re-label and re-class existing cells
      d = cloneDate(view.visStart);
      tbody.find('td').each(function() {
        var td = $(this);
        if (rowCnt > 1) {
          if (d.getMonth() == month) {
            td.removeClass('fc-other-month');
          }else{
            td.addClass('fc-other-month');
          }
        }
        if (+d == +today) {
          td.removeClass('fc-not-today')
            .addClass('fc-today')
            .addClass(tm + '-state-highlight');
        }else{
          td.addClass('fc-not-today')
            .removeClass('fc-today')
            .removeClass(tm + '-state-highlight');
        }
        td.find('div.fc-day-number').text(d.getDate());
        addDays(d, 1);
      });
      
      if (rowCnt == 1) { // more changes likely (week or day view)
      
        // redo column header text and class
        d = cloneDate(view.visStart);
        thead.find('th').each(function() {
          $(this).text(formatDate(d, colFormat, options));
          this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]);
          addDays(d, 1);
        });
        
        // redo cell day-of-weeks
        d = cloneDate(view.visStart);
        tbody.find('td').each(function() {
          this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]);
          addDays(d, 1);
        });
        
      }
    
    }
    
    updateSize();
    fetchEvents(renderEvents);
  
  };
  
  
  function dayClick(ev) {
    var date = addDays(
      cloneDate(view.visStart),
      parseInt(this.className.match(/fc\-day(\d+)/)[1])
    );
    view.trigger('dayClick', this, date, true, ev);
  }
  
  
  function updateSize() {
  
    var height = Math.round(element.width() / options.aspectRatio),
      leftTDs = tbody.find('tr td:first-child'),
      tbodyHeight = height - thead.height(),
      rowHeight1, rowHeight2;
    
    if (options.weekMode == 'variable') {
      rowHeight1 = rowHeight2 = Math.floor(tbodyHeight / (rowCnt==1 ? 2 : 6));
    }else{
      rowHeight1 = Math.floor(tbodyHeight / rowCnt);
      rowHeight2 = tbodyHeight - rowHeight1*(rowCnt-1);
    }
    
    reportTBody(tbody);
    
    if (tdHeightBug == undefined) {
      // bug in firefox where cell height includes padding
      var tr = tbody.find('tr:first'),
        td = tr.find('td:first');
      td.height(rowHeight1);
      tdHeightBug = rowHeight1 != td.height();
    }
    
    if (tdHeightBug) {
      leftTDs.slice(0, -1).height(rowHeight1);
      leftTDs.slice(-1).height(rowHeight2);
    }else{
      setOuterHeight(leftTDs.slice(0, -1), rowHeight1);
      setOuterHeight(leftTDs.slice(-1), rowHeight2);
    }
    
    setOuterWidth(
      thead.find('th').slice(0, -1),
      colWidth = Math.floor(element.width() / colCnt)
    );
    
  }
  
  
  
  /* Event Rendering
  -----------------------------------------------------------------------------*/
  
  
  function renderEvents(events) {
    view.reportEvents(events);
    renderSegs(cachedSegs = compileSegs(events));
          // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          // ~~ begin edit :: add scripts for tooltips
          // $(".calendar").after("<script type='text/javascript'>$(function() {$('"+options.tooltip+"').tipsy({gravity: 's',fade: true});});</script>");
          // ~~ end edit
          // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  }
  
  
  function rerenderEvents(skipCompile) {
    view.clearEvents();
    if (skipCompile) {
      renderSegs(cachedSegs);
    }else{
      renderEvents(view.cachedEvents);
    }
  }
  
  
  function compileSegs(events) {
    var d1 = cloneDate(view.visStart),
      d2 = addDays(cloneDate(d1), colCnt),
      rows = [],
      i=0;
    for (; i<rowCnt; i++) {
      rows.push(stackSegs(view.sliceSegs(events, d1, d2)));
      addDays(d1, 7);
      addDays(d2, 7);
    }
    return rows;
  }
  
  
  function renderSegs(segRows) {
    var i, len = segRows.length, levels,
      tr, td,
      innerDiv,
      top,
      rowContentHeight,
      j, segs,
      levelHeight,
      k, seg,
      event,
      className,
      startElm, endElm,
      left, right,
      eventElement, eventAnchor,
      triggerRes;
    for (i=0; i<len; i++) {
      levels = segRows[i];
      tr = tbody.find('tr:eq('+i+')');
      td = tr.find('td:first');
      innerDiv = td.find('div.fc-day-content div').css('position', 'relative');
      top = safePosition(innerDiv, td, tr, tbody).top;
      rowContentHeight = 0;
      for (j=0; j<levels.length; j++) {
        segs = levels[j];
        levelHeight = 0;
        for (k=0; k<segs.length; k++) {
          seg = segs[k];
          event = seg.event;
          className = 'fc-event fc-event-hori ';
          startElm = seg.isStart ?
            tr.find('td:eq('+((seg.start.getDay()-firstDay+colCnt)%colCnt)+') div div') :
            tbody;
          endElm = seg.isEnd ?
            tr.find('td:eq('+((seg.end.getDay()-firstDay+colCnt-1)%colCnt)+') div div') :
            tbody;
          if (rtl) {
            left = endElm.position().left;
            right = startElm.position().left + startElm.width();
            if (seg.isStart) {
              className += 'fc-corner-right ';
            }
            if (seg.isEnd) {
              className += 'fc-corner-left ';
            }
          }else{
            left = startElm.position().left;
            right = endElm.position().left + endElm.width();
            if (seg.isStart) {
              className += 'fc-corner-left ';
            }
            if (seg.isEnd) {
              className += 'fc-corner-right ';
            }
          }
          eventElement = $("<div class='" + className + event.className.join(' ') + "'/>")
            .append(eventAnchor = $("<a/>")
              .append(event.allDay || !seg.isStart ? null :
                $("<span class='fc-event-time'/>")
                  .html(formatDates(event.start, event.end, view.option('timeFormat'), options)))
              .append($("<span class='fc-event-title'/>")
                .text(event.title)));
          if (event.url) {
            eventAnchor.attr('href', event.url);
          }
          
          // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          // ~~ begin edit :: add tooltips to the events
          /*
          my_sdate = $.fullCalendar.formatDate(event.start, 'MMM dS, yyyy');
          my_stime = $.fullCalendar.formatDate(event.start, 'h:mm TT');
          my_edate = $.fullCalendar.formatDate(event.end, 'MMM dS, yyyy');
          my_etime = $.fullCalendar.formatDate(event.end, 'h:mm TT');
          if (my_sdate == my_edate){
                    myfcdate = my_sdate+' from '+my_stime+' to '+my_etime;
                  } else {
                    myfcdate = 'From '+my_stime+' on '+my_sdate+' to '+my_etime+' on '+my_edate;
                  }
          tt_body = 'What: '+event.title+'<br />When: '+myfcdate+'<br />Where: '+event.location+'<br /><br />'+event.description;
          eventAnchor.attr('class', options.tooltip);
          eventAnchor.attr('title',tt_body);
          */
          // ~~ end edit
          // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          
          triggerRes = view.trigger('eventRender', event, event, eventElement);
          if (triggerRes !== false) {
            if (triggerRes && typeof triggerRes != 'boolean') {
              eventElement = $(triggerRes);
            }
            eventElement
              .css({
                position: 'absolute',
                top: top,
                left: left + (rtlLeftDiff||0),
                zIndex: 8
              })
              .appendTo(element);
            setOuterWidth(eventElement, right-left, true);
            if (rtl && rtlLeftDiff == undefined) {
              // bug in IE6 where offsets are miscalculated with direction:rtl
              rtlLeftDiff = left - eventElement.position().left;
              if (rtlLeftDiff) {
                eventElement.css('left', left + rtlLeftDiff);
              }
            }
            view.eventElementHandlers(event, eventElement);
            if (event.editable || event.editable == undefined && options.editable) {
              draggableEvent(event, eventElement);
              if (seg.isEnd) {
                view.resizableDayEvent(event, eventElement, colWidth);
              }
            }
            view.reportEventElement(event, eventElement);
            levelHeight = Math.max(levelHeight, eventElement.outerHeight(true));
          }
        }
        rowContentHeight += levelHeight;
        top += levelHeight;
      }
      innerDiv.height(rowContentHeight);
    }
  }
  
  
  
  /* Event Dragging
  -----------------------------------------------------------------------------*/
  
  
  function draggableEvent(event, eventElement) {
    if (!options.disableDragging && eventElement.draggable) {
      var matrix;
      eventElement.draggable({
        zIndex: 9,
        delay: 50,
        opacity: view.option('dragOpacity'),
        revertDuration: options.dragRevertDuration,
        start: function(ev, ui) {
          view.hideEvents(event, eventElement);
          view.trigger('eventDragStart', eventElement, event, ev, ui);
          matrix = new HoverMatrix(function(cell) {
            eventElement.draggable('option', 'revert', !cell || !cell.rowDelta && !cell.colDelta);
            if (cell) {
              view.showOverlay(cell);
            }else{
              view.hideOverlay();
            }
          });
          tbody.find('tr').each(function() {
            matrix.row(this);
          });
          var tds = tbody.find('tr:first td');
          if (rtl) {
            tds = $(tds.get().reverse());
          }
          tds.each(function() {
            matrix.col(this);
          });
          matrix.mouse(ev.pageX, ev.pageY);
        },
        drag: function(ev) {
          matrix.mouse(ev.pageX, ev.pageY);
        },
        stop: function(ev, ui) {
          view.hideOverlay();
          view.trigger('eventDragStop', eventElement, event, ev, ui);
          var cell = matrix.cell;
          if (!cell || !cell.rowDelta && !cell.colDelta) {
            if ($.browser.msie) {
              eventElement.css('filter', ''); // clear IE opacity side-effects
            }
            view.showEvents(event, eventElement);
          }else{
            eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link
            view.eventDrop(this, event, cell.rowDelta*7+cell.colDelta*dis, 0, event.allDay, ev, ui);
          }
        }
      });
    }
  }
  
  
  // event resizing w/ 'view' methods...

};


/* Agenda Views: agendaWeek/agendaDay
-----------------------------------------------------------------------------*/

setDefaults({
  allDaySlot: true,
  allDayText: 'all-day',
  firstHour: 6,
  slotMinutes: 30,
  defaultEventMinutes: 120,
  axisFormat: 'h(:mm)tt',
  timeFormat: {
    agenda: 'h:mm{ - h:mm}'
  },
  dragOpacity: {
    agenda: .5
  }
});

views.agendaWeek = function(element, options) {
  return new Agenda(element, options, {
    render: function(date, delta, fetchEvents) {
      if (delta) {
        addDays(date, delta * 7);
      }
      this.title = formatDates(
        this.start = this.visStart = addDays(cloneDate(date), -((date.getDay() - options.firstDay + 7) % 7)),
        addDays(cloneDate(this.end = this.visEnd = addDays(cloneDate(this.start), 7)), -1),
        this.option('titleFormat'),
        options
      );
      this.renderAgenda(7, this.option('columnFormat'), fetchEvents);
    }
  });
};

views.agendaDay = function(element, options) {
  return new Agenda(element, options, {
    render: function(date, delta, fetchEvents) {
      if (delta) {
        addDays(date, delta);
      }
      this.title = formatDate(date, this.option('titleFormat'), options);
      this.start = this.visStart = cloneDate(date, true);
      this.end = this.visEnd = addDays(cloneDate(this.start), 1);
      this.renderAgenda(1, this.option('columnFormat'), fetchEvents);
    }
  });
};

function Agenda(element, options, methods) {

  var head, body, bodyContent, bodyTable, bg,
    colCnt,
    axisWidth, colWidth, slotHeight,
    cachedDaySegs, cachedSlotSegs,
    tm, firstDay,
    rtl, dis, dit,  // day index sign / translate
    // ...
    
  view = $.extend(this, viewMethods, methods, {
    renderAgenda: renderAgenda,
    renderEvents: renderEvents,
    rerenderEvents: rerenderEvents,
    updateSize: updateSize,
    shown: resetScroll,
    defaultEventEnd: function(event) {
      var start = cloneDate(event.start);
      if (event.allDay) {
        return start;
      }
      return addMinutes(start, options.defaultEventMinutes);
    },
    visEventEnd: function(event) {
      if (event.allDay) {
        if (event.end) {
          var end = cloneDate(event.end);
          return (event.allDay || end.getHours() || end.getMinutes()) ? addDays(end, 1) : end;
        }else{
          return addDays(cloneDate(event.start), 1);
        }
      }
      if (event.end) {
        return cloneDate(event.end);
      }else{
        return addMinutes(cloneDate(event.start), options.defaultEventMinutes);
      }
    }
  });
  view.init(element, options);
  
  
  
  /* Time-slot rendering
  -----------------------------------------------------------------------------*/
  
  
  element.addClass('fc-agenda').css('position', 'relative');
  if (element.disableSelection) {
    element.disableSelection();
  }
  
  function renderAgenda(c, colFormat, fetchEvents) {
    colCnt = c;
    
    // update option-derived variables
    tm = options.theme ? 'ui' : 'fc'; 
    firstDay = options.firstDay;
    if (rtl = options.isRTL) {
      dis = -1;
      dit = colCnt - 1;
    }else{
      dis = 1;
      dit = 0;
    }
    
    var d0 = rtl ? addDays(cloneDate(view.visEnd), -1) : cloneDate(view.visStart),
      d = cloneDate(d0),
      today = clearTime(new Date());
    
    if (!head) { // first time rendering, build from scratch
    
      var i,
        minutes,
        slotNormal = options.slotMinutes % 15 == 0, //...
      
      // head
      s = "<div class='fc-agenda-head' style='position:relative;z-index:4'>" +
        "<table style='width:100%'>" +
        "<tr class='fc-first" + (options.allDaySlot ? '' : ' fc-last') + "'>" +
        "<th class='fc-leftmost " +
          tm + "-state-default'>&nbsp;</th>";
      for (i=0; i<colCnt; i++) {
        s += "<th class='fc-" +
          dayIDs[d.getDay()] + ' ' + // needs to be first
          tm + '-state-default' +
          "'>" + formatDate(d, colFormat, options) + "</th>";
        addDays(d, dis);
      }
      s+= "<th class='" + tm + "-state-default'>&nbsp;</th></tr>";
      if (options.allDaySlot) {
        s+= "<tr class='fc-all-day'>" +
            "<th class='fc-axis fc-leftmost " + tm + "-state-default'>" + options.allDayText + "</th>" +
            "<td colspan='" + colCnt + "' class='" + tm + "-state-default'>" +
              "<div class='fc-day-content'><div>&nbsp;</div></div></td>" +
            "<th class='" + tm + "-state-default'>&nbsp;</th>" +
          "</tr><tr class='fc-divider fc-last'><th colspan='" + (colCnt+2) + "' class='" +
            tm + "-state-default fc-leftmost'><div/></th></tr>";
      }
      s+= "</table></div>";
      head = $(s).appendTo(element);
      head.find('td').click(slotClick);
      
      // body
      d = new Date(1970, 0, 1);
      s = "<table>";
      for (i=0; d.getDate() != 2; i++) {
        minutes = d.getMinutes();
        s += "<tr class='" +
          (i==0 ? 'fc-first' : (minutes==0 ? '' : 'fc-minor')) +
          "'><th class='fc-axis fc-leftmost " + tm + "-state-default'>" +
          ((!slotNormal || minutes==0) ? formatDate(d, options.axisFormat) : '&nbsp;') + 
          "</th><td class='fc-slot" + i + ' ' +
            tm + "-state-default'><div>&nbsp;</div></td></tr>";
        addMinutes(d, options.slotMinutes);
      }
      s += "</table>";
      body = $("<div class='fc-agenda-body' style='position:relative;z-index:2;overflow:auto'/>")
        .append(bodyContent = $("<div style='position:relative;overflow:hidden'>")
          .append(bodyTable = $(s)))
        .appendTo(element);
      body.find('td').click(slotClick);
      
      // background stripes
      d = cloneDate(d0);
      s = "<div class='fc-agenda-bg' style='position:absolute;z-index:1'>" +
        "<table style='width:100%;height:100%'><tr class='fc-first'>";
      for (i=0; i<colCnt; i++) {
        s += "<td class='fc-" +
          dayIDs[i] + ' ' + // needs to be first
          tm + '-state-default ' +
          (i==0 ? 'fc-leftmost ' : '') +
          (+d == +today ? tm + '-state-highlight fc-today' : 'fc-not-today') +
          "'><div class='fc-day-content'><div>&nbsp;</div></div></td>";
        addDays(d, dis);
      }
      s += "</tr></table></div>";
      bg = $(s).appendTo(element);
      
    }else{ // skeleton already built, just modify it
    
      view.clearEvents();
      
      // redo column header text and class
      head.find('tr:first th').slice(1, -1).each(function() {
        $(this).text(formatDate(d, colFormat, options));
        this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]);
        addDays(d, dis);
      });
      
      // change classes of background stripes
      d = cloneDate(d0);
      bg.find('td').each(function() {
        this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]);
        if (+d == +today) {
          $(this)
            .removeClass('fc-not-today')
            .addClass('fc-today')
            .addClass(tm + '-state-highlight');
        }else{
          $(this)
            .addClass('fc-not-today')
            .removeClass('fc-today')
            .removeClass(tm + '-state-highlight');
        }
        addDays(d, dis);
      });
    
    }
    
    updateSize();
    resetScroll();
    fetchEvents(renderEvents);
    
  };
  
  
  function resetScroll() {
    var d0 = new Date(1970, 0, 1),
      scrollDate = cloneDate(d0);
    scrollDate.setHours(options.firstHour);
    var go = function() {
      body.scrollTop(timePosition(d0, scrollDate) + 1); // +1 for the border
        // TODO: +1 doesn't apply when firstHour=0
    }
    if ($.browser.opera) {
      setTimeout(go, 0); // opera 10 (and earlier?) needs this
    }else{
      go();
    }
  }
  
  
  function updateSize() {
    
    bodyTable.width('');
    body.height(Math.round(body.width() / options.aspectRatio) - head.height());
    
    // need this for IE6/7. triggers clientWidth to be calculated for 
    // later user in this function. this is ridiculous
    body[0].clientWidth;
    
    var topTDs = head.find('tr:first th'),
      stripeTDs = bg.find('td'),
      contentWidth = body[0].clientWidth;
    bodyTable.width(contentWidth);
    
    // time-axis width
    axisWidth = 0;
    setOuterWidth(
      head.find('tr:lt(2) th:first').add(body.find('tr:first th'))
        .width('')
        .each(function() {
          axisWidth = Math.max(axisWidth, $(this).outerWidth());
        }),
      axisWidth
    );
    
    // column width
    colWidth = Math.floor((contentWidth - axisWidth) / colCnt);
    setOuterWidth(stripeTDs.slice(0, -1), colWidth);
    setOuterWidth(topTDs.slice(1, -2), colWidth);
    setOuterWidth(topTDs.slice(-2, -1), contentWidth - axisWidth - colWidth*(colCnt-1));
    
    bg.css({
      top: head.find('tr').height(),
      left: axisWidth,
      width: contentWidth - axisWidth,
      height: element.height()
    });
    
    slotHeight = body.find('tr:first div').height() + 1;
    
    // TODO:
    //reportTBody(bodyTable.find('tbody'));
    // Opera 9.25 doesn't detect the bug when called from agenda
  }
  
  function slotClick(ev) {
    var col = Math.floor((ev.pageX - bg.offset().left) / colWidth),
      date = addDays(cloneDate(view.visStart), dit + dis*col),
      rowMatch = this.className.match(/fc-slot(\d+)/);
    if (rowMatch) {
      var mins = parseInt(rowMatch[1]) * options.slotMinutes,
        hours = Math.floor(mins/60);
      date.setHours(hours);
      date.setMinutes(mins % 60);
      view.trigger('dayClick', this, date, false, ev);
    }else{
      view.trigger('dayClick', this, date, true, ev);
    }
  }
  
  
  
  /* Event Rendering
  -----------------------------------------------------------------------------*/
  
  
  function renderEvents(events) {
    view.reportEvents(events);
    
    var i, len=events.length,
      dayEvents=[],
      slotEvents=[];
    for (i=0; i<len; i++) {
      if (events[i].allDay) {
        dayEvents.push(events[i]);
      }else{
        slotEvents.push(events[i]);
      }
    }
    
    renderDaySegs(cachedDaySegs = stackSegs(view.sliceSegs(dayEvents, view.visStart, view.visEnd)));
    renderSlotSegs(cachedSlotSegs = compileSlotSegs(slotEvents));
  }
  
  
  function rerenderEvents(skipCompile) {
    view.clearEvents();
    if (skipCompile) {
      renderDaySegs(cachedDaySegs);
      renderSlotSegs(cachedSlotSegs);
    }else{
      renderEvents(view.cachedEvents);
    }
  }
  
  
  function compileSlotSegs(events) {
    var d1 = cloneDate(view.visStart),
      d2 = addDays(cloneDate(d1), 1),
      levels,
      segCols = [],
      i=0;
    for (; i<colCnt; i++) {
      levels = stackSegs(view.sliceSegs(events, d1, d2));
      countForwardSegs(levels);
      segCols.push(levels);
      addDays(d1, 1);
      addDays(d2, 1);
    }
    return segCols;
  }
  
  
  
  // renders 'all-day' events at the top
  
  function renderDaySegs(segRow) {
    if (options.allDaySlot) {
      var td = head.find('td'),
        tdInner = td.find('div div'),
        tr = td.parent(),
        top = safePosition(tdInner, td, tr, tr.parent()).top,
        rowContentHeight = 0,
        i, len=segRow.length, level,
        levelHeight,
        j, seg,
        event,
        className,
        leftDay, leftRounded,
        rightDay, rightRounded,
        left, right,
        eventElement, anchorElement,
        triggerRes;
      for (i=0; i<len; i++) {
        level = segRow[i];
        levelHeight = 0;
        for (j=0; j<level.length; j++) {
          seg = level[j];
          event = seg.event;
          className = 'fc-event fc-event-hori ';
          if (rtl) {
            leftDay = seg.end.getDay() - 1;
            leftRounded = seg.isEnd;
            rightDay = seg.start.getDay();
            rightRounded = seg.isStart;
          }else{
            leftDay = seg.start.getDay();
            leftRounded = seg.isStart;
            rightDay = seg.end.getDay() - 1;
            rightRounded = seg.isEnd;
          }
          if (leftRounded) {
            className += 'fc-corner-left ';
            left = bg.find('td:eq('+(((leftDay-firstDay+colCnt)%colCnt)*dis+dit)+') div div').position().left + axisWidth;
          }else{
            left = axisWidth;
          }
          if (rightRounded) {
            className += 'fc-corner-right ';
            right = bg.find('td:eq('+(((rightDay-firstDay+colCnt)%colCnt)*dis+dit)+') div div');
            right = right.position().left + right.width() + axisWidth;
          }else{
            right = axisWidth + bg.width();
          }
          eventElement = $("<div class='" + className + event.className.join(' ') + "'/>")
            .append(anchorElement = $("<a/>")
              .append($("<span class='fc-event-title' />")
                .text(event.title)));
          if (event.url) {
            anchorElement.attr('href', event.url);
          }
          triggerRes = view.trigger('eventRender', event, event, eventElement);
          if (triggerRes !== false) {
            if (triggerRes && typeof triggerRes != 'boolean') {
              eventElement = $(triggerRes);
            }
            eventElement
              .css({
                position: 'absolute',
                top: top,
                left: left,
                zIndex: 8
              })
              .appendTo(head);
            setOuterWidth(eventElement, right-left, true);
            view.eventElementHandlers(event, eventElement);
            if (event.editable || event.editable == undefined && options.editable) {
              draggableDayEvent(event, eventElement, seg.isStart);
              if (seg.isEnd) {
                view.resizableDayEvent(event, eventElement, colWidth);
              }
            }
            view.reportEventElement(event, eventElement);
            levelHeight = Math.max(levelHeight, eventElement.outerHeight(true));
          }
        }
        top += levelHeight;
        rowContentHeight += levelHeight;
      }
      tdInner.height(rowContentHeight);
      updateSize(); // tdInner might have pushed the body down, so resize
    }
  }
  
  
  
  // renders events in the 'time slots' at the bottom
  
  function renderSlotSegs(segCols) {
    var colI, colLen=segCols.length, col,
      levelI, level,
      segI, seg,
      forward,
      event,
      top, bottom,
      tdInner,
      width, left,
      className,
      eventElement, anchorElement, timeElement, titleElement,
      triggerRes;
    for (colI=0; colI<colLen; colI++) {
      col = segCols[colI];
      for (levelI=0; levelI<col.length; levelI++) {
        level = col[levelI];
        for (segI=0; segI<level.length; segI++) {
          seg = level[segI];
          forward = seg.forward || 0;
          event = seg.event;
          top = timePosition(seg.start, seg.start);
          bottom = timePosition(seg.start, seg.end);
          tdInner = bg.find('td:eq(' + (colI*dis + dit) + ') div div');
          availWidth = tdInner.width();
          if (levelI) {
            // indented and thin
            width = availWidth / (levelI + forward + 1);
          }else{
            if (forward) {
              // moderately wide, aligned left still
              width = ((availWidth / (forward + 1)) - (12/2)) * 2; // 12 is the predicted width of resizer =
            }else{
              // can be entire width, aligned left
              width = availWidth * .96;
            }
          }
          left = axisWidth + tdInner.position().left +       // leftmost possible
            (availWidth / (levelI + forward + 1) * levelI) // indentation
            * dis + (rtl ? availWidth - width : 0);        // rtl
          className = 'fc-event fc-event-vert ';
          if (seg.isStart) {
            className += 'fc-corner-top ';
          }
          if (seg.isEnd) {
            className += 'fc-corner-bottom ';
          }
          eventElement = $("<div class='" + className + event.className.join(' ') + "' />")
            .append(anchorElement = $("<a><span class='fc-event-bg'/></a>")
              .append(timeElement = $("<span class='fc-event-time'/>")
                .text(formatDates(event.start, event.end, view.option('timeFormat'))))
              .append(titleElement = $("<span class='fc-event-title'/>")
                .text(event.title)))
          if (event.url) {
            anchorElement.attr('href', event.url);
          }
          triggerRes = view.trigger('eventRender', event, event, eventElement);
          if (triggerRes !== false) {
            if (triggerRes && typeof triggerRes != 'boolean') {
              eventElement = $(triggerRes);
            }
            eventElement
              .css({
                position: 'absolute',
                zIndex: 8,
                top: top,
                left: left
              })
              .appendTo(bodyContent);
            setOuterWidth(eventElement, width, true);
            setOuterHeight(eventElement, bottom-top, true);
            if (eventElement.height() - titleElement.position().top < 10) {
              // event title doesn't have enough room, put next to the time
              timeElement.text(formatDate(event.start, view.option('timeFormat')) + ' - ' + event.title);
              titleElement.remove();
            }
            view.eventElementHandlers(event, eventElement);
            if (event.editable || event.editable == undefined && options.editable) {
              draggableSlotEvent(event, eventElement, timeElement);
              if (seg.isEnd) {
                resizableSlotEvent(event, eventElement, timeElement);
              }
            }
          }
          view.reportEventElement(event, eventElement);
        }
      }
    }
  }

  
  
  
  /* Event Dragging
  -----------------------------------------------------------------------------*/
  
  
  
  // when event starts out FULL-DAY
  
  function draggableDayEvent(event, eventElement, isStart) {
    if (!options.disableDragging && eventElement.draggable) {
      var origPosition, origWidth,
        resetElement,
        allDay=true,
        matrix;
      eventElement.draggable({
        zIndex: 9,
        opacity: view.option('dragOpacity', 'month'), // use whatever the month view was using
        revertDuration: options.dragRevertDuration,
        start: function(ev, ui) {
          view.hideEvents(event, eventElement);
          view.trigger('eventDragStart', eventElement, event, ev, ui);
          origPosition = eventElement.position();
          origWidth = eventElement.width();
          resetElement = function() {
            if (!allDay) {
              eventElement
                .width(origWidth)
                .height('')
                .draggable('option', 'grid', null);
              allDay = true;
            }
          };
          matrix = new HoverMatrix(function(cell) {
            eventElement.draggable('option', 'revert', !cell || !cell.rowDelta && !cell.colDelta);
            if (cell) {
              if (!cell.row) { // on full-days
                resetElement();
                view.showOverlay(cell);
              }else{ // mouse is over bottom slots
                if (isStart && allDay) {
                  // convert event to temporary slot-event
                  setOuterHeight(
                    eventElement.width(colWidth - 10), // don't use entire width
                    slotHeight * Math.round(
                      (event.end ? ((event.end - event.start)/MINUTE_MS) : options.defaultEventMinutes)
                      /options.slotMinutes)
                  );
                  eventElement.draggable('option', 'grid', [colWidth, 1]);
                  allDay = false;
                }
                view.hideOverlay();
              }
            }else{ // mouse is outside of everything
              view.hideOverlay();
            }
          });
          matrix.row(head.find('td'));
          bg.find('td').each(function() {
            matrix.col(this);
          });
          matrix.row(body);
          matrix.mouse(ev.pageX, ev.pageY);
        },
        drag: function(ev, ui) {
          matrix.mouse(ev.pageX, ev.pageY);
        },
        stop: function(ev, ui) {
          view.hideOverlay();
          view.trigger('eventDragStop', eventElement, event, ev, ui);
          var cell = matrix.cell,
            dayDelta = dis * (
              allDay ? // can't trust cell.colDelta when using slot grid
              (cell ? cell.colDelta : 0) :
              Math.floor((ui.position.left - origPosition.left) / colWidth)
            );
          if (!cell || !dayDelta && !cell.rowDelta) {
            // over nothing (has reverted)
            resetElement();
            if ($.browser.msie) {
              eventElement.css('filter', ''); // clear IE opacity side-effects
            }
            view.showEvents(event, eventElement);
          }else{
            eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link
            view.eventDrop(
              this, event, dayDelta,
              allDay ? 0 : // minute delta
                Math.round((eventElement.offset().top - bodyContent.offset().top) / slotHeight)
                * options.slotMinutes
                - (event.start.getHours() * 60 + event.start.getMinutes()),
              allDay, ev, ui
            );
          }
        }
      });
    }
  }
  
  
  
  // when event starts out IN TIMESLOTS
  
  function draggableSlotEvent(event, eventElement, timeElement) {
    if (!options.disableDragging && eventElement.draggable) {
      var origPosition,
        resetElement,
        prevSlotDelta, slotDelta,
        allDay=false,
        matrix;
      eventElement.draggable({
        zIndex: 9,
        scroll: false,
        grid: [colWidth, slotHeight],
        axis: colCnt==1 ? 'y' : false,
        opacity: view.option('dragOpacity'),
        revertDuration: options.dragRevertDuration,
        start: function(ev, ui) {
          view.hideEvents(event, eventElement);
          view.trigger('eventDragStart', eventElement, event, ev, ui);
          if ($.browser.msie) {
            eventElement.find('span.fc-event-bg').hide(); // nested opacities mess up in IE, just hide
          }
          origPosition = eventElement.position();
          resetElement = function() {
            // convert back to original slot-event
            if (allDay) {
              timeElement.css('display', ''); // show() was causing display=inline
              eventElement.draggable('option', 'grid', [colWidth, slotHeight]);
              allDay = false;
            }
          };
          prevSlotDelta = 0;
          matrix = new HoverMatrix(function(cell) {
            eventElement.draggable('option', 'revert', !cell);
            if (cell) {
              if (!cell.row && options.allDaySlot) { // over full days
                if (!allDay) {
                  // convert to temporary all-day event
                  allDay = true;
                  timeElement.hide();
                  eventElement.draggable('option', 'grid', null);
                }
                view.showOverlay(cell);
              }else{ // on slots
                resetElement();
                view.hideOverlay();
              }
            }else{
              view.hideOverlay();
            }
          });
          if (options.allDaySlot) {
            matrix.row(head.find('td'));
          }
          bg.find('td').each(function() {
            matrix.col(this);
          });
          matrix.row(body);
          matrix.mouse(ev.pageX, ev.pageY);
        },
        drag: function(ev, ui) {
          slotDelta = Math.round((ui.position.top - origPosition.top) / slotHeight);
          if (slotDelta != prevSlotDelta) {
            if (!allDay) {
              // update time header
              var minuteDelta = slotDelta*options.slotMinutes,
                newStart = addMinutes(cloneDate(event.start), minuteDelta),
                newEnd;
              if (event.end) {
                newEnd = addMinutes(cloneDate(event.end), minuteDelta);
              }
              timeElement.text(formatDates(newStart, newEnd, view.option('timeFormat')));
            }
            prevSlotDelta = slotDelta;
          }
          matrix.mouse(ev.pageX, ev.pageY);
        },
        stop: function(ev, ui) {
          view.hideOverlay();
          view.trigger('eventDragStop', eventElement, event, ev, ui);
          var cell = matrix.cell,
            dayDelta = dis * (
              allDay ? // can't trust cell.colDelta when using slot grid
              (cell ? cell.colDelta : 0) : 
              Math.floor((ui.position.left - origPosition.left) / colWidth)
            );
          if (!cell || !slotDelta && !dayDelta) {
            resetElement();
            if ($.browser.msie) {
              eventElement
                .css('filter', '') // clear IE opacity side-effects
                .find('span.fc-event-bg').css('display', ''); // .show() made display=inline
            }
            eventElement.css(origPosition); // sometimes fast drags make event revert to wrong position
            view.showEvents(event, eventElement);
          }else{
            view.eventDrop(
              this, event, dayDelta,
              allDay ? 0 : slotDelta * options.slotMinutes, // minute delta
              allDay, ev, ui
            );
          }
        }
      });
    }
  }
  
  
  
  
  /* Event Resizing
  -----------------------------------------------------------------------------*/
  
  // for TIMESLOT events

  function resizableSlotEvent(event, eventElement, timeElement) {
    if (!options.disableResizing && eventElement.resizable) {
      var slotDelta, prevSlotDelta;
      eventElement
        .resizable({
          handles: 's',
          grid: slotHeight,
          start: function(ev, ui) {
            slotDelta = prevSlotDelta = 0;
            view.hideEvents(event, eventElement);
            if ($.browser.msie && $.browser.version == '6.0') {
              eventElement.css('overflow', 'hidden');
            }
            eventElement.css('z-index', 9);
            view.trigger('eventResizeStart', this, event, ev, ui);
          },
          resize: function(ev, ui) {
            // don't rely on ui.size.height, doesn't take grid into account
            slotDelta = Math.round((Math.max(slotHeight, eventElement.height()) - ui.originalSize.height) / slotHeight);
            if (slotDelta != prevSlotDelta) {
              timeElement.text(
                formatDates(
                  event.start,
                  (!slotDelta && !event.end) ? null : // no change, so don't display time range
                    addMinutes(view.eventEnd(event), options.slotMinutes*slotDelta),
                  view.option('timeFormat')
                )
              );
              prevSlotDelta = slotDelta;
            }
          },
          stop: function(ev, ui) {
            view.trigger('eventResizeStop', this, event, ev, ui);
            if (slotDelta) {
              view.eventResize(this, event, 0, options.slotMinutes*slotDelta, ev, ui);
            }else{
              eventElement.css('z-index', 8);
              view.showEvents(event, eventElement);
              // BUG: if event was really short, need to put title back in span
            }
          }
        })
        .find('div.ui-resizable-s').text('=');
    }
  }
  
  
  // ALL-DAY event resizing w/ 'view' methods...
  
  
  
  
  /* Misc
  -----------------------------------------------------------------------------*/
  
  // get the Y coordinate of the given time on the given day (both Date objects)
  
  function timePosition(day, time) {
    if (time > day && time.getDay() != day.getDay()) {
      return bodyContent.height();
    }
    var slotMinutes = options.slotMinutes,
      minutes = time.getHours()*60 + time.getMinutes(),
      slotI = Math.floor(minutes / slotMinutes),
      tr = body.find('tr:eq(' + slotI + ')'),
      td = tr.find('td'),
      innerDiv = td.find('div');
    return Math.max(0, Math.round(
      safePosition(innerDiv, td, tr, tr.parent()).top - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes)
    ));
  }

}


// count the number of colliding, higher-level segments (for event squishing)

function countForwardSegs(levels) {
  var i, j, k, level, segForward, segBack;
  for (i=levels.length-1; i>0; i--) {
    level = levels[i];
    for (j=0; j<level.length; j++) {
      segForward = level[j];
      for (k=0; k<levels[i-1].length; k++) {
        segBack = levels[i-1][k];
        if (segsCollide(segForward, segBack)) {
          segBack.forward = Math.max(segBack.forward||0, (segForward.forward||0)+1);
        }
      }
    }
  }
}


/* Methods & Utilities for All Views
-----------------------------------------------------------------------------*/

var viewMethods = {

  // TODO: maybe change the 'vis' variables to 'excl'

  /*
   * Objects inheriting these methods must implement the following properties/methods:
   * - title
   * - start
   * - end
   * - visStart
   * - visEnd
   * - defaultEventEnd(event)
   * - visEventEnd(event)
   * - render(events)
   * - rerenderEvents()
   *
   *
   * z-index reservations:
   * 3 - day-overlay
   * 8 - events
   * 9 - dragging/resizing events
   *
   */
  
  

  init: function(element, options) {
    this.element = element;
    this.options = options;
    this.cachedEvents = [];
    this.eventsByID = {};
    this.eventElements = [];
    this.eventElementsByID = {};
  },
  
  
  
  // triggers an event handler, always append view as last arg
  
  trigger: function(name, thisObj) {
    if (this.options[name]) {
      return this.options[name].apply(thisObj || this, Array.prototype.slice.call(arguments, 2).concat([this]));
    }
  },
  
  
  
  // returns a Date object for an event's end
  
  eventEnd: function(event) {
    return event.end ? cloneDate(event.end) : this.defaultEventEnd(event); // TODO: make sure always using copies
  },
  
  
  
  // report when view receives new events
  
  reportEvents: function(events) { // events are already normalized at this point
    var i, len=events.length, event,
      eventsByID = this.eventsByID = {},
      cachedEvents = this.cachedEvents = [];
    for (i=0; i<len; i++) {
      event = events[i];
      if (eventsByID[event._id]) {
        eventsByID[event._id].push(event);
      }else{
        eventsByID[event._id] = [event];
      }
      cachedEvents.push(event);
    }
  },
  
  
  
  // report when view creates an element for an event

  reportEventElement: function(event, element) {
    this.eventElements.push(element);
    var eventElementsByID = this.eventElementsByID;
    if (eventElementsByID[event._id]) {
      eventElementsByID[event._id].push(element);
    }else{
      eventElementsByID[event._id] = [element];
    }
  },
  
  
  
  // event element manipulation
  
  clearEvents: function() { // only remove ELEMENTS
    $.each(this.eventElements, function() {
      this.remove();
    });
    this.eventElements = [];
    this.eventElementsByID = {};
  },
  
  showEvents: function(event, exceptElement) {
    this._eee(event, exceptElement, 'show');
  },
  
  hideEvents: function(event, exceptElement) {
    this._eee(event, exceptElement, 'hide');
  },
  
  _eee: function(event, exceptElement, funcName) { // event-element-each
    var elements = this.eventElementsByID[event._id],
      i, len = elements.length;
    for (i=0; i<len; i++) {
      if (elements[i] != exceptElement) {
        elements[i][funcName]();
      }
    }
  },
  
  
  
  // event modification reporting
  
  eventDrop: function(e, event, dayDelta, minuteDelta, allDay, ev, ui) {
    var view = this,
      oldAllDay = event.allDay;
    view.moveEvents(view.eventsByID[event._id], dayDelta, minuteDelta, allDay);
    view.trigger('eventDrop', e, event, dayDelta, minuteDelta, allDay, function() { // TODO: change docs
      // TODO: investigate cases where this inverse technique might not work
      view.moveEvents(view.eventsByID[event._id], -dayDelta, -minuteDelta, oldAllDay);
      view.rerenderEvents();
    }, ev, ui);
    view.eventsChanged = true;
    view.rerenderEvents();
  },
  
  eventResize: function(e, event, dayDelta, minuteDelta, ev, ui) {
    var view = this;
    view.elongateEvents(view.eventsByID[event._id], dayDelta, minuteDelta);
    view.trigger('eventResize', e, event, dayDelta, minuteDelta, function() {
      // TODO: investigate cases where this inverse technique might not work
      view.elongateEvents(view.eventsByID[event._id], -dayDelta, -minuteDelta);
      view.rerenderEvents();
    }, ev, ui);
    view.eventsChanged = true;
    view.rerenderEvents();
  },
  
  
  
  // event modification
  
  moveEvents: function(events, dayDelta, minuteDelta, allDay) {
    minuteDelta = minuteDelta || 0;
    for (var e, len=events.length, i=0; i<len; i++) {
      e = events[i];
      if (allDay != undefined) {
        e.allDay = allDay;
      }
      addMinutes(addDays(e.start, dayDelta, true), minuteDelta);
      if (e.end) {
        e.end = addMinutes(addDays(e.end, dayDelta, true), minuteDelta);
      }
      normalizeEvent(e, this.options);
    }
  },
  
  elongateEvents: function(events, dayDelta, minuteDelta) {
    minuteDelta = minuteDelta || 0;
    for (var e, len=events.length, i=0; i<len; i++) {
      e = events[i];
      e.end = addMinutes(addDays(this.eventEnd(e), dayDelta, true), minuteDelta);
      normalizeEvent(e, this.options);
    }
  },
  
  
  
  // semi-transparent overlay (while dragging)
  
  showOverlay: function(props) {
    if (!this.dayOverlay) {
      this.dayOverlay = $("<div class='fc-cell-overlay' style='position:absolute;z-index:3;display:none'/>")
        .appendTo(this.element);
    }
    var o = this.element.offset();
    this.dayOverlay
      .css({
        top: props.top - o.top,
        left: props.left - o.left,
        width: props.width,
        height: props.height
      })
      .show();
  },
  
  hideOverlay: function() {
    if (this.dayOverlay) {
      this.dayOverlay.hide();
    }
  },
  
  
  
  // common horizontal event resizing

  resizableDayEvent: function(event, eventElement, colWidth) {
    var view = this;
    if (!view.options.disableResizing && eventElement.resizable) {
      eventElement.resizable({
        handles: view.options.isRTL ? 'w' : 'e',
        grid: colWidth,
        minWidth: colWidth/2, // need this or else IE throws errors when too small
        containment: view.element.parent().parent(), // the main element...
                     // ... a fix. wouldn't allow extending to last column in agenda views (jq ui bug?)
        start: function(ev, ui) {
          eventElement.css('z-index', 9);
          view.hideEvents(event, eventElement);
          view.trigger('eventResizeStart', this, event, ev, ui);
        },
        stop: function(ev, ui) {
          view.trigger('eventResizeStop', this, event, ev, ui);
          // ui.size.width wasn't working with grid correctly, use .width()
          var dayDelta = Math.round((eventElement.width() - ui.originalSize.width) / colWidth);
          if (dayDelta) {
            view.eventResize(this, event, dayDelta, 0, ev, ui);
          }else{
            eventElement.css('z-index', 8);
            view.showEvents(event, eventElement);
          }
        }
      });
    }
  },
  
  
  
  // attaches eventClick, eventMouseover, eventMouseout
  
  eventElementHandlers: function(event, eventElement) {
    var view = this;
    eventElement
      .click(function(ev) {
        if (!eventElement.hasClass('ui-draggable-dragging') &&
          !eventElement.hasClass('ui-resizable-resizing')) {
            return view.trigger('eventClick', this, event, ev);
          }
      })
      .hover(
        function(ev) {
          view.trigger('eventMouseover', this, event, ev);
        },
        function(ev) {
          view.trigger('eventMouseout', this, event, ev);
        }
      );
  },
  
  
  
  // get a property from the 'options' object, using smart view naming
  
  option: function(name, viewName) {
    var v = this.options[name];
    if (typeof v == 'object') {
      return smartProperty(v, viewName || this.name);
    }
    return v;
  },
  
  
  
  // event rendering utilities
  
  sliceSegs: function(events, start, end) {
    var segs = [],
      i, len=events.length, event,
      eventStart, eventEnd,
      segStart, segEnd,
      isStart, isEnd;
    for (i=0; i<len; i++) {
      event = events[i];
      eventStart = event.start;
      eventEnd = this.visEventEnd(event);
      if (eventEnd > start && eventStart < end) {
        if (eventStart < start) {
          segStart = cloneDate(start);
          isStart = false;
        }else{
          segStart = eventStart;
          isStart = true;
        }
        if (eventEnd > end) {
          segEnd = cloneDate(end);
          isEnd = false;
        }else{
          segEnd = eventEnd;
          isEnd = true;
        }
        segs.push({
          event: event,
          start: segStart,
          end: segEnd,
          isStart: isStart,
          isEnd: isEnd,
          msLength: segEnd - segStart
        });
      }
    } 
    return segs.sort(segCmp);
  }
  

};




// event rendering calculation utilities

function stackSegs(segs) {
  var levels = [],
    i, len = segs.length, seg,
    j, collide, k;
  for (i=0; i<len; i++) {
    seg = segs[i];
    j = 0; // the level index where seg should belong
    while (true) {
      collide = false;
      if (levels[j]) {
        for (k=0; k<levels[j].length; k++) {
          if (segsCollide(levels[j][k], seg)) {
            collide = true;
            break;
          }
        }
      }
      if (collide) {
        j++;
      }else{
        break;
      }
    }
    if (levels[j]) {
      levels[j].push(seg);
    }else{
      levels[j] = [seg];
    }
  }
  return levels;
}

function segCmp(a, b) {
  return  (b.msLength - a.msLength) * 100 + (a.event.start - b.event.start);
}

function segsCollide(seg1, seg2) {
  return seg1.end > seg2.start && seg1.start < seg2.end;
}


/* Date Math
-----------------------------------------------------------------------------*/

var DAY_MS = 86400000,
  HOUR_MS = 3600000,
  MINUTE_MS = 60000;

function addYears(d, n, keepTime) {
  d.setFullYear(d.getFullYear() + n);
  if (!keepTime) {
    clearTime(d);
  }
  return d;
}

function addMonths(d, n, keepTime) { // prevents day overflow/underflow
  if (+d) { // prevent infinite looping on invalid dates
    var m = d.getMonth() + n,
      check = cloneDate(d);
    check.setDate(1);
    check.setMonth(m);
    d.setMonth(m);
    if (!keepTime) {
      clearTime(d);
    }
    while (d.getMonth() != check.getMonth()) {
      d.setDate(d.getDate() + (d < check ? 1 : -1));
    }
  }
  return d;
}

function addDays(d, n, keepTime) { // deals with daylight savings
  if (+d) { // prevent infinite looping on invalid dates
    var dd = d.getDate() + n,
      check = cloneDate(d);
    check.setHours(12); // set to middle of day
    check.setDate(dd);
    d.setDate(dd);
    if (!keepTime) {
      clearTime(d);
    }
    while (d.getDate() != check.getDate()) {
      d.setTime(+d + (d < check ? 1 : -1) * HOUR_MS);
    }
  }
  return d;
}

function addMinutes(d, n) {
  d.setMinutes(d.getMinutes() + n);
  return d;
}

function clearTime(d) {
  d.setHours(0);
  d.setMinutes(0);
  d.setSeconds(0); 
  d.setMilliseconds(0);
  return d;
}

function cloneDate(d, dontKeepTime) {
  if (dontKeepTime) {
    return clearTime(new Date(+d));
  }
  return new Date(+d);
}



/* Date Parsing
-----------------------------------------------------------------------------*/

var parseDate = fc.parseDate = function(s) {
  if (typeof s == 'object') { // already a Date object
    return s;
  }
  if (typeof s == 'number') { // a UNIX timestamp
    return new Date(s * 1000);
  }
  if (typeof s == 'string') {
    if (s.match(/^\d+$/)) { // a UNIX timestamp
      return new Date(parseInt(s) * 1000);
    }
    return parseISO8601(s, true) || new Date(s) || null;
  }
  return null;
}

var parseISO8601 = fc.parseISO8601 = function(s, ignoreTimezone) {
  // derived from http://delete.me.uk/2005/03/iso8601.html
  var d = s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?$/);
  if (!d) return null;
  var offset = 0;
  var date = new Date(d[1], 0, 1);
  if (d[3]) { date.setMonth(d[3] - 1); }
  if (d[5]) { date.setDate(d[5]); }
  if (d[7]) { date.setHours(d[7]); }
  if (d[8]) { date.setMinutes(d[8]); }
  if (d[10]) { date.setSeconds(d[10]); }
  if (d[12]) { date.setMilliseconds(Number("0." + d[12]) * 1000); }
  if (!ignoreTimezone) {
    if (d[14]) {
      offset = (Number(d[16]) * 60) + Number(d[17]);
      offset *= ((d[15] == '-') ? 1 : -1);
    }
    offset -= date.getTimezoneOffset();
  }
  return new Date(Number(date) + (offset * 60 * 1000));
}



/* Date Formatting
-----------------------------------------------------------------------------*/

var formatDate = fc.formatDate = function(date, format, options) {
  return formatDates(date, null, format, options);
}

var formatDates = fc.formatDates = function(date1, date2, format, options) {
  options = options || defaults;
  var date = date1,
    otherDate = date2,
    i, len = format.length, c,
    i2, formatter,
    res = '';
  for (i=0; i<len; i++) {
    c = format.charAt(i);
    if (c == "'") {
      for (i2=i+1; i2<len; i2++) {
        if (format.charAt(i2) == "'") {
          if (date) {
            if (i2 == i+1) {
              res += "'";
            }else{
              res += format.substring(i+1, i2);
            }
            i = i2;
          }
          break;
        }
      }
    }
    else if (c == '(') {
      for (i2=i+1; i2<len; i2++) {
        if (format.charAt(i2) == ')') {
          var subres = formatDate(date, format.substring(i+1, i2), options);
          if (parseInt(subres.replace(/\D/, ''))) {
            res += subres;
          }
          i = i2;
          break;
        }
      }
    }
    else if (c == '[') {
      for (i2=i+1; i2<len; i2++) {
        if (format.charAt(i2) == ']') {
          var subformat = format.substring(i+1, i2);
          var subres = formatDate(date, subformat, options);
          if (subres != formatDate(otherDate, subformat, options)) {
            res += subres;
          }
          i = i2;
          break;
        }
      }
    }
    else if (c == '{') {
      date = date2;
      otherDate = date1;
    }
    else if (c == '}') {
      date = date1;
      otherDate = date2;
    }
    else {
      for (i2=len; i2>i; i2--) {
        if (formatter = dateFormatters[format.substring(i, i2)]) {
          if (date) {
            res += formatter(date, options);
          }
          i = i2 - 1;
          break;
        }
      }
      if (i2 == i) {
        if (date) {
          res += c;
        }
      }
    }
  }
  return res;
}

var dateFormatters = {
  s  : function(d)  { return d.getSeconds() },
  ss  : function(d)  { return zeroPad(d.getSeconds()) },
  m  : function(d)  { return d.getMinutes() },
  mm  : function(d)  { return zeroPad(d.getMinutes()) },
  h  : function(d)  { return d.getHours() % 12 || 12 },
  hh  : function(d)  { return zeroPad(d.getHours() % 12 || 12) },
  H  : function(d)  { return d.getHours() },
  HH  : function(d)  { return zeroPad(d.getHours()) },
  d  : function(d)  { return d.getDate() },
  dd  : function(d)  { return zeroPad(d.getDate()) },
  ddd  : function(d,o)  { return o.dayNamesShort[d.getDay()] },
  dddd: function(d,o)  { return o.dayNames[d.getDay()] },
  M  : function(d)  { return d.getMonth() + 1 },
  MM  : function(d)  { return zeroPad(d.getMonth() + 1) },
  MMM  : function(d,o)  { return o.monthNamesShort[d.getMonth()] },
  MMMM: function(d,o)  { return o.monthNames[d.getMonth()] },
  yy  : function(d)  { return (d.getFullYear()+'').substring(2) },
  yyyy: function(d)  { return d.getFullYear() },
  t  : function(d)  { return d.getHours() < 12 ? 'a' : 'p' },
  tt  : function(d)  { return d.getHours() < 12 ? 'am' : 'pm' },
  T  : function(d)  { return d.getHours() < 12 ? 'A' : 'P' },
  TT  : function(d)  { return d.getHours() < 12 ? 'AM' : 'PM' },
  u  : function(d)  { return formatDate(d, "yyyy-MM-dd'T'HH:mm:ss'Z'") },
  S  : function(d)  {
    var date = d.getDate();
    if (date > 10 && date < 20) return 'th';
    return ['st', 'nd', 'rd'][date%10-1] || 'th';
  }
};



/* Element Dimensions
-----------------------------------------------------------------------------*/

function setOuterWidth(element, width, includeMargins) {
  element.each(function() {
    var e = $(this);
    var w = width - (
      (parseInt(e.css('border-left-width')) || 0) +
      (parseInt(e.css('padding-left')) || 0) +
      (parseInt(e.css('padding-right')) || 0) +
      (parseInt(e.css('border-right-width')) || 0));
    if (includeMargins) {
      w -=
        (parseInt(e.css('margin-left')) || 0) +
        (parseInt(e.css('margin-right')) || 0);
    }
    e.width(w);
  });
}

function setOuterHeight(element, height, includeMargins) {
  element.each(function() {
    var e = $(this);
    var h = height - (
      (parseInt(e.css('border-top-width')) || 0) +
      (parseInt(e.css('padding-top')) || 0) +
      (parseInt(e.css('padding-bottom')) || 0) +
      (parseInt(e.css('border-bottom-width')) || 0));
    if (includeMargins) {
      h -=
        (parseInt(e.css('margin-top')) || 0) +
        (parseInt(e.css('margin-bottom')) || 0);
    }
    e.height(h);
  });
}



/* Position Calculation
-----------------------------------------------------------------------------*/
// nasty bugs in opera 9.25
// position() returning relative to direct parent

var operaPositionBug;

function reportTBody(tbody) {
  if (operaPositionBug == undefined) {
    operaPositionBug = tbody.position().top != tbody.find('tr').position().top;
  }
}

function safePosition(element, td, tr, tbody) {
  var position = element.position();
  if (operaPositionBug) {
    position.top += tbody.position().top + tr.position().top - td.position().top;
  }
  return position;
}



/* Hover Matrix
-----------------------------------------------------------------------------*/

function HoverMatrix(changeCallback) {

  var tops=[], lefts=[],
    prevRowE, prevColE,
    origRow, origCol,
    currRow, currCol;
  
  this.row = function(e, topBug) {
    prevRowE = $(e);
    tops.push(prevRowE.offset().top + (
      (operaPositionBug && prevRowE.is('tr')) ? prevRowE.parent().position().top : 0
    ));
  };
  
  this.col = function(e) {
    prevColE = $(e);
    lefts.push(prevColE.offset().left);
  };

  this.mouse = function(x, y) {
    if (origRow == undefined) {
      tops.push(tops[tops.length-1] + prevRowE.outerHeight());
      lefts.push(lefts[lefts.length-1] + prevColE.outerWidth());
      currRow = currCol = -1;
    }
    var r, c;
    for (r=0; r<tops.length && y>=tops[r]; r++) ;
    for (c=0; c<lefts.length && x>=lefts[c]; c++) ;
    r = r >= tops.length ? -1 : r - 1;
    c = c >= lefts.length ? -1 : c - 1;
    if (r != currRow || c != currCol) {
      currRow = r;
      currCol = c;
      if (r == -1 || c == -1) {
        this.cell = null;
      }else{
        if (origRow == undefined) {
          origRow = r;
          origCol = c;
        }
        this.cell = {
          row: r,
          col: c,
          top: tops[r],
          left: lefts[c],
          width: lefts[c+1] - lefts[c],
          height: tops[r+1] - tops[r],
          isOrig: r==origRow && c==origCol,
          rowDelta: r-origRow,
          colDelta: c-origCol
        };
      }
      changeCallback(this.cell);
    }
  };

}



/* Misc Utils
-----------------------------------------------------------------------------*/

var undefined,
  dayIDs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];

function zeroPad(n) {
  return (n < 10 ? '0' : '') + n;
}

function smartProperty(obj, name) { // get a camel-cased/namespaced property
  if (obj[name] != undefined) {
    return obj[name];
  }
  var parts = name.split(/(?=[A-Z])/),
    i=parts.length-1, res;
  for (; i>=0; i--) {
    res = obj[parts[i].toLowerCase()];
    if (res != undefined) {
      return res;
    }
  }
  return obj[''];
}



})(jQuery);