/**
 * @preserve
 * FullCalendar v1.5.2
 * http://arshaw.com/fullcalendar/
 *
 * Use fullcalendar.css for basic styling.
 * For event drag & drop, requires jQuery UI draggable.
 * For event resizing, requires jQuery UI resizable.
 *
 * Copyright (c) 2011 Adam Shaw
 * Dual licensed under the MIT and GPL licenses, located in
 * MIT-LICENSE.txt and GPL-LICENSE.txt respectively.
 *
 * Date: Sun Aug 21 22:06:09 2011 -0700
 *
 */
 
(function($, undefined) {


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: true,
    ignoreTimezone: true,
    
    // event ajax
    lazyFetching: true,
    startParam: 'start',
    endParam: 'end',
    
    // time formats
    titleFormat: {
        month: 'MMMM yyyy',
        week: "MMM d[ yyyy]{ '&#8212;'[ MMM] d yyyy}",
        day: 'dddd, MMM d, yyyy'
    },
    columnFormat: {
        month: 'ddd',
        week: 'ddd M/d',
        day: 'dddd M/d'
    },
    timeFormat: { // for event elements
        '': 'h(:mm)t' // default
    },
    
    // locale
    isRTL: false,
    firstDay: 0,
    monthNames: ['January','February','March','April','May','June','July','August','September','October','November','December'],
    monthNamesShort: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
    dayNames: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
    dayNamesShort: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'],
    buttonText: {
        prev: '&nbsp;&#9668;&nbsp;',
        next: '&nbsp;&#9658;&nbsp;',
        prevYear: '&nbsp;&lt;&lt;&nbsp;',
        nextYear: '&nbsp;&gt;&gt;&nbsp;',
        today: 'today',
        month: 'month',
        week: 'week',
        day: 'day'
    },
    
    // jquery-ui theming
    theme: false,
    buttonIcons: {
        prev: 'circle-triangle-w',
        next: 'circle-triangle-e'
    },
    
    //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'
    }
};



var fc = $.fullCalendar = { version: "1.5.2" };
var fcViews = fc.views = {};


$.fn.fullCalendar = function(options) {


    // method calling
    if (typeof options == 'string') {
        var args = Array.prototype.slice.call(arguments, 1);
        var res;
        this.each(function() {
            var calendar = $.data(this, 'fullCalendar');
            if (calendar && $.isFunction(calendar[options])) {
                var r = calendar[options].apply(calendar, args);
                if (res === undefined) {
                    res = r;
                }
                if (options == 'destroy') {
                    $.removeData(this, 'fullCalendar');
                }
            }
        });
        if (res !== undefined) {
            return res;
        }
        return this;
    }
    
    
    // would like to have this logic in EventManager, but needs to happen before options are recursively extended
    var eventSources = options.eventSources || [];
    delete options.eventSources;
    if (options.events) {
        eventSources.push(options.events);
        delete options.events;
    }
    

    options = $.extend(true, {},
        defaults,
        (options.isRTL || options.isRTL===undefined && defaults.isRTL) ? rtlDefaults : {},
        options
    );
    
    
    this.each(function(i, _element) {
        var element = $(_element);
        var calendar = new Calendar(element, options, eventSources);
        element.data('fullCalendar', calendar); // TODO: look into memory leak implications
        calendar.render();
    });
    
    
    return this;
    
};


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



 
function Calendar(element, options, eventSources) {
    var t = this;
    
    
    // exports
    t.options = options;
    t.render = render;
    t.destroy = destroy;
    t.refetchEvents = refetchEvents;
    t.reportEvents = reportEvents;
    t.reportEventChange = reportEventChange;
    t.rerenderEvents = rerenderEvents;
    t.changeView = changeView;
    t.select = select;
    t.unselect = unselect;
    t.prev = prev;
    t.next = next;
    t.prevYear = prevYear;
    t.nextYear = nextYear;
    t.today = today;
    t.gotoDate = gotoDate;
    t.incrementDate = incrementDate;
    t.formatDate = function(format, date) { return formatDate(format, date, options) };
    t.formatDates = function(format, date1, date2) { return formatDates(format, date1, date2, options) };
    t.getDate = getDate;
    t.getView = getView;
    t.option = option;
    t.trigger = trigger;
    
    
    // imports
    EventManager.call(t, options, eventSources);
    var isFetchNeeded = t.isFetchNeeded;
    var fetchEvents = t.fetchEvents;
    
    
    // locals
    var _element = element[0];
    var header;
    var headerElement;
    var content;
    var tm; // for making theme classes
    var currentView;
    var viewInstances = {};
    var elementOuterWidth;
    var suggestedViewHeight;
    var absoluteViewElement;
    var resizeUID = 0;
    var ignoreWindowResize = 0;
    var date = new Date();
    var events = [];
    var _dragElement;
    
    
    
    /* Main Rendering
    -----------------------------------------------------------------------------*/
    
    
    setYMD(date, options.year, options.month, options.date);
    
    
    function render(inc) {
        if (!content) {
            initialRender();
        }else{
            calcSize();
            markSizesDirty();
            markEventsDirty();
            renderView(inc);
        }
    }
    
    
    function initialRender() {
        tm = options.theme ? 'ui' : 'fc';
        element.addClass('fc');
        if (options.isRTL) {
            element.addClass('fc-rtl');
        }
        if (options.theme) {
            element.addClass('ui-widget');
        }
        content = $("<div class='fc-content' style='position:relative'/>")
            .prependTo(element);
        header = new Header(t, options);
        headerElement = header.render();
        if (headerElement) {
            element.prepend(headerElement);
        }
        changeView(options.defaultView);
        $(window).resize(windowResize);
        // 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 (!currentView.start && bodyVisible()) { // !currentView.start makes sure this never happens more than once
                renderView();
            }
        },0);
    }
    
    
    function destroy() {
        $(window).unbind('resize', windowResize);
        header.destroy();
        content.remove();
        element.removeClass('fc fc-rtl ui-widget');
    }
    
    
    
    function elementVisible() {
        return _element.offsetWidth !== 0;
    }
    
    
    function bodyVisible() {
        return $('body')[0].offsetWidth !== 0;
    }
    
    
    
    /* View Rendering
    -----------------------------------------------------------------------------*/
    
    // TODO: improve view switching (still weird transition in IE, and FF has whiteout problem)
    
    function changeView(newViewName) {
        if (!currentView || newViewName != currentView.name) {
            ignoreWindowResize++; // because setMinHeight might change the height before render (and subsequently setSize) is reached

            unselect();
            
            var oldView = currentView;
            var newViewElement;
                
            if (oldView) {
                (oldView.beforeHide || noop)(); // 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');
            
            currentView = viewInstances[newViewName];
            if (currentView) {
                currentView.element.show();
            }else{
                currentView = viewInstances[newViewName] = new fcViews[newViewName](
                    newViewElement = absoluteViewElement =
                        $("<div class='fc-view fc-view-" + newViewName + "' style='position:absolute'/>")
                            .appendTo(content),
                    t // the calendar object
                );
            }
            
            if (oldView) {
                header.deactivateButton(oldView.name);
            }
            header.activateButton(newViewName);
            
            renderView(); // 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) {
                (currentView.afterShow || noop)(); // called after setting min-height/overflow, so in final scroll state (for Opera)
            }
            
            ignoreWindowResize--;
        }
    }
    
    
    
    function renderView(inc) {
        if (elementVisible()) {
            ignoreWindowResize++; // because renderEvents might temporarily change the height before setSize is reached

            unselect();
            
            if (suggestedViewHeight === undefined) {
                calcSize();
            }
            
            var forceEventRender = false;
            if (!currentView.start || inc || date < currentView.start || date >= currentView.end) {
                // view must render an entire new date range (and refetch/render events)
                currentView.render(date, inc || 0); // responsible for clearing events
                setSize(true);
                forceEventRender = true;
            }
            else if (currentView.sizeDirty) {
                // view must resize (and rerender events)
                currentView.clearEvents();
                setSize();
                forceEventRender = true;
            }
            else if (currentView.eventsDirty) {
                currentView.clearEvents();
                forceEventRender = true;
            }
            currentView.sizeDirty = false;
            currentView.eventsDirty = false;
            updateEvents(forceEventRender);
            
            elementOuterWidth = element.outerWidth();
            
            header.updateTitle(currentView.title);
            var today = new Date();
            if (today >= currentView.start && today < currentView.end) {
                header.disableButton('today');
            }else{
                header.enableButton('today');
            }
            
            ignoreWindowResize--;
            currentView.trigger('viewDisplay', _element);
        }
    }
    
    
    
    /* Resizing
    -----------------------------------------------------------------------------*/
    
    
    function updateSize() {
        markSizesDirty();
        if (elementVisible()) {
            calcSize();
            setSize();
            unselect();
            currentView.clearEvents();
            currentView.renderEvents(events);
            currentView.sizeDirty = false;
        }
    }
    
    
    function markSizesDirty() {
        $.each(viewInstances, function(i, inst) {
            inst.sizeDirty = true;
        });
    }
    
    
    function calcSize() {
        if (options.contentHeight) {
            suggestedViewHeight = options.contentHeight;
        }
        else if (options.height) {
            suggestedViewHeight = options.height - (headerElement ? headerElement.height() : 0) - vsides(content);
        }
        else {
            suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5));
        }
    }
    
    
    function setSize(dateChanged) { // todo: dateChanged?
        ignoreWindowResize++;
        currentView.setHeight(suggestedViewHeight, dateChanged);
        if (absoluteViewElement) {
            absoluteViewElement.css('position', 'relative');
            absoluteViewElement = null;
        }
        currentView.setWidth(content.width(), dateChanged);
        ignoreWindowResize--;
    }
    
    
    function windowResize() {
        if (!ignoreWindowResize) {
            if (currentView.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
                            updateSize();
                            currentView.trigger('windowResize', _element);
                            ignoreWindowResize--;
                        }
                    }
                }, 200);
            }else{
                // calendar must have been initialized in a 0x0 iframe that has just been resized
                lateRender();
            }
        }
    }
    
    
    
    /* Event Fetching/Rendering
    -----------------------------------------------------------------------------*/
    
    
    // fetches events if necessary, rerenders events if necessary (or if forced)
    function updateEvents(forceRender) {
        if (!options.lazyFetching || isFetchNeeded(currentView.visStart, currentView.visEnd)) {
            refetchEvents();
        }
        else if (forceRender) {
            rerenderEvents();
        }
    }
    
    
    function refetchEvents() {
        fetchEvents(currentView.visStart, currentView.visEnd); // will call reportEvents
    }
    
    
    // called when event data arrives
    function reportEvents(_events) {
        events = _events;
        rerenderEvents();
    }
    
    
    // called when a single event's data has been changed
    function reportEventChange(eventID) {
        rerenderEvents(eventID);
    }
    
    
    // attempts to rerenderEvents
    function rerenderEvents(modifiedEventID) {
        markEventsDirty();
        if (elementVisible()) {
            currentView.clearEvents();
            currentView.renderEvents(events, modifiedEventID);
            currentView.eventsDirty = false;
        }
    }
    
    
    function markEventsDirty() {
        $.each(viewInstances, function(i, inst) {
            inst.eventsDirty = true;
        });
    }
    


    /* Selection
    -----------------------------------------------------------------------------*/
    

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

    function unselect() { // safe to be called before renderView
        if (currentView) {
            currentView.unselect();
        }
    }
    
    
    
    /* Date
    -----------------------------------------------------------------------------*/
    
    
    function prev() {
        renderView(-1);
    }
    
    
    function next() {
        renderView(1);
    }
    
    
    function prevYear() {
        addYears(date, -1);
        renderView();
    }
    
    
    function nextYear() {
        addYears(date, 1);
        renderView();
    }
    
    
    function today() {
        date = new Date();
        renderView();
    }
    
    
    function gotoDate(year, month, dateOfMonth) {
        if (year instanceof Date) {
            date = cloneDate(year); // provided 1 argument, a Date
        }else{
            setYMD(date, year, month, dateOfMonth);
        }
        renderView();
    }
    
    
    function incrementDate(years, months, days) {
        if (years !== undefined) {
            addYears(date, years);
        }
        if (months !== undefined) {
            addMonths(date, months);
        }
        if (days !== undefined) {
            addDays(date, days);
        }
        renderView();
    }
    
    
    function getDate() {
        return cloneDate(date);
    }
    
    
    
    /* Misc
    -----------------------------------------------------------------------------*/
    
    
    function getView() {
        return currentView;
    }
    
    
    function option(name, value) {
        if (value === undefined) {
            return options[name];
        }
        if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') {
            options[name] = value;
            updateSize();
        }
    }
    
    
    function trigger(name, thisObj) {
        if (options[name]) {
            return options[name].apply(
                thisObj || _element,
                Array.prototype.slice.call(arguments, 2)
            );
        }
    }
    
    
    
    /* External Dragging
    ------------------------------------------------------------------------*/
    
    if (options.droppable) {
        $(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;
                        currentView.dragStart(_dragElement, ev, ui);
                    }
                }
            })
            .bind('dragstop', function(ev, ui) {
                if (_dragElement) {
                    currentView.dragStop(_dragElement, ev, ui);
                    _dragElement = null;
                }
            });
    }
    

}

function Header(calendar, options) {
    var t = this;
    
    
    // exports
    t.render = render;
    t.destroy = destroy;
    t.updateTitle = updateTitle;
    t.activateButton = activateButton;
    t.deactivateButton = deactivateButton;
    t.disableButton = disableButton;
    t.enableButton = enableButton;
    
    
    // locals
    var element = $([]);
    var tm;
    


    function render() {
        tm = options.theme ? 'ui' : 'fc';
        var sections = options.header;
        if (sections) {
            element = $("<table class='fc-header' style='width:100%'/>")
                .append(
                    $("<tr/>")
                        .append(renderSection('left'))
                        .append(renderSection('center'))
                        .append(renderSection('right'))
                );
            return element;
        }
    }
    
    
    function destroy() {
        element.remove();
    }
    
    
    function renderSection(position) {
        var e = $("<td class='fc-header-" + position + "'/>");
        var buttonStr = options.header[position];
        if (buttonStr) {
            $.each(buttonStr.split(' '), function(i) {
                if (i > 0) {
                    e.append("<span class='fc-header-space'/>");
                }
                var prevButton;
                $.each(this.split(','), function(j, buttonName) {
                    if (buttonName == 'title') {
                        e.append("<span class='fc-header-title'><h2>&nbsp;</h2></span>");
                        if (prevButton) {
                            prevButton.addClass(tm + '-corner-right');
                        }
                        prevButton = null;
                    }else{
                        var buttonClick;
                        if (calendar[buttonName]) {
                            buttonClick = calendar[buttonName]; // calendar method
                        }
                        else if (fcViews[buttonName]) {
                            buttonClick = function() {
                                button.removeClass(tm + '-state-hover'); // forget why
                                calendar.changeView(buttonName);
                            };
                        }
                        if (buttonClick) {
                            var icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null; // why are we using smartProperty here?
                            var text = smartProperty(options.buttonText, buttonName); // why are we using smartProperty here?
                            var button = $(
                                "<span class='fc-button fc-button-" + buttonName + " " + tm + "-state-default'>" +
                                    "<span class='fc-button-inner'>" +
                                        "<span class='fc-button-content'>" +
                                            (icon ?
                                                "<span class='fc-icon-wrap'>" +
                                                    "<span class='ui-icon ui-icon-" + icon + "'/>" +
                                                "</span>" :
                                                text
                                                ) +
                                        "</span>" +
                                        "<span class='fc-button-effect'><span></span></span>" +
                                    "</span>" +
                                "</span>"
                            );
                            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(e);
                                if (!prevButton) {
                                    button.addClass(tm + '-corner-left');
                                }
                                prevButton = button;
                            }
                        }
                    }
                });
                if (prevButton) {
                    prevButton.addClass(tm + '-corner-right');
                }
            });
        }
        return e;
    }
    
    
    function updateTitle(html) {
        element.find('h2')
            .html(html);
    }
    
    
    function activateButton(buttonName) {
        element.find('span.fc-button-' + buttonName)
            .addClass(tm + '-state-active');
    }
    
    
    function deactivateButton(buttonName) {
        element.find('span.fc-button-' + buttonName)
            .removeClass(tm + '-state-active');
    }
    
    
    function disableButton(buttonName) {
        element.find('span.fc-button-' + buttonName)
            .addClass(tm + '-state-disabled');
    }
    
    
    function enableButton(buttonName) {
        element.find('span.fc-button-' + buttonName)
            .removeClass(tm + '-state-disabled');
    }


}

fc.sourceNormalizers = [];
fc.sourceFetchers = [];

var ajaxDefaults = {
    dataType: 'json',
    cache: false
};

var eventGUID = 1;


function EventManager(options, _sources) {
    var t = this;
    
    
    // exports
    t.isFetchNeeded = isFetchNeeded;
    t.fetchEvents = fetchEvents;
    t.addEventSource = addEventSource;
    t.removeEventSource = removeEventSource;
    t.updateEvent = updateEvent;
    t.renderEvent = renderEvent;
    t.removeEvents = removeEvents;
    t.clientEvents = clientEvents;
    t.normalizeEvent = normalizeEvent;
    
    
    // imports
    var trigger = t.trigger;
    var getView = t.getView;
    var reportEvents = t.reportEvents;
    
    
    // locals
    var stickySource = { events: [] };
    var sources = [ stickySource ];
    var rangeStart, rangeEnd;
    var currentFetchID = 0;
    var pendingSourceCnt = 0;
    var loadingLevel = 0;
    var cache = [];
    
    
    for (var i=0; i<_sources.length; i++) {
        _addEventSource(_sources[i]);
    }
    
    
    
    /* Fetching
    -----------------------------------------------------------------------------*/
    
    
    function isFetchNeeded(start, end) {
        return !rangeStart || start < rangeStart || end > rangeEnd;
    }
    
    
    function fetchEvents(start, end) {
        rangeStart = start;
        rangeEnd = end;
        cache = [];
        var fetchID = ++currentFetchID;
        var len = sources.length;
        pendingSourceCnt = len;
        for (var i=0; i<len; i++) {
            fetchEventSource(sources[i], fetchID);
        }
    }
    
    
    function fetchEventSource(source, fetchID) {
        _fetchEventSource(source, function(events) {
            if (fetchID == currentFetchID) {
                if (events) {
                    for (var i=0; i<events.length; i++) {
                        events[i].source = source;
                        normalizeEvent(events[i]);
                    }
                    cache = cache.concat(events);
                }
                pendingSourceCnt--;
                if (!pendingSourceCnt) {
                    reportEvents(cache);
                }
            }
        });
    }
    
    
    function _fetchEventSource(source, callback) {
        var i;
        var fetchers = fc.sourceFetchers;
        var res;
        for (i=0; i<fetchers.length; i++) {
            res = fetchers[i](source, rangeStart, rangeEnd, callback);
            if (res === true) {
                // the fetcher is in charge. made its own async request
                return;
            }
            else if (typeof res == 'object') {
                // the fetcher returned a new source. process it
                _fetchEventSource(res, callback);
                return;
            }
        }
        var events = source.events;
        if (events) {
            if ($.isFunction(events)) {
                pushLoading();
                events(cloneDate(rangeStart), cloneDate(rangeEnd), function(events) {
                    callback(events);
                    popLoading();
                });
            }
            else if ($.isArray(events)) {
                callback(events);
            }
            else {
                callback();
            }
        }else{
            var url = source.url;
            if (url) {
                var success = source.success;
                var error = source.error;
                var complete = source.complete;
                var data = $.extend({}, source.data || {});
                var startParam = firstDefined(source.startParam, options.startParam);
                var endParam = firstDefined(source.endParam, options.endParam);
                if (startParam) {
                    data[startParam] = Math.round(+rangeStart / 1000);
                }
                if (endParam) {
                    data[endParam] = Math.round(+rangeEnd / 1000);
                }
                pushLoading();
                $.ajax($.extend({}, ajaxDefaults, source, {
                    data: data,
                    success: function(events) {
                        events = events || [];
                        var res = applyAll(success, this, arguments);
                        if ($.isArray(res)) {
                            events = res;
                        }
                        callback(events);
                    },
                    error: function() {
                        applyAll(error, this, arguments);
                        callback();
                    },
                    complete: function() {
                        applyAll(complete, this, arguments);
                        popLoading();
                    }
                }));
            }else{
                callback();
            }
        }
    }
    
    
    
    /* Sources
    -----------------------------------------------------------------------------*/
    

    function addEventSource(source) {
        source = _addEventSource(source);
        if (source) {
            pendingSourceCnt++;
            fetchEventSource(source, currentFetchID); // will eventually call reportEvents
        }
    }
    
    
    function _addEventSource(source) {
        if ($.isFunction(source) || $.isArray(source)) {
            source = { events: source };
        }
        else if (typeof source == 'string') {
            source = { url: source };
        }
        if (typeof source == 'object') {
            normalizeSource(source);
            sources.push(source);
            return source;
        }
    }
    

    function removeEventSource(source) {
        sources = $.grep(sources, function(src) {
            return !isSourcesEqual(src, source);
        });
        // remove all client events from that source
        cache = $.grep(cache, function(e) {
            return !isSourcesEqual(e.source, source);
        });
        reportEvents(cache);
    }
    
    
    
    /* Manipulation
    -----------------------------------------------------------------------------*/
    
    
    function updateEvent(event) { // update an existing event
        var i, len = cache.length, e,
            defaultEventEnd = getView().defaultEventEnd, // getView???
            startDelta = event.start - event._start,
            endDelta = event.end ?
                (event.end - (event._end || 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 = cache[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(+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;
                e.color = event.color;
                e.backgroudColor = event.backgroudColor;
                e.borderColor = event.borderColor;
                e.textColor = event.textColor;
                normalizeEvent(e);
            }
        }
        normalizeEvent(event);
        reportEvents(cache);
    }
    
    
    function renderEvent(event, stick) {
        normalizeEvent(event);
        if (!event.source) {
            if (stick) {
                stickySource.events.push(event);
                event.source = stickySource;
            }
            cache.push(event);
        }
        reportEvents(cache);
    }
    
    
    function removeEvents(filter) {
        if (!filter) { // remove all
            cache = [];
            // clear all array sources
            for (var i=0; i<sources.length; i++) {
                if ($.isArray(sources[i].events)) {
                    sources[i].events = [];
                }
            }
        }else{
            if (!$.isFunction(filter)) { // an event ID
                var id = filter + '';
                filter = function(e) {
                    return e._id == id;
                };
            }
            cache = $.grep(cache, filter, true);
            // remove events from array sources
            for (var i=0; i<sources.length; i++) {
                if ($.isArray(sources[i].events)) {
                    sources[i].events = $.grep(sources[i].events, filter, true);
                }
            }
        }
        reportEvents(cache);
    }
    
    
    function clientEvents(filter) {
        if ($.isFunction(filter)) {
            return $.grep(cache, filter);
        }
        else if (filter) { // an event ID
            filter += '';
            return $.grep(cache, function(e) {
                return e._id == filter;
            });
        }
        return cache; // else, return all
    }
    
    
    
    /* Loading State
    -----------------------------------------------------------------------------*/
    
    
    function pushLoading() {
        if (!loadingLevel++) {
            trigger('loading', null, true);
        }
    }
    
    
    function popLoading() {
        if (!--loadingLevel) {
            trigger('loading', null, false);
        }
    }
    
    
    
    /* Event Normalization
    -----------------------------------------------------------------------------*/
    
    
    function normalizeEvent(event) {
        var source = event.source || {};
        var ignoreTimezone = firstDefined(source.ignoreTimezone, options.ignoreTimezone);
        event._id = event._id || (event.id === undefined ? '_fc' + eventGUID++ : event.id + '');
        if (event.date) {
            if (!event.start) {
                event.start = event.date;
            }
            delete event.date;
        }
        event._start = cloneDate(event.start = parseDate(event.start, ignoreTimezone));
        event.end = parseDate(event.end, ignoreTimezone);
        if (event.end && event.end <= event.start) {
            event.end = null;
        }
        event._end = event.end ? cloneDate(event.end) : null;
        if (event.allDay === undefined) {
            event.allDay = firstDefined(source.allDayDefault, 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
    }
    
    
    
    /* Utils
    ------------------------------------------------------------------------------*/
    
    
    function normalizeSource(source) {
        if (source.className) {
            // TODO: repeat code, same code for event classNames
            if (typeof source.className == 'string') {
                source.className = source.className.split(/\s+/);
            }
        }else{
            source.className = [];
        }
        var normalizers = fc.sourceNormalizers;
        for (var i=0; i<normalizers.length; i++) {
            normalizers[i](source);
        }
    }
    
    
    function isSourcesEqual(source1, source2) {
        return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
    }
    
    
    function getSourcePrimitive(source) {
        return ((typeof source == 'object') ? (source.events || source.url) : '') || source;
    }


}


fc.addDays = addDays;
fc.cloneDate = cloneDate;
fc.parseDate = parseDate;
fc.parseISO8601 = parseISO8601;
fc.parseTime = parseTime;
fc.formatDate = formatDate;
fc.formatDates = formatDates;



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

var dayIDs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'],
    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;
}


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


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


function parseDate(s, ignoreTimezone) { // ignoreTimezone defaults to true
    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+(\.\d+)?$/)) { // a UNIX timestamp
            return new Date(parseFloat(s) * 1000);
        }
        if (ignoreTimezone === undefined) {
            ignoreTimezone = true;
        }
        return parseISO8601(s, ignoreTimezone) || (s ? new Date(s) : null);
    }
    // TODO: never return invalid dates (like from new Date(<string>)), return null instead
    return null;
}


function parseISO8601(s, ignoreTimezone) { // ignoreTimezone defaults to false
    // 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);
    if (ignoreTimezone || !m[13]) {
        var check = new Date(m[1], 0, 1, 9, 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);
    }else{
        date.setUTCFullYear(
            m[1],
            m[3] ? m[3] - 1 : 0,
            m[5] || 1
        );
        date.setUTCHours(
            m[7] || 0,
            m[8] || 0,
            m[10] || 0,
            m[12] ? Number("0." + m[12]) * 1000 : 0
        );
        if (m[14]) {
            var offset = Number(m[16]) * 60 + (m[18] ? Number(m[18]) : 0);
            offset *= m[15] == '-' ? 1 : -1;
            date = new Date(+date + (offset * 60 * 1000));
        }
    }
    return date;
}


function parseTime(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], 10);
        if (m[3]) {
            h %= 12;
            if (m[3].toLowerCase().charAt(0) == 'p') {
                h += 12;
            }
        }
        return h * 60 + (m[2] ? parseInt(m[2], 10) : 0);
    }
}



/* Date Formatting
-----------------------------------------------------------------------------*/
// TODO: use same function formatDate(date, [date2], format, [options])


function formatDate(date, format, options) {
    return formatDates(date, null, format, options);
}


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


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



fc.applyAll = applyAll;


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


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



/* Event Sorting
-----------------------------------------------------------------------------*/


// event rendering utilities
function sliceSegs(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);
}


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



/* Event Element Binding
-----------------------------------------------------------------------------*/


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



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


function setOuterWidth(element, width, includeMargins) {
    for (var i=0, e; i<element.length; i++) {
        e = $(element[i]);
        e.width(Math.max(0, width - hsides(e, includeMargins)));
    }
}


function setOuterHeight(element, height, includeMargins) {
    for (var i=0, e; i<element.length; i++) {
        e = $(element[i]);
        e.height(Math.max(0, height - vsides(e, includeMargins)));
    }
}


// TODO: curCSS has been deprecated (jQuery 1.4.3 - 10/16/2010)


function hsides(element, includeMargins) {
    return hpadding(element) + hborders(element) + (includeMargins ? hmargins(element) : 0);
}


function hpadding(element) {
    return (parseFloat($.curCSS(element[0], 'paddingLeft', true)) || 0) +
           (parseFloat($.curCSS(element[0], 'paddingRight', true)) || 0);
}


function hmargins(element) {
    return (parseFloat($.curCSS(element[0], 'marginLeft', true)) || 0) +
           (parseFloat($.curCSS(element[0], 'marginRight', true)) || 0);
}


function hborders(element) {
    return (parseFloat($.curCSS(element[0], 'borderLeftWidth', true)) || 0) +
           (parseFloat($.curCSS(element[0], 'borderRightWidth', true)) || 0);
}


function vsides(element, includeMargins) {
    return vpadding(element) +  vborders(element) + (includeMargins ? vmargins(element) : 0);
}


function vpadding(element) {
    return (parseFloat($.curCSS(element[0], 'paddingTop', true)) || 0) +
           (parseFloat($.curCSS(element[0], 'paddingBottom', true)) || 0);
}


function vmargins(element) {
    return (parseFloat($.curCSS(element[0], 'marginTop', true)) || 0) +
           (parseFloat($.curCSS(element[0], 'marginBottom', true)) || 0);
}


function vborders(element) {
    return (parseFloat($.curCSS(element[0], 'borderTopWidth', true)) || 0) +
           (parseFloat($.curCSS(element[0], 'borderBottomWidth', true)) || 0);
}


function setMinHeight(element, height) {
    height = (typeof height == 'number' ? height + 'px' : height);
    element.each(function(i, _element) {
        _element.style.cssText += ';min-height:' + height + ';_height:' + height;
        // why can't we just use .css() ? i forget
    });
}



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


//TODO: arraySlice
//TODO: isFunction, grep ?


function noop() { }


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


function arrayMax(a) {
    return Math.max.apply(Math, a);
}


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 cssKey(_element) {
    return _element.id + '/' + _element.className + '/' + _element.style.cssText.replace(/(^|;)\s*(top|left|width|height)\s*:[^;]*/ig, '');
}


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


function markFirstLast(e) {
    e.children()
        .removeClass('fc-first fc-last')
        .filter(':first-child')
            .addClass('fc-first')
        .end()
        .filter(':last-child')
            .addClass('fc-last');
}


function setDayID(cell, date) {
    cell.each(function(i, _cell) {
        _cell.className = _cell.className.replace(/^fc-\w*/, 'fc-' + dayIDs[date.getDay()]);
        // TODO: make a way that doesn't rely on order of classes
    });
}


function getSkinCss(event, opt) {
    var source = event.source || {};
    var eventColor = event.color;
    var sourceColor = source.color;
    var optionColor = opt('eventColor');
    var backgroundColor =
        event.backgroundColor ||
        eventColor ||
        source.backgroundColor ||
        sourceColor ||
        opt('eventBackgroundColor') ||
        optionColor;
    var borderColor =
        event.borderColor ||
        eventColor ||
        source.borderColor ||
        sourceColor ||
        opt('eventBorderColor') ||
        optionColor;
    var textColor =
        event.textColor ||
        source.textColor ||
        opt('eventTextColor');
    var statements = [];
    if (backgroundColor) {
        statements.push('background-color:' + backgroundColor);
    }
    if (borderColor) {
        statements.push('border-color:' + borderColor);
    }
    if (textColor) {
        statements.push('color:' + textColor);
    }
    return statements.join(';');
}


function applyAll(functions, thisObj, args) {
    if ($.isFunction(functions)) {
        functions = [ functions ];
    }
    if (functions) {
        var i;
        var ret;
        for (i=0; i<functions.length; i++) {
            ret = functions[i].apply(thisObj, args) || ret;
        }
        return ret;
    }
}


function firstDefined() {
    for (var i=0; i<arguments.length; i++) {
        if (arguments[i] !== undefined) {
            return arguments[i];
        }
    }
}



fcViews.month = MonthView;

function MonthView(element, calendar) {
    var t = this;
    
    
    // exports
    t.render = render;
    
    
    // imports
    BasicView.call(t, element, calendar, 'month');
    var opt = t.opt;
    var renderBasic = t.renderBasic;
    var formatDate = calendar.formatDate;
    
    
    
    function render(date, delta) {
        if (delta) {
            addMonths(date, delta);
            date.setDate(1);
        }
        var start = cloneDate(date, true);
        start.setDate(1);
        var end = addMonths(cloneDate(start), 1);
        var visStart = cloneDate(start);
        var visEnd = cloneDate(end);
        var firstDay = opt('firstDay');
        var nwe = opt('weekends') ? 0 : 1;
        if (nwe) {
            skipWeekend(visStart);
            skipWeekend(visEnd, -1, true);
        }
        addDays(visStart, -((visStart.getDay() - Math.max(firstDay, nwe) + 7) % 7));
        addDays(visEnd, (7 - visEnd.getDay() + Math.max(firstDay, nwe)) % 7);
        var rowCnt = Math.round((visEnd - visStart) / (DAY_MS * 7));
        if (opt('weekMode') == 'fixed') {
            addDays(visEnd, (6 - rowCnt) * 7);
            rowCnt = 6;
        }
        t.title = formatDate(start, opt('titleFormat'));
        t.start = start;
        t.end = end;
        t.visStart = visStart;
        t.visEnd = visEnd;
        renderBasic(6, rowCnt, nwe ? 5 : 7, true);
    }
    
    
}

fcViews.basicWeek = BasicWeekView;

function BasicWeekView(element, calendar) {
    var t = this;
    
    
    // exports
    t.render = render;
    
    
    // imports
    BasicView.call(t, element, calendar, 'basicWeek');
    var opt = t.opt;
    var renderBasic = t.renderBasic;
    var formatDates = calendar.formatDates;
    
    
    
    function render(date, delta) {
        if (delta) {
            addDays(date, delta * 7);
        }
        var start = addDays(cloneDate(date), -((date.getDay() - opt('firstDay') + 7) % 7));
        var end = addDays(cloneDate(start), 7);
        var visStart = cloneDate(start);
        var visEnd = cloneDate(end);
        var weekends = opt('weekends');
        if (!weekends) {
            skipWeekend(visStart);
            skipWeekend(visEnd, -1, true);
        }
        t.title = formatDates(
            visStart,
            addDays(cloneDate(visEnd), -1),
            opt('titleFormat')
        );
        t.start = start;
        t.end = end;
        t.visStart = visStart;
        t.visEnd = visEnd;
        renderBasic(1, 1, weekends ? 7 : 5, false);
    }
    
    
}

fcViews.basicDay = BasicDayView;

//TODO: when calendar's date starts out on a weekend, shouldn't happen


function BasicDayView(element, calendar) {
    var t = this;
    
    
    // exports
    t.render = render;
    
    
    // imports
    BasicView.call(t, element, calendar, 'basicDay');
    var opt = t.opt;
    var renderBasic = t.renderBasic;
    var formatDate = calendar.formatDate;
    
    
    
    function render(date, delta) {
        if (delta) {
            addDays(date, delta);
            if (!opt('weekends')) {
                skipWeekend(date, delta < 0 ? -1 : 1);
            }
        }
        t.title = formatDate(date, opt('titleFormat'));
        t.start = t.visStart = cloneDate(date, true);
        t.end = t.visEnd = addDays(cloneDate(t.start), 1);
        renderBasic(1, 1, 1, false);
    }
    
    
}

setDefaults({
    weekMode: 'fixed'
});


function BasicView(element, calendar, viewName) {
    var t = this;
    
    
    // exports
    t.renderBasic = renderBasic;
    t.setHeight = setHeight;
    t.setWidth = setWidth;
    t.renderDayOverlay = renderDayOverlay;
    t.defaultSelectionEnd = defaultSelectionEnd;
    t.renderSelection = renderSelection;
    t.clearSelection = clearSelection;
    t.reportDayClick = reportDayClick; // for selection (kinda hacky)
    t.dragStart = dragStart;
    t.dragStop = dragStop;
    t.defaultEventEnd = defaultEventEnd;
    t.getHoverListener = function() { return hoverListener };
    t.colContentLeft = colContentLeft;
    t.colContentRight = colContentRight;
    t.dayOfWeekCol = dayOfWeekCol;
    t.dateCell = dateCell;
    t.cellDate = cellDate;
    t.cellIsAllDay = function() { return true };
    t.allDayRow = allDayRow;
    t.allDayBounds = allDayBounds;
    t.getRowCnt = function() { return rowCnt };
    t.getColCnt = function() { return colCnt };
    t.getColWidth = function() { return colWidth };
    t.getDaySegmentContainer = function() { return daySegmentContainer };
    
    
    // imports
    View.call(t, element, calendar, viewName);
    OverlayManager.call(t);
    SelectionManager.call(t);
    BasicEventRenderer.call(t);
    var opt = t.opt;
    var trigger = t.trigger;
    var clearEvents = t.clearEvents;
    var renderOverlay = t.renderOverlay;
    var clearOverlays = t.clearOverlays;
    var daySelectionMousedown = t.daySelectionMousedown;
    var formatDate = calendar.formatDate;
    
    
    // locals
    
    var head;
    var headCells;
    var body;
    var bodyRows;
    var bodyCells;
    var bodyFirstCells;
    var bodyCellTopInners;
    var daySegmentContainer;
    
    var viewWidth;
    var viewHeight;
    var colWidth;
    
    var rowCnt, colCnt;
    var coordinateGrid;
    var hoverListener;
    var colContentPositions;
    
    var rtl, dis, dit;
    var firstDay;
    var nwe;
    var tm;
    var colFormat;
    
    
    
    /* Rendering
    ------------------------------------------------------------*/
    
    
    disableTextSelection(element.addClass('fc-grid'));
    
    
    function renderBasic(maxr, r, c, showNumbers) {
        rowCnt = r;
        colCnt = c;
        updateOptions();
        var firstTime = !body;
        if (firstTime) {
            buildSkeleton(maxr, showNumbers);
        }else{
            clearEvents();
        }
        updateCells(firstTime);
    }
    
    
    
    function updateOptions() {
        rtl = opt('isRTL');
        if (rtl) {
            dis = -1;
            dit = colCnt - 1;
        }else{
            dis = 1;
            dit = 0;
        }
        firstDay = opt('firstDay');
        nwe = opt('weekends') ? 0 : 1;
        tm = opt('theme') ? 'ui' : 'fc';
        colFormat = opt('columnFormat');
    }
    
    
    
    function buildSkeleton(maxRowCnt, showNumbers) {
        var s;
        var headerClass = tm + "-widget-header";
        var contentClass = tm + "-widget-content";
        var i, j;
        var table;
        
        s =
            "<table class='fc-border-separate' style='width:100%' cellspacing='0'>" +
            "<thead>" +
            "<tr>";
        for (i=0; i<colCnt; i++) {
            s +=
                "<th class='fc- " + headerClass + "'/>"; // need fc- for setDayID
        }
        s +=
            "</tr>" +
            "</thead>" +
            "<tbody>";
        for (i=0; i<maxRowCnt; i++) {
            s +=
                "<tr class='fc-week" + i + "'>";
            for (j=0; j<colCnt; j++) {
                s +=
                    "<td class='fc- " + contentClass + " fc-day" + (i*colCnt+j) + "'>" + // need fc- for setDayID
                    "<div>" +
                    (showNumbers ?
                        "<div class='fc-day-number'/>" :
                        ''
                        ) +
                    "<div class='fc-day-content'>" +
                    "<div style='position:relative'>&nbsp;</div>" +
                    "</div>" +
                    "</div>" +
                    "</td>";
            }
            s +=
                "</tr>";
        }
        s +=
            "</tbody>" +
            "</table>";
        table = $(s).appendTo(element);
        
        head = table.find('thead');
        headCells = head.find('th');
        body = table.find('tbody');
        bodyRows = body.find('tr');
        bodyCells = body.find('td');
        bodyFirstCells = bodyCells.filter(':first-child');
        bodyCellTopInners = bodyRows.eq(0).find('div.fc-day-content div');
        
        markFirstLast(head.add(head.find('tr'))); // marks first+last tr/th's
        markFirstLast(bodyRows); // marks first+last td's
        bodyRows.eq(0).addClass('fc-first'); // fc-last is done in updateCells
        
        dayBind(bodyCells);
        
        daySegmentContainer =
            $("<div style='position:absolute;z-index:8;top:0;left:0'/>")
                .appendTo(element);
    }
    
    
    
    function updateCells(firstTime) {
        var dowDirty = firstTime || rowCnt == 1; // could the cells' day-of-weeks need updating?
        var month = t.start.getMonth();
        var today = clearTime(new Date());
        var cell;
        var date;
        var row;
    
        if (dowDirty) {
            headCells.each(function(i, _cell) {
                cell = $(_cell);
                date = indexDate(i);
                cell.html(formatDate(date, colFormat));
                setDayID(cell, date);
            });
        }
        
        bodyCells.each(function(i, _cell) {
            cell = $(_cell);
            date = indexDate(i);
            if (date.getMonth() == month) {
                cell.removeClass('fc-other-month');
            }else{
                cell.addClass('fc-other-month');
            }
            if (+date == +today) {
                cell.addClass(tm + '-state-highlight fc-today');
            }else{
                cell.removeClass(tm + '-state-highlight fc-today');
            }
            cell.find('div.fc-day-number').text(date.getDate());
            if (dowDirty) {
                setDayID(cell, date);
            }
        });
        
        bodyRows.each(function(i, _row) {
            row = $(_row);
            if (i < rowCnt) {
                row.show();
                if (i == rowCnt-1) {
                    row.addClass('fc-last');
                }else{
                    row.removeClass('fc-last');
                }
            }else{
                row.hide();
            }
        });
    }
    
    
    
    function setHeight(height) {
        viewHeight = height;
        
        var bodyHeight = viewHeight - head.height();
        var rowHeight;
        var rowHeightLast;
        var cell;
            
        if (opt('weekMode') == 'variable') {
            rowHeight = rowHeightLast = Math.floor(bodyHeight / (rowCnt==1 ? 2 : 6));
        }else{
            rowHeight = Math.floor(bodyHeight / rowCnt);
            rowHeightLast = bodyHeight - rowHeight * (rowCnt-1);
        }
        
        bodyFirstCells.each(function(i, _cell) {
            if (i < rowCnt) {
                cell = $(_cell);
                setMinHeight(
                    cell.find('> div'),
                    (i==rowCnt-1 ? rowHeightLast : rowHeight) - vsides(cell)
                );
            }
        });
        
    }
    
    
    function setWidth(width) {
        viewWidth = width;
        colContentPositions.clear();
        colWidth = Math.floor(viewWidth / colCnt);
        setOuterWidth(headCells.slice(0, -1), colWidth);
    }
    
    
    
    /* Day clicking and binding
    -----------------------------------------------------------*/
    
    
    function dayBind(days) {
        days.click(dayClick)
            .mousedown(daySelectionMousedown);
    }
    
    
    function dayClick(ev) {
        if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick
            var index = parseInt(this.className.match(/fc\-day(\d+)/)[1]); // TODO: maybe use .data
            var date = indexDate(index);
            trigger('dayClick', this, date, true, ev);
        }
    }
    
    
    
    /* Semi-transparent Overlay Helpers
    ------------------------------------------------------*/
    
    
    function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive
        if (refreshCoordinateGrid) {
            coordinateGrid.build();
        }
        var rowStart = cloneDate(t.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(
                    renderCellOverlay(i, colStart, i, colEnd-1)
                );
            }
            addDays(rowStart, 7);
            addDays(rowEnd, 7);
        }
    }
    
    
    function renderCellOverlay(row0, col0, row1, col1) { // row1,col1 is inclusive
        var rect = coordinateGrid.rect(row0, col0, row1, col1, element);
        return renderOverlay(rect, element);
    }
    
    
    
    /* Selection
    -----------------------------------------------------------------------*/
    
    
    function defaultSelectionEnd(startDate, allDay) {
        return cloneDate(startDate);
    }
    
    
    function renderSelection(startDate, endDate, allDay) {
        renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true); // rebuild every time???
    }
    
    
    function clearSelection() {
        clearOverlays();
    }
    
    
    function reportDayClick(date, allDay, ev) {
        var cell = dateCell(date);
        var _element = bodyCells[cell.row*colCnt + cell.col];
        trigger('dayClick', _element, date, allDay, ev);
    }
    
    
    
    /* External Dragging
    -----------------------------------------------------------------------*/
    
    
    function dragStart(_dragElement, ev, ui) {
        hoverListener.start(function(cell) {
            clearOverlays();
            if (cell) {
                renderCellOverlay(cell.row, cell.col, cell.row, cell.col);
            }
        }, ev);
    }
    
    
    function dragStop(_dragElement, ev, ui) {
        var cell = hoverListener.stop();
        clearOverlays();
        if (cell) {
            var d = cellDate(cell);
            trigger('drop', _dragElement, d, true, ev, ui);
        }
    }
    
    
    
    /* Utilities
    --------------------------------------------------------*/
    
    
    function defaultEventEnd(event) {
        return cloneDate(event.start);
    }
    
    
    coordinateGrid = new CoordinateGrid(function(rows, cols) {
        var e, n, p;
        headCells.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();
        bodyRows.each(function(i, _e) {
            if (i < rowCnt) {
                e = $(_e);
                n = e.offset().top;
                if (i) {
                    p[1] = n;
                }
                p = [n];
                rows[i] = p;
            }
        });
        p[1] = n + e.outerHeight();
    });
    
    
    hoverListener = new HoverListener(coordinateGrid);
    
    
    colContentPositions = new HorizontalPositionCache(function(col) {
        return bodyCellTopInners.eq(col);
    });
    
    
    function colContentLeft(col) {
        return colContentPositions.left(col);
    }
    
    
    function colContentRight(col) {
        return colContentPositions.right(col);
    }
    
    
    
    
    function dateCell(date) {
        return {
            row: Math.floor(dayDiff(date, t.visStart) / 7),
            col: dayOfWeekCol(date.getDay())
        };
    }
    
    
    function cellDate(cell) {
        return _cellDate(cell.row, cell.col);
    }
    
    
    function _cellDate(row, col) {
        return addDays(cloneDate(t.visStart), row*7 + col*dis+dit);
        // what about weekends in middle of week?
    }
    
    
    function indexDate(index) {
        return _cellDate(Math.floor(index/colCnt), index%colCnt);
    }
    
    
    function dayOfWeekCol(dayOfWeek) {
        return ((dayOfWeek - Math.max(firstDay, nwe) + colCnt) % colCnt) * dis + dit;
    }
    
    
    
    
    function allDayRow(i) {
        return bodyRows.eq(i);
    }
    
    
    function allDayBounds(i) {
        return {
            left: 0,
            right: viewWidth
        };
    }
    
    
}

function BasicEventRenderer() {
    var t = this;
    
    
    // exports
    t.renderEvents = renderEvents;
    t.compileDaySegs = compileSegs; // for DayEventRenderer
    t.clearEvents = clearEvents;
    t.bindDaySeg = bindDaySeg;
    
    
    // imports
    DayEventRenderer.call(t);
    var opt = t.opt;
    var trigger = t.trigger;
    //var setOverflowHidden = t.setOverflowHidden;
    var isEventDraggable = t.isEventDraggable;
    var isEventResizable = t.isEventResizable;
    var reportEvents = t.reportEvents;
    var reportEventClear = t.reportEventClear;
    var eventElementHandlers = t.eventElementHandlers;
    var showEvents = t.showEvents;
    var hideEvents = t.hideEvents;
    var eventDrop = t.eventDrop;
    var getDaySegmentContainer = t.getDaySegmentContainer;
    var getHoverListener = t.getHoverListener;
    var renderDayOverlay = t.renderDayOverlay;
    var clearOverlays = t.clearOverlays;
    var getRowCnt = t.getRowCnt;
    var getColCnt = t.getColCnt;
    var renderDaySegs = t.renderDaySegs;
    var resizableDayEvent = t.resizableDayEvent;
    
    
    
    /* Rendering
    --------------------------------------------------------------------*/
    
    
    function renderEvents(events, modifiedEventId) {
        reportEvents(events);
        renderDaySegs(compileSegs(events), modifiedEventId);
    }
    
    
    function clearEvents() {
        reportEventClear();
        getDaySegmentContainer().empty();
    }
    
    
    function compileSegs(events) {
        var rowCnt = getRowCnt(),
            colCnt = getColCnt(),
            d1 = cloneDate(t.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(sliceSegs(events, visEventsEnds, d1, d2));
            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; // not needed anymore
                    segs.push(seg);
                }
            }
            addDays(d1, 7);
            addDays(d2, 7);
        }
        return segs;
    }
    
    
    function bindDaySeg(event, eventElement, seg) {
        if (isEventDraggable(event)) {
            draggableDayEvent(event, eventElement);
        }
        if (seg.isEnd && isEventResizable(event)) {
            resizableDayEvent(event, eventElement, seg);
        }
        eventElementHandlers(event, eventElement);
            // needs to be after, because resizableDayEvent might stopImmediatePropagation on click
    }
    
    
    
    /* Dragging
    ----------------------------------------------------------------------------*/
    
    
    function draggableDayEvent(event, eventElement) {
        var hoverListener = getHoverListener();
        var dayDelta;
        eventElement.draggable({
            zIndex: 9,
            delay: 50,
            opacity: opt('dragOpacity'),
            revertDuration: opt('dragRevertDuration'),
            start: function(ev, ui) {
                trigger('eventDragStart', eventElement, event, ev, ui);
                hideEvents(event, eventElement);
                hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
                    eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta);
                    clearOverlays();
                    if (cell) {
                        //setOverflowHidden(true);
                        dayDelta = rowDelta*7 + colDelta * (opt('isRTL') ? -1 : 1);
                        renderDayOverlay(
                            addDays(cloneDate(event.start), dayDelta),
                            addDays(exclEndDay(event), dayDelta)
                        );
                    }else{
                        //setOverflowHidden(false);
                        dayDelta = 0;
                    }
                }, ev, 'drag');
            },
            stop: function(ev, ui) {
                hoverListener.stop();
                clearOverlays();
                trigger('eventDragStop', eventElement, event, ev, ui);
                if (dayDelta) {
                    eventDrop(this, event, dayDelta, 0, event.allDay, ev, ui);
                }else{
                    eventElement.css('filter', ''); // clear IE opacity side-effects
                    showEvents(event, eventElement);
                }
                //setOverflowHidden(false);
            }
        });
    }


}

fcViews.agendaWeek = AgendaWeekView;

function AgendaWeekView(element, calendar) {
    var t = this;
    
    
    // exports
    t.render = render;
    
    
    // imports
    AgendaView.call(t, element, calendar, 'agendaWeek');
    var opt = t.opt;
    var renderAgenda = t.renderAgenda;
    var formatDates = calendar.formatDates;
    
    
    
    function render(date, delta) {
        if (delta) {
            addDays(date, delta * 7);
        }
        var start = addDays(cloneDate(date), -((date.getDay() - opt('firstDay') + 7) % 7));
        var end = addDays(cloneDate(start), 7);
        var visStart = cloneDate(start);
        var visEnd = cloneDate(end);
        var weekends = opt('weekends');
        if (!weekends) {
            skipWeekend(visStart);
            skipWeekend(visEnd, -1, true);
        }
        t.title = formatDates(
            visStart,
            addDays(cloneDate(visEnd), -1),
            opt('titleFormat')
        );
        t.start = start;
        t.end = end;
        t.visStart = visStart;
        t.visEnd = visEnd;
        renderAgenda(weekends ? 7 : 5);
    }
    

}

fcViews.agendaDay = AgendaDayView;

function AgendaDayView(element, calendar) {
    var t = this;
    
    
    // exports
    t.render = render;
    
    
    // imports
    AgendaView.call(t, element, calendar, 'agendaDay');
    var opt = t.opt;
    var renderAgenda = t.renderAgenda;
    var formatDate = calendar.formatDate;
    
    
    
    function render(date, delta) {
        if (delta) {
            addDays(date, delta);
            if (!opt('weekends')) {
                skipWeekend(date, delta < 0 ? -1 : 1);
            }
        }
        var start = cloneDate(date, true);
        var end = addDays(cloneDate(start), 1);
        t.title = formatDate(date, opt('titleFormat'));
        t.start = t.visStart = start;
        t.end = t.visEnd = end;
        renderAgenda(1);
    }
    

}

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


// TODO: make it work in quirks mode (event corners, all-day height)
// TODO: test liquid width, especially in IE6


function AgendaView(element, calendar, viewName) {
    var t = this;
    
    
    // exports
    t.renderAgenda = renderAgenda;
    t.setWidth = setWidth;
    t.setHeight = setHeight;
    t.beforeHide = beforeHide;
    t.afterShow = afterShow;
    t.defaultEventEnd = defaultEventEnd;
    t.timePosition = timePosition;
    t.dayOfWeekCol = dayOfWeekCol;
    t.dateCell = dateCell;
    t.cellDate = cellDate;
    t.cellIsAllDay = cellIsAllDay;
    t.allDayRow = getAllDayRow;
    t.allDayBounds = allDayBounds;
    t.getHoverListener = function() { return hoverListener };
    t.colContentLeft = colContentLeft;
    t.colContentRight = colContentRight;
    t.getDaySegmentContainer = function() { return daySegmentContainer };
    t.getSlotSegmentContainer = function() { return slotSegmentContainer };
    t.getMinMinute = function() { return minMinute };
    t.getMaxMinute = function() { return maxMinute };
    t.getBodyContent = function() { return slotContent }; // !!??
    t.getRowCnt = function() { return 1 };
    t.getColCnt = function() { return colCnt };
    t.getColWidth = function() { return colWidth };
    t.getSlotHeight = function() { return slotHeight };
    t.defaultSelectionEnd = defaultSelectionEnd;
    t.renderDayOverlay = renderDayOverlay;
    t.renderSelection = renderSelection;
    t.clearSelection = clearSelection;
    t.reportDayClick = reportDayClick; // selection mousedown hack
    t.dragStart = dragStart;
    t.dragStop = dragStop;
    
    
    // imports
    View.call(t, element, calendar, viewName);
    OverlayManager.call(t);
    SelectionManager.call(t);
    AgendaEventRenderer.call(t);
    var opt = t.opt;
    var trigger = t.trigger;
    var clearEvents = t.clearEvents;
    var renderOverlay = t.renderOverlay;
    var clearOverlays = t.clearOverlays;
    var reportSelection = t.reportSelection;
    var unselect = t.unselect;
    var daySelectionMousedown = t.daySelectionMousedown;
    var slotSegHtml = t.slotSegHtml;
    var formatDate = calendar.formatDate;
    
    
    // locals
    
    var dayTable;
    var dayHead;
    var dayHeadCells;
    var dayBody;
    var dayBodyCells;
    var dayBodyCellInners;
    var dayBodyFirstCell;
    var dayBodyFirstCellStretcher;
    var slotLayer;
    var daySegmentContainer;
    var allDayTable;
    var allDayRow;
    var slotScroller;
    var slotContent;
    var slotSegmentContainer;
    var slotTable;
    var slotTableFirstInner;
    var axisFirstCells;
    var gutterCells;
    var selectionHelper;
    
    var viewWidth;
    var viewHeight;
    var axisWidth;
    var colWidth;
    var gutterWidth;
    var slotHeight; // TODO: what if slotHeight changes? (see issue 650)
    var savedScrollTop;
    
    var colCnt;
    var slotCnt;
    var coordinateGrid;
    var hoverListener;
    var colContentPositions;
    var slotTopCache = {};
    
    var tm;
    var firstDay;
    var nwe;            // no weekends (int)
    var rtl, dis, dit;  // day index sign / translate
    var minMinute, maxMinute;
    var colFormat;
    

    
    /* Rendering
    -----------------------------------------------------------------------------*/
    
    
    disableTextSelection(element.addClass('fc-agenda'));
    
    
    function renderAgenda(c) {
        colCnt = c;
        updateOptions();
        if (!dayTable) {
            buildSkeleton();
        }else{
            clearEvents();
        }
        updateCells();
    }
    
    
    
    function updateOptions() {
        tm = opt('theme') ? 'ui' : 'fc';
        nwe = opt('weekends') ? 0 : 1;
        firstDay = opt('firstDay');
        if (rtl = opt('isRTL')) {
            dis = -1;
            dit = colCnt - 1;
        }else{
            dis = 1;
            dit = 0;
        }
        minMinute = parseTime(opt('minTime'));
        maxMinute = parseTime(opt('maxTime'));
        colFormat = opt('columnFormat');
    }
    
    
    
    function buildSkeleton() {
        var headerClass = tm + "-widget-header";
        var contentClass = tm + "-widget-content";
        var s;
        var i;
        var d;
        var maxd;
        var minutes;
        var slotNormal = opt('slotMinutes') % 15 == 0;
        
        s =
            "<table style='width:100%' class='fc-agenda-days fc-border-separate' cellspacing='0'>" +
            "<thead>" +
            "<tr>" +
            "<th class='fc-agenda-axis " + headerClass + "'>&nbsp;</th>";
        for (i=0; i<colCnt; i++) {
            s +=
                "<th class='fc- fc-col" + i + ' ' + headerClass + "'/>"; // fc- needed for setDayID
        }
        s +=
            "<th class='fc-agenda-gutter " + headerClass + "'>&nbsp;</th>" +
            "</tr>" +
            "</thead>" +
            "<tbody>" +
            "<tr>" +
            "<th class='fc-agenda-axis " + headerClass + "'>&nbsp;</th>";
        for (i=0; i<colCnt; i++) {
            s +=
                "<td class='fc- fc-col" + i + ' ' + contentClass + "'>" + // fc- needed for setDayID
                "<div>" +
                "<div class='fc-day-content'>" +
                "<div style='position:relative'>&nbsp;</div>" +
                "</div>" +
                "</div>" +
                "</td>";
        }
        s +=
            "<td class='fc-agenda-gutter " + contentClass + "'>&nbsp;</td>" +
            "</tr>" +
            "</tbody>" +
            "</table>";
        dayTable = $(s).appendTo(element);
        dayHead = dayTable.find('thead');
        dayHeadCells = dayHead.find('th').slice(1, -1);
        dayBody = dayTable.find('tbody');
        dayBodyCells = dayBody.find('td').slice(0, -1);
        dayBodyCellInners = dayBodyCells.find('div.fc-day-content div');
        dayBodyFirstCell = dayBodyCells.eq(0);
        dayBodyFirstCellStretcher = dayBodyFirstCell.find('> div');
        
        markFirstLast(dayHead.add(dayHead.find('tr')));
        markFirstLast(dayBody.add(dayBody.find('tr')));
        
        axisFirstCells = dayHead.find('th:first');
        gutterCells = dayTable.find('.fc-agenda-gutter');
        
        slotLayer =
            $("<div style='position:absolute;z-index:2;left:0;width:100%'/>")
                .appendTo(element);
                
        if (opt('allDaySlot')) {
        
            daySegmentContainer =
                $("<div style='position:absolute;z-index:8;top:0;left:0'/>")
                    .appendTo(slotLayer);
        
            s =
                "<table style='width:100%' class='fc-agenda-allday' cellspacing='0'>" +
                "<tr>" +
                "<th class='" + headerClass + " fc-agenda-axis'>" + opt('allDayText') + "</th>" +
                "<td>" +
                "<div class='fc-day-content'><div style='position:relative'/></div>" +
                "</td>" +
                "<th class='" + headerClass + " fc-agenda-gutter'>&nbsp;</th>" +
                "</tr>" +
                "</table>";
            allDayTable = $(s).appendTo(slotLayer);
            allDayRow = allDayTable.find('tr');
            
            dayBind(allDayRow.find('td'));
            
            axisFirstCells = axisFirstCells.add(allDayTable.find('th:first'));
            gutterCells = gutterCells.add(allDayTable.find('th.fc-agenda-gutter'));
            
            slotLayer.append(
                "<div class='fc-agenda-divider " + headerClass + "'>" +
                "<div class='fc-agenda-divider-inner'/>" +
                "</div>"
            );
            
        }else{
        
            daySegmentContainer = $([]); // in jQuery 1.4, we can just do $()
        
        }
        
        slotScroller =
            $("<div style='position:absolute;width:100%;overflow-x:hidden;overflow-y:auto'/>")
                .appendTo(slotLayer);
                
        slotContent =
            $("<div style='position:relative;width:100%;overflow:hidden'/>")
                .appendTo(slotScroller);
                
        slotSegmentContainer =
            $("<div style='position:absolute;z-index:8;top:0;left:0'/>")
                .appendTo(slotContent);
        
        s =
            "<table class='fc-agenda-slots' style='width:100%' cellspacing='0'>" +
            "<tbody>";
        d = zeroDate();
        maxd = addMinutes(cloneDate(d), maxMinute);
        addMinutes(d, minMinute);
        slotCnt = 0;
        for (i=0; d < maxd; i++) {
            minutes = d.getMinutes();
            s +=
                "<tr class='fc-slot" + i + ' ' + (!minutes ? '' : 'fc-minor') + "'>" +
                "<th class='fc-agenda-axis " + headerClass + "'>" +
                ((!slotNormal || !minutes) ? formatDate(d, opt('axisFormat')) : '&nbsp;') +
                "</th>" +
                "<td class='" + contentClass + "'>" +
                "<div style='position:relative'>&nbsp;</div>" +
                "</td>" +
                "</tr>";
            addMinutes(d, opt('slotMinutes'));
            slotCnt++;
        }
        s +=
            "</tbody>" +
            "</table>";
        slotTable = $(s).appendTo(slotContent);
        slotTableFirstInner = slotTable.find('div:first');
        
        slotBind(slotTable.find('td'));
        
        axisFirstCells = axisFirstCells.add(slotTable.find('th:first'));
    }
    
    
    
    function updateCells() {
        var i;
        var headCell;
        var bodyCell;
        var date;
        var today = clearTime(new Date());
        for (i=0; i<colCnt; i++) {
            date = colDate(i);
            headCell = dayHeadCells.eq(i);
            headCell.html(formatDate(date, colFormat));
            bodyCell = dayBodyCells.eq(i);
            if (+date == +today) {
                bodyCell.addClass(tm + '-state-highlight fc-today');
            }else{
                bodyCell.removeClass(tm + '-state-highlight fc-today');
            }
            setDayID(headCell.add(bodyCell), date);
        }
    }
    
    
    
    function setHeight(height, dateChanged) {
        if (height === undefined) {
            height = viewHeight;
        }
        viewHeight = height;
        slotTopCache = {};
    
        var headHeight = dayBody.position().top;
        var allDayHeight = slotScroller.position().top; // including divider
        var bodyHeight = Math.min( // total body height, including borders
            height - headHeight,   // when scrollbars
            slotTable.height() + allDayHeight + 1 // when no scrollbars. +1 for bottom border
        );
        
        dayBodyFirstCellStretcher
            .height(bodyHeight - vsides(dayBodyFirstCell));
        
        slotLayer.css('top', headHeight);
        
        slotScroller.height(bodyHeight - allDayHeight - 1);
        
        slotHeight = slotTableFirstInner.height() + 1; // +1 for border
        
        if (dateChanged) {
            resetScroll();
        }
    }
    
    
    
    function setWidth(width) {
        viewWidth = width;
        colContentPositions.clear();
        
        axisWidth = 0;
        setOuterWidth(
            axisFirstCells
                .width('')
                .each(function(i, _cell) {
                    axisWidth = Math.max(axisWidth, $(_cell).outerWidth());
                }),
            axisWidth
        );
        
        var slotTableWidth = slotScroller[0].clientWidth; // needs to be done after axisWidth (for IE7)
        //slotTable.width(slotTableWidth);
        
        gutterWidth = slotScroller.width() - slotTableWidth;
        if (gutterWidth) {
            setOuterWidth(gutterCells, gutterWidth);
            gutterCells
                .show()
                .prev()
                .removeClass('fc-last');
        }else{
            gutterCells
                .hide()
                .prev()
                .addClass('fc-last');
        }
        
        colWidth = Math.floor((slotTableWidth - axisWidth) / colCnt);
        setOuterWidth(dayHeadCells.slice(0, -1), colWidth);
    }
    


    function resetScroll() {
        var d0 = zeroDate();
        var scrollDate = cloneDate(d0);
        scrollDate.setHours(opt('firstHour'));
        var top = timePosition(d0, scrollDate) + 1; // +1 for the border
        function scroll() {
            slotScroller.scrollTop(top);
        }
        scroll();
        setTimeout(scroll, 0); // overrides any previous scroll state made by the browser
    }
    
    
    function beforeHide() {
        savedScrollTop = slotScroller.scrollTop();
    }
    
    
    function afterShow() {
        slotScroller.scrollTop(savedScrollTop);
    }
    
    
    
    /* Slot/Day clicking and binding
    -----------------------------------------------------------------------*/
    

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


    function slotBind(cells) {
        cells.click(slotClick)
            .mousedown(slotSelectionMousedown);
    }
    
    
    function slotClick(ev) {
        if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick
            var col = Math.min(colCnt-1, Math.floor((ev.pageX - dayTable.offset().left - axisWidth) / colWidth));
            var date = colDate(col);
            var rowMatch = this.parentNode.className.match(/fc-slot(\d+)/); // TODO: maybe use data
            if (rowMatch) {
                var mins = parseInt(rowMatch[1]) * opt('slotMinutes');
                var hours = Math.floor(mins/60);
                date.setHours(hours);
                date.setMinutes(mins%60 + minMinute);
                trigger('dayClick', dayBodyCells[col], date, false, ev);
            }else{
                trigger('dayClick', dayBodyCells[col], date, true, ev);
            }
        }
    }
    
    
    
    /* Semi-transparent Overlay Helpers
    -----------------------------------------------------*/
    

    function renderDayOverlay(startDate, endDate, refreshCoordinateGrid) { // endDate is exclusive
        if (refreshCoordinateGrid) {
            coordinateGrid.build();
        }
        var visStart = cloneDate(t.visStart);
        var startCol, endCol;
        if (rtl) {
            startCol = dayDiff(endDate, visStart)*dis+dit+1;
            endCol = dayDiff(startDate, visStart)*dis+dit+1;
        }else{
            startCol = dayDiff(startDate, visStart);
            endCol = dayDiff(endDate, visStart);
        }
        startCol = Math.max(0, startCol);
        endCol = Math.min(colCnt, endCol);
        if (startCol < endCol) {
            dayBind(
                renderCellOverlay(0, startCol, 0, endCol-1)
            );
        }
    }
    
    
    function renderCellOverlay(row0, col0, row1, col1) { // only for all-day?
        var rect = coordinateGrid.rect(row0, col0, row1, col1, slotLayer);
        return renderOverlay(rect, slotLayer);
    }
    

    function renderSlotOverlay(overlayStart, overlayEnd) {
        var dayStart = cloneDate(t.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, slotContent); // only use it for horizontal coords
                var top = timePosition(dayStart, stretchStart);
                var bottom = timePosition(dayStart, stretchEnd);
                rect.top = top;
                rect.height = bottom - top;
                slotBind(
                    renderOverlay(rect, slotContent)
                );
            }
            addDays(dayStart, 1);
            addDays(dayEnd, 1);
        }
    }
    
    
    
    /* Coordinate Utilities
    -----------------------------------------------------------------------------*/
    
    
    coordinateGrid = new CoordinateGrid(function(rows, cols) {
        var e, n, p;
        dayHeadCells.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 (opt('allDaySlot')) {
            e = allDayRow;
            n = e.offset().top;
            rows[0] = [n, n+e.outerHeight()];
        }
        var slotTableTop = slotContent.offset().top;
        var slotScrollerTop = slotScroller.offset().top;
        var slotScrollerBottom = slotScrollerTop + slotScroller.outerHeight();
        function constrain(n) {
            return Math.max(slotScrollerTop, Math.min(slotScrollerBottom, n));
        }
        for (var i=0; i<slotCnt; i++) {
            rows.push([
                constrain(slotTableTop + slotHeight*i),
                constrain(slotTableTop + slotHeight*(i+1))
            ]);
        }
    });
    
    
    hoverListener = new HoverListener(coordinateGrid);
    
    
    colContentPositions = new HorizontalPositionCache(function(col) {
        return dayBodyCellInners.eq(col);
    });
    
    
    function colContentLeft(col) {
        return colContentPositions.left(col);
    }
    
    
    function colContentRight(col) {
        return colContentPositions.right(col);
    }
    
    
    
    
    function dateCell(date) { // "cell" terminology is now confusing
        return {
            row: Math.floor(dayDiff(date, t.visStart) / 7),
            col: dayOfWeekCol(date.getDay())
        };
    }
    
    
    function cellDate(cell) {
        var d = colDate(cell.col);
        var slotIndex = cell.row;
        if (opt('allDaySlot')) {
            slotIndex--;
        }
        if (slotIndex >= 0) {
            addMinutes(d, minMinute + slotIndex * opt('slotMinutes'));
        }
        return d;
    }
    
    
    function colDate(col) { // returns dates with 00:00:00
        return addDays(cloneDate(t.visStart), col*dis+dit);
    }
    
    
    function cellIsAllDay(cell) {
        return opt('allDaySlot') && !cell.row;
    }
    
    
    function dayOfWeekCol(dayOfWeek) {
        return ((dayOfWeek - Math.max(firstDay, nwe) + colCnt) % colCnt)*dis+dit;
    }
    
    
    
    
    // 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 slotTable.height();
        }
        var slotMinutes = opt('slotMinutes'),
            minutes = time.getHours()*60 + time.getMinutes() - minMinute,
            slotI = Math.floor(minutes / slotMinutes),
            slotTop = slotTopCache[slotI];
        if (slotTop === undefined) {
            slotTop = slotTopCache[slotI] = slotTable.find('tr:eq(' + slotI + ') td div')[0].offsetTop; //.position().top; // need this optimization???
        }
        return Math.max(0, Math.round(
            slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes)
        ));
    }
    
    
    function allDayBounds() {
        return {
            left: axisWidth,
            right: viewWidth - gutterWidth
        }
    }
    
    
    function getAllDayRow(index) {
        return allDayRow;
    }
    
    
    function defaultEventEnd(event) {
        var start = cloneDate(event.start);
        if (event.allDay) {
            return start;
        }
        return addMinutes(start, opt('defaultEventMinutes'));
    }
    
    
    
    /* Selection
    ---------------------------------------------------------------------------------*/
    
    
    function defaultSelectionEnd(startDate, allDay) {
        if (allDay) {
            return cloneDate(startDate);
        }
        return addMinutes(cloneDate(startDate), opt('slotMinutes'));
    }
    
    
    function renderSelection(startDate, endDate, allDay) { // only for all-day
        if (allDay) {
            if (opt('allDaySlot')) {
                renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true);
            }
        }else{
            renderSlotSelection(startDate, endDate);
        }
    }
    
    
    function renderSlotSelection(startDate, endDate) {
        var helperOption = opt('selectHelper');
        coordinateGrid.build();
        if (helperOption) {
            var col = dayDiff(startDate, t.visStart) * dis + dit;
            if (col >= 0 && col < colCnt) { // only works when times are on same day
                var rect = coordinateGrid.rect(0, col, 0, col, slotContent); // 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(slotContent);
                        }
                    }else{
                        rect.isStart = true; // conside rect a "seg" now
                        rect.isEnd = true;   //
                        selectionHelper = $(slotSegHtml(
                            {
                                title: '',
                                start: startDate,
                                end: endDate,
                                className: ['fc-select-helper'],
                                editable: false
                            },
                            rect
                        ));
                        selectionHelper.css('opacity', opt('dragOpacity'));
                    }
                    if (selectionHelper) {
                        slotBind(selectionHelper);
                        slotContent.append(selectionHelper);
                        setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended
                        setOuterHeight(selectionHelper, rect.height, true);
                    }
                }
            }
        }else{
            renderSlotOverlay(startDate, endDate);
        }
    }
    
    
    function clearSelection() {
        clearOverlays();
        if (selectionHelper) {
            selectionHelper.remove();
            selectionHelper = null;
        }
    }
    
    
    function slotSelectionMousedown(ev) {
        if (ev.which == 1 && opt('selectable')) { // ev.which==1 means left mouse button
            unselect(ev);
            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), opt('slotMinutes')),
                        d2,
                        addMinutes(cloneDate(d2), opt('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]) {
                        reportDayClick(dates[0], false, ev);
                    }
                    reportSelection(dates[0], dates[3], false, ev);
                }
            });
        }
    }
    
    
    function reportDayClick(date, allDay, ev) {
        trigger('dayClick', dayBodyCells[dayOfWeekCol(date.getDay())], date, allDay, ev);
    }
    
    
    
    /* External Dragging
    --------------------------------------------------------------------------------*/
    
    
    function dragStart(_dragElement, ev, ui) {
        hoverListener.start(function(cell) {
            clearOverlays();
            if (cell) {
                if (cellIsAllDay(cell)) {
                    renderCellOverlay(cell.row, cell.col, cell.row, cell.col);
                }else{
                    var d1 = cellDate(cell);
                    var d2 = addMinutes(cloneDate(d1), opt('defaultEventMinutes'));
                    renderSlotOverlay(d1, d2);
                }
            }
        }, ev);
    }
    
    
    function dragStop(_dragElement, ev, ui) {
        var cell = hoverListener.stop();
        clearOverlays();
        if (cell) {
            trigger('drop', _dragElement, cellDate(cell), cellIsAllDay(cell), ev, ui);
        }
    }


}

function AgendaEventRenderer() {
    var t = this;
    
    
    // exports
    t.renderEvents = renderEvents;
    t.compileDaySegs = compileDaySegs; // for DayEventRenderer
    t.clearEvents = clearEvents;
    t.slotSegHtml = slotSegHtml;
    t.bindDaySeg = bindDaySeg;
    
    
    // imports
    DayEventRenderer.call(t);
    var opt = t.opt;
    var trigger = t.trigger;
    //var setOverflowHidden = t.setOverflowHidden;
    var isEventDraggable = t.isEventDraggable;
    var isEventResizable = t.isEventResizable;
    var eventEnd = t.eventEnd;
    var reportEvents = t.reportEvents;
    var reportEventClear = t.reportEventClear;
    var eventElementHandlers = t.eventElementHandlers;
    var setHeight = t.setHeight;
    var getDaySegmentContainer = t.getDaySegmentContainer;
    var getSlotSegmentContainer = t.getSlotSegmentContainer;
    var getHoverListener = t.getHoverListener;
    var getMaxMinute = t.getMaxMinute;
    var getMinMinute = t.getMinMinute;
    var timePosition = t.timePosition;
    var colContentLeft = t.colContentLeft;
    var colContentRight = t.colContentRight;
    var renderDaySegs = t.renderDaySegs;
    var resizableDayEvent = t.resizableDayEvent; // TODO: streamline binding architecture
    var getColCnt = t.getColCnt;
    var getColWidth = t.getColWidth;
    var getSlotHeight = t.getSlotHeight;
    var getBodyContent = t.getBodyContent;
    var reportEventElement = t.reportEventElement;
    var showEvents = t.showEvents;
    var hideEvents = t.hideEvents;
    var eventDrop = t.eventDrop;
    var eventResize = t.eventResize;
    var renderDayOverlay = t.renderDayOverlay;
    var clearOverlays = t.clearOverlays;
    var calendar = t.calendar;
    var formatDate = calendar.formatDate;
    var formatDates = calendar.formatDates;
    
    
    
    /* Rendering
    ----------------------------------------------------------------------------*/
    

    function renderEvents(events, modifiedEventId) {
        reportEvents(events);
        var i, len=events.length,
            dayEvents=[],
            slotEvents=[];
        for (i=0; i<len; i++) {
            if (events[i].allDay) {
                dayEvents.push(events[i]);
            }else{
                slotEvents.push(events[i]);
            }
        }
        if (opt('allDaySlot')) {
            renderDaySegs(compileDaySegs(dayEvents), modifiedEventId);
            setHeight(); // no params means set to viewHeight
        }
        renderSlotSegs(compileSlotSegs(slotEvents), modifiedEventId);
    }
    
    
    function clearEvents() {
        reportEventClear();
        getDaySegmentContainer().empty();
        getSlotSegmentContainer().empty();
    }
    
    
    function compileDaySegs(events) {
        var levels = stackSegs(sliceSegs(events, $.map(events, exclEndDay), t.visStart, t.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; // not needed anymore
                segs.push(seg);
            }
        }
        return segs;
    }
    
    
    function compileSlotSegs(events) {
        var colCnt = getColCnt(),
            minMinute = getMinMinute(),
            maxMinute = getMaxMinute(),
            d = addMinutes(cloneDate(t.visStart), minMinute),
            visEventEnds = $.map(events, slotEventEnd),
            i, col,
            j, level,
            k, seg,
            segs=[];
        for (i=0; i<colCnt; i++) {
            col = stackSegs(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;
    }
    
    
    function slotEventEnd(event) {
        if (event.end) {
            return cloneDate(event.end);
        }else{
            return addMinutes(cloneDate(event.start), opt('defaultEventMinutes'));
        }
    }
    
    
    // renders events in the 'time slots' at the bottom
    
    function renderSlotSegs(segs, modifiedEventId) {
    
        var i, segCnt=segs.length, seg,
            event,
            classes,
            top, bottom,
            colI, levelI, forward,
            leftmost,
            availWidth,
            outerWidth,
            left,
            html='',
            eventElements,
            eventElement,
            triggerRes,
            vsideCache={},
            hsideCache={},
            key, val,
            contentElement,
            height,
            slotSegmentContainer = getSlotSegmentContainer(),
            rtl, dis, dit,
            colCnt = getColCnt();
            
        if (rtl = opt('isRTL')) {
            dis = -1;
            dit = colCnt - 1;
        }else{
            dis = 1;
            dit = 0;
        }
            
        // calculate position/dimensions, create html
        for (i=0; i<segCnt; i++) {
            seg = segs[i];
            event = seg.event;
            top = timePosition(seg.start, seg.start);
            bottom = timePosition(seg.start, seg.end);
            colI = seg.col;
            levelI = seg.level;
            forward = seg.forward || 0;
            leftmost = colContentLeft(colI*dis + dit);
            availWidth = colContentRight(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);
        }
        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 = 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) {
                    bindSlotSeg(event, eventElement, seg);
                }else{
                    eventElement[0]._fci = i; // for lazySegBind
                }
                reportEventElement(event, eventElement);
            }
        }
        
        lazySegBind(slotSegmentContainer, segs, bindSlotSeg);
        
        // 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, true)) : val;
                val = hsideCache[key];
                seg.hsides = val === undefined ? (hsideCache[key] = hsides(eventElement, true)) : val;
                contentElement = eventElement.find('div.fc-event-content');
                if (contentElement.length) {
                    seg.contentTop = contentElement[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 = Math.max(0, seg.outerWidth - seg.hsides) + 'px';
                height = Math.max(0, seg.outerHeight - seg.vsides);
                eventElement[0].style.height = height + 'px';
                event = seg.event;
                if (seg.contentTop !== undefined && height - seg.contentTop < 10) {
                    // not enough room for title, put it in the time header
                    eventElement.find('div.fc-event-time')
                        .text(formatDate(event.start, opt('timeFormat')) + ' - ' + event.title);
                    eventElement.find('div.fc-event-title')
                        .remove();
                }
                trigger('eventAfterRender', event, event, eventElement);
            }
        }
                    
    }
    
    
    function slotSegHtml(event, seg) {
        var html = "<";
        var url = event.url;
        var skinCss = getSkinCss(event, opt);
        var skinCssAttr = (skinCss ? " style='" + skinCss + "'" : '');
        var classes = ['fc-event', 'fc-event-skin', 'fc-event-vert'];
        if (isEventDraggable(event)) {
            classes.push('fc-event-draggable');
        }
        if (seg.isStart) {
            classes.push('fc-corner-top');
        }
        if (seg.isEnd) {
            classes.push('fc-corner-bottom');
        }
        classes = classes.concat(event.className);
        if (event.source) {
            classes = classes.concat(event.source.className || []);
        }
        if (url) {
            html += "a href='" + htmlEscape(event.url) + "'";
        }else{
            html += "div";
        }
        html +=
            " class='" + classes.join(' ') + "'" +
            " style='position:absolute;z-index:8;top:" + seg.top + "px;left:" + seg.left + "px;" + skinCss + "'" +
            ">" +
            "<div class='fc-event-inner fc-event-skin'" + skinCssAttr + ">" +
            "<div class='fc-event-head fc-event-skin'" + skinCssAttr + ">" +
            "<div class='fc-event-time'>" +
            htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) +
            "</div>" +
            "</div>" +
            "<div class='fc-event-content'>" +
            "<div class='fc-event-title'>" +
            htmlEscape(event.title) +
            "</div>" +
            "</div>" +
            "<div class='fc-event-bg'></div>" +
            "</div>"; // close inner
        if (seg.isEnd && isEventResizable(event)) {
            html +=
                "<div class='ui-resizable-handle ui-resizable-s'>=</div>";
        }
        html +=
            "</" + (url ? "a" : "div") + ">";
        return html;
    }
    
    
    function bindDaySeg(event, eventElement, seg) {
        if (isEventDraggable(event)) {
            draggableDayEvent(event, eventElement, seg.isStart);
        }
        if (seg.isEnd && isEventResizable(event)) {
            resizableDayEvent(event, eventElement, seg);
        }
        eventElementHandlers(event, eventElement);
            // needs to be after, because resizableDayEvent might stopImmediatePropagation on click
    }
    
    
    function bindSlotSeg(event, eventElement, seg) {
        var timeElement = eventElement.find('div.fc-event-time');
        if (isEventDraggable(event)) {
            draggableSlotEvent(event, eventElement, timeElement);
        }
        if (seg.isEnd && isEventResizable(event)) {
            resizableSlotEvent(event, eventElement, timeElement);
        }
        eventElementHandlers(event, eventElement);
    }
    
    
    
    /* Dragging
    -----------------------------------------------------------------------------------*/
    
    
    // when event starts out FULL-DAY
    
    function draggableDayEvent(event, eventElement, isStart) {
        var origWidth;
        var revert;
        var allDay=true;
        var dayDelta;
        var dis = opt('isRTL') ? -1 : 1;
        var hoverListener = getHoverListener();
        var colWidth = getColWidth();
        var slotHeight = getSlotHeight();
        var minMinute = getMinMinute();
        eventElement.draggable({
            zIndex: 9,
            opacity: opt('dragOpacity', 'month'), // use whatever the month view was using
            revertDuration: opt('dragRevertDuration'),
            start: function(ev, ui) {
                trigger('eventDragStart', eventElement, event, ev, ui);
                hideEvents(event, eventElement);
                origWidth = eventElement.width();
                hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
                    clearOverlays();
                    if (cell) {
                        //setOverflowHidden(true);
                        revert = false;
                        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) {
                                if (allDay) {
                                    // convert event to temporary slot-event
                                    eventElement.width(colWidth - 10); // don't use entire width
                                    setOuterHeight(
                                        eventElement,
                                        slotHeight * Math.round(
                                            (event.end ? ((event.end - event.start) / MINUTE_MS) : opt('defaultEventMinutes'))
                                            / opt('slotMinutes')
                                        )
                                    );
                                    eventElement.draggable('option', 'grid', [colWidth, 1]);
                                    allDay = false;
                                }
                            }else{
                                revert = true;
                            }
                        }
                        revert = revert || (allDay && !dayDelta);
                    }else{
                        resetElement();
                        //setOverflowHidden(false);
                        revert = true;
                    }
                    eventElement.draggable('option', 'revert', revert);
                }, ev, 'drag');
            },
            stop: function(ev, ui) {
                hoverListener.stop();
                clearOverlays();
                trigger('eventDragStop', eventElement, event, ev, ui);
                if (revert) {
                    // hasn't moved or is out of bounds (draggable has already reverted)
                    resetElement();
                    eventElement.css('filter', ''); // clear IE opacity side-effects
                    showEvents(event, eventElement);
                }else{
                    // changed!
                    var minuteDelta = 0;
                    if (!allDay) {
                        minuteDelta = Math.round((eventElement.offset().top - getBodyContent().offset().top) / slotHeight)
                            * opt('slotMinutes')
                            + minMinute
                            - (event.start.getHours() * 60 + event.start.getMinutes());
                    }
                    eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui);
                }
                //setOverflowHidden(false);
            }
        });
        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) {
        var origPosition;
        var allDay=false;
        var dayDelta;
        var minuteDelta;
        var prevMinuteDelta;
        var dis = opt('isRTL') ? -1 : 1;
        var hoverListener = getHoverListener();
        var colCnt = getColCnt();
        var colWidth = getColWidth();
        var slotHeight = getSlotHeight();
        eventElement.draggable({
            zIndex: 9,
            scroll: false,
            grid: [colWidth, slotHeight],
            axis: colCnt==1 ? 'y' : false,
            opacity: opt('dragOpacity'),
            revertDuration: opt('dragRevertDuration'),
            start: function(ev, ui) {
                trigger('eventDragStart', eventElement, event, ev, ui);
                hideEvents(event, eventElement);
                origPosition = eventElement.position();
                minuteDelta = prevMinuteDelta = 0;
                hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
                    eventElement.draggable('option', 'revert', !cell);
                    clearOverlays();
                    if (cell) {
                        dayDelta = colDelta * dis;
                        if (opt('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) * opt('slotMinutes');
                if (minuteDelta != prevMinuteDelta) {
                    if (!allDay) {
                        updateTimeText(minuteDelta);
                    }
                    prevMinuteDelta = minuteDelta;
                }
            },
            stop: function(ev, ui) {
                var cell = hoverListener.stop();
                clearOverlays();
                trigger('eventDragStop', eventElement, event, ev, ui);
                if (cell && (dayDelta || minuteDelta || allDay)) {
                    // changed!
                    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('filter', ''); // clear IE opacity side-effects
                    eventElement.css(origPosition); // sometimes fast drags make event revert to wrong position
                    updateTimeText(0);
                    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, opt('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;
            }
        }
    }
    
    
    
    /* Resizing
    --------------------------------------------------------------------------------------*/
    
    
    function resizableSlotEvent(event, eventElement, timeElement) {
        var slotDelta, prevSlotDelta;
        var slotHeight = getSlotHeight();
        eventElement.resizable({
            handles: {
                s: 'div.ui-resizable-s'
            },
            grid: slotHeight,
            start: function(ev, ui) {
                slotDelta = prevSlotDelta = 0;
                hideEvents(event, eventElement);
                eventElement.css('z-index', 9);
                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(eventEnd(event), opt('slotMinutes')*slotDelta),
                            opt('timeFormat')
                        )
                    );
                    prevSlotDelta = slotDelta;
                }
            },
            stop: function(ev, ui) {
                trigger('eventResizeStop', this, event, ev, ui);
                if (slotDelta) {
                    eventResize(this, event, 0, opt('slotMinutes')*slotDelta, ev, ui);
                }else{
                    eventElement.css('z-index', 8);
                    showEvents(event, eventElement);
                    // BUG: if event was really short, need to put title back in span
                }
            }
        });
    }
    

}


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




function View(element, calendar, viewName) {
    var t = this;
    
    
    // exports
    t.element = element;
    t.calendar = calendar;
    t.name = viewName;
    t.opt = opt;
    t.trigger = trigger;
    //t.setOverflowHidden = setOverflowHidden;
    t.isEventDraggable = isEventDraggable;
    t.isEventResizable = isEventResizable;
    t.reportEvents = reportEvents;
    t.eventEnd = eventEnd;
    t.reportEventElement = reportEventElement;
    t.reportEventClear = reportEventClear;
    t.eventElementHandlers = eventElementHandlers;
    t.showEvents = showEvents;
    t.hideEvents = hideEvents;
    t.eventDrop = eventDrop;
    t.eventResize = eventResize;
    // t.title
    // t.start, t.end
    // t.visStart, t.visEnd
    
    
    // imports
    var defaultEventEnd = t.defaultEventEnd;
    var normalizeEvent = calendar.normalizeEvent; // in EventManager
    var reportEventChange = calendar.reportEventChange;
    
    
    // locals
    var eventsByID = {};
    var eventElements = [];
    var eventElementsByID = {};
    var options = calendar.options;
    
    
    
    function opt(name, viewNameOverride) {
        var v = options[name];
        if (typeof v == 'object') {
            return smartProperty(v, viewNameOverride || viewName);
        }
        return v;
    }

    
    function trigger(name, thisObj) {
        return calendar.trigger.apply(
            calendar,
            [name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t])
        );
    }
    
    
    /*
    function setOverflowHidden(bool) {
        element.css('overflow', bool ? 'hidden' : '');
    }
    */
    
    
    function isEventDraggable(event) {
        return isEventEditable(event) && !opt('disableDragging');
    }
    
    
    function isEventResizable(event) { // but also need to make sure the seg.isEnd == true
        return isEventEditable(event) && !opt('disableResizing');
    }
    
    
    function isEventEditable(event) {
        return firstDefined(event.editable, (event.source || {}).editable, opt('editable'));
    }
    
    
    
    /* Event Data
    ------------------------------------------------------------------------------*/
    
    
    // report when view receives new events
    function reportEvents(events) { // events are already normalized at this point
        eventsByID = {};
        var i, len=events.length, event;
        for (i=0; i<len; i++) {
            event = events[i];
            if (eventsByID[event._id]) {
                eventsByID[event._id].push(event);
            }else{
                eventsByID[event._id] = [event];
            }
        }
    }
    
    
    // returns a Date object for an event's end
    function eventEnd(event) {
        return event.end ? cloneDate(event.end) : defaultEventEnd(event);
    }
    
    
    
    /* Event Elements
    ------------------------------------------------------------------------------*/
    
    
    // report when view creates an element for an event
    function reportEventElement(event, element) {
        eventElements.push(element);
        if (eventElementsByID[event._id]) {
            eventElementsByID[event._id].push(element);
        }else{
            eventElementsByID[event._id] = [element];
        }
    }
    
    
    function reportEventClear() {
        eventElements = [];
        eventElementsByID = {};
    }
    
    
    // attaches eventClick, eventMouseover, eventMouseout
    function eventElementHandlers(event, eventElement) {
        eventElement
            .click(function(ev) {
                if (!eventElement.hasClass('ui-draggable-dragging') &&
                    !eventElement.hasClass('ui-resizable-resizing')) {
                        return trigger('eventClick', this, event, ev);
                    }
            })
            .hover(
                function(ev) {
                    trigger('eventMouseover', this, event, ev);
                },
                function(ev) {
                    trigger('eventMouseout', this, event, ev);
                }
            );
        // TODO: don't fire eventMouseover/eventMouseout *while* dragging is occuring (on subject element)
        // TODO: same for resizing
    }
    
    
    function showEvents(event, exceptElement) {
        eachEventElement(event, exceptElement, 'show');
    }
    
    
    function hideEvents(event, exceptElement) {
        eachEventElement(event, exceptElement, 'hide');
    }
    
    
    function eachEventElement(event, exceptElement, funcName) {
        var elements = eventElementsByID[event._id],
            i, len = elements.length;
        for (i=0; i<len; i++) {
            if (!exceptElement || elements[i][0] != exceptElement[0]) {
                elements[i][funcName]();
            }
        }
    }
    
    
    
    /* Event Modification Reporting
    ---------------------------------------------------------------------------------*/
    
    
    function eventDrop(e, event, dayDelta, minuteDelta, allDay, ev, ui) {
        var oldAllDay = event.allDay;
        var eventId = event._id;
        moveEvents(eventsByID[eventId], dayDelta, minuteDelta, allDay);
        trigger(
            'eventDrop',
            e,
            event,
            dayDelta,
            minuteDelta,
            allDay,
            function() {
                // TODO: investigate cases where this inverse technique might not work
                moveEvents(eventsByID[eventId], -dayDelta, -minuteDelta, oldAllDay);
                reportEventChange(eventId);
            },
            ev,
            ui
        );
        reportEventChange(eventId);
    }
    
    
    function eventResize(e, event, dayDelta, minuteDelta, ev, ui) {
        var eventId = event._id;
        elongateEvents(eventsByID[eventId], dayDelta, minuteDelta);
        trigger(
            'eventResize',
            e,
            event,
            dayDelta,
            minuteDelta,
            function() {
                // TODO: investigate cases where this inverse technique might not work
                elongateEvents(eventsByID[eventId], -dayDelta, -minuteDelta);
                reportEventChange(eventId);
            },
            ev,
            ui
        );
        reportEventChange(eventId);
    }
    
    
    
    /* Event Modification Math
    ---------------------------------------------------------------------------------*/
    
    
    function moveEvents(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, options);
        }
    }
    
    
    function elongateEvents(events, dayDelta, minuteDelta) {
        minuteDelta = minuteDelta || 0;
        for (var e, len=events.length, i=0; i<len; i++) {
            e = events[i];
            e.end = addMinutes(addDays(eventEnd(e), dayDelta, true), minuteDelta);
            normalizeEvent(e, options);
        }
    }
    

}

function DayEventRenderer() {
    var t = this;

    
    // exports
    t.renderDaySegs = renderDaySegs;
    t.resizableDayEvent = resizableDayEvent;
    
    
    // imports
    var opt = t.opt;
    var trigger = t.trigger;
    var isEventDraggable = t.isEventDraggable;
    var isEventResizable = t.isEventResizable;
    var eventEnd = t.eventEnd;
    var reportEventElement = t.reportEventElement;
    var showEvents = t.showEvents;
    var hideEvents = t.hideEvents;
    var eventResize = t.eventResize;
    var getRowCnt = t.getRowCnt;
    var getColCnt = t.getColCnt;
    var getColWidth = t.getColWidth;
    var allDayRow = t.allDayRow;
    var allDayBounds = t.allDayBounds;
    var colContentLeft = t.colContentLeft;
    var colContentRight = t.colContentRight;
    var dayOfWeekCol = t.dayOfWeekCol;
    var dateCell = t.dateCell;
    var compileDaySegs = t.compileDaySegs;
    var getDaySegmentContainer = t.getDaySegmentContainer;
    var bindDaySeg = t.bindDaySeg; //TODO: streamline this
    var formatDates = t.calendar.formatDates;
    var renderDayOverlay = t.renderDayOverlay;
    var clearOverlays = t.clearOverlays;
    var clearSelection = t.clearSelection;
    
    
    
    /* Rendering
    -----------------------------------------------------------------------------*/
    
    
    function renderDaySegs(segs, modifiedEventId) {
        var segmentContainer = getDaySegmentContainer();
        var rowDivs;
        var rowCnt = getRowCnt();
        var colCnt = getColCnt();
        var i = 0;
        var rowI;
        var levelI;
        var colHeights;
        var j;
        var segCnt = segs.length;
        var seg;
        var top;
        var k;
        segmentContainer[0].innerHTML = daySegHTML(segs); // faster than .html()
        daySegElementResolve(segs, segmentContainer.children());
        daySegElementReport(segs);
        daySegHandlers(segs, segmentContainer, modifiedEventId);
        daySegCalcHSides(segs);
        daySegSetWidths(segs);
        daySegCalcHeights(segs);
        rowDivs = getRowDivs();
        // set row heights, calculate event tops (in relation to row top)
        for (rowI=0; rowI<rowCnt; rowI++) {
            levelI = 0;
            colHeights = [];
            for (j=0; j<colCnt; j++) {
                colHeights[j] = 0;
            }
            while (i<segCnt && (seg = segs[i]).row == rowI) {
                // loop through segs in a row
                top = arrayMax(colHeights.slice(seg.startCol, seg.endCol));
                seg.top = top;
                top += seg.outerHeight;
                for (k=seg.startCol; k<seg.endCol; k++) {
                    colHeights[k] = top;
                }
                i++;
            }
            rowDivs[rowI].height(arrayMax(colHeights));
        }
        daySegSetTops(segs, getRowTops(rowDivs));
    }
    
    
    function renderTempDaySegs(segs, adjustRow, adjustTop) {
        var tempContainer = $("<div/>");
        var elements;
        var segmentContainer = getDaySegmentContainer();
        var i;
        var segCnt = segs.length;
        var element;
        tempContainer[0].innerHTML = daySegHTML(segs); // faster than .html()
        elements = tempContainer.children();
        segmentContainer.append(elements);
        daySegElementResolve(segs, elements);
        daySegCalcHSides(segs);
        daySegSetWidths(segs);
        daySegCalcHeights(segs);
        daySegSetTops(segs, getRowTops(getRowDivs()));
        elements = [];
        for (i=0; i<segCnt; i++) {
            element = segs[i].element;
            if (element) {
                if (segs[i].row === adjustRow) {
                    element.css('top', adjustTop);
                }
                elements.push(element[0]);
            }
        }
        return $(elements);
    }
    
    
    function daySegHTML(segs) { // also sets seg.left and seg.outerWidth
        var rtl = opt('isRTL');
        var i;
        var segCnt=segs.length;
        var seg;
        var event;
        var url;
        var classes;
        var bounds = allDayBounds();
        var minLeft = bounds.left;
        var maxLeft = bounds.right;
        var leftCol;
        var rightCol;
        var left;
        var right;
        var skinCss;
        var html = '';
        // calculate desired position/dimensions, create html
        for (i=0; i<segCnt; i++) {
            seg = segs[i];
            event = seg.event;
            classes = ['fc-event', 'fc-event-skin', 'fc-event-hori'];
            if (isEventDraggable(event)) {
                classes.push('fc-event-draggable');
            }
            if (rtl) {
                if (seg.isStart) {
                    classes.push('fc-corner-right');
                }
                if (seg.isEnd) {
                    classes.push('fc-corner-left');
                }
                leftCol = dayOfWeekCol(seg.end.getDay()-1);
                rightCol = dayOfWeekCol(seg.start.getDay());
                left = seg.isEnd ? colContentLeft(leftCol) : minLeft;
                right = seg.isStart ? colContentRight(rightCol) : maxLeft;
            }else{
                if (seg.isStart) {
                    classes.push('fc-corner-left');
                }
                if (seg.isEnd) {
                    classes.push('fc-corner-right');
                }
                leftCol = dayOfWeekCol(seg.start.getDay());
                rightCol = dayOfWeekCol(seg.end.getDay()-1);
                left = seg.isStart ? colContentLeft(leftCol) : minLeft;
                right = seg.isEnd ? colContentRight(rightCol) : maxLeft;
            }
            classes = classes.concat(event.className);
            if (event.source) {
                classes = classes.concat(event.source.className || []);
            }
            url = event.url;
            skinCss = getSkinCss(event, opt);
            if (url) {
                html += "<a href='" + htmlEscape(url) + "'";
            }else{
                html += "<div";
            }
            html +=
                " class='" + classes.join(' ') + "'" +
                " style='position:absolute;z-index:8;left:"+left+"px;" + skinCss + "'" +
                ">" +
                "<div" +
                " class='fc-event-inner fc-event-skin'" +
                (skinCss ? " style='" + skinCss + "'" : '') +
                ">";
            if (!event.allDay && seg.isStart) {
                html +=
                    "<span class='fc-event-time'>" +
                    htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) +
                    "</span>";
            }
            html +=
                "<span class='fc-event-title'>" + htmlEscape(event.title) + "</span>" +
                "</div>";
            if (seg.isEnd && isEventResizable(event)) {
                html +=
                    "<div class='ui-resizable-handle ui-resizable-" + (rtl ? 'w' : 'e') + "'>" +
                    "&nbsp;&nbsp;&nbsp;" + // makes hit area a lot better for IE6/7
                    "</div>";
            }
            html +=
                "</" + (url ? "a" : "div" ) + ">";
            seg.left = left;
            seg.outerWidth = right - left;
            seg.startCol = leftCol;
            seg.endCol = rightCol + 1; // needs to be exclusive
        }
        return html;
    }
    
    
    function daySegElementResolve(segs, elements) { // sets seg.element
        var i;
        var segCnt = segs.length;
        var seg;
        var event;
        var element;
        var triggerRes;
        for (i=0; i<segCnt; i++) {
            seg = segs[i];
            event = seg.event;
            element = $(elements[i]); // faster than .eq()
            triggerRes = trigger('eventRender', event, event, element);
            if (triggerRes === false) {
                element.remove();
            }else{
                if (triggerRes && triggerRes !== true) {
                    triggerRes = $(triggerRes)
                        .css({
                            position: 'absolute',
                            left: seg.left
                        });
                    element.replaceWith(triggerRes);
                    element = triggerRes;
                }
                seg.element = element;
            }
        }
    }
    
    
    function daySegElementReport(segs) {
        var i;
        var segCnt = segs.length;
        var seg;
        var element;
        for (i=0; i<segCnt; i++) {
            seg = segs[i];
            element = seg.element;
            if (element) {
                reportEventElement(seg.event, element);
            }
        }
    }
    
    
    function daySegHandlers(segs, segmentContainer, modifiedEventId) {
        var i;
        var segCnt = segs.length;
        var seg;
        var element;
        var event;
        // retrieve elements, run through eventRender callback, bind handlers
        for (i=0; i<segCnt; i++) {
            seg = segs[i];
            element = seg.element;
            if (element) {
                event = seg.event;
                if (event._id === modifiedEventId) {
                    bindDaySeg(event, element, seg);
                }else{
                    element[0]._fci = i; // for lazySegBind
                }
            }
        }
        lazySegBind(segmentContainer, segs, bindDaySeg);
    }
    
    
    function daySegCalcHSides(segs) { // also sets seg.key
        var i;
        var segCnt = segs.length;
        var seg;
        var element;
        var key, val;
        var hsideCache = {};
        // record event horizontal sides
        for (i=0; i<segCnt; i++) {
            seg = segs[i];
            element = seg.element;
            if (element) {
                key = seg.key = cssKey(element[0]);
                val = hsideCache[key];
                if (val === undefined) {
                    val = hsideCache[key] = hsides(element, true);
                }
                seg.hsides = val;
            }
        }
    }
    
    
    function daySegSetWidths(segs) {
        var i;
        var segCnt = segs.length;
        var seg;
        var element;
        for (i=0; i<segCnt; i++) {
            seg = segs[i];
            element = seg.element;
            if (element) {
                element[0].style.width = Math.max(0, seg.outerWidth - seg.hsides) + 'px';
            }
        }
    }
    
    
    function daySegCalcHeights(segs) {
        var i;
        var segCnt = segs.length;
        var seg;
        var element;
        var key, val;
        var vmarginCache = {};
        // record event heights
        for (i=0; i<segCnt; i++) {
            seg = segs[i];
            element = seg.element;
            if (element) {
                key = seg.key; // created in daySegCalcHSides
                val = vmarginCache[key];
                if (val === undefined) {
                    val = vmarginCache[key] = vmargins(element);
                }
                seg.outerHeight = element[0].offsetHeight + val;
            }
        }
    }
    
    
    function getRowDivs() {
        var i;
        var rowCnt = getRowCnt();
        var rowDivs = [];
        for (i=0; i<rowCnt; i++) {
            rowDivs[i] = allDayRow(i)
                .find('td:first div.fc-day-content > div'); // optimal selector?
        }
        return rowDivs;
    }
    
    
    function getRowTops(rowDivs) {
        var i;
        var rowCnt = rowDivs.length;
        var tops = [];
        for (i=0; i<rowCnt; i++) {
            tops[i] = rowDivs[i][0].offsetTop; // !!?? but this means the element needs position:relative if in a table cell!!!!
        }
        return tops;
    }
    
    
    function daySegSetTops(segs, rowTops) { // also triggers eventAfterRender
        var i;
        var segCnt = segs.length;
        var seg;
        var element;
        var event;
        for (i=0; i<segCnt; i++) {
            seg = segs[i];
            element = seg.element;
            if (element) {
                element[0].style.top = rowTops[seg.row] + (seg.top||0) + 'px';
                event = seg.event;
                trigger('eventAfterRender', event, event, element);
            }
        }
    }
    
    
    
    /* Resizing
    -----------------------------------------------------------------------------------*/
    
    
    function resizableDayEvent(event, element, seg) {
        var rtl = opt('isRTL');
        var direction = rtl ? 'w' : 'e';
        var handle = element.find('div.ui-resizable-' + direction);
        var isResizing = false;
        
        // TODO: look into using jquery-ui mouse widget for this stuff
        disableTextSelection(element); // prevent native <a> selection for IE
        element
            .mousedown(function(ev) { // prevent native <a> selection for others
                ev.preventDefault();
            })
            .click(function(ev) {
                if (isResizing) {
                    ev.preventDefault(); // prevent link from being visited (only method that worked in IE6)
                    ev.stopImmediatePropagation(); // prevent fullcalendar eventClick handler from being called
                                                   // (eventElementHandlers needs to be bound after resizableDayEvent)
                }
            });
        
        handle.mousedown(function(ev) {
            if (ev.which != 1) {
                return; // needs to be left mouse button
            }
            isResizing = true;
            var hoverListener = t.getHoverListener();
            var rowCnt = getRowCnt();
            var colCnt = getColCnt();
            var dis = rtl ? -1 : 1;
            var dit = rtl ? colCnt-1 : 0;
            var elementTop = element.css('top');
            var dayDelta;
            var helpers;
            var eventCopy = $.extend({}, event);
            var minCell = dateCell(event.start);
            clearSelection();
            $('body')
                .css('cursor', direction + '-resize')
                .one('mouseup', mouseup);
            trigger('eventResizeStart', this, event, ev);
            hoverListener.start(function(cell, origCell) {
                if (cell) {
                    var r = Math.max(minCell.row, cell.row);
                    var c = cell.col;
                    if (rowCnt == 1) {
                        r = 0; // hack for all-day area in agenda views
                    }
                    if (r == minCell.row) {
                        if (rtl) {
                            c = Math.min(minCell.col, c);
                        }else{
                            c = Math.max(minCell.col, c);
                        }
                    }
                    dayDelta = (r*7 + c*dis+dit) - (origCell.row*7 + origCell.col*dis+dit);
                    var newEnd = addDays(eventEnd(event), dayDelta, true);
                    if (dayDelta) {
                        eventCopy.end = newEnd;
                        var oldHelpers = helpers;
                        helpers = renderTempDaySegs(compileDaySegs([eventCopy]), seg.row, elementTop);
                        helpers.find('*').css('cursor', direction + '-resize');
                        if (oldHelpers) {
                            oldHelpers.remove();
                        }
                        hideEvents(event);
                    }else{
                        if (helpers) {
                            showEvents(event);
                            helpers.remove();
                            helpers = null;
                        }
                    }
                    clearOverlays();
                    renderDayOverlay(event.start, addDays(cloneDate(newEnd), 1)); // coordinate grid already rebuild at hoverListener.start
                }
            }, ev);
            
            function mouseup(ev) {
                trigger('eventResizeStop', this, event, ev);
                $('body').css('cursor', '');
                hoverListener.stop();
                clearOverlays();
                if (dayDelta) {
                    eventResize(this, event, dayDelta, 0, ev);
                    // event redraw will clear helpers
                }
                // otherwise, the drag handler already restored the old events
                
                setTimeout(function() { // make this happen after the element's click event
                    isResizing = false;
                },0);
            }
            
        });
    }
    

}

//BUG: unselect needs to be triggered when events are dragged+dropped

function SelectionManager() {
    var t = this;
    
    
    // exports
    t.select = select;
    t.unselect = unselect;
    t.reportSelection = reportSelection;
    t.daySelectionMousedown = daySelectionMousedown;
    
    
    // imports
    var opt = t.opt;
    var trigger = t.trigger;
    var defaultSelectionEnd = t.defaultSelectionEnd;
    var renderSelection = t.renderSelection;
    var clearSelection = t.clearSelection;
    
    
    // locals
    var selected = false;



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

    function select(startDate, endDate, allDay) {
        unselect();
        if (!endDate) {
            endDate = defaultSelectionEnd(startDate, allDay);
        }
        renderSelection(startDate, endDate, allDay);
        reportSelection(startDate, endDate, allDay);
    }
    
    
    function unselect(ev) {
        if (selected) {
            selected = false;
            clearSelection();
            trigger('unselect', null, ev);
        }
    }
    
    
    function reportSelection(startDate, endDate, allDay, ev) {
        selected = true;
        trigger('select', null, startDate, endDate, allDay, ev);
    }
    
    
    function daySelectionMousedown(ev) { // not really a generic manager method, oh well
        var cellDate = t.cellDate;
        var cellIsAllDay = t.cellIsAllDay;
        var hoverListener = t.getHoverListener();
        var reportDayClick = t.reportDayClick; // this is hacky and sort of weird
        if (ev.which == 1 && opt('selectable')) { // which==1 means left mouse button
            unselect(ev);
            var _mousedownElement = this;
            var dates;
            hoverListener.start(function(cell, origCell) { // TODO: maybe put cellDate/cellIsAllDay info in cell
                clearSelection();
                if (cell && cellIsAllDay(cell)) {
                    dates = [ cellDate(origCell), cellDate(cell) ].sort(cmp);
                    renderSelection(dates[0], dates[1], true);
                }else{
                    dates = null;
                }
            }, ev);
            $(document).one('mouseup', function(ev) {
                hoverListener.stop();
                if (dates) {
                    if (+dates[0] == +dates[1]) {
                        reportDayClick(dates[0], true, ev);
                    }
                    reportSelection(dates[0], dates[1], true, ev);
                }
            });
        }
    }


}
 
function OverlayManager() {
    var t = this;
    
    
    // exports
    t.renderOverlay = renderOverlay;
    t.clearOverlays = clearOverlays;
    
    
    // locals
    var usedOverlays = [];
    var unusedOverlays = [];
    
    
    function renderOverlay(rect, parent) {
        var e = 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);
        }
        usedOverlays.push(e.css(rect).show());
        return e;
    }
    

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


}

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

}

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

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 = {};
    };
    
}

})(jQuery);
