(function($, undefined) {


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


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

    var defaults = {

        // display
        defaultView: 'month',
        aspectRatio: 1.35,
        header: {
            left: 'title',
            center: '',
            right: 'today prev,next'
        },
        weekends: true,
        // editing
        //editable: false,
        //disableDragging: false,
        //disableResizing: false,

        allDayDefault: false,
        // event ajax
        lazyFetching: true,
        startParam: 'start',
        endParam: 'end',

        // time formats
        titleFormat: {
            month: 'MMMM yyyy',
            week: "MMM d[ yyyy]{ '-'[ 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
        },
        topHeight: 200,
        // locale
        isRTL: false,
        firstDay: 0,
        //monthNames: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
        monthNames: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
        monthNamesShort: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
        dayNames: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'],
        dayNamesShort: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'],
        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'
        },
        addEvent: false,
        // jquery-ui theming
        theme: false,
        buttonIcons: {
            prev: 'circle-triangle-w',
            next: 'circle-triangle-e'
        },
        //selectable: false,
        unselectAuto: true,

        dropAccept: '*'

    };

    // 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 data = $.data(this, 'fullCalendar');
                if (data) {
                    var meth = data[options];
                    if (meth) {
                        var r = meth.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 = $(_element).addClass('fc'),
			elementOuterWidth,
			content = $("<div class='fc-content " + tm + "-widget-content' style='position:relative'/>").prependTo(_element),
			suggestedViewHeight,
			resizeUID = 0,
			ignoreWindowResize = 0,
			date = new Date(),
			viewName,  // the current view name (TODO: look into getting rid of)
			view,      // the current view
			viewInstances = {},
			absoluteViewElement;



            if (options.isRTL) {
                element.addClass('fc-rtl');
            }
            if (options.theme) {
                element.addClass('ui-widget');
            }

            setYMD(date, options.year, options.month, options.date);



            /* View Rendering
            -----------------------------------------------------------------------------*/

            function changeView(v) {
                if (v != viewName) {
                    ignoreWindowResize++; // because setMinHeight might change the height before render (and subsequently setSize) is reached

                    viewUnselect();

                    var oldView = view,
					newViewElement;

                    if (oldView) {
                        if (oldView.eventsChanged) {
                            eventsDirty();
                            oldView.eventDirty = oldView.eventsChanged = false;
                        }
                        if (oldView.beforeHide) {
                            oldView.beforeHide(); // called before changing min-height. if called after, scroll state is reset (in Opera)
                        }
                        setMinHeight(content, content.height());
                        oldView.element.hide();
                    } else {
                        setMinHeight(content, 1); // needs to be 1 (not 0) for IE7, or else view dimensions miscalculated
                    }
                    content.css('overflow', 'hidden');

                    if (viewInstances[v]) {
                        (view = viewInstances[v]).element.show();
                    } else {
                        view = viewInstances[v] = fc.views[v](
						newViewElement = absoluteViewElement =
							$("<div class='fc-view fc-view-" + v + "' style='position:absolute'/>")
								.appendTo(content),
						options,
						v // the view's name
					);
                    }

                    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');
                    }

                    viewName = v;
                    render(); // after height has been set, will make absoluteViewElement's position=relative, then set to null
                    content.css('overflow', '');
                    if (oldView) {
                        setMinHeight(content, 1);
                    }
                    if (!newViewElement && view.afterShow) {
                        view.afterShow(); // called after setting min-height/overflow, so in final scroll state (for Opera)
                    }

                    ignoreWindowResize--;
                }
            }


            function render(inc) {
                if (elementVisible()) {
                    ignoreWindowResize++; // because view.renderEvents might temporarily change the height before setSize is reached

                    viewUnselect();

                    if (suggestedViewHeight === undefined) {
                        calcSize();
                    }

                    if (!view.start || inc || date < view.start || date >= view.end) {
                        view.render(date, inc || 0); // responsible for clearing events
                        setSize(true);

                        if (!eventStart || !options.lazyFetching || view.visStart < eventStart || view.visEnd > eventEnd) {
                            fetchAndRenderEvents();
                        } else {
                            view.renderEvents(events); // don't refetch
                        }
                    }
                    else if (view.sizeDirty || view.eventsDirty || !options.lazyFetching) {
                        view.clearEvents();
                        if (view.sizeDirty) {
                            setSize();

                        }
                        if (options.lazyFetching) {
                            view.renderEvents(events); // don't refetch
                        } else {
                            fetchAndRenderEvents();
                        }
                    }
                    elementOuterWidth = element.outerWidth();
                    view.sizeDirty = false;
                    view.eventsDirty = false;

                    if (header) {
                        // update title text
                        header.find('h2.fc-header-title').html(view.title);
                        // 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');
                        }
                    }

                    ignoreWindowResize--;
                    view.trigger('viewDisplay', _element);
                }
            }


            function elementVisible() {
                return _element.offsetWidth !== 0;
            }

            function bodyVisible() {
                return $('body')[0].offsetWidth !== 0;
            }

            function viewUnselect() {
                if (view) {
                    view.unselect();
                }
            }


            // called when any event objects have been added/removed/changed, rerenders
            function eventsChanged() {
                eventsDirty();
                if (elementVisible()) {
                    view.clearEvents();
                    view.renderEvents(events);
                    view.eventsDirty = false;
                }
            }

            // marks other views' events as dirty
            function eventsDirty() {
                $.each(viewInstances, function() {
                    this.eventsDirty = true;
                });
            }

            // called when we know the element size has changed
            function sizeChanged() {
                sizesDirty();
                if (elementVisible()) {
                    calcSize();
                    setSize();
                    viewUnselect();
                    view.rerenderEvents();
                    view.sizeDirty = false;
                }
            }

            // marks other views' sizes as dirty
            function sizesDirty() {
                $.each(viewInstances, function() {
                    this.sizeDirty = true;
                });
            }




            /* 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) {
				        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
						$.inArray(src, eventSources) != -1) {              // makes sure source hasn't been removed
				        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);
                    if (options.cacheParam) {
                        params[options.cacheParam] = (new Date()).getTime(); // TODO: deprecate cacheParam
                    }
                    pushLoading();
                    $.ajax({
                        url: src,
                        dataType: 'json',
                        data: params,
                        cache: options.cacheParam || false, // don't let jquery prevent caching if cacheParam is being used
                        success: reportEventsAndPop
                    });
                }
                else if ($.isFunction(src)) {

                    pushLoading();
                    src(cloneDate(eventStart), cloneDate(eventEnd), reportEventsAndPop);
                }
                else {
                    reportEvents(src); // src is an array
                }
            }


            // for convenience
            function fetchAndRenderEvents() {
                fetchEvents(function(events) {
                    view.renderEvents(events); // maintain `this` in view
                });
            }

            /* 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: function() {
                    calcSize();
                    sizesDirty();
                    eventsDirty();
                    render();
                },
                getStart: function() { return eventStart },
                getEnd: function() { return eventEnd },
                changeView: changeView,

                getView: function() {
                    return view;
                },

                getDate: function() {
                    return date;
                },

                option: function(name, value) {
                    if (value === undefined) {
                        return options[name];
                    }
                    if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') {
                        options[name] = value;
                        sizeChanged();
                    }
                },

                destroy: function() {
                    $(window).unbind('resize', windowResize);
                    if (header) {
                        header.remove();
                    }
                    content.remove();
                    $.removeData(_element, 'fullCalendar');
                },

                //
                // 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 {
                        setYMD(date, year, month, 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: eventsChanged, // TODO: think of renaming eventsChanged

                //
                // Event Source
                //

                addEventSource: function(source) {
                    eventSources.push(source);
                    fetchEventSource(source, 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);
                },

                //
                // selection
                //

                select: function(start, end, allDay) {
                    view.select(start, end, allDay === undefined ? true : allDay);
                },

                unselect: function() {
                    view.unselect();
                }

            };

            $.data(this, 'fullCalendar', publicMethods); // TODO: look into memory leak implications



            /* 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'>&nbsp;</h2></td>");
                                if (prevButton) {
                                    prevButton.addClass(tm + '-corner-right');
                                }
                                prevButton = null;
                            } 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
            -----------------------------------------------------------------------------*/


            function calcSize() {
                if (options.contentHeight) {
                    suggestedViewHeight = options.contentHeight;
                }
                else if (options.height) {
                    suggestedViewHeight = options.height - (header ? header.height() : 0) - vsides(content[0]);
                }
                else {
                    suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5));
                }
            }


            function setSize(dateChanged) {
                ignoreWindowResize++;
                view.setHeight(suggestedViewHeight, dateChanged);
                if (absoluteViewElement) {
                    absoluteViewElement.css('position', 'relative');
                    absoluteViewElement = null;
                }
                view.setWidth(content.width(), dateChanged);
                ignoreWindowResize--;
            }


            function windowResize() {
                if (!ignoreWindowResize) {
                    if (view.start) { // view has already been rendered
                        var uid = ++resizeUID;
                        setTimeout(function() { // add a delay
                            if (uid == resizeUID && !ignoreWindowResize && elementVisible()) {
                                if (elementOuterWidth != (elementOuterWidth = element.outerWidth())) {
                                    ignoreWindowResize++; // in case the windowResize callback changes the height
                                    sizeChanged();
                                    view.trigger('windowResize', _element);
                                    ignoreWindowResize--;
                                }
                            }
                        }, 200);
                    } else {
                        // calendar must have been initialized in a 0x0 iframe that has just been resized
                        lateRender();
                    }
                }
            }
            $(window).resize(windowResize);



            /* External event dropping
            --------------------------------------------------------*/

            if (options.droppable) {
                var _dragElement;
                $(document)
				.bind('dragstart', function(ev, ui) {
				    var _e = ev.target;
				    var e = $(_e);
				    if (!e.parents('.fc').length) { // not already inside a calendar
				        var accept = options.dropAccept;
				        if ($.isFunction(accept) ? accept.call(_e, e) : e.is(accept)) {
				            _dragElement = _e;
				            view.dragStart(_dragElement, ev, ui);
				        }
				    }
				})
				.bind('dragstop', function(ev, ui) {
				    if (_dragElement) {
				        view.dragStop(_dragElement, ev, ui);
				        _dragElement = null;
				    }
				});
            }



            // let's begin...
            changeView(options.defaultView);


            // needed for IE in a 0x0 iframe, b/c when it is resized, never triggers a windowResize
            if (!bodyVisible()) {
                lateRender();
            }


            // called when we know the calendar couldn't be rendered when it was initialized,
            // but we think it's ready now
            function lateRender() {
                setTimeout(function() { // IE7 needs this so dimensions are calculated correctly
                    if (!view.start && bodyVisible()) { // !view.start makes sure this never happens more than once
                        render();
                    }
                }, 0);
            }


        });

        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 = [];
        }
    }
    // TODO: if there is no start date, return false to indicate an invalid event


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

    setDefaults({
        weekMode: 'fixed'
    });

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

    views.basicWeek = function(element, options, viewName) {
        return new Grid(element, options, {
            render: function(date, delta) {
                if (delta) {
                    addDays(date, delta * 7);
                }
                var visStart = this.visStart = cloneDate(
					this.start = addDays(cloneDate(date), -((date.getDay() - options.firstDay + 7) % 7))
				),
				visEnd = this.visEnd = cloneDate(
					this.end = addDays(cloneDate(visStart), 7)
				);
                if (!options.weekends) {
                    skipWeekend(visStart);
                    skipWeekend(visEnd, -1, true);
                }

                this.title = formatDates(
				visStart,
				addDays(cloneDate(visEnd), -1),
				this.option('titleFormat'),
				options
			);
                this.renderGrid(
				1, options.weekends ? 7 : 5,
				this.option('columnFormat'),
				false
			);
            }
        }, viewName);
    };

    views.basicDay = function(element, options, viewName) {
        return new Grid(element, options, {
            render: function(date, delta) {
                if (delta) {
                    addDays(date, delta);
                    if (!options.weekends) {
                        skipWeekend(date, delta < 0 ? -1 : 1);
                    }
                }
                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
			);
            }
        }, viewName);
    };


    // rendering bugs

    var tdHeightBug;


    function Grid(element, options, methods, viewName) {

        var tm, firstDay,
		nwe,            // no weekends (int)
		rtl, dis, dit,  // day index sign / translate
		viewWidth, viewHeight,
		rowCnt, colCnt,
		colWidth,
		thead, tbody,
		cachedEvents = [],
		segmentContainer,
		dayContentPositions = new HorizontalPositionCache(function(dayOfWeek) {
		    return tbody.find('td:eq(' + ((dayOfWeek - Math.max(firstDay, nwe) + colCnt) % colCnt) + ') div div');
		}),
        // ...

        // initialize superclass
	view = $.extend(this, viewMethods, methods, {
	    renderGrid: renderGrid,
	    renderEvents: renderEvents,
	    rerenderEvents: rerenderEvents,
	    clearEvents: clearEvents,
	    setHeight: setHeight,
	    setWidth: setWidth,
	    defaultEventEnd: function(event) { // calculates an end if event doesnt have one, mostly for resizing
	        return cloneDate(event.start);
	    }
	});
        view.name = viewName;
        view.init(element, options);



        /* Grid Rendering
        -----------------------------------------------------------------------------*/


        disableTextSelection(element.addClass('fc-grid'));


        function renderGrid(r, c, colFormat, showNumbers) {

            rowCnt = r;
            colCnt = c;

            // update option-derived variables
            tm = options.theme ? 'ui' : 'fc';
            nwe = options.weekends ? 0 : 1;
            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 divTableContain = $("<div/>").css({ "overflow-y": "scroll","overflow-x": "hidden", "height": $(document.documentElement).height()-35}).appendTo(element);
                //var table = $("<table/>").appendTo(divTableContain);
                var table = $("<table/>").appendTo(element);
                s = "<thead ><tr class='calender-header'>";
                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);
                    if (nwe) {
                        skipWeekend(d);
                    }
                }

                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 && options.defaultView != "basicDay" ?
						' fc-today ' + tm + '-state-highlight' :
						' fc-not-today') + "'>" +
                        						"<div class='add-toolbar'><span date='" + d.Format("yyyy-MM-dd hh:mm:ss") + "'  title='添加日常' class='add_daily'>&nbsp;</span>" +
                       						"</div>" +
						(showNumbers ? "<div class='fc-day-number'>" + (d.getMonth() + 1) + "/" + d.getDate() + "</div>" : '') +
						"<div class='fc-day-content'><div style='position:relative'>&nbsp;</div></div></td>";
                        addDays(d, 1);
                        if (nwe) {
                            skipWeekend(d);
                        }
                        //$.alert()添加动作图标
                    }
                    s += "</tr>";
                    //$.alert(s);
                }
                tbody = $(s + "</tbody>").appendTo(table);
                $(".add_daily", tbody).each(function() {
                    if (options.addEvent && $.isFunction(options.addEvent)) {
                        $(this).click(function() {
                            options.addEvent.call(this, $(this).attr("date"));
                        });
                    }
                });
                dayBind(tbody.find('td'));
                //alert(table.parent().parent().html())
                segmentContainer = $("<div style='position:absolute;z-index:8;top:0;left:0'/>").appendTo(element);

            } else { // NOT first time, reuse as many cells as possible

                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 style='position:relative'>&nbsp;</div></div>" +
							"</td>";
                            addDays(d, 1);
                            if (nwe) {
                                skipWeekend(d);
                            }
                        }
                        s += "</tr>";
                    }

                    tbody.append(s);
                }
                dayBind(tbody.find('td.fc-new').removeClass('fc-new'));

                // 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.getMonth() + 1) + "/" + d.getDate());
                    addDays(d, 1);
                    if (nwe) {
                        skipWeekend(d);
                    }
                });

                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);
                        if (nwe) {
                            skipWeekend(d);
                        }
                    });

                    // 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);
                        if (nwe) {
                            skipWeekend(d);
                        }
                    });

                }


            }



        }
        function setHeight(height) {
            viewHeight = height;
            var leftTDs = tbody.find('tr td:first-child'),
			tbodyHeight = viewHeight,
			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);
            }
            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);
            }
        }


        function setWidth(width) {
            viewWidth = width;
            dayContentPositions.clear();
            setOuterWidth(
			thead.find('th').slice(0, -1),
			colWidth = Math.floor(viewWidth / colCnt)
		);
        }



        /* Event Rendering
        -----------------------------------------------------------------------------*/


        function renderEvents(events) {

            view.reportEvents(cachedEvents = events);
            renderSegs(compileSegs(events));
        }


        function rerenderEvents(modifiedEventId) {

            clearEvents();
            renderSegs(compileSegs(cachedEvents), modifiedEventId);
        }


        function clearEvents() {
            view._clearEvents(); // only clears the hashes
            segmentContainer.empty();
        }


        function compileSegs(events) {
            var d1 = cloneDate(view.visStart),
			d2 = addDays(cloneDate(d1), colCnt),
			visEventsEnds = $.map(events, exclEndDay),
			i, row,
			j, level,
			k, seg,
			segs = [];

            for (i = 0; i < rowCnt; i++) {
                row = stackSegs(view.sliceSegs(events, visEventsEnds, d1, d2));
                //                alert(row.length)
                for (j = 0; j < row.length; j++) {
                    level = row[j];
                    for (k = 0; k < level.length; k++) {
                        seg = level[k];
                        seg.row = i;
                        seg.level = j;
                        segs.push(seg);
                    }
                }
                addDays(d1, 7);
                addDays(d2, 7);
            }
            return segs;
        }


        function renderSegs(segs, modifiedEventId) {
            _renderDaySegs(
			segs,
			rowCnt,
			view,
			0,
			viewWidth,
			function(i) { return tbody.find('tr:eq(' + i + ')') },
			dayContentPositions.left,
			dayContentPositions.right,
			segmentContainer,
			bindSegHandlers,
			modifiedEventId
		);
        }


        function bindSegHandlers(event, eventElement, seg) {
            view.eventElementHandlers(event, eventElement);
            if (event.editable || event.editable === undefined && options.editable) {
                draggableEvent(event, eventElement);
                if (seg.isEnd) {
                    view.resizableDayEvent(event, eventElement, colWidth);
                }
            }
        }



        /* Event Dragging
        -----------------------------------------------------------------------------*/


        function draggableEvent(event, eventElement) {
            if (!options.disableDragging && eventElement.draggable) {
                var dayDelta;
                eventElement.draggable({
                    zIndex: 9,
                    delay: 50,
                    opacity: view.option('dragOpacity'),
                    revertDuration: options.dragRevertDuration,
                    start: function(ev, ui) {
                        view.trigger('eventDragStart', eventElement, event, ev, ui);
                        view.hideEvents(event, eventElement);
                        hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
                            eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta);
                            clearOverlay();
                            if (cell) {
                                dayDelta = rowDelta * 7 + colDelta * dis;
                                renderDayOverlay(
								addDays(cloneDate(event.start), dayDelta),
								addDays(exclEndDay(event), dayDelta)
							);
                            } else {
                                dayDelta = 0;
                            }
                        }, ev, 'drag');
                    },
                    stop: function(ev, ui) {
                        hoverListener.stop();
                        clearOverlay();
                        view.trigger('eventDragStop', eventElement, event, ev, ui);
                        if (dayDelta) {
                            eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link
                            view.eventDrop(this, event, dayDelta, 0, event.allDay, ev, ui);
                        } else {
                            if ($.browser.msie) {
                                eventElement.css('filter', ''); // clear IE opacity side-effects
                            }
                            view.showEvents(event, eventElement);
                        }
                    }
                });
            }
        }



        /* Day clicking and binding
        ---------------------------------------------------------*/

        function dayBind(days) {
            days.dblclick(dayClick) //改成双击事件
			.mousedown(selectionMousedown);
        }

        function dayClick(ev) {
            if (!view.option('selectable')) { // SelectionManager will worry about dayClick
                var n = parseInt(this.className.match(/fc\-day(\d+)/)[1]),
				date = addDays(
					cloneDate(view.visStart),
					Math.floor(n / colCnt) * 7 + n % colCnt
				);
                // TODO: what about weekends in middle of week?
                view.trigger('dayClick', this, date, true, ev);
            }
        }



        /* Coordinate Utilities
        --------------------------------------------------------*/

        var coordinateGrid = new CoordinateGrid(function(rows, cols) {
            var e, n, p;
            var tds = tbody.find('tr:first td');
            if (rtl) {
                tds = $(tds.get().reverse());
            }
            tds.each(function(i, _e) {
                e = $(_e);
                n = e.offset().left;
                if (i) {
                    p[1] = n;
                }
                p = [n];
                cols[i] = p;
            });
            p[1] = n + e.outerWidth();
            tbody.find('tr').each(function(i, _e) {
                e = $(_e);
                n = e.offset().top;
                if (i) {
                    p[1] = n;
                }
                p = [n];
                rows[i] = p;
            });
            p[1] = n + e.outerHeight();
        });

        var hoverListener = new HoverListener(coordinateGrid);



        /* Selecting
        --------------------------------------------------------*/

        var selected = false;
        var selectionMousedown = selection_dayMousedown(
		view, hoverListener, cellDate, function() { return true }, renderDayOverlay, clearOverlay, reportSelection, unselect
	);

        view.select = function(startDate, endDate, allDay) {
            coordinateGrid.build();
            unselect();
            if (!endDate) {
                endDate = cloneDate(startDate);
            }
            renderDayOverlay(startDate, addDays(cloneDate(endDate), 1));
            reportSelection(startDate, endDate, allDay);
        };

        function reportSelection(startDate, endDate, allDay, ev) {
            selected = true;
            view.trigger('select', view, startDate, endDate, allDay, ev);
        }

        function unselect(ev) {
            if (selected) {
                clearOverlay();
                selected = false;
                view.trigger('unselect', view, ev);
            }
        }
        view.unselect = unselect;

        selection_unselectAuto(view, unselect);



        /* External dragging
        ------------------------------------------------------*/

        view.dragStart = function(_dragElement, ev, ui) {
            hoverListener.start(function(cell) {
                clearOverlay();
                if (cell) {
                    _renderDayOverlay(cell.row, cell.col, cell.row, cell.col);
                }
            }, ev);
        };

        view.dragStop = function(_dragElement, ev, ui) {
            var cell = hoverListener.stop();
            clearOverlay();
            if (cell) {
                var d = cellDate(cell);
                view.trigger('drop', _dragElement, d, true, ev, ui);
            }
        };



        /* Semi-transparent Overlay Helpers
        ------------------------------------------------------*/

        function renderDayOverlay(overlayStart, overlayEnd) { // overlayEnd is exclusive
            var rowStart = cloneDate(view.visStart);
            var rowEnd = addDays(cloneDate(rowStart), colCnt);
            for (var i = 0; i < rowCnt; i++) {
                var stretchStart = new Date(Math.max(rowStart, overlayStart));
                var stretchEnd = new Date(Math.min(rowEnd, overlayEnd));
                if (stretchStart < stretchEnd) {
                    var colStart, colEnd;
                    if (rtl) {
                        colStart = dayDiff(stretchEnd, rowStart) * dis + dit + 1;
                        colEnd = dayDiff(stretchStart, rowStart) * dis + dit + 1;
                    } else {
                        colStart = dayDiff(stretchStart, rowStart);
                        colEnd = dayDiff(stretchEnd, rowStart);
                    }
                    dayBind(
					_renderDayOverlay(i, colStart, i, colEnd - 1)
				);
                }
                addDays(rowStart, 7);
                addDays(rowEnd, 7);
            }
        }

        function _renderDayOverlay(row0, col0, row1, col1) { // row1,col1 is inclusive
            var rect = coordinateGrid.rect(row0, col0, row1, col1, element);
            return view.renderOverlay(rect, element);
        }

        function clearOverlay() {
            view.clearOverlays();
        }



        /* Date Utils
        ---------------------------------------------------*/


        function cellDate(cell) {
            return addDays(cloneDate(view.visStart), cell.row * 7 + cell.col * dis + dit);
            // TODO: what about weekends in middle of week?
        }


    }

    function FormatTwo(n)//格式化数字为两位字符表示
    {
        var m = new String();
        var tmp = new String(n);
        if (n < 10 && tmp.length < 2) {
            m = "0" + n;
        }
        else {
            m = n;
        }

        return m;

    }
    function getNameByClass(className) {

        switch ($.trim(className)) {

            case "attentionstyle": { return "我关注的日程:"; }
            case "myselfstyle": return "我的日程:";
            case "notepatestyle": return "便笺:";
            default: return "其他:"

        }
    }
    function _renderDaySegs(segs, rowCnt, view, minLeft, maxLeft, getRow, dayContentLeft, dayContentRight, segmentContainer, bindSegHandlers, modifiedEventId) {

        var options = view.options,
		rtl = options.isRTL,
		i, segCnt = segs.length, seg,
		event,
		className,
		left, right,
		html = '',
		eventElements,
		eventElement,
		triggerRes,
		hsideCache = {},
		vmarginCache = {},
		key, val,
		rowI, top, levelI, levelHeight,
		rowDivs = [],
		rowDivTops = [];

        // calculate desired position/dimensions, create html
        for (i = 0; i < segCnt; i++) {
            seg = segs[i];

            if (seg.event.end == null || seg.event.end == undefined) {
                seg.event.end = seg.event.start;
            }
            event = seg.event;

            className = 'fc-event fc-event-hori ';
            if (rtl) {
                if (seg.isStart) {
                    className += 'fc-corner-right ';
                }
                if (seg.isEnd) {
                    className += 'fc-corner-left ';
                }
                left = seg.isEnd ? dayContentLeft(seg.end.getDay() - 1) : minLeft;
                right = seg.isStart ? dayContentRight(seg.start.getDay()) : maxLeft;
            } else {
                if (seg.isStart) {
                    className += 'fc-corner-left ';
                }
                if (seg.isEnd) {
                    className += 'fc-corner-right ';
                }
                left = seg.isStart ? dayContentLeft(seg.start.getDay()) : minLeft;
                right = seg.isEnd ? dayContentRight(seg.end.getDay() - 1) : maxLeft;
            }
            var strHTML="";
            if(options.defaultView!="basicDay")
            {
                strHTML="<br/>";
            }
            html +=
			"<div class='" + className + event.className.join(' ') + "' style='position:absolute;z-index:8;left:" + left + "px'>" +
				"<a" + (event.url ? " href='" + htmlEscape(event.url) + "'" : '') + ">" +
					(true ?
						"<span class='fc-event-time'>" +htmlEscape(FormatTwo(event.start.getHours()) + ":" + FormatTwo(event.start.getMinutes()) + "-" + FormatTwo(event.end.getHours()) + ":" + FormatTwo(event.end.getMinutes())) +""+strHTML+"</span>": '') +
				"<span class='fc-event-title' title='" + getNameByClass(event.className) + htmlEscape(event.title) + "' style='color:" + $.trim(event.Color) + "'>" + ((event.className == 'attentionstyle') ? "(" + event.createname + ")&nbsp;" : "") + htmlEscape(event.title) + "</span>" +
				 "<span class='fc-event-daysort'>" +
                             (typeof event.dayIndex == "undefined" ? "" : event.dayIndex) +
				"</span>" + (event.isHaveAnnexSheet == "true" ? "<span class='fc-event-attachment' title='日程有附件'>&nbsp;</span>" : "") +
				"</a>" +
				((event.editable || event.editable === undefined && options.editable) && !options.disableResizing && $.fn.resizable ?
					"<div class='ui-resizable-handle ui-resizable-" + (rtl ? 'w' : 'e') + "'></div>"
					: '') +
			"</div>";
            seg.left = left;
            seg.outerWidth = right - left;
        }
        segmentContainer[0].innerHTML = html; // faster than html()
        eventElements = segmentContainer.children();
        // retrieve elements, run through eventRender callback, bind handlers
        for (i = 0; i < segCnt; i++) {
            seg = segs[i];
            eventElement = $(eventElements[i]); // faster than eq()
            event = seg.event;
            triggerRes = view.trigger('eventRender', event, event, eventElement);
            if (triggerRes === false) {
                eventElement.remove();
            } else {
                if (triggerRes && triggerRes !== true) {
                    eventElement.remove();
                    eventElement = $(triggerRes)
					.css({
					    position: 'absolute',
					    left: seg.left
					})
					.appendTo(segmentContainer);
                }
                seg.element = eventElement;
                if (event._id === modifiedEventId) {
                    bindSegHandlers(event, eventElement, seg);
                } else {
                    eventElement[0]._fci = i; // for lazySegBind
                }
                view.reportEventElement(event, eventElement);
            }
        }

        lazySegBind(segmentContainer, segs, bindSegHandlers);

        // record event horizontal sides
        for (i = 0; i < segCnt; i++) {
            seg = segs[i];
            if (eventElement = seg.element) {
                val = hsideCache[key = seg.key = cssKey(eventElement[0])];
                seg.hsides = val === undefined ? (hsideCache[key] = hsides(eventElement[0], true)) : val;
            }
        }

        // set event widths
        for (i = 0; i < segCnt; i++) {
            seg = segs[i];
            if (eventElement = seg.element) {
                eventElement[0].style.width = seg.outerWidth - seg.hsides + 'px';
            }
        }

        // record event heights
        for (i = 0; i < segCnt; i++) {
            seg = segs[i];
            if (eventElement = seg.element) {
                val = vmarginCache[key = seg.key];
                seg.outerHeight = eventElement[0].offsetHeight + (
				val === undefined ? (vmarginCache[key] = vmargins(eventElement[0])) : val
			);
            }
        }

        // set row heights, calculate event tops (in relation to row top)
        for (i = 0, rowI = 0; rowI < rowCnt; rowI++) {
            top = levelI = levelHeight = 0;
            while (i < segCnt && (seg = segs[i]).row == rowI) {
                if (seg.level != levelI) {
                    top += levelHeight;
                    levelHeight = 0;
                    levelI++;
                }
                levelHeight = Math.max(levelHeight, seg.outerHeight || 0);
                seg.top = top;
                i++;
            }
            rowDivs[rowI] = getRow(rowI).find('td:first div.fc-day-content > div') // optimal selector?
			.height(top + levelHeight);
        }

        // calculate row tops
        for (rowI = 0; rowI < rowCnt; rowI++) {
            rowDivTops[rowI] = rowDivs[rowI][0].offsetTop;
        }

        // set event tops
        for (i = 0; i < segCnt; i++) {
            seg = segs[i];
            if (eventElement = seg.element) {
                eventElement[0].style.top = rowDivTops[seg.row] + seg.top + 'px';
                event = seg.event;
                view.trigger('eventAfterRender', event, event, eventElement);
            }
        }



    }



    /* 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
        },
        minTime: 0,
        maxTime: 24
    });

    views.agendaWeek = function(element, options, viewName) {
        return new Agenda(element, options, {
            render: function(date, delta) {
                if (delta) {
                    addDays(date, delta * 7);
                }
                var visStart = this.visStart = cloneDate(
					this.start = addDays(cloneDate(date), -((date.getDay() - options.firstDay + 7) % 7))
				),
				visEnd = this.visEnd = cloneDate(
					this.end = addDays(cloneDate(visStart), 7)
				);
                if (!options.weekends) {
                    skipWeekend(visStart);
                    skipWeekend(visEnd, -1, true);
                }

                this.title = formatDates(
				visStart,
				addDays(cloneDate(visEnd), -1),
				this.option('titleFormat'),
				options
			);
                this.renderAgenda(
				options.weekends ? 7 : 5,
				this.option('columnFormat')
			);
            }
        }, viewName);
    };

    views.agendaDay = function(element, options, viewName) {
        return new Agenda(element, options, {
            render: function(date, delta) {
                if (delta) {
                    addDays(date, delta);
                    if (!options.weekends) {
                        skipWeekend(date, delta < 0 ? -1 : 1);
                    }
                }
                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')
			);
            }
        }, viewName);
    };

    function Agenda(element, options, methods, viewName) {

        var head, body, bodyContent, bodyTable, bg,
		colCnt,
		slotCnt = 0, // spanning all the way across
		axisWidth, colWidth, slotHeight,
		viewWidth, viewHeight,
		savedScrollTop,
		cachedEvents = [],
		daySegmentContainer,
		slotSegmentContainer,
		tm, firstDay,
		nwe,            // no weekends (int)
		rtl, dis, dit,  // day index sign / translate
		minMinute, maxMinute,
		colContentPositions = new HorizontalPositionCache(function(col) {
		    return bg.find('td:eq(' + col + ') div div');
		}),
		slotTopCache = {},
        // ...

	view = $.extend(this, viewMethods, methods, {
	    renderAgenda: renderAgenda,
	    renderEvents: renderEvents,
	    rerenderEvents: rerenderEvents,
	    clearEvents: clearEvents,
	    setHeight: setHeight,
	    setWidth: setWidth,
	    beforeHide: function() {
	        savedScrollTop = body.scrollTop();
	    },
	    afterShow: function() {
	        body.scrollTop(savedScrollTop);
	    },
	    defaultEventEnd: function(event) {
	        var start = cloneDate(event.start);
	        if (event.allDay) {
	            return start;
	        }
	        return addMinutes(start, options.defaultEventMinutes);
	    }
	});
        view.name = viewName;
        view.init(element, options);



        /* Time-slot rendering
        -----------------------------------------------------------------------------*/


        disableTextSelection(element.addClass('fc-agenda'));


        function renderAgenda(c, colFormat) {

            colCnt = c;

            // update option-derived variables
            tm = options.theme ? 'ui' : 'fc';
            nwe = options.weekends ? 0 : 1;
            firstDay = options.firstDay;
            if (rtl = options.isRTL) {
                dis = -1;
                dit = colCnt - 1;
            } else {
                dis = 1;
                dit = 0;
            }
            minMinute = parseTime(options.minTime);
            maxMinute = parseTime(options.maxTime);

            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 calender-header" + (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);
                    if (nwe) {
                        skipWeekend(d, dis);
                    }
                }
                s += "<th class='" + tm + "-state-default'>&nbsp;</th></tr>";
                if (options.allDaySlot) {
                    s += "<tr id='trAllDay' 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 style='position:relative'>&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);

                dayBind(head.find('td'));

                // all-day event container
                daySegmentContainer = $("<div style='position:absolute;z-index:8;top:0;left:0'/>").appendTo(head);

                // body
                d = zeroDate();
                var maxd = addMinutes(cloneDate(d), maxMinute);
                addMinutes(d, minMinute);

                s = "<table>";
                for (i = 0; d < maxd; i++) {
                    minutes = d.getMinutes();
                    s += "<tr class='" +
					(!i ? 'fc-first' : (!minutes ? '' : 'fc-minor')) +
					"'><th class='fc-axis fc-leftmost " + tm + "-state-default'>" +
					((!slotNormal || !minutes) ? formatDate(d, options.axisFormat) : '&nbsp;') +
					"</th><td class='fc-slot" + i + ' ' +
						tm + "-state-default'><div style='position:relative'>&nbsp;</div></td></tr>";
                    addMinutes(d, options.slotMinutes);
                    slotCnt++;
                }
                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);
                slotBind(body.find('td'));
                //$.alert(s);
                // slot event container
                slotSegmentContainer = $("<div style='position:absolute;z-index:8;top:0;left:0'/>").appendTo(bodyContent);

                // 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[d.getDay()] + ' ' + // needs to be first
					tm + '-state-default ' +
					(!i ? '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);
                    if (nwe) {
                        skipWeekend(d, dis);
                    }
                }
                s += "</tr></table></div>";
                bg = $(s).appendTo(element);

            } else { // skeleton already built, just modify it

                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);
                    if (nwe) {
                        skipWeekend(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);
                    if (nwe) {
                        skipWeekend(d, dis);
                    }
                });

            }
            $("#trAllDay", body.parent()).hide();
        }


        function resetScroll() {
            var d0 = zeroDate(),
			scrollDate = cloneDate(d0);
            scrollDate.setHours(options.firstHour);
            var top = timePosition(d0, scrollDate) + 1, // +1 for the border
			scroll = function() {
			    body.scrollTop(top);
			};
            // scroll();
            //setTimeout(scroll, 0); // overrides any previous scroll state made by the browser
        }

        function setHeight(height, dateChanged) {

            height = $(document.documentElement).height();
            viewHeight = height - options.topHeight;
            slotTopCache = {};
            body.height(height - options.topHeight);
            slotHeight = body.find('tr:first div').height() + 1;
            bg.css({
                top: head.find('tr').height(),
                height: height
            });

            if (dateChanged) {
                resetScroll();
            }


        }
        function setWidth(width) {
            viewWidth = width;
            colContentPositions.clear();

            body.width(width);
            bodyTable.width('');

            var topTDs = head.find('tr:first th'),
			stripeTDs = bg.find('td'),
			clientWidth = body[0].clientWidth;

            bodyTable.width(clientWidth);

            // 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((clientWidth - axisWidth) / colCnt);
            setOuterWidth(stripeTDs.slice(0, -1), colWidth);
            setOuterWidth(topTDs.slice(1, -2), colWidth);
            setOuterWidth(topTDs.slice(-2, -1), clientWidth - axisWidth - colWidth * (colCnt - 1));

            bg.css({
                left: axisWidth,
                width: clientWidth - axisWidth
            });
        }



        /* Slot/Day clicking and binding
        -----------------------------------------------------------------------*/


        function dayBind(tds) {
            tds.click(slotClick)
			.mousedown(daySelectionMousedown);
        }


        function slotBind(tds) {
            tds.click(slotClick)
			.mousedown(slotSelectionMousedown);
        }


        function slotClick(ev) {
            if (!view.option('selectable')) { // SelectionManager will worry about dayClick
                var col = Math.min(colCnt - 1, Math.floor((ev.pageX - bg.offset().left) / colWidth)),
				date = addDays(cloneDate(view.visStart), col * dis + dit),
				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 + minMinute);
                    view.trigger('dayClick', this, date, false, ev);
                } else {
                    view.trigger('dayClick', this, date, true, ev);
                }
            }
        }



        /* Event Rendering
        -----------------------------------------------------------------------------*/

        function renderEvents(events, modifiedEventId) {
            view.reportEvents(cachedEvents = 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(compileDaySegs(dayEvents), modifiedEventId);
            renderSlotSegs(compileSlotSegs(slotEvents), modifiedEventId);
        }


        function rerenderEvents(modifiedEventId) {
            clearEvents();
            renderEvents(cachedEvents, modifiedEventId);
        }


        function clearEvents() {
            view._clearEvents(); // only clears the hashes
            daySegmentContainer.empty();
            slotSegmentContainer.empty();
        }





        function compileDaySegs(events) {
            var levels = stackSegs(view.sliceSegs(events, $.map(events, exclEndDay), view.visStart, view.visEnd)),
			i, levelCnt = levels.length, level,
			j, seg,
			segs = [];
            for (i = 0; i < levelCnt; i++) {
                level = levels[i];
                for (j = 0; j < level.length; j++) {
                    seg = level[j];
                    seg.row = 0;
                    seg.level = i;
                    segs.push(seg);
                }
            }
            return segs;
        }


        function compileSlotSegs(events) {
            var d = addMinutes(cloneDate(view.visStart), minMinute),
			visEventEnds = $.map(events, slotEventEnd),
			i, col,
			j, level,
			k, seg,
			segs = [];
            for (i = 0; i < colCnt; i++) {
                col = stackSegs(view.sliceSegs(events, visEventEnds, d, addMinutes(cloneDate(d), maxMinute - minMinute)));
                countForwardSegs(col);
                for (j = 0; j < col.length; j++) {
                    level = col[j];
                    for (k = 0; k < level.length; k++) {
                        seg = level[k];
                        seg.col = i;
                        seg.level = j;
                        segs.push(seg);
                    }
                }
                addDays(d, 1, true);
            }
            return segs;
        }




        // renders 'all-day' events at the top

        function renderDaySegs(segs, modifiedEventId) {
            if (options.allDaySlot) {
                _renderDaySegs(
				segs,
				1,
				view,
				axisWidth,
				viewWidth,
				function() {
				    return head.find('tr.fc-all-day');
				},
				function(dayOfWeek) {
				    return axisWidth + colContentPositions.left(dayOfWeekCol(dayOfWeek));
				},
				function(dayOfWeek) {
				    return axisWidth + colContentPositions.right(dayOfWeekCol(dayOfWeek));
				},
				daySegmentContainer,
				daySegBind,
				modifiedEventId
			);
                setHeight(viewHeight); // might have pushed the body down, so resize
            }
        }



        // renders events in the 'time slots' at the bottom

        function renderSlotSegs(segs, modifiedEventId) {

            var i, segCnt = segs.length, seg,
			event,
			className,
			top, bottom,
			colI, levelI, forward,
			leftmost,
			availWidth,
			outerWidth,
			left,
			html = '',
			eventElements,
			eventElement,
			triggerRes,
			vsideCache = {},
			hsideCache = {},
			key, val,
			titleSpan,
			height;

            // calculate position/dimensions, create html
            for (i = 0; i < segCnt; i++) {
                seg = segs[i];
                event = seg.event;
                className = 'fc-event fc-event-vert ';
                if (seg.isStart) {
                    className += 'fc-corner-top ';
                }
                if (seg.isEnd) {
                    className += 'fc-corner-bottom ';
                }
                top = timePosition(seg.start, seg.start);
                bottom = timePosition(seg.start, seg.end);
                colI = seg.col;
                levelI = seg.level;
                forward = seg.forward || 0;
                leftmost = axisWidth + colContentPositions.left(colI * dis + dit);
                availWidth = axisWidth + colContentPositions.right(colI * dis + dit) - leftmost;
                availWidth = Math.min(availWidth - 6, availWidth * .95); // TODO: move this to CSS
                if (levelI) {
                    // indented and thin
                    outerWidth = availWidth / (levelI + forward + 1);
                } else {
                    if (forward) {
                        // moderately wide, aligned left still
                        outerWidth = ((availWidth / (forward + 1)) - (12 / 2)) * 2; // 12 is the predicted width of resizer =
                    } else {
                        // can be entire width, aligned left
                        outerWidth = availWidth;
                    }
                }
                left = leftmost +                                  // leftmost possible
				(availWidth / (levelI + forward + 1) * levelI) // indentation
				* dis + (rtl ? availWidth - outerWidth : 0);   // rtl
                seg.top = top;
                seg.left = left;
                seg.outerWidth = outerWidth;
                seg.outerHeight = bottom - top;
                html += slotSegHtml(event, seg, className);
            }
            slotSegmentContainer[0].innerHTML = html; // faster than html()
            eventElements = slotSegmentContainer.children();

            // retrieve elements, run through eventRender callback, bind event handlers
            for (i = 0; i < segCnt; i++) {
                seg = segs[i];
                event = seg.event;
                eventElement = $(eventElements[i]); // faster than eq()
                triggerRes = view.trigger('eventRender', event, event, eventElement);
                if (triggerRes === false) {
                    eventElement.remove();
                } else {
                    if (triggerRes && triggerRes !== true) {
                        eventElement.remove();
                        eventElement = $(triggerRes)
						.css({
						    position: 'absolute',
						    top: seg.top,
						    left: seg.left
						})
						.appendTo(slotSegmentContainer);
                    }
                    seg.element = eventElement;
                    if (event._id === modifiedEventId) {
                        slotSegBind(event, eventElement, seg);
                    } else {
                        eventElement[0]._fci = i; // for lazySegBind
                    }
                    view.reportEventElement(event, eventElement);
                }
            }

            lazySegBind(slotSegmentContainer, segs, slotSegBind);

            // record event sides and title positions
            for (i = 0; i < segCnt; i++) {
                seg = segs[i];
                if (eventElement = seg.element) {
                    val = vsideCache[key = seg.key = cssKey(eventElement[0])];
                    seg.vsides = val === undefined ? (vsideCache[key] = vsides(eventElement[0], true)) : val;
                    val = hsideCache[key];
                    seg.hsides = val === undefined ? (hsideCache[key] = hsides(eventElement[0], true)) : val;
                    titleSpan = eventElement.find('span.fc-event-title');
                    if (titleSpan.length) {
                        seg.titleTop = titleSpan[0].offsetTop;
                    }
                }
            }

            // set all positions/dimensions at once
            for (i = 0; i < segCnt; i++) {
                seg = segs[i];
                if (eventElement = seg.element) {
                    eventElement[0].style.width = seg.outerWidth - seg.hsides + 'px';
                    eventElement[0].style.height = (height = seg.outerHeight - seg.vsides) + 'px';
                    event = seg.event;
                    if (seg.titleTop !== undefined && height - seg.titleTop < 10) {
                        // not enough room for title, put it in the time header
                        eventElement.find('span.fc-event-time')
						.text(formatDate(event.start, view.option('timeFormat')) + ' - ' + event.title);
                        eventElement.find('span.fc-event-title')
						.remove();
                    }
                    view.trigger('eventAfterRender', event, event, eventElement);
                }
            }

        }

        function slotSegHtml(event, seg, className) {
            return "<div class='" + className + event.className.join(' ') + "' style='position:absolute;z-index:8;top:" + seg.top + "px;left:" + seg.left + "px'>" +
			"<a" + (event.url ? " href='" + htmlEscape(event.url) + "'" : '') + ">" +
				"<span class='fc-event-bg'></span>" +
				"<span class='fc-event-time'>" + htmlEscape(formatDates(event.start, event.end, view.option('timeFormat'))) + "</span>" +
				"<span class='fc-event-title'>" + htmlEscape(event.title) + "</span>" +
			"</a>" +
			((event.editable || event.editable === undefined && options.editable) && !options.disableResizing && $.fn.resizable ?
				"<div class='ui-resizable-handle ui-resizable-s'>=</div>"
				: '') +
		"</div>";
        }



        function daySegBind(event, eventElement, seg) {
            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);
                }
            }
        }



        function slotSegBind(event, eventElement, seg) {
            view.eventElementHandlers(event, eventElement);
            if (event.editable || event.editable === undefined && options.editable) {
                var timeElement = eventElement.find('span.fc-event-time');
                draggableSlotEvent(event, eventElement, timeElement);
                if (seg.isEnd) {
                    resizableSlotEvent(event, eventElement, timeElement);
                }
            }
        }




        /* Event Dragging
        -----------------------------------------------------------------------------*/



        // when event starts out FULL-DAY

        function draggableDayEvent(event, eventElement, isStart) {
            if (!options.disableDragging && eventElement.draggable) {
                var origWidth;
                var allDay = true;
                var dayDelta;
                eventElement.draggable({
                    zIndex: 9,
                    opacity: view.option('dragOpacity', 'month'), // use whatever the month view was using
                    revertDuration: options.dragRevertDuration,
                    start: function(ev, ui) {
                        view.trigger('eventDragStart', eventElement, event, ev, ui);
                        view.hideEvents(event, eventElement);
                        origWidth = eventElement.width();
                        hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
                            eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta);
                            clearOverlay();
                            if (cell) {
                                dayDelta = colDelta * dis;
                                if (!cell.row) {
                                    // on full-days
                                    renderDayOverlay(
									addDays(cloneDate(event.start), dayDelta),
									addDays(exclEndDay(event), dayDelta)
								);
                                    resetElement();
                                } 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;
                                    }
                                }
                            }
                        }, ev, 'drag');
                    },
                    stop: function(ev, ui) {
                        var cell = hoverListener.stop();
                        clearOverlay();
                        view.trigger('eventDragStop', eventElement, event, ev, ui);
                        if (cell && (!allDay || dayDelta)) {
                            // changed!
                            eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link
                            var minuteDelta = 0;
                            if (!allDay) {
                                minuteDelta = Math.round((eventElement.offset().top - bodyContent.offset().top) / slotHeight)
								* options.slotMinutes
								+ minMinute
								- (event.start.getHours() * 60 + event.start.getMinutes());
                            }
                            view.eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui);
                        } else {
                            // hasn't moved or is out of bounds (draggable has already reverted)
                            resetElement();
                            if ($.browser.msie) {
                                eventElement.css('filter', ''); // clear IE opacity side-effects
                            }
                            view.showEvents(event, eventElement);
                        }
                    }
                });
                function resetElement() {
                    if (!allDay) {
                        eventElement
						.width(origWidth)
						.height('')
						.draggable('option', 'grid', null);
                        allDay = true;
                    }
                }
            }
        }



        // when event starts out IN TIMESLOTS

        function draggableSlotEvent(event, eventElement, timeElement) {
            if (!options.disableDragging && eventElement.draggable) {
                var origPosition;
                var allDay = false;
                var dayDelta;
                var minuteDelta;
                var prevMinuteDelta;
                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.trigger('eventDragStart', eventElement, event, ev, ui);
                        view.hideEvents(event, eventElement);
                        if ($.browser.msie) {
                            eventElement.find('span.fc-event-bg').hide(); // nested opacities mess up in IE, just hide
                        }
                        origPosition = eventElement.position();
                        minuteDelta = prevMinuteDelta = 0;
                        hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
                            eventElement.draggable('option', 'revert', !cell);
                            clearOverlay();
                            if (cell) {
                                dayDelta = colDelta * dis;
                                if (options.allDaySlot && !cell.row) {
                                    // over full days
                                    if (!allDay) {
                                        // convert to temporary all-day event
                                        allDay = true;
                                        timeElement.hide();
                                        eventElement.draggable('option', 'grid', null);
                                    }
                                    renderDayOverlay(
									addDays(cloneDate(event.start), dayDelta),
									addDays(exclEndDay(event), dayDelta)
								);
                                } else {
                                    // on slots
                                    resetElement();
                                }
                            }
                        }, ev, 'drag');
                    },
                    drag: function(ev, ui) {
                        minuteDelta = Math.round((ui.position.top - origPosition.top) / slotHeight) * options.slotMinutes;
                        if (minuteDelta != prevMinuteDelta) {
                            if (!allDay) {
                                updateTimeText(minuteDelta);
                            }
                            prevMinuteDelta = minuteDelta;
                        }
                    },
                    stop: function(ev, ui) {
                        var cell = hoverListener.stop();
                        clearOverlay();
                        view.trigger('eventDragStop', eventElement, event, ev, ui);
                        if (cell && (dayDelta || minuteDelta || allDay)) {
                            // changed!
                            view.eventDrop(this, event, dayDelta, allDay ? 0 : minuteDelta, allDay, ev, ui);
                        } else {
                            // either no change or out-of-bounds (draggable has already reverted)
                            resetElement();
                            eventElement.css(origPosition); // sometimes fast drags make event revert to wrong position
                            updateTimeText(0);
                            if ($.browser.msie) {
                                eventElement
								.css('filter', '') // clear IE opacity side-effects
								.find('span.fc-event-bg')
									.css('display', ''); // .show() made display=inline
                            }
                            view.showEvents(event, eventElement);
                        }
                    }
                });
                function updateTimeText(minuteDelta) {
                    var newStart = addMinutes(cloneDate(event.start), minuteDelta);
                    var newEnd;
                    if (event.end) {
                        newEnd = addMinutes(cloneDate(event.end), minuteDelta);
                    }

                    timeElement.text(formatDates(newStart, newEnd, view.option('timeFormat')));
                }
                function resetElement() {
                    // convert back to original slot-event
                    if (allDay) {
                        timeElement.css('display', ''); // show() was causing display=inline
                        eventElement.draggable('option', 'grid', [colWidth, slotHeight]);
                        allDay = false;
                    }
                }
            }
        }




        /* Event Resizing
        -----------------------------------------------------------------------------*/

        // for TIMESLOT events

        function resizableSlotEvent(event, eventElement, timeElement) {
            if (!options.disableResizing && eventElement.resizable) {
                var slotDelta, prevSlotDelta;
                eventElement.resizable({
                    handles: {
                        s: 'div.ui-resizable-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
                        }
                    }
                });
            }
        }




        /* Coordinate Utilities
        -----------------------------------------------------------------------------*/

        var coordinateGrid = new CoordinateGrid(function(rows, cols) {
            var e, n, p;
            bg.find('td').each(function(i, _e) {
                e = $(_e);
                n = e.offset().left;
                if (i) {
                    p[1] = n;
                }
                p = [n];
                cols[i] = p;
            });
            p[1] = n + e.outerWidth();
            if (options.allDaySlot) {
                e = head.find('td');
                n = e.offset().top;
                rows[0] = [n, n + e.outerHeight()];
            }
            var bodyContentTop = bodyContent.offset().top;
            var bodyTop = body.offset().top;
            var bodyBottom = bodyTop + body.outerHeight();
            function constrain(n) {
                return Math.max(bodyTop, Math.min(bodyBottom, n));
            }
            for (var i = 0; i < slotCnt; i++) {
                rows.push([
				constrain(bodyContentTop + slotHeight * i),
				constrain(bodyContentTop + slotHeight * (i + 1))
			]);
            }
        });

        var hoverListener = new HoverListener(coordinateGrid);

        // get the Y coordinate of the given time on the given day (both Date objects)
        function timePosition(day, time) { // both date objects. day holds 00:00 of current day
            day = cloneDate(day, true);
            if (time < addMinutes(cloneDate(day), minMinute)) {
                return 0;
            }
            if (time >= addMinutes(cloneDate(day), maxMinute)) {
                return bodyContent.height();
            }
            var slotMinutes = options.slotMinutes,
			minutes = time.getHours() * 60 + time.getMinutes() - minMinute,
			slotI = Math.floor(minutes / slotMinutes),
			slotTop = slotTopCache[slotI];
            if (slotTop === undefined) {
                slotTop = slotTopCache[slotI] = body.find('tr:eq(' + slotI + ') td div')[0].offsetTop;
            }
            return Math.max(0, Math.round(
			slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes)
		));
        }




        /* Selecting
        -----------------------------------------------------------------------------*/

        var selected = false;
        var daySelectionMousedown = selection_dayMousedown(
		view, hoverListener, cellDate, cellIsAllDay, renderDayOverlay, clearOverlay, reportSelection, unselect
	);

        function slotSelectionMousedown(ev) {
            if (view.option('selectable')) {
                unselect(ev);
                var _mousedownElement = this;
                var dates;
                hoverListener.start(function(cell, origCell) {
                    clearSelection();
                    if (cell && cell.col == origCell.col && !cellIsAllDay(cell)) {
                        var d1 = cellDate(origCell);
                        var d2 = cellDate(cell);
                        dates = [
						d1,
						addMinutes(cloneDate(d1), options.slotMinutes),
						d2,
						addMinutes(cloneDate(d2), options.slotMinutes)
					].sort(cmp);
                        renderSlotSelection(dates[0], dates[3]);
                    } else {
                        dates = null;
                    }
                }, ev);
                $(document).one('mouseup', function(ev) {
                    hoverListener.stop();
                    if (dates) {
                        if (+dates[0] == +dates[1]) {
                            view.trigger('dayClick', _mousedownElement, dates[0], false, ev);
                            // BUG: _mousedownElement will sometimes be the overlay
                        }
                        reportSelection(dates[0], dates[3], false, ev);
                    }
                });
            }
        }

        view.select = function(startDate, endDate, allDay) {
            coordinateGrid.build();
            unselect();
            if (allDay) {
                if (options.allDaySlot) {
                    if (!endDate) {
                        endDate = cloneDate(startDate);
                    }
                    renderDayOverlay(startDate, addDays(cloneDate(endDate), 1));
                }
            } else {
                if (!endDate) {
                    endDate = addMinutes(cloneDate(startDate), options.slotMinutes);
                }
                renderSlotSelection(startDate, endDate);
            }
            reportSelection(startDate, endDate, allDay);
        };

        function reportSelection(startDate, endDate, allDay, ev) {
            selected = true;
            view.trigger('select', view, startDate, endDate, allDay, ev);
        }

        function unselect(ev) {
            if (selected) {
                clearSelection();
                selected = false;
                view.trigger('unselect', view, ev);
            }
        }
        view.unselect = unselect;

        selection_unselectAuto(view, unselect);




        /* Selecting drawing utils
        -----------------------------------------------------------------------------*/

        var selectionHelper;

        function renderSlotSelection(startDate, endDate) {
            var helperOption = view.option('selectHelper');
            if (helperOption) {
                var col = dayDiff(startDate, view.visStart) * dis + dit;
                if (col >= 0 && col < colCnt) { // only works when times are on same day
                    var rect = coordinateGrid.rect(0, col, 0, col, bodyContent); // only for horizontal coords
                    var top = timePosition(startDate, startDate);
                    var bottom = timePosition(startDate, endDate);
                    if (bottom > top) { // protect against selections that are entirely before or after visible range
                        rect.top = top;
                        rect.height = bottom - top;
                        rect.left += 2;
                        rect.width -= 5;
                        if ($.isFunction(helperOption)) {
                            var helperRes = helperOption(startDate, endDate);
                            if (helperRes) {
                                rect.position = 'absolute';
                                rect.zIndex = 8;
                                selectionHelper = $(helperRes)
								.css(rect)
								.appendTo(bodyContent);
                            }
                        } else {
                            selectionHelper = $(slotSegHtml(
							{
							    title: '',
							    start: startDate,
							    end: endDate,
							    className: [],
							    editable: false
							},
							rect,
							'fc-event fc-event-vert fc-corner-top fc-corner-bottom '
						));
                            if ($.browser.msie) {
                                selectionHelper.find('span.fc-event-bg').hide(); // nested opacities mess up in IE, just hide
                            }
                            selectionHelper.css('opacity', view.option('dragOpacity'));
                        }
                        if (selectionHelper) {
                            slotBind(selectionHelper);
                            bodyContent.append(selectionHelper);
                            setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended
                            setOuterHeight(selectionHelper, rect.height, true);
                        }
                    }
                }
            } else {
                renderSlotOverlay(startDate, endDate);
            }
        }

        function clearSelection() {
            clearOverlay();
            if (selectionHelper) {
                selectionHelper.remove();
                selectionHelper = null;
            }
        }




        /* Semi-transparent Overlay Helpers
        -----------------------------------------------------*/

        function renderDayOverlay(startDate, endDate) {
            var startCol, endCol;
            if (rtl) {
                startCol = dayDiff(endDate, view.visStart) * dis + dit + 1;
                endCol = dayDiff(startDate, view.visStart) * dis + dit + 1;
            } else {
                startCol = dayDiff(startDate, view.visStart);
                endCol = dayDiff(endDate, view.visStart);
            }
            startCol = Math.max(0, startCol);
            endCol = Math.min(colCnt, endCol);
            if (startCol < endCol) {
                dayBind(
				_renderDayOverlay(0, startCol, 0, endCol - 1)
			);
            }
        }

        function _renderDayOverlay(col0, row0, col1, row1) {
            var rect = coordinateGrid.rect(col0, row0, col1, row1, head);
            return view.renderOverlay(rect, head);
        }

        function renderSlotOverlay(overlayStart, overlayEnd) {
            var dayStart = cloneDate(view.visStart);
            var dayEnd = addDays(cloneDate(dayStart), 1);
            for (var i = 0; i < colCnt; i++) {
                var stretchStart = new Date(Math.max(dayStart, overlayStart));
                var stretchEnd = new Date(Math.min(dayEnd, overlayEnd));
                if (stretchStart < stretchEnd) {
                    var col = i * dis + dit;
                    var rect = coordinateGrid.rect(0, col, 0, col, bodyContent); // only use it for horizontal coords
                    var top = timePosition(dayStart, stretchStart);
                    var bottom = timePosition(dayStart, stretchEnd);
                    rect.top = top;
                    rect.height = bottom - top;
                    slotBind(
					view.renderOverlay(rect, bodyContent)
				);
                }
                addDays(dayStart, 1);
                addDays(dayEnd, 1);
            }
        }

        function clearOverlay() {
            view.clearOverlays();
        }




        /* External dragging
        -----------------------------------------------------*/

        view.dragStart = function(_dragElement, ev, ui) {
            hoverListener.start(function(cell) {
                clearOverlay();
                if (cell) {
                    if (cellIsAllDay(cell)) {
                        _renderDayOverlay(cell.row, cell.col, cell.row, cell.col);
                    } else {
                        var d1 = cellDate(cell);
                        var d2 = addMinutes(cloneDate(d1), options.defaultEventMinutes);
                        renderSlotOverlay(d1, d2);
                    }
                }
            }, ev);
        };

        view.dragStop = function(_dragElement, ev, ui) {
            var cell = hoverListener.stop();
            clearOverlay();
            if (cell) {
                view.trigger('drop', _dragElement, cellDate(cell), cellIsAllDay(cell), ev, ui);
            }
        };




        /* Date Utilities
        ----------------------------------------------------*/

        function slotEventEnd(event) {
            if (event.end) {
                return cloneDate(event.end);
            } else {
                return addMinutes(cloneDate(event.start), options.defaultEventMinutes);
            }
        }

        function dayOfWeekCol(dayOfWeek) {
            return ((dayOfWeek - Math.max(firstDay, nwe) + colCnt) % colCnt) * dis + dit;
        }

        function cellDate(cell) {
            var d = addDays(cloneDate(view.visStart), cell.col * dis + dit);
            var slotIndex = cell.row;
            if (options.allDaySlot) {
                slotIndex--;
            }
            if (slotIndex >= 0) {
                addMinutes(d, minMinute + slotIndex * options.slotMinutes);
            }
            return d;
        }

        function cellIsAllDay(cell) {
            return options.allDaySlot && !cell.row;
        }



    }


    // 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 = {

        /*
        * Objects inheriting these methods must implement the following properties/methods:
        * - title
        * - start
        * - end
        * - visStart
        * - visEnd
        * - defaultEventEnd(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.eventsByID = {};
            this.eventElements = [];
            this.eventElementsByID = {};
            this.usedOverlays = [];
            this.unusedOverlays = [];
        },



        // 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 = {};
            for (i = 0; i < len; i++) {
                event = events[i];
                if (eventsByID[event._id]) {
                    eventsByID[event._id].push(event);
                } else {
                    eventsByID[event._id] = [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 resets hashes
            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][0] != exceptElement[0]) { // AHAHAHAHAHAHAHAH
                    elements[i][funcName]();
                }
            }
        },



        // event modification reporting

        eventDrop: function(e, event, dayDelta, minuteDelta, allDay, ev, ui) {
            var view = this,
			oldAllDay = event.allDay,
			eventId = event._id;
            view.moveEvents(view.eventsByID[eventId], 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[eventId], -dayDelta, -minuteDelta, oldAllDay);
                view.rerenderEvents();
            }, ev, ui);
            view.eventsChanged = true;
            view.rerenderEvents(eventId);
        },

        eventResize: function(e, event, dayDelta, minuteDelta, ev, ui) {
            var view = this,
			eventId = event._id;
            view.elongateEvents(view.eventsByID[eventId], dayDelta, minuteDelta);
            view.trigger('eventResize', e, event, dayDelta, minuteDelta, function() {
                // TODO: investigate cases where this inverse technique might not work
                view.elongateEvents(view.eventsByID[eventId], -dayDelta, -minuteDelta);
                view.rerenderEvents();
            }, ev, ui);
            view.eventsChanged = true;
            view.rerenderEvents(eventId);
        },



        // 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 or selecting)

        renderOverlay: function(rect, parent) {
            var e = this.unusedOverlays.shift();
            if (!e) {
                e = $("<div class='fc-cell-overlay' style='position:absolute;z-index:3'/>");
            }
            if (e[0].parentNode != parent[0]) {
                e.appendTo(parent);
            }
            this.usedOverlays.push(e.css(rect).show());
            return e;
        },

        clearOverlays: function() {
            var e;
            while (e = this.usedOverlays.shift()) {
                this.unusedOverlays.push(e.hide().unbind());
            }
        },




        // 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: 'div.ui-resizable-w'} : { e: 'div.ui-resizable-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
			.dblclick(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, visEventEnds, 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 = visEventEnds[i];
                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);
        }


    };



    function lazySegBind(container, segs, bindHandlers) {
        container.unbind('mouseover').mouseover(function(ev) {
            var parent = ev.target, e,
			i, seg;
            while (parent != this) {
                e = parent;
                parent = parent.parentNode;
            }
            if ((i = e._fci) !== undefined) {
                e._fci = undefined;
                seg = segs[i];
                bindHandlers(seg.event, seg.element, seg);
                $(ev.target).trigger(ev);
            }
            ev.stopPropagation();
        });
    }



    // 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;
    }





    function selection_dayMousedown(view, hoverListener, cellDate, cellIsAllDay, renderSelection, clearSelection, reportSelection, unselect) {
        return function(ev) {
            if (view.option('selectable')) {
                unselect(ev);
                var _mousedownElement = this;
                var dates;
                hoverListener.start(function(cell, origCell) {
                    clearSelection();
                    if (cell && cellIsAllDay(cell)) {
                        dates = [cellDate(origCell), cellDate(cell)].sort(cmp);
                        renderSelection(dates[0], addDays(cloneDate(dates[1]), 1), true);
                    } else {
                        dates = null;
                    }
                }, ev);
                $(document).one('mouseup', function(ev) {
                    hoverListener.stop();
                    if (dates) {
                        if (+dates[0] == +dates[1]) {
                            view.trigger('dayClick', _mousedownElement, dates[0], true, ev);
                            // BUG: _mousedownElement will sometimes be the overlay
                        }
                        reportSelection(dates[0], dates[1], true, ev);
                    }
                });
            }
        }
    }


    function selection_unselectAuto(view, unselect) {
        if (view.option('selectable') && view.option('unselectAuto')) {
            $(document).mousedown(function(ev) {
                var ignore = view.option('unselectCancel');
                if (ignore) {
                    if ($(ev.target).parents(ignore).length) { // could be optimized to stop after first match
                        return;
                    }
                }
                unselect(ev);
            });
        }
    }

    /* 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) {
            var dd = d.getDate() + n,
			check = cloneDate(d);
            check.setHours(9); // set to middle of day
            check.setDate(dd);
            d.setDate(dd);
            if (!keepTime) {
                clearTime(d);
            }
            fixDate(d, check);
        }
        return d;
    }
    fc.addDays = addDays;

    function fixDate(d, check) { // force d to be on check's YMD, for daylight savings purposes
        if (+d) { // prevent infinite looping on invalid dates
            while (d.getDate() != check.getDate()) {
                d.setTime(+d + (d < check ? 1 : -1) * HOUR_MS);
            }
        }
    }

    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);
    }
    fc.cloneDate = cloneDate;

    function zeroDate() { // returns a Date with time 00:00:00 and dateOfMonth=1
        var i = 0, d;
        do {
            d = new Date(1970, i++, 1);
        } while (d.getHours()); // != 0
        return d;
    }

    function skipWeekend(date, inc, excl) {
        inc = inc || 1;
        while (!date.getDay() || (excl && date.getDay() == 1 || !excl && date.getDay() == 6)) {
            addDays(date, inc);
        }
        return date;
    }

    function dayDiff(d1, d2) { // d1 - d2
        return Math.round((cloneDate(d1, true) - cloneDate(d2, true)) / DAY_MS);
    }

    function setYMD(date, y, m, d) {
        if (y !== undefined && y != date.getFullYear()) {
            date.setDate(1);
            date.setMonth(0);
            date.setFullYear(y);
        }
        if (m !== undefined && m != date.getMonth()) {
            date.setDate(1);
            date.setMonth(m);
        }
        if (d !== undefined) {
            date.setDate(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) || (s ? new Date(s) : null);
        }
        // TODO: never return invalid dates (like from new Date(<string>)), return null instead
        return null;
    };

    var parseISO8601 = fc.parseISO8601 = function(s, ignoreTimezone) {
        // derived from http://delete.me.uk/2005/03/iso8601.html
        // TODO: for a know glitch/feature, read tests/issue_206_parseDate_dst.html
        var m = 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 (!m) {
            return null;
        }
        var date = new Date(m[1], 0, 1),
		check = new Date(m[1], 0, 1, 9, 0),
		offset = 0;
        if (m[3]) {
            date.setMonth(m[3] - 1);
            check.setMonth(m[3] - 1);
        }
        if (m[5]) {
            date.setDate(m[5]);
            check.setDate(m[5]);
        }
        fixDate(date, check);
        if (m[7]) {
            date.setHours(m[7]);
        }
        if (m[8]) {
            date.setMinutes(m[8]);
        }
        if (m[10]) {
            date.setSeconds(m[10]);
        }
        if (m[12]) {
            date.setMilliseconds(Number("0." + m[12]) * 1000);
        }
        fixDate(date, check);
        if (!ignoreTimezone) {
            if (m[14]) {
                offset = Number(m[16]) * 60 + Number(m[17]);
                offset *= m[15] == '-' ? 1 : -1;
            }
            offset -= date.getTimezoneOffset();
        }
        return new Date(+date + (offset * 60 * 1000));
    };

    var parseTime = fc.parseTime = function(s) { // returns minutes since start of day
        if (typeof s == 'number') { // an hour
            return s * 60;
        }
        if (typeof s == 'object') { // a Date object
            return s.getHours() * 60 + s.getMinutes();
        }
        var m = s.match(/(\d+)(?::(\d+))?\s*(\w+)?/);
        if (m) {
            var h = parseInt(m[1]);
            if (m[3]) {
                h %= 12;
                if (m[3].toLowerCase().charAt(0) == 'p') {
                    h += 12;
                }
            }
            return h * 60 + (m[2] ? parseInt(m[2]) : 0);
        }
    };



    /* 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 = (typeof format != 'undefined') ? format.length : 0,
		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() + "日" },
        ddddd: 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() + "年" },
        yyy: 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(i, _element) {
            _element.style.width = width - hsides(_element, includeMargins) + 'px';
        });
    }

    function setOuterHeight(element, height, includeMargins) {
        element.each(function(i, _element) {
            _element.style.height = height - vsides(_element, includeMargins) + 'px';
        });
    }


    function hsides(_element, includeMargins) {
        return (parseFloat(jQuery.curCSS(_element, 'paddingLeft', true)) || 0) +
	       (parseFloat(jQuery.curCSS(_element, 'paddingRight', true)) || 0) +
	       (parseFloat(jQuery.curCSS(_element, 'borderLeftWidth', true)) || 0) +
	       (parseFloat(jQuery.curCSS(_element, 'borderRightWidth', true)) || 0) +
	       (includeMargins ? hmargins(_element) : 0);
    }

    function hmargins(_element) {
        return (parseFloat(jQuery.curCSS(_element, 'marginLeft', true)) || 0) +
	       (parseFloat(jQuery.curCSS(_element, 'marginRight', true)) || 0);
    }

    function vsides(_element, includeMargins) {
        return (parseFloat(jQuery.curCSS(_element, 'paddingTop', true)) || 0) +
	       (parseFloat(jQuery.curCSS(_element, 'paddingBottom', true)) || 0) +
	       (parseFloat(jQuery.curCSS(_element, 'borderTopWidth', true)) || 0) +
	       (parseFloat(jQuery.curCSS(_element, 'borderBottomWidth', true)) || 0) +
	       (includeMargins ? vmargins(_element) : 0);
    }

    function vmargins(_element) {
        return (parseFloat(jQuery.curCSS(_element, 'marginTop', true)) || 0) +
	       (parseFloat(jQuery.curCSS(_element, 'marginBottom', true)) || 0);
    }




    function setMinHeight(element, h) {
        h = typeof h == 'number' ? h + 'px' : h;
        element[0].style.cssText += ';min-height:' + h + ';_height:' + h;
    }



    /* Position Calculation
    -----------------------------------------------------------------------------*/
    // nasty bugs in opera 9.25
    // position()'s top returning incorrectly with TR/TD or elements within TD

    var topBug;

    function topCorrect(tr) { // tr/th/td or anything else
        if (topBug !== false) {
            var cell;
            if (tr.is('th,td')) {
                tr = (cell = tr).parent();
            }
            if (topBug === undefined && tr.is('tr')) {
                topBug = tr.position().top != tr.children().position().top;
            }
            if (topBug) {
                return tr.parent().position().top + (cell ? tr.position().top - cell.position().top : 0);
            }
        }
        return 0;
    }



    /* Coordinate Grid
    -----------------------------------------------------------------------------*/

    function CoordinateGrid(buildFunc) {

        var t = this;
        var rows;
        var cols;

        t.build = function() {
            rows = [];
            cols = [];
            buildFunc(rows, cols);
        };

        t.cell = function(x, y) {
            var rowCnt = rows.length;
            var colCnt = cols.length;
            var i, r = -1, c = -1;
            for (i = 0; i < rowCnt; i++) {
                if (y >= rows[i][0] && y < rows[i][1]) {
                    r = i;
                    break;
                }
            }
            for (i = 0; i < colCnt; i++) {
                if (x >= cols[i][0] && x < cols[i][1]) {
                    c = i;
                    break;
                }
            }
            return (r >= 0 && c >= 0) ? { row: r, col: c} : null;
        };

        t.rect = function(row0, col0, row1, col1, originElement) { // row1,col1 is inclusive
            var origin = originElement.offset();
            return {
                top: rows[row0][0] - origin.top,
                left: cols[col0][0] - origin.left,
                width: cols[col1][1] - cols[col0][0],
                height: rows[row1][1] - rows[row0][0]
            };
        };

    }



    /* Hover Listener
    -----------------------------------------------------------------------------*/

    function HoverListener(coordinateGrid) {

        var t = this;
        var bindType;
        var change;
        var firstCell;
        var cell;

        t.start = function(_change, ev, _bindType) {
            change = _change;
            firstCell = cell = null;
            coordinateGrid.build();
            mouse(ev);
            bindType = _bindType || 'mousemove';
            $(document).bind(bindType, mouse);
        };

        function mouse(ev) {
            var newCell = coordinateGrid.cell(ev.pageX, ev.pageY);
            if (!newCell != !cell || newCell && (newCell.row != cell.row || newCell.col != cell.col)) {
                if (newCell) {
                    if (!firstCell) {
                        firstCell = newCell;
                    }
                    change(newCell, firstCell, newCell.row - firstCell.row, newCell.col - firstCell.col);
                } else {
                    change(newCell, firstCell);
                }
                cell = newCell;
            }
        }

        t.stop = function() {
            $(document).unbind(bindType, mouse);
            return cell;
        };

    }



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

    var 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 of an object
        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[''];
    }

    function htmlEscape(s) {
        return s.replace(/&/g, '&amp;')
		.replace(/</g, '&lt;')
		.replace(/>/g, '&gt;')
		.replace(/'/g, '&#039;')
		.replace(/"/g, '&quot;')
		.replace(/\n/g, '<br />');
    }



    function HorizontalPositionCache(getElement) {

        var t = this,
		elements = {},
		lefts = {},
		rights = {};

        function e(i) {
            return elements[i] = elements[i] || getElement(i);
        }

        t.left = function(i) {
            return lefts[i] = lefts[i] === undefined ? e(i).position().left : lefts[i];
        };

        t.right = function(i) {
            return rights[i] = rights[i] === undefined ? t.left(i) + e(i).width() : rights[i];
        };

        t.clear = function() {
            elements = {};
            lefts = {};
            rights = {};
        };

    }



    function cssKey(_element) {
        return _element.id + '/' + _element.className + '/' + _element.style.cssText.replace(/(^|;)\s*(top|left|width|height)\s*:[^;]*/ig, '');
    }



    function cmp(a, b) {
        return a - b;
    }



    function exclEndDay(event) {
        if (event.end) {
            return _exclEndDay(event.end, event.allDay);
        } else {
            return addDays(cloneDate(event.start), 1);
        }
    }

    function _exclEndDay(end, allDay) {
        end = cloneDate(end);
        return allDay || end.getHours() || end.getMinutes() ? addDays(end, 1) : clearTime(end);
    }



    function disableTextSelection(element) {
        element
		.attr('unselectable', 'on')
		.css('MozUserSelect', 'none')
		.bind('selectstart.ui', function() { return false; });
    }

    /*
    function enableTextSelection(element) {
    element
    .attr('unselectable', 'off')
    .css('MozUserSelect', '')
    .unbind('selectstart.ui');
    }
    */




})(jQuery);