/**
 * kronolith.js - Base application logic.
 *
 * TODO: loadingImg()
 *
 * Copyright 2008-2017 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file COPYING for license information (GPL). If you
 * did not receive this file, see http://www.horde.org/licenses/gpl.
 *
 * @author Jan Schneider <jan@horde.org>
 */

/* Kronolith object. */
KronolithCore = {
    // Vars used and defaulting to null/false:
    //   weekSizes, daySizes,
    //   groupLoading, colorPicker, duration, timeMarker, monthDays,
    //   allDays, eventsWeek, initialized

    view: '',
    ecache: $H(),
    cacheStart: null,
    cacheEnd: null,
    holidays: [],
    tcache: $H(),
    eventsLoading: {},
    loading: 0,
    viewLoading: [],
    fbLoading: 0,
    redBoxLoading: false,
    date: Date.today(),
    tasktype: 'incomplete',
    knl: {},
    wrongFormat: $H(),
    mapMarker: null,
    map: null,
    mapInitialized: false,
    freeBusy: $H(),
    search: 'future',
    effectDur: 0.4,
    macos: navigator.appVersion.indexOf('Mac') != -1,
    orstart: null,
    orend: null,
    lastRecurType: 'None',
    uatts: null,
    ucb: null,
    resourceACCache: { choices: [], map: $H() },
    paramsCache: null,
    attendees: [],
    resources: [],

    /**
     * Flag that indicates if the event currently displayed in the event
     * properties window is a recurring event.
     *
     * @type boolean
     */
    recurs: false,

    /**
     * The location that was open before the current location.
     *
     * @var string
     */
    lastLocation: '',

    /**
     * The currently open location.
     *
     * @var string
     */
    openLocation: '',

    /**
     * The current (main) location.
     *
     * This is different from openLocation as it isn't updated for any
     * locations that are opened in a popup view, e.g. events.
     *
     * @var string
     */
    currentLocation: '',

    kronolithBody: $('kronolithBody'),

    onException: function(parentfunc, r, e)
    {
        /* Make sure loading images are closed. */
        this.loading--;
        if (!this.loading) {
            $('kronolithLoading').hide();
        }
        this.closeRedBox();
        HordeCore.notify(HordeCore.text.ajax_error, 'horde.error');
        parentfunc(r, e);
    },

    setTitle: function(title)
    {
        document.title = Kronolith.conf.name + ' :: ' + title;
        return title;
    },

    // url = (string) URL to redirect to
    // hash = (boolean) If true, url is treated as hash information to alter
    //        on the current page
    redirect: function(url, hash)
    {
        if (hash) {
            window.location.hash = escape(url);
            window.location.reload();
        } else {
            HordeCore.redirect(url);
        }
    },

    go: function(fullloc, data)
    {
        if (!this.initialized) {
            this.go.bind(this, fullloc, data).defer();
            return;
        }

        if (this.viewLoading.size()) {
            this.viewLoading.push([ fullloc, data ]);
            return;
        }

        var locParts = fullloc.split(':');
        var loc = locParts.shift();

        if (this.openLocation == fullloc) {
            return;
        }

        this.viewLoading.push([ fullloc, data ]);

        if (loc != 'search') {
            HordeTopbar.searchGhost.reset();
        }

        this.switchTaskView(false);

        switch (loc) {
        case 'day':
        case 'week':
        case 'workweek':
        case 'month':
        case 'year':
        case 'agenda':
        case 'tasks':
            this.closeView(loc);
            var locCap = loc.capitalize();
            $('kronolithNav' + locCap).up().addClassName('horde-active');

            switch (loc) {
            case 'day':
            case 'agenda':
            case 'week':
            case 'workweek':
            case 'month':
            case 'year':
                var date = locParts.shift();
                if (date) {
                    date = this.parseDate(date);
                } else {
                    date = this.date;
                }

                if (this.view != 'agenda' &&
                    this.view == loc &&
                    date.getYear() == this.date.getYear() &&
                    ((loc == 'year') ||
                     (loc == 'month' && date.getMonth() == this.date.getMonth()) ||
                     ((loc == 'week' || loc == 'workweek') && date.getRealWeek() == this.date.getRealWeek()) ||
                     ((loc == 'day'  || loc == 'agenda') && date.dateString() == this.date.dateString()))) {
                         this.setViewTitle(date, loc);
                         this.addHistory(fullloc);
                         this.loadNextView();
                         return;
                }

                this.addHistory(fullloc);
                this.view = loc;
                this.date = date;
                this.updateView(date, loc);
                var dates = this.viewDates(date, loc);
                this.loadEvents(dates[0], dates[1], loc);
                $('kronolithView' + locCap).appear({
                        duration: this.effectDur,
                        queue: 'end',
                        afterFinish: function() {
                            if (loc == 'week' || loc == 'workweek' || loc == 'day') {
                                this.calculateRowSizes(loc + 'Sizes', 'kronolithView' + locCap);
                                if ($('kronolithTimeMarker')) {
                                    this.positionTimeMarker();
                                }
                                if ($('kronolithTimeMarker')) {
                                    $('kronolithTimeMarker').show();
                                }
                                // Scroll to the work day start time.
                                $('kronolithView' + locCap).down('.kronolithViewBody').scrollTop = 9 * this[loc + 'Sizes'].height;
                            }
                            this.loadNextView();
                        }.bind(this)
                });
                $('kronolithLoading' + loc).insert($('kronolithLoading').remove());
                this.updateMinical(date, loc);

                break;

            case 'tasks':
                var tasktype = locParts.shift() || this.tasktype;


                this.switchTaskView(true);
                $('kronolithCurrent')
                    .update(this.setTitle(Kronolith.text.tasks));
                if (this.view == loc && this.tasktype == tasktype) {
                    this.addHistory(fullloc);
                    this.loadNextView();
                    return;
                }
                if (!$w('all complete incomplete future').include(tasktype)) {
                    this.loadNextView();
                    return;
                }

                this.addHistory(fullloc);
                this.view = loc;
                this.tasktype = tasktype;
                $w('All Complete Incomplete Future').each(function(tasktype) {
                    $('kronolithTasks' + tasktype).up().removeClassName('horde-active');
                });
                $('kronolithTasks' + this.tasktype.capitalize()).up().addClassName('horde-active');
                this.loadTasks(this.tasktype);
                $('kronolithView' + locCap).appear({
                    duration: this.effectDur,
                    queue: 'end',
                    afterFinish: function() {
                        this.loadNextView();
                    }.bind(this) });
                $('kronolithLoading' + loc).insert($('kronolithLoading').remove());
                this.updateMinical(this.date);

                break;

            default:
                if (!$('kronolithView' + locCap)) {
                    break;
                }
                this.addHistory(fullloc);
                this.view = loc;
                $('kronolithView' + locCap).appear({
                    duration: this.effectDur,
                    queue: 'end',
                    afterFinish: function() {
                        this.loadNextView();
                    }.bind(this) });
                break;
            }

            break;

        case 'search':
            var cals = [], time = locParts[0], term = locParts[1],
                query = Object.toJSON({ title: term });

            if (!($w('all past future').include(time))) {
                this.loadNextView();
                return;
            }

            this.addHistory(fullloc);
            this.search = time;
            $w('All Past Future').each(function(time) {
                $('kronolithSearch' + time).up().removeClassName('horde-active');
            });
            $('kronolithSearch' + this.search.capitalize()).up().addClassName('horde-active');
            this.closeView('agenda');
            this.view = 'agenda';
            this.updateView(null, 'search', term);
            $H(Kronolith.conf.calendars).each(function(type) {
                $H(type.value).each(function(calendar) {
                    if (calendar.value.show) {
                        cals.push(type.key + '|' + calendar.key);
                    }
                });
            });
            $('kronolithAgendaNoItems').hide();
            this.startLoading('search', query);

            HordeCore.doAction('searchEvents', {
                cals: Object.toJSON(cals),
                query: query,
                time: this.search
            }, {
                callback: function(r) {
                    // Hide spinner.
                    this.loading--;
                    if (!this.loading) {
                        $('kronolithLoading').hide();
                    }
                    if (r.view != 'search' ||
                        r.query != this.eventsLoading.search) {
                        return;
                    }
                    if (Object.isUndefined(r.events)) {
                        $('kronolithAgendaNoItems').show();
                        return;
                    }
                    delete this.eventsLoading.search;
                    $H(r.events).each(function(calendars) {
                        $H(calendars.value).each(function(events) {
                            this.createAgendaDay(events.key);
                            $H(events.value).each(function(event) {
                                event.value.calendar = calendars.key;
                                event.value.start = Date.parse(event.value.s);
                                event.value.end = Date.parse(event.value.e);
                                this.insertEvent(event, events.key, 'agenda');
                            }, this);
                        }, this);
                    }, this);
                }.bind(this)
            });

            $('kronolithViewAgenda').appear({
                duration: this.effectDur,
                queue: 'end',
                afterFinish: function() {
                    this.loadNextView();
                }.bind(this) });
            $('kronolithLoadingagenda').insert($('kronolithLoading').remove());
            this.updateMinical(this.date);
            break;

        case 'event':
            // Load view first if necessary.
            if (!this.view ) {
                this.viewLoading.pop();
                this.go(Kronolith.conf.login_view);
                this.go.bind(this, fullloc, data).defer();
                return;
            }

            if (this.currentLocation == fullloc) {
                this.loadNextView();
                return;
            }

            this.addHistory(fullloc, false);
            switch (locParts.length) {
            case 0:
                // New event.
                this.editEvent();
                break;
            case 1:
                // New event on a certain date.
                this.editEvent(null, null, locParts[0]);
                break;
            default:
                // Editing event.
                var date = locParts.pop(),
                    event = locParts.pop(),
                    calendar = locParts.join(':');
                this.editEvent(calendar, event, date);
                break;
            }
            this.loadNextView();
            break;

        case 'task':
            // Load view first if necessary.
            if (!this.view ) {
                this.viewLoading.pop();
                this.go('tasks');
                this.go.bind(this, fullloc, data).defer();
                return;
            }

            this.switchTaskView(true);
            switch (locParts.length) {
            case 0:
                this.addHistory(fullloc, false);
                this.editTask();
                break;
            case 2:
                this.addHistory(fullloc, false);
                this.editTask(locParts[0], locParts[1]);
                break;
            }
            this.loadNextView();
            break;

        case 'calendar':
            if (!this.view) {
                this.viewLoading.pop();
                this.go(Kronolith.conf.login_view);
                this.go.bind(this, fullloc, data).defer();
                return;
            }
            this.addHistory(fullloc, false);
            this.editCalendar(locParts.join(':'));
            this.loadNextView();
            break;

        default:
            this.loadNextView();
            break;
        }
    },

    /**
     * Removes the last loaded view from the stack and loads the last added
     * view, if the stack is still not empty.
     *
     * We want to load views from a LIFO queue, because the queue is only
     * building up if the user switches to another view while the current view
     * still loads. In that case we can go directly to the most recently
     * clicked view and drop the remaining queue.
     */
    loadNextView: function()
    {
        var current = this.viewLoading.shift();
        if (this.viewLoading.size()) {
            var next = this.viewLoading.pop();
            this.viewLoading = [];
            if (current[0] != next[0] || current[1] || next[1]) {
                this.go(next[0], next[1]);
            }
        }
    },

    /**
     * Rebuilds one of the calendar views for a new date.
     *
     * @param Date date    The date to show in the calendar.
     * @param string view  The view that's rebuilt.
     * @param mixed data   Any additional data that might be required.
     */
    updateView: function(date, view, data)
    {
        this.holidays = [];

        switch (view) {
        case 'day':
            var today = Date.today();
            this.dayEvents = [];
            this.dayGroups = [];
            this.allDayEvents = [];
            $('kronolithCurrent')
                .update(this.setViewTitle(date, view, data));
            $('kronolithViewDay')
                .down('.kronolithAllDayContainer')
                .store('date', date.dateString());
            $('kronolithEventsDay').store('date', date.dateString());
            if (date.equals(today)) {
                this.addTimeMarker('kronolithEventsDay');
            }
            break;

        case 'week':
        case 'workweek':
            this.dayEvents = [];
            this.dayGroups = [];
            this.allDayEvents = [];
            this.allDays = {};
            this.eventsWeek = {};
            var what = view == 'week' ? 'Week' : 'Workweek',
                div = $('kronolithEvents' + what).down('div'),
                th = $('kronolithView' + what + 'Head').down('.kronolithWeekDay'),
                td = $('kronolithView' + what + 'Head').down('tbody td').next('td'),
                hourRow = $('kronolithView' + what + 'Body').down('tr'),
                dates = this.viewDates(date, view),
                today = Date.today(),
                day, dateString, i, hourCol;

            $('kronolithCurrent')
                .update(this.setViewTitle(date, view, data));

            for (i = 0; i < 24; i++) {
                day = dates[0].clone();
                hourCol = hourRow.down('td').next('td');
                while (hourCol) {
                    hourCol.removeClassName('kronolith-today');
                    if (day.equals(today)) {
                        hourCol.addClassName('kronolith-today');
                    }
                    hourCol = hourCol.next('td');
                    day.next().day();
                }
                hourRow = hourRow.next('tr');
            }
            day = dates[0].clone();

            for (i = 0; i < (view == 'week' ? 7 : 5); i++) {
                dateString = day.dateString();
                this.allDays['kronolithAllDay' + dateString] = td.down('div');
                this.eventsWeek['kronolithEvents' + what + dateString] = div;
                div.store('date', dateString)
                    .writeAttribute('id', 'kronolithEvents' + what + dateString);
                th.store('date', dateString)
                    .down('span').update(day.toString('dddd, d'));
                td.removeClassName('kronolith-today');
                this.allDays['kronolithAllDay' + dateString]
                    .writeAttribute('id', 'kronolithAllDay' + dateString)
                    .store('date', dateString);
                if (day.equals(today)) {
                    td.addClassName('kronolith-today');
                    this.addTimeMarker('kronolithEvents' + what + dateString);
                }
                new Drop(td.down('div'));
                div = div.next('div');
                th = th.next('td');
                td = td.next('td');
                day.next().day();
            }
            break;

        case 'month':
            var tbody = $('kronolith-month-body'),
                dates = this.viewDates(date, view),
                day = dates[0].clone();

            $('kronolithCurrent')
                .update(this.setViewTitle(date, view, data));

            // Remove old rows. Maybe we should only rebuild the calendars if
            // necessary.
            tbody.childElements().each(function(row) {
                if (row.identify() != 'kronolithRowTemplate') {
                    row.purge();
                    row.remove();
                }
            });

            // Build new calendar view.
            this.monthDays = {};
            while (!day.isAfter(dates[1])) {
                tbody.insert(this.createWeekRow(day, date.getMonth(), dates).show());
                day.next().week();
            }
            this.equalRowHeights(tbody);

            break;

        case 'year':
            var month;

            $('kronolithCurrent').update(this.setViewTitle(date, view, data));

            // Build new calendar view.
            for (month = 0; month < 12; month++) {
                $('kronolithYear' + month).update(this.createYearMonth(date.getFullYear(), month, 'kronolithYear').show());
            }

            break;

        case 'agenda':
        case 'search':
            // Agenda days are only created on demand, if there are any events
            // to add.
            if (view == 'agenda') {
                var dates = this.viewDates(date, view);
                $('kronolithCurrent')
                    .update(this.setViewTitle(date, view, data));
                $('kronolithSearchNavigation').up().up().hide();
            } else {
                $('kronolithCurrent')
                    .update(this.setViewTitle(date, view, data));
                $('kronolithSearchNavigation').up().up().show();
            }

            // Remove old rows. Maybe we should only rebuild the calendars if
            // necessary.
            tbody = $('kronolithViewAgendaBody').childElements().each(function(row) {
                if (row.identify() != 'kronolithAgendaTemplate' &&
                    row.identify() != 'kronolithAgendaNoItems') {
                    row.purge();
                    row.remove();
                }
            });

            break;
        }
    },

    /**
     * Sets the browser title of the calendar views.
     *
     * @param Date date    The date to show in the calendar.
     * @param string view  The view that's displayed.
     * @param mixed data   Any additional data that might be required.
     */
    setViewTitle: function(date, view, data)
    {
        switch (view) {
        case 'day':
            return this.setTitle(date.toString('D'));

        case 'week':
        case 'workweek':
            var dates = this.viewDates(date, view);
            return this.setTitle(dates[0].toString(Kronolith.conf.date_format) + ' - ' + dates[1].toString(Kronolith.conf.date_format));

        case 'month':
            return this.setTitle(date.toString('MMMM yyyy'));

        case 'year':
            return this.setTitle(date.toString('yyyy'));

        case 'agenda':
            var dates = this.viewDates(date, view);
            return this.setTitle(dates[0].toString(Kronolith.conf.date_format) + ' - ' + dates[1].toString(Kronolith.conf.date_format));

        case 'search':
            return this.setTitle(Kronolith.text.searching.interpolate({ term: data })).escapeHTML();
        }
    },

    /**
     * Closes the currently active view.
     */
    closeView: function(loc)
    {
        $w('Day Workweek Week Month Year Tasks Agenda').each(function(a) {
            a = $('kronolithNav' + a);
            if (a) {
                a.up().removeClassName('horde-active');
            }
        });
        if (this.view && this.view != loc) {
            $('kronolithView' + this.view.capitalize()).fade({
                duration: this.effectDur,
                queue: 'end'
            });
            this.view = null;
        }
    },

    /**
     * Creates a single row of day cells for usage in the month and multi-week
     * views.
     *
     * @param Date date        The first day to show in the row.
     * @param integer month    The current month. Days not from the current
     *                         month get the kronolith-other-month CSS class
     *                         assigned.
     * @param array viewDates  Array of Date objects with the start and end
     *                         dates of the view.
     *
     * @return Element  The element rendering a week row.
     */
    createWeekRow: function(date, month, viewDates)
    {
        var day = date.clone(), today = new Date().dateString(),
            row, cell, dateString;

        // Create a copy of the row template.
        row = $('kronolithRowTemplate').clone(true);
        row.removeAttribute('id');

        // Fill week number and day cells.
        cell = row.down()
            .setText(date.getRealWeek())
            .store('date', date.dateString())
            .next();
        while (cell) {
            dateString = day.dateString();
            this.monthDays['kronolithMonthDay' + dateString] = cell;
            cell.id = 'kronolithMonthDay' + dateString;
            cell.store('date', dateString);
            cell.removeClassName('kronolith-other-month').removeClassName('kronolith-today');
            if (day.getMonth() != month) {
                cell.addClassName('kronolith-other-month');
            }
            if (dateString == today) {
                cell.addClassName('kronolith-today');
            }
            new Drop(cell);
            cell.store('date', dateString)
                .down('.kronolith-day')
                .store('date', dateString)
                .update(day.getDate());

            cell = cell.next();
            day.add(1).day();
        }

        return row;
    },

    /**
     * Creates a table row for a single day in the agenda view, if it doesn't
     * exist yet.
     *
     * @param string date    The day to show in the row.
     *
     * @return Element  The element rendering a week row.
     */
    createAgendaDay: function(date)
    {
        // Exit if row exists already.
        if ($('kronolithAgendaDay' + date)) {
            return;
        }

        // Create a copy of the row template.
        var body = $('kronolithViewAgendaBody'),
            row = $('kronolithAgendaTemplate').clone(true);

        // Fill week number and day cells.
        row.store('date', date)
            .down()
            .setText(this.parseDate(date).toString('D'))
            .next()
            .writeAttribute('id', 'kronolithAgendaDay' + date);
        row.removeAttribute('id');

        // Insert row.
        var nextRow;
        body.childElements().each(function(elm) {
            if (elm.retrieve('date') > date) {
                nextRow = elm;
                throw $break;
            }
        });
        if (nextRow) {
            nextRow.insert({ before: row.show() });
        } else {
            body.insert(row.show());
        }

        return row;
    },

    /**
     * Creates a table for a single month in the year view.
     *
     * @param integer year     The year.
     * @param integer month    The month.
     * @param string idPrefix  If present, each day will get a DOM ID with this
     *                         prefix
     *
     * @return Element  The element rendering a month table.
     */
    createYearMonth: function(year, month, idPrefix)
    {
        // Create a copy of the month template.
        var table = $('kronolithYearTemplate').clone(true),
            tbody = table.down('tbody');
        table.removeAttribute('id');
        tbody.writeAttribute('id', 'kronolithYearTable' + month);

        // Set month name.
        table.down('tr.kronolith-minical-nav th')
            .store('date', year.toPaddedString(4) + (month + 1).toPaddedString(2) + '01')
            .update(Date.CultureInfo.monthNames[month]);

        // Build month table.
        this.buildMinical(tbody, new Date(year, month, 1), null, idPrefix, year);

        return table;
    },

    equalRowHeights: function(tbody)
    {
        var children = tbody.childElements();
        children.invoke('setStyle', { height: (100 / (children.size() - 1)) + '%' });
    },

    /**
     * Calculates some dimensions for the day and week view.
     *
     * @param string storage  Property name where the dimensions are stored.
     * @param string view     DOM node ID of the view.
     */
    calculateRowSizes: function(storage, view)
    {
        if (!Object.isUndefined(this[storage])) {
            return;
        }

        var td = $(view).down('.kronolithViewBody tr td').next('td'),
            layout = td.getLayout(),
            spacing = td.up('table').getStyle('borderSpacing');

        // FIXME: spacing is hardcoded for IE 7 because it doesn't know about
        // border-spacing, but still uses it. WTF?
        spacing = spacing ? parseInt($w(spacing)[1], 10) : 2;
        this[storage] = {};
        this[storage].height = layout.get('margin-box-height') + spacing;
        this[storage].spacing = this[storage].height - layout.get('padding-box-height') - layout.get('border-bottom');
    },

    /**
     * Adds a horizontal ruler representing the current time to the specified
     * container.
     *
     * @param string|Element  The container of the current day.
     */
    addTimeMarker: function(container)
    {
        if ($('kronolithTimeMarker')) {
            $('kronolithTimeMarker').remove();
            this.timeMarker.stop();
        }
        $(container).insert(new Element('div', { id: 'kronolithTimeMarker' }).setStyle({ position: 'absolute' }).hide());
        this.timeMarker = new PeriodicalExecuter(this.positionTimeMarker.bind(this), 60);
    },

    /**
     * Updates the horizontal ruler representing the current time.
     */
    positionTimeMarker: function()
    {
        var today = Date.today(), now;

        switch (this.view) {
        case 'day':
            if (!this.date.equals(today)) {
                $('kronolithTimeMarker').remove();
                this.timeMarker.stop();
                return;
            }
            break;
        case 'week':
        case 'workweek':
            if ($('kronolithTimeMarker').up().retrieve('date') != today.dateString()) {
                var newContainer = this.eventsWeek['kronolithEvents' + (this.view == 'week' ? 'Week' : 'Workweek') + today.dateString()];
                $('kronolithTimeMarker').remove();
                if (newContainer) {
                    this.addTimeMarker(newContainer);
                } else {
                    this.timeMarker.stop();
                }
                return;
            }
            break;
        default:
            $('kronolithTimeMarker').remove();
            this.timeMarker.stop();
            return;
        }

        now = new Date();
        $('kronolithTimeMarker').setStyle({ top: ((now.getHours() * 60 + now.getMinutes()) * this[this.view + 'Sizes'].height / 60 | 0) + 'px' });
    },

    /**
     * Rebuilds the mini calendar.
     *
     * @param Date date    The date to show in the calendar.
     * @param string view  The view that's displayed, determines which days in
     *                     the mini calendar are highlighted.
     */
    updateMinical: function(date, view)
    {
        // Update header.
        $('kronolithMinicalDate')
            .store('date', date.dateString())
            .update(date.toString('MMMM yyyy'));

        this.buildMinical($('kronolith-minical').down('tbody'), date, view);
    },

    /**
     * Creates a mini calendar suitable for the navigation calendar and the
     * year view.
     *
     * @param Element tbody    The table body to add the days to.
     * @param Date date        The date to show in the calendar.
     * @param string view      The view that's displayed, determines which days
     *                         in the mini calendar are highlighted.
     * @param string idPrefix  If present, each day will get a DOM ID with this
     *                         prefix
     * @param integer year     If present, generating mini calendars for the
     *                         year view of this year.
     */
    buildMinical: function(tbody, date, view, idPrefix, year)
    {
        var dates = this.viewDates(date, 'month'),
            day = dates[0].clone(),
            date7 = date.clone().add(1).week(),
            today = Date.today(),
            week = this.viewDates(this.date, 'week'),
            workweek = this.viewDates(this.date, 'workweek'),
            dateString, td, tr, i;

        // Remove old calendar rows. Maybe we should only rebuild the minical
        // if necessary.
        tbody.childElements().invoke('remove');

        for (i = 0; i < 42; i++) {
            dateString = day.dateString();
            // Create calendar row and insert week number.
            if (day.getDay() == Kronolith.conf.week_start) {
                tr = new Element('tr');
                tbody.insert(tr);
                td = new Element('td', { className: 'kronolith-minical-week' })
                    .store('weekdate', dateString);
                td.update(day.getRealWeek());
                tr.insert(td);
                weekStart = day.clone();
                weekEnd = day.clone();
                weekEnd.add(6).days();
            }

            // Insert day cell.
            td = new Element('td').store('date', dateString);
            if (day.getMonth() != date.getMonth()) {
                td.addClassName('kronolith-other-month');
            } else if (!Object.isUndefined(idPrefix)) {
                td.id = idPrefix + dateString;
            }

            // Highlight days currently being displayed.
            if (view &&
                ((view == 'month' && this.date.between(dates[0], dates[1])) ||
                 (view == 'week' && day.between(week[0], week[1])) ||
                 (view == 'workweek' && day.between(workweek[0], workweek[1])) ||
                 (view == 'day' && day.equals(this.date)) ||
                 (view == 'agenda' && !day.isBefore(date) && day.isBefore(date7)))) {
                td.addClassName('kronolith-selected');
            }

            // Highlight today.
            if (day.equals(today) &&
                (Object.isUndefined(year) ||
                 (day.getYear() + 1900 == year &&
                  date.getMonth() == day.getMonth()))) {
                td.addClassName('kronolith-today');
            }
            td.insert(new Element('a').update(day.getDate()));
            tr.insert(td);
            day.next().day();
        }
    },

    /**
     * Inserts a calendar entry in the sidebar menu.
     *
     * @param string type  The calendar type.
     * @param string id    The calendar id.
     * @param object cal   The calendar object.
     * @param Element div  Container DIV where to add the entry (optional).
     */
    insertCalendarInList: function(type, id, cal, div)
    {
        var noItems, calendar, link;
        if (!div) {
            div = this.getCalendarList(type, cal.owner);
        }
        noItems = div.previous();
        if (noItems &&
            noItems.tagName == 'DIV' &&
            noItems.className == 'horde-info') {
            noItems.hide();
        }
        link = new Element('span', { className: type != 'resourcegroup' ? (cal.show ? 'horde-resource-on' : 'horde-resource-off') : 'horde-resource-none' })
            .insert(cal.name.escapeHTML());
        calendar = new Element('div')
            .store('calendar', id)
            .store('calendarclass', type)
            .setStyle({ backgroundColor: cal.bg, color: cal.fg });
        if (type != 'holiday' && type != 'external') {
            calendar.insert(
                new Element('span', { className: 'horde-resource-edit-' + cal.fg.substring(1) })
                    .setStyle({ backgroundColor: cal.bg, color: cal.fg })
                    .insert('&#9658;'));
        }
        calendar.insert(
            new Element('div', { className: 'horde-resource-link' })
                .insert(link));
        this.addShareIcon(cal, link);
        div.insert(calendar);
        if (cal.show) {
            this.addCalendarLegend(type, id, cal);
        }
    },

    /**
     * Add the share icon after the calendar name in the calendar list.
     *
     * @param object cal       A calendar object from Kronolith.conf.calendars.
     * @param Element element  The calendar element in the list.
     */
    addShareIcon: function(cal, element)
    {
        if (cal.owner && cal.perms) {
            $H(cal.perms).each(function(perm) {
                if (perm.key != 'type' &&
                    ((Object.isArray(perm.value) && perm.value.size()) ||
                     (!Object.isArray(perm.value) && perm.value))) {
                    element.insert(' ').insert(new Element('img', { src: Kronolith.conf.images.attendees.replace(/fff/, cal.fg.substring(1)), title: Kronolith.text.shared }));
                    throw $break;
                }
            });
        }
    },

    /**
     * Rebuilds the list of calendars.
     */
    updateCalendarList: function()
    {
        var ext = $H(), extNames = $H(),
            extContainer = $('kronolithExternalCalendars');

        $H(Kronolith.conf.calendars.internal).each(function(cal) {
            this.insertCalendarInList('internal', cal.key, cal.value);
        }, this);

        if (Kronolith.conf.tasks) {
            $H(Kronolith.conf.calendars.tasklists).each(function(cal) {
                this.insertCalendarInList('tasklists', cal.key, cal.value);
            }, this);
        }

        if (Kronolith.conf.calendars.resource) {
            $H(Kronolith.conf.calendars.resource).each(function(cal) {
               this.insertCalendarInList('resource', cal.key, cal.value);
            }, this);
        }

        if (Kronolith.conf.calendars.resourcegroup) {
            $H(Kronolith.conf.calendars.resourcegroup).each(function(cal) {
                this.insertCalendarInList('resourcegroup', cal.key, cal.value);
            }, this);
        }

        $H(Kronolith.conf.calendars.external).each(function(cal) {
            var parts = cal.key.split('/'), api = parts.shift();
            if (!ext.get(api)) {
                ext.set(api, $H());
            }
            ext.get(api).set(parts.join('/'), cal.value);
            extNames.set(api, cal.value.api ? cal.value.api : Kronolith.text.external_category);
        });
        ext.each(function(api) {
            extContainer
                .insert(new Element('div', { className: 'horde-sidebar-split' }))
                .insert(new Element('div')
                        .insert(new Element('h3')
                                .insert(new Element('span', { className: 'horde-expand', title: HordeSidebar.text.expand })
                                        .insert({ bottom: extNames.get(api.key).escapeHTML() })))
                        .insert(new Element('div', { id: 'kronolithExternalCalendar' + api.key, className: 'horde-resources', style: 'display:none' })));
            api.value.each(function(cal) {
                this.insertCalendarInList('external', api.key + '/' + cal.key, cal.value, $('kronolithExternalCalendar' + api.key));
            }, this);
        }, this);

        $H(Kronolith.conf.calendars.remote).each(function(cal) {
            this.insertCalendarInList('remote', cal.key, cal.value);
        }, this);

        if (Kronolith.conf.calendars.holiday) {
            $H(Kronolith.conf.calendars.holiday).each(function(cal) {
                if (cal.value.show) {
                   this.insertCalendarInList('holiday', cal.key, cal.value);
                }
            }, this);
        } else {
            $('kronolithAddholiday').up().hide();
            $('kronolithHolidayCalendars').hide();
        }
    },

    /**
     * Returns the DIV container that holds all calendars of a certain type.
     *
     * @param string type  A calendar type
     *
     * @return Element  The container of the calendar type.
     */
    getCalendarList: function(type, personal)
    {
        switch (type) {
        case 'internal':
            return personal
                ? $('kronolithMyCalendars')
                : $('kronolithSharedCalendars');
        case 'resource':
            return $('kronolithResourceCalendars');
        case 'resourcegroup':
            return $('kronolithResourceGroups');
        case 'tasklists':
            return personal
                ? $('kronolithMyTasklists')
                : $('kronolithSharedTasklists');
        case 'external':
            return $('kronolithExternalCalendars');
        case 'remote':
            return $('kronolithRemoteCalendars');
        case 'holiday':
            return $('kronolithHolidayCalendars');
        }
    },

    /**
     * Loads a certain calendar, if the current view is still a calendar view.
     *
     * @param string type      The calendar type.
     * @param string calendar  The calendar id.
     */
    loadCalendar: function(type, calendar)
    {
        if (Kronolith.conf.calendars[type][calendar].show &&
            $w('day workweek week month year agenda').include(this.view)) {
            var dates = this.viewDates(this.date, this.view);
            this.deleteCache([type, calendar]);
            this.loadEvents(dates[0], dates[1], this.view, [[type, calendar]]);
        }
    },

    /**
     * Toggles a calendars visibility.
     *
     * @param string type      The calendar type.
     * @param string calendar  The calendar id.
     */
    toggleCalendar: function(type, calendar)
    {
        var elt = $('kronolithMenuCalendars').select('div').find(function(div) {
            return div.retrieve('calendarclass') == type &&
            div.retrieve('calendar') == calendar;
        }).down('.horde-resource-link').down('span');

        Kronolith.conf.calendars[type][calendar].show = !Kronolith.conf.calendars[type][calendar].show;
        elt.toggleClassName('horde-resource-on');
        elt.toggleClassName('horde-resource-off');

        if (Kronolith.conf.calendars[type][calendar].show) {
            this.addCalendarLegend(type, calendar, Kronolith.conf.calendars[type][calendar]);
        } else {
            this.deleteCalendarLegend(type, calendar);
        }

        switch (this.view) {
        case 'month':
        case 'agenda':
            if (Object.isUndefined(this.ecache.get(type)) ||
                Object.isUndefined(this.ecache.get(type).get(calendar))) {
                this.loadCalendar(type, calendar);
            } else {
                var allEvents = this.kronolithBody.select('div').findAll(function(el) {
                    return el.retrieve('calendar') == type + '|' + calendar;
                });
                if (this.view == 'month' && Kronolith.conf.max_events) {
                    var dates = this.viewDates(this.date, this.view);
                    if (elt.hasClassName('horde-resource-off')) {
                        var day, more, events, calendars = [];
                        $H(Kronolith.conf.calendars).each(function(type) {
                            $H(type.value).each(function(cal) {
                                if (cal.value.show) {
                                    calendars.push(type.key + '|' + cal.key);
                                }
                            });
                        });
                        allEvents.each(function(el) {
                            if (el.retrieve('calendar').startsWith('holiday|')) {
                                this.holidays = this.holidays.without(el.retrieve('eventid'));
                            }
                            el.remove();
                        }, this);
                        for (var date = dates[0]; !date.isAfter(dates[1]); date.add(1).days()) {
                            day = this.monthDays['kronolithMonthDay' + date.dateString()];
                            more = day.select('.kronolithMore');
                            events = day.select('.kronolith-event');
                            if (more.size() &&
                                events.size() < Kronolith.conf.max_events) {
                                more[0].purge();
                                more[0].remove();
                                events.invoke('remove');
                                calendars.each(function(calendar) {
                                    this.insertEvents([date, date], 'month', calendar);
                                }, this);
                            }
                        }
                    } else {
                        this.insertEvents(dates, 'month', type + '|' + calendar);
                    }
                } else {
                    allEvents.invoke('toggle');
                }
            }
            break;

        case 'year':
        case 'week':
        case 'workweek':
        case 'day':
            if (Object.isUndefined(this.ecache.get(type)) ||
                Object.isUndefined(this.ecache.get(type).get(calendar))) {
                this.loadCalendar(type, calendar);
            } else {
                this.insertEvents(this.viewDates(this.date, this.view), this.view);
            }
            break;

        case 'tasks':
            if (type != 'tasklists') {
                break;
            }
            var tasklist = calendar.substr(6);
            if (elt.hasClassName('horde-resource-off')) {
                $('kronolithViewTasksBody').select('tr').findAll(function(el) {
                    return el.retrieve('tasklist') == tasklist;
                }).invoke('remove');
            } else {
                this.loadTasks(this.tasktype, [ tasklist ]);
            }
            break;
        }

        if ($w('tasklists remote external holiday resource').include(type)) {
            calendar = type + '_' + calendar;
        }
        HordeCore.doAction('saveCalPref', { toggle_calendar: calendar });
    },

    /**
     * Propagates a SELECT drop down list with the editable calendars.
     *
     * @param string id  The id of the SELECT element.
     */
    updateCalendarDropDown: function(id)
    {
        $(id).update();
        ['internal', 'remote'].each(function(type) {
            $H(Kronolith.conf.calendars[type]).each(function(cal) {
                if (cal.value.edit) {
                    $(id).insert(new Element('option', { value: type + '|' + cal.key })
                                 .setStyle({ backgroundColor: cal.value.bg, color: cal.value.fg })
                                 .update(cal.value.name.escapeHTML()));
                }
            });
        });
    },

    /**
     * Adds a calendar entry to the print legend.
     *
     * @param string type  The calendar type.
     * @param string id    The calendar id.
     * @param object cal   The calendar object.
     */
    addCalendarLegend: function(type, id, cal)
    {
        $('kronolith-legend').insert(
            new Element('span')
                .insert(cal.name.escapeHTML())
                .store('calendar', id)
                .store('calendarclass', type)
                .setStyle({ backgroundColor: cal.bg, color: cal.fg })
        );
    },

    /**
     * Deletes a calendar entry from the print legend.
     *
     * @param string type  The calendar type.
     * @param string id    The calendar id.
     */
    deleteCalendarLegend: function(type, id)
    {
        var legend = $('kronolith-legend').select('span').find(function(span) {
            return span.retrieve('calendarclass') == type &&
                span.retrieve('calendar') == id;
        });
        if (legend) {
            legend.remove();
        }
    },

    /**
     * Opens a tab in a form.
     *
     * @param Element  The A element of a tab.
     */
    openTab: function(elt)
    {
        var dialog = elt.up('form'), tab = $(elt.id.replace(/Link/, 'Tab')),
            field;
        dialog.select('.kronolithTabsOption').invoke('hide');
        dialog.select('.tabset li').invoke('removeClassName', 'horde-active');
        tab.show();
        elt.up().addClassName('horde-active');
        if (elt.id == 'kronolithEventLinkMap') {
            if (!this.mapInitialized) {
                this.initializeMap();
            }
        }
        field = tab.down('textarea');
        if (!field) {
            field = tab.down('input');
        }
        if (field) {
            try {
                field.focus();
            } catch (e) {}
        }
        switch (tab.identify()) {
        case 'kronolithEventTabAttendees':
            this.attendeeStartDateHandler(this.getFBDate());
            break;
        case 'kronolithEventTabResources':
            this.resourceStartDateHandler(this.getFBDate());
            break;
        }
    },

    /**
     * Sets the load signature and show the loading spinner.
     *
     * @param string resource   The loading resource.
     * @param string signatrue  The signature for this request.
     */
    startLoading: function(resource, signature)
    {
        this.eventsLoading[resource] = signature;
        this.loading++;
        $('kronolithLoading').show();
    },

    /**
     */
    loadEvents: function(firstDay, lastDay, view, calendars)
    {
        var loading = false;

        if (typeof calendars == 'undefined') {
            calendars = [];
            $H(Kronolith.conf.calendars).each(function(type) {
                $H(type.value).each(function(cal) {
                    if (cal.value.show) {
                        calendars.push([type.key, cal.key]);
                    }
                });
            });
        }

        calendars.each(function(cal) {
            var startDay = firstDay.clone(), endDay = lastDay.clone(),
                cals = this.ecache.get(cal[0]);

            if (typeof cals != 'undefined' &&
                typeof cals.get(cal[1]) != 'undefined') {
                cals = cals.get(cal[1]);
                while (!Object.isUndefined(cals.get(startDay.dateString())) &&
                       startDay.isBefore(endDay)) {
                    if (view != 'year') {
                        this.insertEvents([startDay, startDay], view, cal.join('|'));
                    }
                    startDay.add(1).day();
                }
                while (!Object.isUndefined(cals.get(endDay.dateString())) &&
                       (!startDay.isAfter(endDay))) {
                    if (view != 'year') {
                        this.insertEvents([endDay, endDay], view, cal.join('|'));
                    }
                    endDay.add(-1).day();
                }
                if (startDay.compareTo(endDay) > 0) {
                    return;
                }
            }
            var start = startDay.dateString(), end = endDay.dateString(),
                calendar = cal.join('|');
            loading = true;
            this.startLoading(calendar, start + end);
            this.storeCache($H(), calendar, null, true);

            HordeCore.doAction('listEvents', {
                start: start,
                end: end,
                cal: calendar,
                sig: start + end,
                view: view
            }, {
                callback: function(r) {
                    this.loadEventsCallback(r, true);
                }.bind(this)
            });
        }, this);

        if (!loading && view == 'year') {
            this.insertEvents([firstDay, lastDay], 'year');
        }
    },

    /**
     * Callback method for inserting events in the current view.
     *
     * @param object r             The ajax response object.
     * @param boolean createCache  Whether to create a cache list entry for the
     *                             response, if none exists yet. Useful for
     *                             (not) adding individual events to the cache
     *                             if it doesn't match any cached views.
     */
    loadEventsCallback: function(r, createCache)
    {
        // Hide spinner.
        this.loading--;
        if (!this.loading) {
            $('kronolithLoading').hide();
        }

        var start = this.parseDate(r.sig.substr(0, 8)),
            end = this.parseDate(r.sig.substr(8, 8)),
            dates = [start, end],
            currentDates;

        this.storeCache(r.events || {}, r.cal, dates, createCache);

        // Check if this is the still the result of the most current request.
        if (r.sig != this.eventsLoading[r.cal]) {
            return;
        }
        delete this.eventsLoading[r.cal];

        // Check if the calendar is still visible.
        var calendar = r.cal.split('|');
        if (!Kronolith.conf.calendars[calendar[0]][calendar[1]].show) {
            return;
        }

        // Check if the result is still for the current view.
        currentDates = this.viewDates(this.date, this.view);
        if (r.view != this.view ||
            !start.between(currentDates[0], currentDates[1])) {

            return;
        }

        if (this.view == 'day' ||
            this.view == 'week' ||
            this.view == 'workweek' ||
            this.view == 'month' ||
            this.view == 'agenda' ||
            (this.view == 'year' && !$H(this.eventsLoading).size())) {
            this.insertEvents(dates, this.view, r.cal);
        }
    },

    /**
     * Reads events from the cache and inserts them into the view.
     *
     * If inserting events into day and week views, the calendar parameter is
     * ignored, and events from all visible calendars are inserted instead.
     * This is necessary because the complete view has to be re-rendered if
     * events are not in chronological order.
     * The year view is specially handled too because there are no individual
     * events, only a summary of all events per day.
     *
     * @param Array dates      Start and end of dates to process.
     * @param string view      The view to update.
     * @param string calendar  The calendar to update.
     */
    insertEvents: function(dates, view, calendar)
    {
        switch (view) {
        case 'day':
        case 'week':
        case 'workweek':
            // The day and week views require the view to be completely
            // loaded, to correctly calculate the dimensions.
            if (this.viewLoading.size() || this.view != view) {
                this.insertEvents.bind(this, [dates[0].clone(), dates[1].clone()], view, calendar).defer();
                return;
            }
            break;
        }

        var day = dates[0].clone(),
                  viewDates = this.viewDates(this.date, this.view),
                  date, more, title, titles, events, monthDay, busyHours;
        while (!day.isAfter(dates[1])) {
            // Skip if somehow events slipped in though the view is gone.
            if (!day.between(viewDates[0], viewDates[1])) {
                if (window.console) {
                    window.console.trace();
                }
                day.next().day();
                continue;
            }

            date = day.dateString();
            switch (view) {
            case 'day':
            case 'week':
            case 'workweek':
                this.dayEvents = [];
                this.dayGroups = [];
                this.allDayEvents = [];
                if (view == 'day') {
                    $$('.kronolith-event').invoke('remove');
                } else {
                    this.eventsWeek['kronolithEvents' + (view == 'week' ? 'Week' : 'Workweek') + date]
                        .select('.kronolith-event')
                        .invoke('remove');
                    this.allDays['kronolithAllDay' + date]
                        .childElements()
                        .invoke('remove');
                }
                break;

            case 'month':
                monthDay = this.monthDays['kronolithMonthDay' + date];
                monthDay.select('div')
                    .findAll(function(el) { return el.retrieve('calendar') == calendar; })
                    .invoke('remove');
                break;

            case 'year':
                titles = [];
                busyHours = 0;
            }

            if (view == 'month' || view == 'agenda') {
                events = this.getCacheForDate(date, calendar);
            } else {
                events = this.getCacheForDate(date);
            }
            events.sortBy(this.sortEvents).each(function(event) {
                var insertBefore;
                switch (view) {
                case 'month':
                case 'agenda':
                    if (calendar.startsWith('holiday|')) {
                        if (this.holidays.include(event.key)) {
                            return;
                        }
                        this.holidays.push(event.key);
                    }
                    if (view == 'month' && Kronolith.conf.max_events) {
                        more = monthDay.down('.kronolithMore');
                        if (more) {
                            more.purge();
                            more.remove();
                        }
                    }
                    if (view == 'month') {
                        if (Kronolith.conf.max_events) {
                        var events = monthDay.select('.kronolith-event');
                        if (events.size() >= Kronolith.conf.max_events) {
                            if (date == (new Date().dateString())) {
                                // This is today.
                                if (event.value.al || event.value.end.isBefore()) {
                                    // No room for all-day or finished events.
                                    this.insertMore(date);
                                    return;
                                }
                                var remove, max;
                                // Find an event that is earlier than now or
                                // later then the current event.
                                events.each(function(elm) {
                                    var calendar = elm.retrieve('calendar').split('|'),
                                        event = this.ecache.get(calendar[0]).get(calendar[1]).get(date).get(elm.retrieve('eventid'));
                                    if (event.start.isBefore()) {
                                        remove = elm;
                                        throw $break;
                                    }
                                    if (!max || event.start.isAfter(max)) {
                                        max = event.start;
                                        remove = elm;
                                    }
                                }, this);
                                if (remove) {
                                    remove.purge();
                                    remove.remove();
                                    insertBefore = this.findInsertBefore(events.without(remove), event, date);
                                } else {
                                    this.insertMore(date);
                                    return;
                                }
                            } else {
                                // Not today.
                                var allDays = events.findAll(function(elm) {
                                    var calendar = elm.retrieve('calendar').split('|');
                                    return this.ecache.get(calendar[0]).get(calendar[1]).get(date).get(elm.retrieve('eventid')).al;
                                }.bind(this));
                                if (event.value.al) {
                                    // We want one all-day event.
                                    if (allDays.size()) {
                                        // There already is an all-day event.
                                        if (event.value.x == Kronolith.conf.status.confirmed ||
                                            event.value.x == Kronolith.conf.status.tentative) {
                                            // But is there a less important
                                            // one?
                                            var status = [Kronolith.conf.status.free, Kronolith.conf.status.cancelled];
                                            if (event.value.x == Kronolith.conf.status.confirmed) {
                                                status.push(Kronolith.conf.status.tentative);
                                            }
                                            var free = allDays.detect(function(elm) {
                                                var calendar = elm.retrieve('calendar').split('|');
                                                return status.include(this.ecache.get(calendar[0]).get(calendar[1]).get(date).get(elm.retrieve('eventid')).x);
                                            }.bind(this));
                                            if (!free) {
                                                this.insertMore(date);
                                                return;
                                            }
                                            insertBefore = free.next();
                                            free.purge();
                                            free.remove();
                                        } else {
                                            // No.
                                            this.insertMore(date);
                                            return;
                                        }
                                    } else {
                                        // Remove the last event to make room
                                        // for this one.
                                        var elm = events.pop();
                                        elm.purge();
                                        elm.remove();
                                        insertBefore = events.first();
                                    }
                                } else {
                                    if (allDays.size() > 1) {
                                        // We don't want more than one all-day
                                        // event.
                                        var elm = allDays.pop();
                                        // Remove element from events as well.
                                        events = events.without(elm);
                                        elm.purge();
                                        elm.remove();
                                        insertBefore = this.findInsertBefore(events, event, date);
                                    } else {
                                        // This day is full.
                                        this.insertMore(date);
                                        return;
                                    }
                                }
                            }
                            this.insertMore(date);
                        } else {
                            insertBefore = this.findInsertBefore(events, event, date);
                        }
                        } else {
                            var events = monthDay.select('.kronolith-event');
                            insertBefore = this.findInsertBefore(events, event, date);
                        }
                    }
                    break;

                case 'year':
                    title = '';
                    if (event.value.al) {
                        title += Kronolith.text.allday;
                    } else {
                        title += event.value.start.toString('t') + '-' + event.value.end.toString('t');
                    }
                    if (event.value.t) {
                        title += ': ' + event.value.t.escapeHTML();
                    }
                    if (event.value.x == Kronolith.conf.status.tentative ||
                        event.value.x == Kronolith.conf.status.confirmed) {
                        busyHours += event.value.start.getElapsed(event.value.end) / 3600000;
                    }
                    titles.push(title);
                    return;
                }
                this.insertEvent(event, date, view, insertBefore);
            }, this);

            switch (view) {
            case 'agenda':
                if ($('kronolithViewAgendaBody').select('tr').length > 2) {
                    $('kronolithAgendaNoItems').hide();
                } else {
                    $('kronolithAgendaNoItems').show();
                }
                break;

            case 'year':
                var td = $('kronolithYear' + date);
                if (td.className == 'kronolith-minical-empty') {
                    continue;
                }
                if (td.hasClassName('kronolith-today')) {
                    td.className = 'kronolith-today';
                } else {
                    td.className = '';
                }
                if (titles.length) {
                    td.addClassName('kronolithHasEvents');
                    if (busyHours > 0) {
                        td.addClassName(this.getHeatmapClass(busyHours));
                        busyHours = 0;
                    }
                    td.down('a').writeAttribute('nicetitle', Object.toJSON(titles));
                }
            }

            day.next().day();
        }
        // Workaround Firebug bug.
        Prototype.emptyFunction();
    },

    findInsertBefore: function(events, event, date)
    {
        var insertBefore, insertSort;
        events.each(function(elm) {
            var calendar = elm.retrieve('calendar').split('|'),
                existing = this.ecache
                    .get(calendar[0])
                    .get(calendar[1])
                    .get(date)
                    .get(elm.retrieve('eventid'));
            if (event.value.sort < existing.sort &&
                (!insertSort || existing.sort < insertSort)) {
                insertBefore = elm;
                insertSort = existing.sort;
            }
        }, this);
        return insertBefore;
    },

    getHeatmapClass: function(hours)
    {
        return 'heat' + Math.min(Math.ceil(hours / 2), 6);
    },

    /**
     * Creates the DOM node for an event bubble and inserts it into the view.
     *
     * @param object event    A Hash member with the event to insert.
     * @param string date     The day to update.
     * @param string view     The view to update.
     * @param Element before  Insert the event before this element (month view).
     */
    insertEvent: function(event, date, view, before)
    {
        var calendar = event.value.calendar.split('|');
        event.value.nodeId = ('kronolithEvent' + view + event.value.calendar + date + event.key).replace(new RegExp('[^a-zA-Z0-9]', 'g'), '');

        var _createElement = function(event) {
            var className ='kronolith-event';
            switch (event.value.x) {
            case 3:
                className += ' kronolith-event-cancelled';
                break;
            case 1:
            case 4:
                className += ' kronolith-event-tentative';
                break;
            }
            var el = new Element('div', { id: event.value.nodeId, className: className })
                .store('calendar', event.value.calendar)
                .store('eventid', event.key);
            if (!Object.isUndefined(event.value.aj)) {
                el.store('ajax', event.value.aj);
            }
            return el;
        };

        switch (view) {
        case 'day':
        case 'week':
        case 'workweek':
            var storage = view + 'Sizes',
                what = view == 'week' ? 'Week' : 'Workweek',
                div = _createElement(event),
                margin = view == 'day' ? 1 : 3,
                style = { backgroundColor: Kronolith.conf.calendars[calendar[0]][calendar[1]].bg,
                          color: Kronolith.conf.calendars[calendar[0]][calendar[1]].fg };

            div.writeAttribute('title', event.value.t);

            if (event.value.al) {
                if (view == 'day') {
                    $('kronolithViewDay').down('.kronolithAllDayContainer').insert(div.setStyle(style));
                } else {
                    var allDay = this.allDays['kronolithAllDay' + date],
                        existing = allDay.childElements(),
                        weekHead = $('kronolithView' + what + 'Head');
                    if (existing.size() == 3) {
                        if (existing[2].className != 'kronolithMore') {
                            existing[2].purge();
                            existing[2].remove();
                            allDay.insert({ bottom: new Element('span', { className: 'kronolithMore' }).store('date', date).insert(Kronolith.text.more) });
                        }
                    } else {
                        allDay.insert(div.setStyle(style));
                        if (event.value.pe) {
                            div.addClassName('kronolithEditable');
                            var layout = div.getLayout(),
                                minLeft = weekHead.down('.kronolith-first-col').getWidth() + this[storage].spacing + (parseInt(div.getStyle('marginLeft'), 10) || 0),
                                minTop = weekHead.down('thead').getHeight() + this[storage].spacing + (parseInt(div.getStyle('marginTop'), 10) || 0),
                                maxLeft = weekHead.getWidth() - layout.get('margin-box-width'),
                                maxTop = weekHead.down('thead').getHeight() + weekHead.down('.kronolith-all-day').getHeight(),
                                opts = {
                                    threshold: 5,
                                    parentElement: function() {
                                        return $('kronolithView' + what).down('.kronolith-view-head');
                                    },
                                    snap: function(x, y) {
                                        return [Math.min(Math.max(x, minLeft), maxLeft),
                                                Math.min(Math.max(y, minTop), maxTop - div.getHeight())];
                                    }
                                };
                            var d = new Drag(event.value.nodeId, opts);
                            div.store('drags', []);
                            Object.extend(d, {
                                event: event,
                                innerDiv: new Element('div'),
                                midnight: this.parseDate(date)
                            });
                            div.retrieve('drags').push(d);
                        }
                    }
                }
                break;
            }

            var midnight = this.parseDate(date),
                resizable = event.value.pe && (Object.isUndefined(event.value.vl) || event.value.vl),
                innerDiv = new Element('div', { className: 'kronolith-event-info' }),
                minHeight = 0, parentElement, draggerTop, draggerBottom,
                elapsed = (event.value.start.getHours() - midnight.getHours()) * 60 + (event.value.start.getMinutes() - midnight.getMinutes());
            switch (view) {
            case 'day':
                parentElement = $('kronolithEventsDay');
                break;
            case 'week':
                parentElement = this.eventsWeek['kronolithEventsWeek' + date];
                break;
            case 'workweek':
                parentElement = this.eventsWeek['kronolithEventsWorkweek' + date];
                break;
            }
            if (event.value.fi) {
                div.addClassName('kronolithFirst');
                if (resizable) {
                    draggerTop = new Element('div', { id: event.value.nodeId + 'top', className: 'kronolithDragger kronolithDraggerTop' }).setStyle(style);
                }
            } else {
                innerDiv.setStyle({ top: 0 });
            }
            if (event.value.la) {
                div.addClassName('kronolithLast');
                if (resizable) {
                    draggerBottom = new Element('div', { id: event.value.nodeId + 'bottom', className: 'kronolithDragger kronolithDraggerBottom' }).setStyle(style);
                }
            } else {
                innerDiv.setStyle({ bottom: 0 });
            }

            div.setStyle({
                top: (elapsed * this[storage].height / 60 | 0) + 'px',
                width: 100 - margin + '%'
            })
                .insert(innerDiv.setStyle(style));
            if (draggerTop) {
                div.insert(draggerTop);
            }
            if (draggerBottom) {
                div.insert(draggerBottom);
            }
            parentElement.insert(div);
            if (draggerTop) {
                minHeight += draggerTop.getHeight();
            }
            if (draggerBottom) {
                minHeight += draggerBottom.getHeight();
            }
            if (!minHeight) {
                minHeight = parseInt(innerDiv.getStyle('lineHeight'), 10)
                    + (parseInt(innerDiv.getStyle('paddingTop'), 10) || 0)
                    + (parseInt(innerDiv.getStyle('paddingBottom'), 10) || 0);
            }
            div.setStyle({ height: Math.max(Math.round(event.value.start.getElapsed(event.value.end) / 60000) * this[storage].height / 60 - this[storage].spacing | 0, minHeight) + 'px' });

            if (event.value.pe) {
                div.addClassName('kronolithEditable');
                div.store('drags', []);
                // Number of pixels that cover 10 minutes.
                var step = this[storage].height / 6,
                    stepX, minLeft, maxLeft, maxTop,
                    minBottom, maxBottom, dragBottomHeight;
                if (draggerBottom) {
                    // Height of bottom dragger
                    dragBottomHeight = draggerBottom.getHeight();
                }
                if (draggerTop) {
                    // Bottom-most position (maximum y) of top dragger
                    maxTop = div.offsetTop
                        - draggerTop.getHeight()
                        - parseInt(innerDiv.getStyle('lineHeight'), 10);
                    if (draggerBottom) {
                        maxTop += draggerBottom.offsetTop;
                    }
                }
                if (draggerBottom) {
                    // Top-most position (minimum y) of bottom dragger (upper
                    // edge)
                    minBottom = div.offsetTop
                        + parseInt(innerDiv.getStyle('lineHeight'), 10);
                    // Bottom-most position (maximum y) of bottom dragger
                    // (upper edge)
                    maxBottom = 24 * this[storage].height
                        + dragBottomHeight;
                    if (draggerTop) {
                        minBottom += draggerTop.getHeight();
                    }
                }
                    // Height of the whole event div
                var divHeight = div.getHeight(),
                    // Maximum height of the whole event div
                    maxDiv = 24 * this[storage].height - divHeight,
                    // Whether the top dragger is dragged, vs. the bottom
                    // dragger
                    opts = {
                        threshold: 5,
                        constraint: 'vertical',
                        scroll: this.kronolithBody,
                        nodrop: true,
                        parentElement: function() {
                            return parentElement;
                        }
                    };

                if (draggerTop) {
                    opts.snap = function(x, y) {
                        y = Math.max(0, step * (Math.min(maxTop, y - this.scrollTop) / step | 0));
                        return [0, y];
                    }.bind(this);
                    var d = new Drag(event.value.nodeId + 'top', opts);
                    Object.extend(d, {
                        event: event,
                        innerDiv: innerDiv,
                        midnight: midnight
                    });
                    div.retrieve('drags').push(d);
                }

                if (draggerBottom) {
                    opts.snap = function(x, y) {
                        y = Math.min(maxBottom + dragBottomHeight + KronolithCore[storage].spacing, step * ((Math.max(minBottom, y - this.scrollTop) + dragBottomHeight + KronolithCore[storage].spacing) / step | 0)) - dragBottomHeight - KronolithCore[storage].spacing;
                        return [0, y];
                    }.bind(this);
                    var d = new Drag(event.value.nodeId + 'bottom', opts);
                    Object.extend(d, {
                        event: event,
                        innerDiv: innerDiv,
                        midnight: midnight
                    });
                    div.retrieve('drags').push(d);
                }

                if (view == 'week' || view == 'workweek') {
                    var dates = this.viewDates(midnight, view);
                    minLeft = this.eventsWeek['kronolithEvents' + what + dates[0].dateString()].offsetLeft - this.eventsWeek['kronolithEvents' + what + date].offsetLeft;
                    maxLeft = this.eventsWeek['kronolithEvents' + what + dates[1].dateString()].offsetLeft - this.eventsWeek['kronolithEvents' + what + date].offsetLeft;
                    stepX = (maxLeft - minLeft) / (view == 'week' ? 6 : 4);
                }
                var d = new Drag(div, {
                    threshold: 5,
                    nodrop: true,
                    parentElement: function() { return parentElement; },
                    snap: function(x, y) {
                        x = (view == 'week' || view == 'workweek')
                            ? Math.max(minLeft, stepX * ((Math.min(maxLeft, x - (x < 0 ? stepX : 0)) + stepX / 2) / stepX | 0))
                            : 0;
                        y = Math.max(0, step * (Math.min(maxDiv, y - this.scrollTop) / step | 0));
                        return [x, y];
                    }.bind(this)
                });
                Object.extend(d, {
                    divHeight: divHeight,
                    startTop: div.offsetTop,
                    event: event,
                    midnight: midnight,
                    stepX: stepX
                });
                div.retrieve('drags').push(d);
            }

            var
                // The current column that we're probing for available space.
                column = 1,
                // The number of columns in the current conflict group.
                columns,
                // The column width in the current conflict group.
                width,
                // The first event that conflict with the current event.
                conflict = false,
                // The conflict group where this event should go.
                pos = this.dayGroups.length,
                // The event below that the current event fits.
                placeFound = false,
                // The minimum (virtual) duration of each event, defined by the
                // minimum height of an event DIV.
                minMinutes = (minHeight + this[storage].spacing) * 60 / this[storage].height;

            // this.dayEvents contains all events of the current day.
            // this.dayGroups contains conflict groups, i.e. all events that
            // conflict with each other and share a set of columns.
            //
            // Go through all events that have been added to this day already.
            this.dayEvents.each(function(ev) {
                // Due to the minimum height of an event DIV, events might
                // visually overlap, even if they physically don't.
                var minEnd = ev.start.clone().add(minMinutes).minutes(),
                    end = ev.end.isAfter(minEnd) ? ev.end : minEnd;

                // If it doesn't conflict with the current event, go ahead.
                if (!end.isAfter(event.value.start)) {
                    return;
                }

                // Found a conflicting event, now find its conflict group.
                for (pos = 0; pos < this.dayGroups.length; pos++) {
                    if (this.dayGroups[pos].indexOf(ev) != -1) {
                        // Increase column for each conflicting event in this
                        // group.
                        this.dayGroups[pos].each(function(ce) {
                            var minEnd = ce.start.clone().add(minMinutes).minutes(),
                                end = ce.end.isAfter(minEnd) ? ce.end : minEnd;
                            if (end.isAfter(event.value.start)) {
                                column++;
                            }
                        });
                        throw $break;
                    }
                }
            }, this);
            event.value.column = event.value.columns = column;

            if (Object.isUndefined(this.dayGroups[pos])) {
                this.dayGroups[pos] = [];
            }
            this.dayGroups[pos].push(event.value);

            // See if the current event had to add yet another column.
            columns = Math.max(this.dayGroups[pos][0].columns, column);

            // Update the widths of all events in a conflict group.
            width = 100 / columns;
            this.dayGroups[pos].each(function(ev) {
                ev.columns = columns;
                $(ev.nodeId).setStyle({ width: width - margin + '%', left: (width * (ev.column - 1)) + '%' });
            });
            this.dayEvents.push(event.value);

            div = innerDiv;
            break;

        case 'month':
            var monthDay = this.monthDays['kronolithMonthDay' + date],
                div = _createElement(event)
                .setStyle({ backgroundColor: Kronolith.conf.calendars[calendar[0]][calendar[1]].bg,
                            color: Kronolith.conf.calendars[calendar[0]][calendar[1]].fg });
            div.writeAttribute('title', event.value.t);
            if (before) {
                before.insert({ before: div });
            } else {
                monthDay.insert(div);
            }
            if (event.value.pe) {
                div.setStyle({ cursor: 'move' });
                new Drag(event.value.nodeId, { threshold: 5, parentElement: function() { return $('kronolith-month-body'); }, snapToParent: true });
            }
            if (Kronolith.conf.max_events) {
                var more = monthDay.down('.kronolithMore');
                if (more) {
                    monthDay.insert({ bottom: more.remove() });
                }
            }
            break;

        case 'agenda':
            var div = _createElement(event)
                .setStyle({ backgroundColor: Kronolith.conf.calendars[calendar[0]][calendar[1]].bg,
                            color: Kronolith.conf.calendars[calendar[0]][calendar[1]].fg });
            this.createAgendaDay(date);
            $('kronolithAgendaDay' + date).insert(div);
            break;
        }

        this.setEventText(div, event.value,
                          { time: view == 'agenda' || Kronolith.conf.show_time })
            .observe('mouseover', div.addClassName.curry('kronolith-selected'))
            .observe('mouseout', div.removeClassName.curry('kronolith-selected'));
    },

    /**
     * Re-renders the necessary parts of the current view, if any event changes
     * in those parts require re-rendering.
     *
     * @param Array dates  The date strings of days to re-render.
     */
    reRender: function(dates)
    {
        switch (this.view) {
        case 'week':
        case 'workweek':
        case 'day':
            dates.each(function(date) {
                date = this.parseDate(date);
                this.insertEvents([ date, date ], this.view);
            }, this);
            break;
        case 'month':
            dates.each(function(date) {
                var day = this.monthDays['kronolithMonthDay' + date];
                day.select('.kronolith-event').each(function(event) {
                    if (event.retrieve('calendar').startsWith('holiday')) {
                        delete this.holidays[event.retrieve('eventid')];
                    }
                    event.remove();
                }, this);
                day.select('.kronolithMore').invoke('remove');
                date = this.parseDate(date);
                this.loadEvents(date, date, 'month');
            }, this);
            break;
        }
    },

    /**
     * Returns all dates of the current view that contain (recurrences) of a
     * certain event.
     *
     * @param String cal      A calendar string.
     * @param String eventid  An event id.
     *
     * @return Array  A list of date strings that contain a recurrence of the
     *                event.
     */
    findEventDays: function(cal, eventid)
    {
        cal = cal.split('|');
        var cache = this.ecache.get(cal[0]).get(cal[1]),
            dates = this.viewDates(this.date, this.view),
            day = dates[0], days = [], dateString;
        while (!day.isAfter(dates[1])) {
            dateString = day.dateString();
            if (cache.get(dateString).get(eventid)) {
                days.push(dateString);
            }
            day.add(1).days();
        }
        return days;
    },

    /**
     * Adds a "more..." button to the month view cell that links to the days,
     * or moves it to the buttom.
     *
     * @param string date  The date string of the day cell.
     */
    insertMore: function(date)
    {
        var monthDay = this.monthDays['kronolithMonthDay' + date],
            more = monthDay.down('.kronolithMore');
        if (more) {
            monthDay.insert({ bottom: more.remove() });
        } else {
            monthDay.insert({ bottom: new Element('span', { className: 'kronolithMore' }).store('date', date).insert(Kronolith.text.more) });
        }
    },

    setEventText: function(div, event, opts)
    {
        var calendar = event.calendar.split('|'),
            span = new Element('span'),
            time, end;
        opts = Object.extend({ time: false }, opts || {});

        div.update();
        if (event.ic) {
            div.insert(new Element('img', { src: event.ic, className: 'kronolithEventIcon' }));
        }
        if (opts.time && !event.al) {
            time = new Element('span', { className: 'kronolith-time' })
                .insert(event.start.toString(Kronolith.conf.time_format));
            if (!event.start.equals(event.end)) {
                end = event.end.clone();
                if (end.getHours() == 23 &&
                    end.getMinutes() == 59 &&
                    end.getSeconds() == 59) {
                    end.add(1).second();
                }
                time.insert('-' + end.toString(Kronolith.conf.time_format));
            }
            div.insert(time).insert(' ');
        }
        div.insert(event.t.escapeHTML());
        div.insert(span);
        if (event.a) {
            span.insert(' ')
                .insert(new Element('img', { src: Kronolith.conf.images.alarm.replace(/fff/, Kronolith.conf.calendars[calendar[0]][calendar[1]].fg.substr(1)), title: Kronolith.text.alarm + ' ' + event.a }));
        }
        if (event.r) {
            span.insert(' ')
                .insert(new Element('img', { src: Kronolith.conf.images.recur.replace(/fff/, Kronolith.conf.calendars[calendar[0]][calendar[1]].fg.substr(1)), title: Kronolith.text.recur[event.r] }));
        } else if (event.bid) {
            div.store('bid', event.bid);
            span.insert(' ')
                .insert(new Element('img', { src: Kronolith.conf.images.exception.replace(/fff/, Kronolith.conf.calendars[calendar[0]][calendar[1]].fg.substr(1)), title: Kronolith.text.recur.exception }));
        }
        return div;
    },

    /**
     * Finally removes events from the DOM and the cache.
     *
     * @param string calendar  A calendar name.
     * @param string event     An event id. If empty, all events from the
     *                         calendar are deleted.
     */
    removeEvent: function(calendar, event)
    {
        this.deleteCache(calendar, event);
        this.kronolithBody.select('div.kronolith-event').findAll(function(el) {
            return el.retrieve('calendar') == calendar &&
                (!event || el.retrieve('eventid') == event);
        }).invoke('remove');
    },

    /**
     * Removes all events that reprensent exceptions to the event series
     * represented by uid.
     *
     * @param string calendar  A calendar name.
     * @param string uid       An event uid.
     */
    removeException: function(calendar, uid)
    {
        this.kronolithBody.select('div.kronolith-event').findAll(function(el) {
            if (el.retrieve('calendar') == calendar && el.retrieve('bid') == uid) {
                this.removeEvent(calendar, el.retrieve('eventid'));
            }
        }.bind(this));
    },

    /**
     * Calculates the event's start and end dates based on some drag and drop
     * information.
     */
    calculateEventDates: function(event, storage, step, offset, height, start, end)
    {
        if (!Object.isUndefined(start)) {
            event.start = start;
            event.end = end;
        }
        event.start.set({
            hour: offset / this[storage].height | 0,
            minute: Math.round(offset % this[storage].height / step) * 10
        });
        var hour = (offset + height + this[storage].spacing) / this[storage].height | 0,
            minute = Math.round((offset + height + this[storage].spacing) % this[storage].height / step) * 10,
            second = 0;
        if (hour == 24) {
            hour = 23;
            minute = 59;
            second = 59;
        }
        event.end.set({
            hour: hour,
            minute: minute,
            second: second
        });
    },

    switchTaskView: function(on)
    {
        if (on) {
            $('kronolithNewEvent', 'kronolithNewTask').compact()[0]
                .replace(Kronolith.conf.new_task);
            $('kronolithQuickEvent').addClassName('kronolithNewTask');
            $('kronolithHeader').down('.kronolithPrev').up().addClassName('disabled');
            $('kronolithHeader').down('.kronolithNext').up().addClassName('disabled');
        } else {
            $('kronolithNewEvent', 'kronolithNewTask').compact()[0]
                .replace(Kronolith.conf.new_event);
            $('kronolithQuickEvent').removeClassName('kronolithNewTask');
            $('kronolithHeader').down('.kronolithPrev').up().removeClassName('disabled');
            $('kronolithHeader').down('.kronolithNext').up().removeClassName('disabled');
        }
    },

    /**
     * Returns the task cache storage names that hold the tasks of the
     * requested task type.
     *
     * @param string tasktype  The task type.
     *
     * @return array  The list of task cache storage names.
     */
    getTaskStorage: function(tasktype)
    {
        var tasktypes;
        if (tasktype == 'all' || tasktype == 'future') {
            tasktypes = [ 'complete', 'incomplete' ];
        } else {
            tasktypes = [ tasktype ];
        }
        return tasktypes;
    },

    /**
     * Loads tasks, either from cache or from the server.
     *
     * @param integer tasktype  The tasks type (all, incomplete, complete, or
     *                          future).
     * @param Array tasksLists  The lists from where to obtain the tasks.
     */
    loadTasks: function(tasktype, tasklists)
    {
        var tasktypes = this.getTaskStorage(tasktype), loading = false,
            spinner = $('kronolithLoading');

        if (Object.isUndefined(tasklists)) {
            tasklists = [];
            $H(Kronolith.conf.calendars.tasklists).each(function(tasklist) {
                if (tasklist.value.show)
                {
                    tasklists.push(tasklist.key.substring(6));
                }
            });
        }

        tasktypes.each(function(type) {
            tasklists.each(function(list) {
                if (Object.isUndefined(this.tcache.get(type)) ||
                    Object.isUndefined(this.tcache.get(type).get(list))) {
                    loading = true;
                    this.loading++;
                    spinner.show();
                    HordeCore.doAction('listTasks', {
                        type: type,
                        list: list
                    }, {
                        callback: function(r) {
                            this.loadTasksCallback(r, true);
                        }.bind(this)
                    });
                }
            }, this);
        }, this);

        if (!loading) {
            tasklists.each(function(list) {
                this.insertTasks(tasktype, list);
            }, this);
        }
    },

    /**
     * Callback method for inserting tasks in the current view.
     *
     * @param object r             The ajax response object.
     * @param boolean createCache  Whether to create a cache list entry for the
     *                             response, if none exists yet. Useful for
     *                             (not) adding individual tasks to the cache
     *                             without assuming to have all tasks of the
     *                             list.
     */
    loadTasksCallback: function(r, createCache)
    {
        // Hide spinner.
        this.loading--;
        if (!this.loading) {
            $('kronolithLoading').hide();
        }

        this.storeTasksCache(r.tasks || {}, r.type, r.list, createCache);

        // Check if result is still valid for the current view.
        // There could be a rare race condition where two responses for the
        // same task(s) arrive in the wrong order. Checking this too, like we
        // do for events seems not worth it.
        var tasktypes = this.getTaskStorage(this.tasktype),
            tasklist = Kronolith.conf.calendars.tasklists['tasks/' + r.list];
        if (this.view != 'tasks' ||
            !tasklist || !tasklist.show ||
            !tasktypes.include(r.type)) {
            return;
        }
        this.insertTasks(this.tasktype, r.list);
    },

    /**
     * Reads tasks from the cache and inserts them into the view.
     *
     * @param integer tasktype  The tasks type (all, incomplete, complete, or
     *                          future).
     * @param string tasksList  The task list to be drawn.
     */
    insertTasks: function(tasktype, tasklist)
    {
        var tasktypes = this.getTaskStorage(tasktype), now = new Date();

        $('kronolithViewTasksBody').select('tr').findAll(function(el) {
            return el.retrieve('tasklist') == tasklist;
        }).invoke('remove');

        tasktypes.each(function(type) {
            if (!this.tcache.get(type)) {
                return;
            }
            var tasks = this.tcache.get(type).get(tasklist);
            $H(tasks).each(function(task) {
                switch (tasktype) {
                case 'complete':
                    if (!task.value.cp) {
                        return;
                    }
                    break;
                case 'incomplete':
                    if (task.value.cp ||
                        (!Object.isUndefined(task.value.start) &&
                         task.value.start.isAfter(now))) {
                        return;
                    }
                    break;
                case 'future':
                    if (task.value.cp ||
                        Object.isUndefined(task.value.start) ||
                        !task.value.start.isAfter(now)) {
                        return;
                    }
                    break;
                }
                this.insertTask(task);
            }, this);
        }, this);

        if ($('kronolithViewTasksBody').select('tr').length > 2) {
            $('kronolithTasksNoItems').hide();
        } else {
            $('kronolithTasksNoItems').show();
        }
    },

    /**
     * Creates the DOM node for a task and inserts it into the view.
     *
     * @param object task  A Hash with the task to insert
     */
    insertTask: function(task)
    {
        var row = $('kronolithTasksTemplate').clone(true),
            col = row.down(), tagc;


        row.removeAttribute('id');
        row.store('tasklist', task.value.l);
        row.store('taskid', task.key);
        col.addClassName('kronolithTask' + (!!task.value.cp ? 'Completed' : ''));
        col.setStyle({
            backgroundColor: Kronolith.conf.calendars.tasklists['tasks/' + task.value.l].bg,
            color: Kronolith.conf.calendars.tasklists['tasks/' + task.value.l].fg,
            textIndent: task.value.i + 'em'
        });
        col.insert(task.value.n.escapeHTML());
        if (!Object.isUndefined(task.value.due)) {
            var now = new Date();
            if (!now.isBefore(task.value.due)) {
                col.addClassName('kronolithTaskDue');
            }
            col.insert(new Element('span', { className: 'kronolithSeparator' }).update(' &middot; '));
            col.insert(new Element('span', { className: 'kronolithDate' }).update(task.value.due.toString(Kronolith.conf.date_format)));
            if (task.value.r) {
                col.insert(' ')
                    .insert(new Element('img', { src: Kronolith.conf.images.recur.replace(/fff/, Kronolith.conf.calendars.tasklists['tasks/' + task.value.l].fg.substr(1)), title: Kronolith.text.recur[task.value.r] }));
            }
        }

        if (!Object.isUndefined(task.value.sd)) {
            col.insert(new Element('span', { className: 'kronolithSeparator' }).update(' &middot; '));
            col.insert(new Element('span', { className: 'kronolithInfo' }).update(task.value.sd.escapeHTML()));
        }

        if (task.value.t && task.value.t.size() > 0) {
            tagc = new Element('ul', { className: 'horde-tags' });
            task.value.t.each(function(x) {
                tagc.insert(new Element('li').update(x.escapeHTML()));
            });
            col.insert(tagc);
        }
        row.insert(col.show());
        this.insertTaskPosition(row, task);
    },

    /**
     * Inserts the task row in the correct position.
     *
     * @param Element newRow  The new row to be inserted.
     * @param object newTask  A Hash with the task being added.
     */
    insertTaskPosition: function(newRow, newTask)
    {
        var rows = $('kronolithViewTasksBody').select('tr'),
            rowTasklist, rowTaskId, rowTask, parentFound;
        // The first row is a template, ignoring.
        for (var i = 2; i < rows.length; i++) {
            rowTasklist = rows[i].retrieve('tasklist');
            rowTaskId = rows[i].retrieve('taskid');
            if (newTask.value.p) {
                if (rowTaskId == newTask.value.p) {
                    parentFound = true;
                    continue;
                }
                if (!parentFound) {
                    continue;
                }
            }
            rowTask = this.tcache.inject(null, function(acc, list) {
                if (acc) {
                    return acc;
                }
                if (!Object.isUndefined(list.value.get(rowTasklist))) {
                    return list.value.get(rowTasklist).get(rowTaskId);
                }
            });

            if (Object.isUndefined(rowTask)) {
                // TODO: Throw error
                return;
            }
            if (!this.isTaskAfter(newTask.value, rowTask)) {
                break;
            }
        }
        rows[--i].insert({ after: newRow.show() });
    },

    /**
     * Analyzes which task should be drawn first.
     *
     * TODO: Very incomplete, only a dummy version
     */
    isTaskAfter: function(taskA, taskB)
    {
        // TODO: Make all ordering system
        if ((taskA.p || taskB.p) && taskA.p != taskB.p) {
            return !taskA.p;
        }
        return (taskA.pr >= taskB.pr);
    },

    /**
     * Completes/uncompletes a task.
     *
     * @param string tasklist          The task list to which the tasks belongs.
     * @param string taskid            The id of the task.
     * @param boolean|string complete  True if the task is completed, a
     *                                 due date if there are still
     *                                 incomplete recurrences.
     */
    toggleCompletion: function(tasklist, taskid, complete)
    {
        // Update the cache.
        var task = this.tcache.inject(null, function(acc, list) {
            if (acc) {
                return acc;
            }
            if (!Object.isUndefined(list.value.get(tasklist))) {
                return list.value.get(tasklist).get(taskid);
            }
        });
        if (Object.isUndefined(task)) {
            // This shouldn't happen.
            this.toggleCompletionClass(taskid);
            return;
        }
        if (Object.isUndefined(complete) || complete === true) {
            task.cp = !task.cp;
        }

        if (this.tcache.get(task.cp ? 'complete' : 'incomplete')) {
            this.tcache.get(task.cp ? 'complete' : 'incomplete').get(tasklist).set(taskid, task);
        }
        if (this.tcache.get(task.cp ? 'incomplete' : 'complete')) {
            this.tcache.get(task.cp ? 'incomplete' : 'complete').get(tasklist).unset(taskid);
        }

        // Remove row if necessary.
        var row = this.getTaskRow(taskid);
        if (!row) {
            return;
        }
        if ((this.tasktype == 'complete' && !task.cp) ||
            ((this.tasktype == 'incomplete' || this.tasktype == 'future_incomplete') && task.cp) ||
            ((complete === true) && (this.tasktype == 'future'))) {

            row.fade({
                duration: this.effectDur,
                afterFinish: function() {
                    row.purge();
                    row.remove();

                    //Check if items remained in interface
                    if ($('kronolithViewTasksBody').select('tr').length < 3) {
                        $('kronolithTasksNoItems').show();
                    }
                }
            });
        }

        // Update due date if necessary.
        if (!Object.isUndefined(complete) && complete !== true) {
            var now = new Date(), due = Date.parse(complete);
            row.down('span.kronolithDate')
                .update(due.toString(Kronolith.conf.date_format));
            if (now.isBefore(due)) {
                row.down('td.kronolithTaskCol')
                    .removeClassName('kronolithTaskDue');
            }
        }
    },

    /**
     * Toggles the CSS class to show that a task is completed/uncompleted.
     *
     * @param string taskid  The id of the task.
     */
    toggleCompletionClass: function(taskid)
    {
        var row = this.getTaskRow(taskid);
        if (!row) {
            return;
        }
        var col = row.down('td.kronolithTaskCol');
        col.toggleClassName('kronolithTask');
        col.toggleClassName('kronolithTaskCompleted');
    },

    /**
     * Returns the table row of a task.
     *
     * @param string taskid  The id of the task.
     *
     * @return Element  The table row of the task list, if found.
     */
    getTaskRow: function(taskid)
    {
        return $('kronolithViewTasksBody').select('tr').find(function(el) {
            return el.retrieve('taskid') == taskid;
        });
    },

    editTask: function(tasklist, id, desc)
    {
        if (this.redBoxLoading) {
            return;
        }

        if (Object.isUndefined(HordeImple.AutoCompleter.kronolithTaskTags)) {
            this.editTask.bind(this, tasklist, id, desc).defer();
            return;
        }

        this.closeRedBox();
        this.quickClose();
        this.redBoxOnDisplay = RedBox.onDisplay;
        RedBox.onDisplay = function() {
            if (this.redBoxOnDisplay) {
                this.redBoxOnDisplay();
            }
            try {
                $('kronolithTaskForm').focusFirstElement();
            } catch(e) {}
            RedBox.onDisplay = this.redBoxOnDisplay;
        }.bind(this);

        this.openTab($('kronolithTaskForm').down('.tabset a.kronolithTabLink'));
        $('kronolithTaskForm').enable();
        $('kronolithTaskForm').reset();
        HordeImple.AutoCompleter.kronolithTaskTags.reset();
        $('kronolithTaskSave').show().enable();
        $('kronolithTaskDelete').show().enable();
        $('kronolithTaskForm').down('.kronolithFormActions .kronolithSeparator').show();
        this.updateTasklistDropDown();
        this.disableAlarmMethods('Task');
        this.knl.kronolithTaskDueTime.markSelected();
        if (id) {
            RedBox.loading();
            this.updateTaskParentDropDown(tasklist);
            this.updateTaskAssigneeDropDown(tasklist);
            HordeCore.doAction('getTask', {
                list: tasklist,
                id: id
            }, {
                callback: this.editTaskCallback.bind(this)
            });
            $('kronolithTaskTopTags').update();
        } else {
            $('kronolithTaskId').clear();
            $('kronolithTaskOldList').clear();
            $('kronolithTaskList').setValue(Kronolith.conf.tasks.default_tasklist);
            this.updateTaskParentDropDown(Kronolith.conf.tasks.default_tasklist);
            this.updateTaskAssigneeDropDown(Kronolith.conf.tasks.default_tasklist);
            $('kronolithTaskParent').setValue('');
            $('kronolithTaskAssignee').setValue('');
            //$('kronolithTaskLocation').setValue('http://');
            HordeCore.doAction('listTopTags', {}, {
                callback: this.topTagsCallback.curry('kronolithTaskTopTags', 'kronolithTaskTag')
            });
            $('kronolithTaskPriority').setValue(3);
            if (Kronolith.conf.tasks.default_due) {
                this.setDefaultDue();
            }
            if (desc) {
                $('kronolithTaskDescription').setValue(desc);
            }
            this.toggleRecurrence(false, 'None');
            $('kronolithTaskDelete').hide();
            this.redBoxLoading = true;
            RedBox.showHtml($('kronolithTaskDialog').show());
        }
    },

    /**
     * Callback method for showing task forms.
     *
     * @param object r  The ajax response object.
     */
    editTaskCallback: function(r)
    {
        if (!r.task) {
            RedBox.close();
            this.go(this.lastLocation);
            return;
        }

        var task = r.task;

        /* Basic information */
        $('kronolithTaskId').setValue(task.id);
        $('kronolithTaskOldList').setValue(task.l);
        $('kronolithTaskList').setValue(task.l);
        $('kronolithTaskTitle').setValue(task.n);
        $('kronolithTaskParent').setValue(task.p);
        $('kronolithTaskAssignee').setValue(task.as);
        //$('kronolithTaskLocation').setValue(task.l);
        if (task.dd) {
            $('kronolithTaskDueDate').setValue(task.dd);
        }
        if (task.dt) {
            $('kronolithTaskDueTime').setValue(task.dt);
            this.knl.kronolithTaskDueTime.setSelected(task.dt);
        }
        $('kronolithTaskDescription').setValue(task.de);
        $('kronolithTaskPriority').setValue(task.pr);
        $('kronolithTaskCompleted').setValue(task.cp);

        /* Alarm */
        if (task.a) {
            this.enableAlarm('Task', task.a);
            if (task.m) {
                $('kronolithTaskAlarmDefaultOff').checked = true;
                $H(task.m).each(function(method) {
                    if (!$('kronolithTaskAlarm' + method.key)) {
                        return;
                    }
                    $('kronolithTaskAlarm' + method.key).setValue(1);
                    if ($('kronolithTaskAlarm' + method.key + 'Params')) {
                        $('kronolithTaskAlarm' + method.key + 'Params').show();
                    }
                    $H(method.value).each(function(param) {
                        var input = $('kronolithTaskAlarmParam' + param.key);
                        if (!input) {
                            return;
                        }
                        if (input.type == 'radio') {
                            input.up('form').select('input[type=radio]').each(function(radio) {
                                if (radio.name == input.name &&
                                    radio.value == param.value) {
                                    radio.setValue(1);
                                    throw $break;
                                }
                            });
                        } else {
                            input.setValue(param.value);
                        }
                    });
                });
            }
        } else {
            $('kronolithTaskAlarmOff').setValue(true);
        }

        /* Recurrence */
        if (task.r) {
            this.setRecurrenceFields(false, task.r);
        } else {
            this.toggleRecurrence(false, 'None');
        }

        HordeImple.AutoCompleter.kronolithTaskTags.reset(task.t);

        if (!task.pe) {
            $('kronolithTaskSave').hide();
            $('kronolithTaskForm').disable();
        } else {
            HordeCore.doAction('listTopTags', {}, {
                callback: this.topTagsCallback.curry('kronolithTaskTopTags', 'kronolithTaskTag')
            });
        }

        if (!task.pd) {
            $('kronolithTaskDelete').show();
        }
        if (!task.pe && !task.pd) {
            $('kronolithTaskForm').down('.kronolithFormActions .kronolithSeparator').hide();
        }

        this.setTitle(task.n);
        this.redBoxLoading = true;
        RedBox.showHtml($('kronolithTaskDialog').show());

        /* Hide alarm message for this task. */
        if (r.msgs) {
            r.msgs = r.msgs.reject(function(msg) {
                if (msg.type != 'horde.alarm') {
                    return false;
                }
                var alarm = msg.flags.alarm;
                if (alarm.params && alarm.params.notify &&
                    alarm.params.notify.show &&
                    alarm.params.notify.show.tasklist &&
                    alarm.params.notify.show.task &&
                    alarm.params.notify.show.tasklist == task.l &&
                    alarm.params.notify.show.task == task.id) {
                    return true;
                }
                return false;
            });
        }
    },

    /**
     * Propagates a SELECT drop down list with the editable task lists.
     */
    updateTasklistDropDown: function()
    {
        var tasklist = $('kronolithTaskList');
        tasklist.update();
        $H(Kronolith.conf.calendars.tasklists).each(function(cal) {
            if (cal.value.edit) {
                tasklist.insert(new Element('option', { value: cal.key.substring(6) })
                                .setStyle({ backgroundColor: cal.value.bg, color: cal.value.fg })
                                .update(cal.value.name.escapeHTML()));
            }
        });
    },

    /**
     * Propagates a SELECT drop down list with the tasks of a task list.
     *
     * @param string list  A task list ID.
     */
    updateTaskParentDropDown: function(list)
    {
        var parents = $('kronolithTaskParent');
        parents.update(new Element('option', { value: '' })
                       .update(Kronolith.text.no_parent));
        HordeCore.doAction('listTasks', {
            type: 'future_incomplete',
            list: list
        }, {
            ajaxopts: { asynchronuous: false },
            callback: function(r) {
                $H(r.tasks).each(function(task) {
                    parents.insert(new Element('option', { value: task.key })
                                .setStyle({ textIndent: task.value.i + 'em' })
                                .update(task.value.n.escapeHTML()));
                });
            }.bind(this)
        });
    },

    /**
     * Propagates a SELECT drop down list with the users of a task list.
     *
     * @param string list  A task list ID.
     */
    updateTaskAssigneeDropDown: function(list)
    {
        var assignee = $('kronolithTaskAssignee');
        assignee.update(new Element('option', { value: '' })
                       .update(Kronolith.text.no_assignee));
        $H(Kronolith.conf.calendars.tasklists['tasks/' + list].users).each(function(user) {
            assignee.insert(new Element('option', { value: user.key })
                            .update(user.value.escapeHTML()));
        });
    },

    /**
     * Sets the default due date and time for tasks.
     */
    setDefaultDue: function()
    {
        if ($F('kronolithTaskDueDate') || $F('kronolithTaskDueTime')) {
            return;
        }
        $('kronolithTaskDueDate').setValue(new Date().add(Kronolith.conf.tasks.default_due_days).days().toString(Kronolith.conf.date_format));
        if (Kronolith.conf.tasks.default_due_time == 'now') {
            $('kronolithTaskDueTime').setValue(new Date().toString(Kronolith.conf.time_format));
        } else {
            var date = new Date();
            date.setHours(Kronolith.conf.tasks.default_due_time.replace(/:.*$/, ''));
            date.setMinutes(0);
            $('kronolithTaskDueTime').setValue(date.toString(Kronolith.conf.time_format));
        }
    },

    /**
     * Finally removes tasks from the DOM and the cache.
     *
     * @param string list  A task list name.
     * @param string task  A task id. If empty, all tasks from the list are
     *                     deleted.
     */
    removeTask: function(list, task)
    {
        this.deleteTasksCache(list, task);
        $('kronolithViewTasksBody').select('tr').findAll(function(el) {
            return el.retrieve('tasklist') == list &&
                (!task || el.retrieve('taskid') == task);
        }).invoke('remove');
        this.removeEvent('tasklists|tasks/' + list, task ? '_tasks' + task : null);
        if ($('kronolithViewTasksBody').select('tr').length > 2) {
            $('kronolithTasksNoItems').hide();
        } else {
            $('kronolithTasksNoItems').show();
        }
    },

    /**
     * Submits the task edit form to create or update a task.
     */
    saveTask: function()
    {
        if (this.wrongFormat.size() ||
            (($F('kronolithTaskAlarmOn')) && $F('kronolithTaskDueDate').length == 0)) {
            HordeCore.notify(Kronolith.text.fix_form_values, 'horde.warning');
            return;
        }

        var tasklist = $F('kronolithTaskOldList'),
            target = $F('kronolithTaskList'),
            taskid = $F('kronolithTaskId'),
            viewDates = this.viewDates(this.date, this.view),
            start = viewDates[0].dateString(),
            end = viewDates[1].dateString();

        HordeImple.AutoCompleter.kronolithTaskTags.shutdown();
        $('kronolithTaskSave').disable();
        this.startLoading('tasklists|tasks/' + target, start + end + this.tasktype);
        this.loading++;
        HordeCore.doAction(
            'saveTask',
            $H($('kronolithTaskForm').serialize({ hash: true })).merge({
                sig: start + end + this.tasktype,
                view: this.view,
                view_start: start,
                view_end: end
            }), {
                callback: function(r) {
                    if (r.tasks && taskid) {
                        this.removeTask(tasklist, taskid);
                    }
                    this.loadTasksCallback(r, false);
                    this.loadEventsCallback(r, false);
                    if (r.tasks) {
                        this.closeRedBox();
                        this.go(this.lastLocation);
                    } else {
                        $('kronolithTaskSave').enable();
                    }
                }.bind(this)
            }
        );
    },

    quickSaveTask: function()
    {
        var text = $F('kronolithQuicktaskQ'),
            viewDates = this.viewDates(this.date, 'tasks'),
            start = viewDates[0].dateString(),
            end = viewDates[1].dateString(),
            params = {
                sig: start + end + this.tasktype,
                view: 'tasks',
                view_start: start,
                view_end: end,
                tasklist: Kronolith.conf.tasks.default_tasklist,
                text: text
            };

        this.closeRedBox();
        this.startLoading('tasklists|tasks/' + Kronolith.conf.tasks.default_tasklist,
                          params.sig);
        this.loading++;
        HordeCore.doAction('quickSaveTask', params, {
            callback: function(r) {
                this.loadTasksCallback(r, false);
                this.loadEventsCallback(r, false);
                if (!r.tasks || !$H(r.tasks).size()) {
                    this.editTask(null, null, text);
                } else {
                    $('kronolithQuicktaskQ').value = '';
                }
            }.bind(this)
         });
    },

    /**
     * Opens the form for editing a calendar.
     *
     * @param string calendar  Calendar type and calendar id, separated by '|'.
     */
    editCalendar: function(calendar)
    {
        if (this.redBoxLoading) {
            return;
        }

        this.closeRedBox();
        this.quickClose();

        var type = calendar.split('|')[0], cal = calendar.split('|')[1];
        if (!$w('internal tasklists remote holiday resource resourcegroup').include(type)) {
            return;
        }

        if (cal &&
            (Object.isUndefined(Kronolith.conf.calendars[type]) ||
             Object.isUndefined(Kronolith.conf.calendars[type][cal])) &&
            (type == 'internal' || type == 'tasklists')) {
            HordeCore.doAction('getCalendar', {
                cal: cal
            }, {
                callback: function(r) {
                    if (r.calendar) {
                        Kronolith.conf.calendars[type][cal] = r.calendar;
                        this.insertCalendarInList(type, cal, r.calendar);
                        $('kronolithSharedCalendars').show();
                        this.editCalendar(type + '|' + cal);
                    } else {
                        this.go(this.lastLocation);
                    }
                }.bind(this)
            });
            return;
        }

        this.redBoxOnDisplay = RedBox.onDisplay;
        RedBox.onDisplay = function() {
            if (this.redBoxOnDisplay) {
                this.redBoxOnDisplay();
            }
            try {
                $('kronolithCalendarForm' + type).focusFirstElement();
            } catch(e) {}
            RedBox.onDisplay = this.redBoxOnDisplay;
        }.bind(this);

        if ($('kronolithCalendarDialog')) {
            this.redBoxLoading = true;
            RedBox.showHtml($('kronolithCalendarDialog').show());
            this.editCalendarCallback(calendar);
        } else {
            RedBox.loading();
            HordeCore.doAction('chunkContent', {
                chunk: 'calendar'
            }, {
                callback: function(r) {
                    if (r.chunk) {
                        this.redBoxLoading = true;
                        RedBox.showHtml(r.chunk);
                        ['internal', 'tasklists'].each(function(type) {
                            $('kronolithC' + type + 'PGList').observe('change', function() {
                                $('kronolithC' + type + 'PG').setValue(1);
                                this.permsClickHandler(type, 'G');
                            }.bind(this));
                        }, this);
                        this.editCalendarCallback(calendar);
                    } else {
                        this.closeRedBox();
                    }
                }.bind(this)
            });
        }
    },

    /**
     * Callback for editing a calendar. Fills the edit form with the correct
     * values.
     *
     * @param string calendar  Calendar type and calendar id, separated by '|'.
     */
    editCalendarCallback: function(calendar)
    {
        calendar = calendar.split('|');
        var type = calendar[0];
        calendar = calendar.length == 1 ? null : calendar[1];

        var form = $('kronolithCalendarForm' + type),
            firstTab = form.down('.tabset a.kronolithTabLink'),
            info;

        form.enable();
        form.reset();
        if (firstTab) {
            this.openTab(firstTab);
        }
        $('kronolithCalendarDialog').select('.kronolithCalendarDiv').invoke('hide');
        $('kronolithCalendar' + type + '1').show();
        form.select('.kronolithCalendarContinue').invoke('enable');
        $('kronolithC' + type + 'PUNew', 'kronolithC' + type + 'PGNew').compact().each(function(elm) {
            if (elm.tagName == 'SELECT') {
                $A(elm.options).each(function(option) {
                    option.writeAttribute('disabled', false);
                });
            }
        });

        var newCalendar = !calendar;
        if (calendar &&
            (Object.isUndefined(Kronolith.conf.calendars[type]) ||
             Object.isUndefined(Kronolith.conf.calendars[type][calendar]))) {
            if (type != 'remote') {
                this.closeRedBox();
                this.go(this.lastLocation);
                return;
            }
            newCalendar = true;
        }
        if (type == 'resourcegroup') {
            this.updateResourcegroupSelect();
        }
        if (newCalendar) {
            switch (type) {
            case 'internal':
                HordeImple.AutoCompleter.kronolithCalendarinternalTags.reset();
                // Fall through.
            case 'tasklists':
                $('kronolithCalendar' + type + 'LinkExport').up('span').hide();
                break;
            case 'remote':
                if (calendar) {
                    $('kronolithCalendarremoteUrl').setValue(calendar);
                    $('kronolithCalendarremoteId').setValue(calendar);
                }
                break;
            case 'holiday':
                $('kronolithCalendarholidayDriver').update();
                $H(Kronolith.conf.calendars.holiday).each(function(calendar) {
                    if (calendar.value.show) {
                        return;
                    }
                    $('kronolithCalendarholidayDriver').insert(
                        new Element('option', { value: calendar.key })
                            .setStyle({ color: calendar.value.fg, backgroundColor: calendar.value.bg })
                            .insert(calendar.value.name.escapeHTML())
                    );
                });
                break;
            }
            $('kronolithCalendar' + type + 'Id').clear();
            var color = '#', i;
            for (i = 0; i < 3; i++) {
                color += (Math.random() * 256 | 0).toColorPart();
            }
            $('kronolithCalendar' + type + 'Color').setValue(color).setStyle({ backgroundColor: color, color: Color.brightness(Color.hex2rgb(color)) < 125 ? '#fff' : '#000' });
            form.down('.kronolithCalendarDelete').hide();
            $('kronolithCalendarinternalImportButton').hide();
        } else {
            info = Kronolith.conf.calendars[type][calendar];

            $('kronolithCalendar' + type + 'Id').setValue(calendar);
            $('kronolithCalendar' + type + 'Name').setValue(info.name);
            $('kronolithCalendar' + type + 'Color').setValue(info.bg).setStyle({ backgroundColor: info.bg, color: info.fg });
            $('kronolithCalendarinternalImportButton').hide();

            switch (type) {
            case 'internal':
                HordeImple.AutoCompleter.kronolithCalendarinternalTags.reset(Kronolith.conf.calendars.internal[calendar].tg);
                $('kronolithCalendar' + type + 'ImportCal').setValue('internal_' + calendar);
                if ($('kronolithCalendar' + type + 'LinkImport')) {
                    if (info.edit) {
                        $('kronolithCalendar' + type + 'LinkImport').up('li').show();
                    } else {
                        $('kronolithCalendar' + type + 'LinkImport').up('li').hide();
                    }
                }
                $('kronolithCalendar' + type + 'UrlFeed').setValue(info.feed);
                $('kronolithCalendar' + type + 'EmbedUrl').setValue(info.embed);
                // Fall through.
            case 'tasklists':
                $('kronolithCalendar' + type + 'Description').setValue(info.desc);
                if ($('kronolithCalendar' + type + 'LinkExport')) {
                    $('kronolithCalendar' + type + 'LinkExport').up('span').show();
                    $('kronolithCalendar' + type + 'Export').href = type == 'internal'
                        ? Kronolith.conf.URI_CALENDAR_EXPORT.interpolate({ calendar: calendar })
                        : Kronolith.conf.tasks.URI_TASKLIST_EXPORT.interpolate({ tasklist: calendar.substring(6) });
                }
                $('kronolithCalendar' + type + 'LinkUrls').up().show();
                if (info.caldav) {
                    $('kronolithCalendar' + type + 'UrlCaldav').setValue(info.caldav);
                    $('kronolithCalendar' + type + 'Caldav').show();
                } else {
                    $('kronolithCalendar' + type + 'Caldav').hide();
                }
                $('kronolithCalendar' + type + 'UrlWebdav').setValue(info.sub);
                break;
            case 'remote':
                $('kronolithCalendarremoteUrl').setValue(calendar);
                $('kronolithCalendarremoteDescription').setValue(info.desc);
                $('kronolithCalendarremoteUsername').setValue(info.user);
                $('kronolithCalendarremotePassword').setValue(info.password);
                break;
            case 'resourcegroup':
                $('kronolithCalendarresourcegroupDescription').setValue(info.desc);
                $('kronolithCalendarresourcegroupmembers').setValue(info.members);
                break;
            case 'resource':
                $('kronolithCalendarresourceDescription').setValue(info.desc);
                $('kronolithCalendarresourceResponseType').setValue(info.response_type);
                $('kronolithCalendarresourceExport').href = Kronolith.conf.URI_RESOURCE_EXPORT.interpolate({ calendar: calendar });
            }
        }

        if (newCalendar || info.owner) {
            if (type == 'internal' || type == 'tasklists') {
                this.updateGroupDropDown([['kronolithC' + type + 'PGList', this.updateGroupPerms.bind(this, type)],
                                          ['kronolithC' + type + 'PGNew']]);
                $('kronolithC' + type + 'PBasic').show();
                $('kronolithC' + type + 'PAdvanced').hide();
                $('kronolithC' + type + 'PNone').setValue(1);
                if ($('kronolithC' + type + 'PAllShow')) {
                    $('kronolithC' + type + 'PAllShow').disable();
                }
                $('kronolithC' + type + 'PGList').disable();
                $('kronolithC' + type + 'PGPerms').disable();
                $('kronolithC' + type + 'PUList').disable();
                $('kronolithC' + type + 'PUPerms').disable();
                $('kronolithC' + type + 'PAdvanced').select('tr').findAll(function(tr) {
                    return tr.retrieve('remove');
                }).invoke('remove');
                $('kronolithCalendar' + type + 'LinkUrls').up().show();
                form.down('.kronolithColorPicker').show();
                if (type == 'internal') {
                    HordeCore.doAction('listTopTags', {}, {
                        callback: this.topTagsCallback.curry('kronolithCalendarinternalTopTags', 'kronolithCalendarTag')
                    });
                }
                form.down('.kronolithCalendarSubscribe').hide();
                form.down('.kronolithCalendarUnsubscribe').hide();
                if ($('kronolithCalendar' + type + 'LinkPerms')) {
                    $('kronolithCalendar' + type + 'LinkPerms').up('span').show();
                }
                if (!Object.isUndefined(info) && info.owner) {
                    this.setPermsFields(type, info.perms);
                }
            }
            if (type == 'remote' || type == 'internal' || type == 'tasklists') {
                if (newCalendar ||
                    (type == 'internal' && calendar == Kronolith.conf.user) ||
                    (type == 'tasklists' && calendar == 'tasks/' + Kronolith.conf.user)) {
                    form.select('.kronolithCalendarDelete').invoke('hide');
                } else {
                    form.select('.kronolithCalendarDelete').invoke('show');
                }
            }
            form.down('.kronolithCalendarSave').show();
            form.down('.kronolithFormActions .kronolithSeparator').show();
        } else {
            form.disable();
            form.down('.kronolithColorPicker').hide();
            form.down('.kronolithCalendarDelete').hide();
            form.down('.kronolithCalendarSave').hide();
            if (type == 'internal' || type == 'tasklists') {
                $('kronolithCalendar' + type + 'UrlCaldav').enable();
                $('kronolithCalendar' + type + 'UrlAccount').enable();
                $('kronolithCalendar' + type + 'UrlWebdav').enable();
                if (type == 'internal') {
                    $('kronolithCalendar' + type + 'UrlFeed').enable();
                    $('kronolithCalendar' + type + 'EmbedUrl').enable();
                    if (info.edit) {
                        $('kronolithCalendarinternalImport').enable();
                        if (info.del) {
                            $('kronolithCalendarinternalImportOver').enable();
                        }
                        $('kronolithCalendarinternalImportButton').show().enable();
                    }
                }
                HordeImple.AutoCompleter.kronolithCalendarinternalTags.disable();
                if (Kronolith.conf.calendars[type][calendar].show) {
                    form.down('.kronolithCalendarSubscribe').hide();
                    form.down('.kronolithCalendarUnsubscribe').show().enable();
                } else {
                    form.down('.kronolithCalendarSubscribe').show().enable();
                    form.down('.kronolithCalendarUnsubscribe').hide();
                }
                form.down('.kronolithFormActions .kronolithSeparator').show();
                if ($('kronolithCalendar' + type + 'LinkPerms')) {
                    $('kronolithCalendar' + type + 'LinkPerms').up('span').hide();
                }
            } else {
                form.down('.kronolithFormActions .kronolithSeparator').hide();
            }
        }
    },

    /**
     * Updates the select list in the resourcegroup calendar dialog.
     */
    updateResourcegroupSelect: function()
    {
        if (!Kronolith.conf.calendars.resource) {
            return;
        }
        $('kronolithCalendarresourcegroupmembers').update();
        $H(Kronolith.conf.calendars.resource).each(function(r) {
            var o = new Element('option', { value: r.value.id }).update(r.value.name.escapeHTML());
            $('kronolithCalendarresourcegroupmembers').insert(o);
        });
    },

    /**
     * Handles clicks on the radio boxes of the basic permissions screen.
     *
     * @param string type  The calendar type, 'internal' or 'taskslists'.
     * @param string perm  The permission to activate, 'None', 'All', or
     *                     'Group'.
     */
    permsClickHandler: function(type, perm)
    {
        $('kronolithC' + type + 'PAdvanced')
            .select('input[type=checkbox]')
            .invoke('setValue', 0);
        $('kronolithC' + type + 'PAdvanced').select('tr').findAll(function(tr) {
            return tr.retrieve('remove');
        }).invoke('remove');

        switch (perm) {
        case 'None':
            if ($('kronolithC' + type + 'PAllShow')) {
                $('kronolithC' + type + 'PAllShow').disable();
            }
            $('kronolithC' + type + 'PGList').disable();
            $('kronolithC' + type + 'PGPerms').disable();
            $('kronolithC' + type + 'PUList').disable();
            $('kronolithC' + type + 'PUPerms').disable();
            break;
        case 'All':
            $('kronolithC' + type + 'PAllShow').enable();
            $('kronolithC' + type + 'PGList').disable();
            $('kronolithC' + type + 'PGPerms').disable();
            $('kronolithC' + type + 'PUList').disable();
            $('kronolithC' + type + 'PUPerms').disable();
            var perms = {
                'default': Kronolith.conf.perms.read,
                'guest': Kronolith.conf.perms.read
            };
            if ($F('kronolithC' + type + 'PAllShow')) {
                perms['default'] |= Kronolith.conf.perms.show;
                perms['guest'] |= Kronolith.conf.perms.show;
            }
            this.setPermsFields(type, perms);
            break;
        case 'G':
            if ($('kronolithC' + type + 'PAllShow')) {
                $('kronolithC' + type + 'PAllShow').disable();
            }
            $('kronolithC' + type + 'PGList').enable();
            $('kronolithC' + type + 'PGPerms').enable();
            $('kronolithC' + type + 'PUList').disable();
            $('kronolithC' + type + 'PUPerms').disable();
            var group = $F('kronolithC' + type + 'PGSingle')
                ? $F('kronolithC' + type + 'PGSingle')
                : $F('kronolithC' + type + 'PGList');
            this.insertGroupOrUser(type, 'group', group, true);
            $('kronolithC' + type + 'PGshow_' + group).setValue(1);
            $('kronolithC' + type + 'PGread_' + group).setValue(1);
            if ($F('kronolithC' + type + 'PGPerms') == 'edit') {
                $('kronolithC' + type + 'PGedit_' + group).setValue(1);
            } else {
                $('kronolithC' + type + 'PGedit_' + group).setValue(0);
            }
            $('kronolithC' + type + 'PGdel_' + group).setValue(0);
            if ($('kronolithC' + type + 'PGdelegate_' + group)) {
                $('kronolithC' + type + 'PGdelegate_' + group).setValue(0);
            }
            break;
        case 'U':
            if ($('kronolithC' + type + 'PAllShow')) {
                $('kronolithC' + type + 'PAllShow').disable();
            }
            $('kronolithC' + type + 'PGList').disable();
            $('kronolithC' + type + 'PGPerms').disable();
            $('kronolithC' + type + 'PUList').enable();
            $('kronolithC' + type + 'PUPerms').enable();
            var users = $F('kronolithC' + type + 'PUList').strip();
            users = users ? users.split(/\s*(?:,|\n)\s*/) : [];
            users.each(function(user) {
                if (!this.insertGroupOrUser(type, 'user', user, true)) {
                    return;
                }
                $('kronolithC' + type + 'PUshow_' + user).setValue(1);
                $('kronolithC' + type + 'PUread_' + user).setValue(1);
                if ($F('kronolithC' + type + 'PUPerms') == 'edit') {
                    $('kronolithC' + type + 'PUedit_' + user).setValue(1);
                } else {
                    $('kronolithC' + type + 'PUedit_' + user).setValue(0);
                }
                $('kronolithC' + type + 'PUdel_' + user).setValue(0);
                if ($('kronolithC' + type + 'PUdelegate_' + user)) {
                    $('kronolithC' + type + 'PUdelegate_' + user).setValue(0);
                }
            }, this);
            break;
        }
    },

    /**
     * Populates the permissions field matrix.
     *
     * @param string type   The calendar type, 'internal' or 'taskslists'.
     * @param object perms  An object with the resource permissions.
     */
    setPermsFields: function(type, perms)
    {
        if (this.groupLoading) {
            this.setPermsFields.bind(this, type, perms).defer();
            return;
        }

        var allperms = $H(Kronolith.conf.perms),
            advanced = false, users = [],
            basic, same, groupPerms, groupId, userPerms;
        $H(perms).each(function(perm) {
            switch (perm.key) {
            case 'default':
            case 'guest':
                if (Object.isUndefined(same)) {
                    same = perm.value;
                } else if (Object.isUndefined(basic) &&
                           same == perm.value &&
                           (perm.value == Kronolith.conf.perms.read ||
                            perm.value == (Kronolith.conf.perms.read | Kronolith.conf.perms.show))) {
                    basic = perm.value == Kronolith.conf.perms.read ? 'all_read' : 'all_show';
                } else if (perm.value != 0) {
                    advanced = true;
                }
                break;
            case 'creator':
                if (perm.value != 0) {
                    advanced = true;
                }
                break;
            case 'groups':
                if (!Object.isArray(perm.value)) {
                    $H(perm.value).each(function(group) {
                        if (!this.insertGroupOrUser(type, 'group', group.key)) {
                            return;
                        }
                        if (!$('kronolithC' + type + 'PGshow_' + group.key)) {
                            // Group doesn't exist anymore.
                            delete perm.value[group.key];
                            return;
                        }
                        groupPerms = group.value;
                        groupId = group.key;
                    }, this);
                    if (Object.isUndefined(basic) &&
                        $H(perm.value).size() == 1 &&
                        (groupPerms == (Kronolith.conf.perms.show | Kronolith.conf.perms.read) ||
                         groupPerms == (Kronolith.conf.perms.show | Kronolith.conf.perms.read | Kronolith.conf.perms.edit))) {
                        basic = groupPerms == (Kronolith.conf.perms.show | Kronolith.conf.perms.read) ? 'group_read' : 'group_edit';
                    } else {
                        advanced = true;
                    }
                }
                break;
            case 'users':
                if (!Object.isArray(perm.value)) {
                    $H(perm.value).each(function(user) {
                        if (user.key != Kronolith.conf.user) {
                            if (!this.insertGroupOrUser(type, 'user', user.key)) {
                                return;
                            }
                            if (!$('kronolithC' + type + 'PUshow_' + user.key)) {
                                // User doesn't exist anymore.
                                delete perm.value[user.key];
                                return;
                            }
                            // Check if we already have other basic permissions.
                            if (Object.isUndefined(userPerms) &&
                                !Object.isUndefined(basic)) {
                                advanced = true;
                            }
                            // Check if all users have the same permissions.
                            if (!Object.isUndefined(userPerms) &&
                                userPerms != user.value) {
                                advanced = true;
                            }
                            userPerms = user.value;
                            if (!advanced &&
                                (userPerms == (Kronolith.conf.perms.show | Kronolith.conf.perms.read) ||
                                 userPerms == (Kronolith.conf.perms.show | Kronolith.conf.perms.read | Kronolith.conf.perms.edit))) {
                                basic = userPerms == (Kronolith.conf.perms.show | Kronolith.conf.perms.read) ? 'user_read' : 'user_edit';
                                users.push(user.key);
                            } else {
                                advanced = true;
                            }
                        }
                    }, this);
                }
                break;
            }

            allperms.each(function(baseperm) {
                if (baseperm.key == 'all') {
                    return;
                }
                switch (perm.key) {
                case 'default':
                case 'guest':
                case 'creator':
                    if (baseperm.value & perm.value) {
                        $('kronolithC' + type + 'P' + perm.key + baseperm.key).setValue(1);
                    }
                    break;
                case 'groups':
                    $H(perm.value).each(function(group) {
                        if (baseperm.value & group.value) {
                            $('kronolithC' + type + 'PG' + baseperm.key + '_' + group.key).setValue(1);
                        }
                    });
                    break;
                case 'users':
                    $H(perm.value).each(function(user) {
                        if (baseperm.value & user.value &&
                            user.key != Kronolith.conf.user) {
                            $('kronolithC' + type + 'PU' + baseperm.key + '_' + user.key).setValue(1);
                        }
                    });
                    break;
                }
            });
        }.bind(this));

        if (advanced) {
            this.activateAdvancedPerms(type);
        } else {
            switch (basic) {
            case 'all_read':
                $('kronolithC' + type + 'PAll').setValue(1);
                $('kronolithC' + type + 'PAllShow').setValue(0);
                $('kronolithC' + type + 'PAllShow').enable();
                break;
            case 'all_show':
                $('kronolithC' + type + 'PAll').setValue(1);
                $('kronolithC' + type + 'PAllShow').setValue(1);
                $('kronolithC' + type + 'PAllShow').enable();
                break;
            case 'group_read':
            case 'group_edit':
                var setGroup = function(group) {
                    if ($('kronolithC' + type + 'PGList').visible()) {
                        $('kronolithC' + type + 'PGList').setValue(group);
                        if ($('kronolithC' + type + 'PGList').getValue() != group) {
                            // Group no longer exists.
                            this.permsClickHandler(type, 'None');
                        }
                    } else if ($('kronolithC' + type + 'PGSingle').getValue() != group) {
                        // Group no longer exists.
                        this.permsClickHandler(type, 'None');
                    }
                }.bind(this, groupId);
                if (this.groupLoading) {
                    setGroup.defer();
                } else {
                    setGroup();
                }
                $('kronolithC' + type + 'PG').setValue(1);
                $('kronolithC' + type + 'PGPerms').setValue(basic.substring(6));
                $('kronolithC' + type + 'PAdvanced').hide();
                $('kronolithC' + type + 'PBasic').show();
                $('kronolithC' + type + 'PGPerms').enable();
                break;
            case 'user_read':
            case 'user_edit':
                $('kronolithC' + type + 'PUList').enable().setValue(users.join(', '));
                $('kronolithC' + type + 'PU').setValue(1);
                $('kronolithC' + type + 'PUPerms').setValue(basic.substring(5));
                $('kronolithC' + type + 'PAdvanced').hide();
                $('kronolithC' + type + 'PBasic').show();
                $('kronolithC' + type + 'PUPerms').enable();
                break;
            }
        }
   },

    /**
     * Propagates a SELECT drop down list with the groups.
     *
     * @param array params  A two-dimensional array with the following values
     *                      in each element:
     *                      - The id of the SELECT element.
     *                      - A callback method that is invoked with the group
     *                        list passes as an argument.
     */
    updateGroupDropDown: function(params)
    {
        this.groupLoading = true;
        params.each(function(param) {
            var elm = $(param[0]), options = elm.childElements();
            options.invoke('remove');
            elm.up('form').disable();
        });
        HordeCore.doAction('listGroups', {}, {
            callback: function(r) {
                var groups;
                if (r.groups) {
                    groups = $H(r.groups);
                    params.each(function(param) {
                        groups.each(function(group) {
                            $(param[0]).insert(new Element('option', { value: group.key }).update(group.value.escapeHTML()));
                        });
                    });
                }
                params.each(function(param) {
                    $(param[0]).up('form').enable();
                    if (param[1]) {
                        param[1](groups);
                    }
                });
                this.groupLoading = false;
            }.bind(this)
        });
    },

    /**
     * Updates the group permission interface after the group list has
     * been loaded.
     *
     * @param string type  The calendar type, 'internal' or 'taskslists'.
     * @param Hash groups  The list of groups.
     */
    updateGroupPerms: function(type, groups)
    {
        $('kronolithC' + type + 'PGSingle').clear();
        if (!groups) {
            $('kronolithC' + type + 'PGNew').up('div').hide();
            $('kronolithC' + type + 'PG').up('span').hide();
        } else {
            $('kronolithC' + type + 'PGNew').up('div').show();
            $('kronolithC' + type + 'PG').up('span').show();
            if (groups.size() == 1) {
                $('kronolithC' + type + 'PGName')
                    .update('&quot;' + groups.values()[0].escapeHTML() + '&quot;')
                    .show();
                $('kronolithC' + type + 'PGSingle').setValue(groups.keys()[0]);
                $('kronolithC' + type + 'PGList').hide();
            } else {
                $('kronolithC' + type + 'PGName').hide();
                $('kronolithC' + type + 'PGList').show();
            }
        }
    },

    /**
     * Inserts a group or user row into the advanced permissions interface.
     *
     * @param string type          The calendar type, 'internal' or
     *                             'taskslists'.
     * @param what string          Either 'group' or 'user'.
     * @param group string         The group id or user name to insert.
     *                             Defaults to the value of the drop down.
     * @param stay_basic boolean   Enforces to NOT switch to the advanced
     *                             permissions screen.
     *
     * @return boolean  Whether a row has been inserted.
     */
    insertGroupOrUser: function(type, what, id, stay_basic)
    {
        var elm = $(what == 'user' ? 'kronolithC' + type + 'PUNew' : 'kronolithC' + type + 'PGNew');
        if (id) {
            elm.setValue(id);
        }
        var value = elm.getValue();
        if (!value) {
            if (id) {
                HordeCore.notify(Kronolith.text.invalid_user + ': ' + id, 'horde.error');
            }
            return false;
        }

        var tr = elm.up('tr'),
            row = tr.clone(true).store('remove', true),
            td = row.down('td'),
            clearName = elm.tagName == 'SELECT' ? elm.options[elm.selectedIndex].text: elm.getValue();

        td.update();
        td.insert(clearName.escapeHTML())
            .insert(new Element('input', { type: 'hidden', name: (what == 'user' ? 'u' : 'g') + '_names[' + value + ']', value: value }));
        row.select('input[type=checkbox]').each(function(input) {
            input.writeAttribute('name', input.name.replace(/\[.*?$/, '[' + value + ']'))
                .writeAttribute('id', input.id.replace(/new/, value))
                .next()
                .writeAttribute('for', input.id);
        });
        tr.insert({ before: row });

        if (elm.tagName == 'SELECT') {
            elm.options[elm.selectedIndex].writeAttribute('disabled', true);
            elm.selectedIndex = 0;
        } else {
            elm.clear();
        }

        if (!stay_basic) {
            this.activateAdvancedPerms(type);
        }

        return true;
    },

    /**
     * Activates the advanced permissions.
     *
     * @param string type  The calendar type, 'internal' or 'taskslists'.
     */
    activateAdvancedPerms: function(type)
    {
        [$('kronolithC' + type + 'PNone'),
         $('kronolithC' + type + 'PU'),
         $('kronolithC' + type + 'PG')].each(function(radio) {
            radio.checked = false;
        });
        if ($('kronolithC' + type + 'PAll')) {
            $('kronolithC' + type + 'PAll').checked = false;
        }
        $('kronolithC' + type + 'PBasic').hide();
        $('kronolithC' + type + 'PAdvanced').show();
    },

    /**
     * Opens the next screen of the calendar management wizard.
     *
     * @param string type  The calendar type.
     */
    calendarNext: function(type)
    {
        var i = 1;
        while (!$('kronolithCalendar' + type + i).visible()) {
            i++;
        }
        $('kronolithCalendar' + type + i).hide();
        $('kronolithCalendar' + type + (++i)).show();
        if (this.colorPicker) {
            this.colorPicker.hide();
        }
    },

    /**
     * Submits the calendar form to save the calendar data.
     *
     * @param Element form  The form node.
     *
     * @return boolean  Whether the save request was successfully sent.
     */
    saveCalendar: function(form)
    {
        if (this.colorPicker) {
            this.colorPicker.hide();
        }
        var data = form.serialize({ hash: true });

        if (data.type == 'holiday') {
            this.insertCalendarInList('holiday', data.driver, Kronolith.conf.calendars.holiday[data.driver]);
            this.toggleCalendar('holiday', data.driver);
            form.down('.kronolithCalendarSave').enable();
            this.closeRedBox();
            this.go(this.lastLocation);
            return;
        }

        if (data.name.empty()) {
            HordeCore.notify(data.type == 'tasklists' ? Kronolith.text.no_tasklist_title : Kronolith.text.no_calendar_title, 'horde.warning');
            $('kronolithCalendar' + data.type + 'Name').focus();
            return false;
        }
        HordeCore.doAction('saveCalendar', data, {
            callback: this.saveCalendarCallback.bind(this, form, data)
        });
        return true;
    },

    calendarImport: function(form, disableForm)
    {
        if ($F('kronolithCalendarinternalImport')) {
            HordeCore.notify(Kronolith.text.import_warning, 'horde.message');
            this.loading++;
            $('kronolithLoading').show();
            var name = 'kronolithIframe' + Math.round(Math.random() * 1000),
                iframe = new Element('iframe', { src: 'about:blank', name: name, id: name }).setStyle({ display: 'none' });
            document.body.insert(iframe);
            form.enable();
            form.target = name;
            form.submit();
            if (disableForm) {
                form.disable();
            }
        }
    },

    /**
     * Callback method after saving a calendar.
     *
     * @param Element form  The form node.
     * @param object data   The serialized form data.
     * @param object r      The ajax response object.
     */
    saveCalendarCallback: function(form, data, r)
    {
        var type = form.id.replace(/kronolithCalendarForm/, '');

        // If saving the calendar changed the owner, we need to delete
        // and re-insert the calendar.
        if (r.deleted) {
            this.deleteCalendar(type, data.calendar);
            delete data.calendar;
        }
        if (r.saved) {
            this.calendarImport(form, false);
            var cal = r.calendar, id;
            if (data.calendar) {
                var color = {
                    backgroundColor: cal.bg,
                    color: cal.fg
                },
                legendSpan;
                id = data.calendar;
                this.getCalendarList(type, cal.owner).select('div').each(function(element) {
                    if (element.retrieve('calendar') == id) {
                        var link = element.down('.horde-resource-link span');
                        element.setStyle(color);
                        link.update(cal.name.escapeHTML());
                        this.addShareIcon(cal, link);
                        throw $break;
                    }
                }, this);
                this.kronolithBody.select('div').each(function(el) {
                    if (el.retrieve('calendar') == type + '|' + id) {
                        el.setStyle(color);
                    }
                });
                legendSpan = $('kronolith-legend').select('span')
                    .find(function(span) {
                        return span.retrieve('calendarclass') == type &&
                            span.retrieve('calendar') == id;
                    });
                if (legendSpan) {
                    legendSpan.setStyle(color).update(cal.name.escapeHTML());
                }
                Kronolith.conf.calendars[type][id] = cal;
            } else {
                id = r.id;
                if (!Kronolith.conf.calendars[type]) {
                    Kronolith.conf.calendars[type] = [];
                }
                Kronolith.conf.calendars[type][id] = cal;
                this.insertCalendarInList(type, id, cal);
                this.storeCache($H(), [type, id], this.viewDates(this.date, this.view), true);
                if (type == 'tasklists') {
                    this.storeTasksCache($H(), this.tasktype, id.replace(/^tasks\//, ''), true);
                }
            }
            if (type == 'remote') {
                this.loadCalendar(type, id);
            }
        }
        form.down('.kronolithCalendarSave').enable();
        this.closeRedBox();
        this.go(this.lastLocation);
    },

    /**
     * Deletes a calendar and all its events from the interface and cache.
     *
     * @param string type      The calendar type.
     * @param string calendar  The calendar id.
     */
    deleteCalendar: function(type, calendar)
    {
        var container = this.getCalendarList(type, Kronolith.conf.calendars[type][calendar].owner),
            noItems = container.previous(),
            div = container.select('div').find(function(element) {
                return element.retrieve('calendar') == calendar;
            }),
            arrow = div.down('span');
        arrow.purge();
        arrow.remove();
        div.purge();
        div.remove();
        if (noItems &&
            noItems.tagName == 'DIV' &&
            noItems.className == 'horde-info' &&
            !container.childElements().size()) {
            noItems.show();
        }
        this.deleteCalendarLegend(type, calendar);
        this.removeEvent(type + '|' + calendar);
        this.deleteCache([type, calendar]);
        if (type == 'tasklists' && this.view == 'tasks') {
            this.removeTask(calendar.replace(/^tasks\//, ''));
        }
        delete Kronolith.conf.calendars[type][calendar];
    },

    /**
     * Parses a date attribute string into a Date object.
     *
     * For other strings use Date.parse().
     *
     * @param string date  A yyyyMMdd date string.
     *
     * @return Date  A date object.
     */
    parseDate: function(date)
    {
        var d = new Date(date.substr(0, 4), date.substr(4, 2) - 1, date.substr(6, 2));
        if (date.length == 12) {
            d.setHours(date.substr(8, 2));
            d.setMinutes(date.substr(10, 2));
        }
        return d;
    },

    /**
     * Calculates first and last days being displayed.
     *
     * @var Date date    The date of the view.
     * @var string view  A view name.
     *
     * @return array  Array with first and last day of the view.
     */
    viewDates: function(date, view)
    {
        var start = date.clone(), end = date.clone();

        switch (view) {
        case 'week':
        case 'workweek':
            if (view == 'workweek') {
                start.add(1).days();
            }
            start.moveToBeginOfWeek(view == 'week' ? Kronolith.conf.week_start : 1);
            end = start.clone();
            end.moveToEndOfWeek(Kronolith.conf.week_start);
            if (view == 'workweek') {
                end.add(Kronolith.conf.week_start == 0 ? -1 : -2).days();
            }
            break;
        case 'month':
            start.setDate(1);
            start.moveToBeginOfWeek(Kronolith.conf.week_start);
            end.moveToLastDayOfMonth();
            end.moveToEndOfWeek(Kronolith.conf.week_start);
            break;
        case 'year':
            start.setDate(1);
            start.setMonth(0);
            end.setMonth(11);
            end.moveToLastDayOfMonth();
            break;
        case 'agenda':
            end.add(6).days();
            break;
        }

        return [start, end];
    },

    /**
     * Stores a set of events in the cache.
     *
     * For dates in the specified date ranges that don't contain any events,
     * empty cache entries are created so that those dates aren't re-fetched
     * each time.
     *
     * @param object events        A list of calendars and events as returned
     *                             from an ajax request.
     * @param string calendar      A calendar string or array.
     * @param string dates         A date range in the format yyyymmddyyyymmdd
     *                             as used in the ajax response signature.
     * @param boolean createCache  Whether to create a cache list entry for the
     *                             response, if none exists yet.
     */
    storeCache: function(events, calendar, dates, createCache)
    {
        if (Object.isString(calendar)) {
            calendar = calendar.split('|');
        }

        // Create cache entry for the calendar.
        if (!this.ecache.get(calendar[0])) {
            if (!createCache) {
                return;
            }
            this.ecache.set(calendar[0], $H());
        }
        if (!this.ecache.get(calendar[0]).get(calendar[1])) {
            if (!createCache) {
                return;
            }
            this.ecache.get(calendar[0]).set(calendar[1], $H());
        }
        var calHash = this.ecache.get(calendar[0]).get(calendar[1]);

        // Create empty cache entries for all dates.
        if (!!dates) {
            var day = dates[0].clone(), date;
            while (!day.isAfter(dates[1])) {
                date = day.dateString();
                if (!calHash.get(date)) {
                    if (!createCache) {
                        return;
                    }
                    if (!this.cacheStart || this.cacheStart.isAfter(day)) {
                        this.cacheStart = day.clone();
                    }
                    if (!this.cacheEnd || this.cacheEnd.isBefore(day)) {
                        this.cacheEnd = day.clone();
                    }
                    calHash.set(date, $H());
                }
                day.add(1).day();
            }
        }

        var cal = calendar.join('|');
        $H(events).each(function(date) {
            // We might not have a cache for this date if the event lasts
            // longer than the current view
            if (!calHash.get(date.key)) {
                return;
            }

            // Store calendar string and other useful information in event
            // objects.
            $H(date.value).each(function(event) {
                event.value.calendar = cal;
                event.value.start = Date.parse(event.value.s);
                event.value.end = Date.parse(event.value.e);
            });

            // Store events in cache.
            calHash.set(date.key, calHash.get(date.key).merge(date.value));
        });
    },

    /**
     * Stores a set of tasks in the cache.
     *
     * @param Hash tasks           The tasks to be stored.
     * @param string tasktypes     The task type that's being stored.
     * @param string tasklist      The task list to which the tasks belong.
     * @param boolean createCache  Whether to create a cache list entry for the
     *                             response, if none exists yet.
     */
    storeTasksCache: function(tasks, tasktypes, tasklist, createCache)
    {
        var taskHashes = {}, cacheExists = {};

        if (tasktypes == 'all' || tasktypes == 'future') {
            tasktypes = [ 'complete', 'incomplete' ];
        } else {
            tasktypes = [ tasktypes ];
        }

        tasktypes.each(function(tasktype) {
            cacheExists[tasktype] = false;
            if (!this.tcache.get(tasktype)) {
                if (!createCache) {
                    return;
                }
                this.tcache.set(tasktype, $H());
            }
            if (!tasklist) {
                return;
            }
            if (!this.tcache.get(tasktype).get(tasklist)) {
                if (!createCache) {
                    return;
                }
                this.tcache.get(tasktype).set(tasklist, $H());
                cacheExists[tasktype] = true;
            } else {
                cacheExists[tasktype] = true;
            }
            taskHashes[tasktype] = this.tcache.get(tasktype).get(tasklist);
        }, this);

        $H(tasks).each(function(task) {
            var tasktype = task.value.cp ? 'complete' : 'incomplete';
            if (!cacheExists[tasktype]) {
                return;
            }
            if (!Object.isUndefined(task.value.s)) {
                task.value.start = Date.parse(task.value.s);
            }
            if (!Object.isUndefined(task.value.du)) {
                task.value.due = Date.parse(task.value.du);
            }
            taskHashes[tasktype].set(task.key, task.value);
        });
    },

    /**
     * Deletes an event or a complete calendar from the cache.
     *
     * @param string calendar  A calendar string or array.
     * @param string event     An event ID or empty if deleting the calendar.
     * @param string day       A specific day to delete in yyyyMMdd form.
     */
    deleteCache: function(calendar, event, day)
    {
        if (Object.isString(calendar)) {
            calendar = calendar.split('|');
        }
        if (!this.ecache.get(calendar[0]) ||
            !this.ecache.get(calendar[0]).get(calendar[1])) {
            return;
        }
        if (event) {
            this.ecache.get(calendar[0]).get(calendar[1]).each(function(day) {
                day.value.unset(event);
            });
        } else if (day) {
            this.ecache.get(calendar[0]).get(calendar[1]).unset(day);
        } else {
            this.ecache.get(calendar[0]).unset(calendar[1]);
        }
    },

    /**
     * Deletes tasks from the cache.
     *
     * @param string list  A task list string.
     * @param string task  A task ID. If empty, all tasks from the list are
     *                     deleted.
     */
    deleteTasksCache: function(list, task)
    {
        this.deleteCache([ 'external', 'tasks/' + list ], task);
        $w('complete incomplete').each(function(type) {
            if (!Object.isUndefined(this.tcache.get(type)) &&
                !Object.isUndefined(this.tcache.get(type).get(list))) {
                if (task) {
                    this.tcache.get(type).get(list).unset(task);
                } else {
                    this.tcache.get(type).unset(list);
                }
            }
        }, this);
    },

    /**
     * Return all events for a single day from all displayed calendars merged
     * into a single hash.
     *
     * @param string date  A yyyymmdd date string.
     *
     * @return Hash  An event hash which event ids as keys and event objects as
     *               values.
     */
    getCacheForDate: function(date, calendar)
    {
        if (calendar) {
            var cals = calendar.split('|');
            if (!this.ecache.get(cals[0]) ||
                !this.ecache.get(cals[0]).get(cals[1])) {
                return $H();
            }
            var x = this.ecache.get(cals[0]).get(cals[1]).get(date);
            return this.ecache.get(cals[0]).get(cals[1]).get(date);
        }

        var events = $H();
        this.ecache.each(function(type) {
            type.value.each(function(cal) {
                if (!Kronolith.conf.calendars[type.key][cal.key].show) {
                    return;
                }
                events = events.merge(cal.value.get(date));
            });
        });
        return events;
    },

    /**
     * Helper method for Enumerable.sortBy to sort events first by start time,
     * second by end time reversed.
     *
     * @param Hash event  A hash entry with the event object as the value.
     *
     * @return string  A comparable string.
     */
    sortEvents: function(event)
    {
        return event.value.sort;
    },

    /**
     * Adds a new location to the history and displays it in the URL hash.
     *
     * This is not really a history, because only the current and the last
     * location are stored.
     *
     * @param string loc    The location to save.
     * @param boolean save  Whether to actually save the location. This should
     *                      be false for any location that are displayed on top
     *                      of another location, i.e. in a popup view.
     */
    addHistory: function(loc, save)
    {
        location.hash = encodeURIComponent(loc);
        this.lastLocation = this.currentLocation;
        if (Object.isUndefined(save) || save) {
            this.currentLocation = loc;
        }
        this.openLocation = loc;
    },

    /**
     * Loads an external page.
     *
     * @param string loc  The URL of the page to load.
     */
    loadPage: function(loc)
    {
        window.location.assign(loc);
    },

    searchSubmit: function(e)
    {
        this.go('search:' + this.search + ':' + $F('horde-search-input'));
    },

    searchReset: function(e)
    {
        HordeTopbar.searchGhost.reset();
    },

    /**
     * Event handler for HordeCore:showNotifications events.
     */
    showNotification: function(e)
    {
        if (!e.memo.flags ||
            !e.memo.flags.alarm ||
            !e.memo.flags.growl ||
            !e.memo.flags.alarm.params.notify.ajax) {
            return;
        }

        var growl = e.memo.flags.growl, link = growl.down('A');

        if (link) {
            link.observe('click', function(ee) {
                ee.stop();
                HordeCore.Growler.ungrowl(growl);
                this.go(e.memo.flags.alarm.params.notify.ajax);
            }.bind(this));
        }
    },

    /* Keydown event handler */
    keydownHandler: function(e)
    {
        if (e.stopped) {
            return;
        }

        var kc = e.keyCode || e.charCode,
            form = e.findElement('FORM'), trigger = e.findElement();

        switch (trigger.id) {
        case 'kronolithEventLocation':
            if (kc == Event.KEY_RETURN && $F('kronolithEventLocation')) {
                this.initializeMap(true);
                this.geocode($F('kronolithEventLocation'));
                e.stop();
                return;
            }
            break;

        case 'kronolithCalendarinternalUrlCaldav':
        case 'kronolithCalendarinternalUrlWebdav':
        case 'kronolithCalendarinternalUrlAccount':
        case 'kronolithCalendarinternalUrlFeed':
        case 'kronolithCalendartasklistsUrlCaldav':
        case 'kronolithCalendartasklistsUrlWebdav':
        case 'kronolithCalendartasklistsUrlAccount':
            if (String.fromCharCode(kc) != 'C' ||
                (this.macos && !e.metaKey) ||
                (!this.macos && !e.ctrlKey)) {
                e.stop();
                return;
            }
            break;
        }

        if (form) {
            switch (kc) {
            case Event.KEY_RETURN:
                switch (form.identify()) {
                case 'kronolithEventForm':
                    if (e.element().tagName != 'TEXTAREA') {
                        this.saveEvent();
                        e.stop();
                    }
                    break;

                case 'kronolithTaskForm':
                    if (e.element().tagName != 'TEXTAREA') {
                        this.saveTask();
                        e.stop();
                    }
                    break;

                case 'kronolithQuickinsertForm':
                    this.quickSaveEvent();
                    e.stop();
                    break;

                case 'kronolithCalendarForminternal':
                case 'kronolithCalendarFormtasklists':
                case 'kronolithCalendarFormremote':
                    // Disabled for now, we have to also catch Continue buttons.
                    //var saveButton = form.down('.kronolithCalendarSave');
                    //saveButton.disable();
                    //if (!this.saveCalendar(form)) {
                    //    saveButton.enable();
                    //}
                    //e.stop();
                    break;
                }
                break;

            case Event.KEY_ESC:
                switch (form.identify()) {
                case 'kronolithQuickinsertForm':
                case 'kronolithQuicktaskForm':
                    this.quickClose();
                    break;
                case 'kronolithEventForm':
                case 'kronolithTaskForm':
                    Horde_Calendar.hideCal();
                    this.closeRedBox();
                    this.go(this.lastLocation);
                    break;
                }
                break;
            }

            return;
        }

        switch (kc) {
        case Event.KEY_ESC:
            Horde_Calendar.hideCal();
            this.closeRedBox();
            break;
        }
    },

    keyupHandler: function(e)
    {
        switch (e.element().readAttribute('id')) {
        case 'kronolithEventLocation':
            if ($F('kronolithEventLocation') && Kronolith.conf.maps.driver) {
                $('kronolithEventMapLink').show();
            } else if (Kronolith.conf.maps.driver) {
                $('kronolithEventMapLink').hide();
                this.removeMapMarker();
            }
            return;

        case 'kronolithEventStartTime':
        case 'kronolithEventEndTime':
            var field = $(e.element().readAttribute('id')), kc = e.keyCode;

            switch(e.keyCode) {
            case Event.KEY_UP:
            case Event.KEY_DOWN:
            case Event.KEY_RIGHT:
            case Event.KEY_LEFT:
                return;
            default:
                if ($F(field) !== this.knl[field.identify()].getCurrentEntry()) {
                    this.knl[field.identify()].markSelected(null);
                }
                return;
            }
        }

    },

    clickHandler: function(e, dblclick)
    {
        if (e.isRightClick() || typeof e.element != 'function') {
            return;
        }

        var elt = e.element(),
            orig = e.element(),
            id, tmp, calendar;

        while (Object.isElement(elt)) {
            id = elt.readAttribute('id');

            switch (id) {
            case 'kronolithNewEvent':
                this.go('event');
                e.stop();
                return;

            case 'kronolithNewTask':
                this.go('task');
                e.stop();
                return;

            case 'kronolithQuickEvent':
                if (this.view == 'tasks') {
                    RedBox.showHtml($('kronolithQuicktask').show());
                } else {
                    this.updateCalendarDropDown('kronolithQuickinsertCalendars');
                    $('kronolithQuickinsertCalendars').setValue(Kronolith.conf.default_calendar);
                    RedBox.showHtml($('kronolithQuickinsert').show());
                }
                e.stop();
                return;

            case 'kronolithQuickinsertSave':
                this.quickSaveEvent();
                e.stop();
                return;

            case 'kronolithQuicktaskSave':
                this.quickSaveTask();
                e.stop();
                return;

            case 'kronolithQuickinsertCancel':
            case 'kronolithQuicktaskCancel':
                this.quickClose();
                e.stop();
                return;

            case 'kronolithGotoToday':
                var view = this.view;
                if (!$w('day workweek week month year agenda').include(view)) {
                    view = Kronolith.conf.login_view;
                }
                this.go(view + ':' + new Date().dateString());
                e.stop();
                return;

            case 'kronolithEventAllday':
                this.toggleAllDay();
                break;

            case 'kronolithEventAlarmDefaultOn':
                this.disableAlarmMethods('Event');
                break;

            case 'kronolithTaskAlarmDefaultOn':
                this.disableAlarmMethods('Task');
                break;

            case 'kronolithEventAlarmPrefs':
                HordeCore.redirect(HordeCore.addURLParam(
                    Kronolith.conf.prefs_url,
                    {
                        group: 'notification'
                    }
                ));
                e.stop();
                break;

            case 'kronolithTaskAlarmPrefs':
                if (Kronolith.conf.tasks.prefs_url) {
                    HordeCore.redirect(HordeCore.addURLParam(
                        Kronolith.conf.tasks.prefs_url,
                        {
                            group: 'notification'
                        }
                    ));
                }
                e.stop();
                break;

            case 'kronolithEventLinkNone':
            case 'kronolithEventLinkDaily':
            case 'kronolithEventLinkWeekly':
            case 'kronolithEventLinkMonthly':
            case 'kronolithEventLinkYearly':
            case 'kronolithEventLinkLength':
            case 'kronolithTaskLinkNone':
            case 'kronolithTaskLinkDaily':
            case 'kronolithTaskLinkWeekly':
            case 'kronolithTaskLinkMonthly':
            case 'kronolithTaskLinkYearly':
            case 'kronolithTaskLinkLength':
                this.toggleRecurrence(
                    id.startsWith('kronolithEvent'),
                    id.substring(id.startsWith('kronolithEvent') ? 18 : 17));
                break;

            case 'kronolithEventRepeatDaily':
            case 'kronolithEventRepeatWeekly':
            case 'kronolithEventRepeatMonthly':
            case 'kronolithEventRepeatYearly':
            case 'kronolithEventRepeatLength':
            case 'kronolithTaskRepeatDaily':
            case 'kronolithTaskRepeatWeekly':
            case 'kronolithTaskRepeatMonthly':
            case 'kronolithTaskRepeatYearly':
            case 'kronolithTaskRepeatLength':
                this.toggleRecurrence(
                    id.startsWith('kronolithEvent'),
                    id.substring(id.startsWith('kronolithEvent') ? 20 : 19));
                break;

            case 'kronolithEventSave':
                if (!elt.disabled) {
                    this._checkDate($('kronolithEventStartDate'));
                    this._checkDate($('kronolithEventEndDate'));
                    if ($F('kronolithEventAttendees') && $F('kronolithEventId')) {
                        $('kronolithEventSendUpdates').setValue(0);
                        $('kronolithEventDiv').hide();
                        $('kronolithUpdateDiv').show();
                        e.stop();
                        break;
                    }
                }
            case 'kronolithEventSendUpdateYes':
                if (this.uatts) {
                    this.uatts.u = true;
                } else {
                    $('kronolithEventSendUpdates').setValue(1);
                }
            case 'kronolithEventSendUpdateNo':
                if (this.uatts) {
                    this.doDragDropUpdate(this.uatts, this.ucb);
                    this.uatts = null;
                    this.ucb = null;
                    this.closeRedBox();
                    $('kronolithUpdateDiv').hide();
                    $('kronolithEventDiv').show();
                } else if (!elt.disabled) {
                    this.saveEvent();
                }
                e.stop();
                break;
            case 'kronolithEventConflictYes':
                this.doSaveEvent();
                e.stop();
                break;
            case 'kronolithEventConflictNo':
                $('kronolithConflictDiv').hide();
                $('kronolithEventDiv').show();
                e.stop();
                break;
            case 'kronolithEventSaveAsNew':
                if (!elt.disabled) {
                    $('kronolithEventSendUpdates').setValue(1);
                    this.saveEvent(true);
                }
                e.stop();
                break;

            case 'kronolithTaskSave':
                if (!elt.disabled) {
                    this.saveTask();
                }
                e.stop();
                break;

            case 'kronolithEventDeleteCancel':
                $('kronolithDeleteDiv').hide();
                $('kronolithEventDiv').show();
                e.stop();
                return;

            case 'kronolithEventSendCancellationYes':
                $('kronolithRecurDeleteAll').enable();
                $('kronolithRecurDeleteCurrent').enable();
                $('kronolithRecurDeleteFuture').enable();
                this.paramsCache.sendupdates = 1;
            case 'kronolithEventSendCancellationNo':
                $('kronolithRecurDeleteAll').enable();
                $('kronolithRecurDeleteCurrent').enable();
                $('kronolithRecurDeleteFuture').enable();
                $('kronolithCancellationDiv').hide();
                this.delete_verified = true;
            case 'kronolithEventDelete':
                if ((Kronolith.conf.confirm_delete || this.recurs) && !this.delete_verified) {
                    $('kronolithEventDiv').hide();
                    $('kronolithDeleteDiv').show();
                    e.stop();
                    break;
                } else {
                    $('kronolithEventDiv').hide();
                    this.delete_verified = false;
                }
                // Fallthrough
            case 'kronolithRecurDeleteAll':
            case 'kronolithRecurDeleteCurrent':
            case 'kronolithRecurDeleteFuture':
            case 'kronolithEventDeleteConfirm':
                if (elt.disabled) {
                    e.stop();
                    break;
                }
                elt.disable();
                var cal = $F('kronolithEventCalendar'),
                    eventid = $F('kronolithEventId');
                if (id != 'kronolithEventSendCancellationNo' &&
                    id != 'kronolithEventSendCancellationYes') {
                    this.paramsCache = {
                        cal: cal,
                        id: eventid,
                        rstart: $F('kronolithEventRecurOStart'),
                        cstart: this.cacheStart.toISOString(),
                        cend: this.cacheEnd.toISOString()
                    };
                    switch (id) {
                    case 'kronolithRecurDeleteAll':
                        this.paramsCache.r = 'all';
                        break;
                    case 'kronolithRecurDeleteCurrent':
                        this.paramsCache.r = 'current';
                        break;
                    case 'kronolithRecurDeleteFuture':
                        this.paramsCache.r = 'future';
                        break;
                    }
                }

                if (id != 'kronolithEventSendCancellationNo'
                    && id != 'kronolithEventSendCancellationYes'
                    && $F('kronolithEventAttendees')) {

                    $('kronolithDeleteDiv').hide();
                    $('kronolithCancellationDiv').show();
                    e.stop();
                    break;
                }

                this.kronolithBody.select('div').findAll(function(el) {
                    return el.retrieve('calendar') == cal &&
                        el.retrieve('eventid') == eventid;
                }).invoke('hide');
                var viewDates = this.viewDates(this.date, this.view),
                start = viewDates[0].toString('yyyyMMdd'),
                end = viewDates[1].toString('yyyyMMdd');
                this.paramsCache.sig = start + end + (Math.random() + '').slice(2);
                this.paramsCache.view_start = start;
                this.paramsCache.view_end = end;

                HordeCore.doAction('deleteEvent', this.paramsCache, {
                    callback: function(elt,r) {
                        if (r.deleted) {
                            var days;
                            if (this.view == 'month' ||
                                this.view == 'week' ||
                                this.view == 'workweek' ||
                                this.view == 'day') {
                                days = this.findEventDays(cal, eventid);
                                days.each(function(day) {
                                    this.refreshResources(day, cal, eventid);
                                }.bind(this));
                            }
                            this.removeEvent(cal, eventid);
                            if (r.uid) {
                                this.removeException(cal, r.uid);
                            }
                            this.loadEventsCallback(r, false);
                            if (days && days.length) {
                                this.reRender(days);
                            }
                        } else {
                            this.kronolithBody.select('div').findAll(function(el) {
                                return el.retrieve('calendar') == cal &&
                                       el.retrieve('eventid') == eventid;
                            }).invoke('show');
                        }
                        elt.enable();
                    }.curry(elt).bind(this)
                });

                $('kronolithDeleteDiv').hide();
                $('kronolithEventDiv').show();
                this.closeRedBox();
                this.go(this.lastLocation);
                e.stop();
                break;

            case 'kronolithTaskDelete':
                if (elt.disabled) {
                    e.stop();
                    break;
                }

                elt.disable();
                var tasklist = $F('kronolithTaskOldList'),
                    taskid = $F('kronolithTaskId');

                HordeCore.doAction('deleteTask', {
                    list: tasklist,
                    id: taskid
                }, {
                    callback: function(r) {
                        if (r.deleted) {
                            this.removeTask(tasklist, taskid);
                        } else {
                            elt.enable();
                            $('kronolithViewTasksBody').select('tr').find(function(el) {
                                return el.retrieve('tasklist') == tasklist &&
                                       el.retrieve('taskid') == taskid;
                            }).toggle();
                        }
                    }.bind(this)
                });

                var taskrow = $('kronolithViewTasksBody').select('tr').find(function(el) {
                    return el.retrieve('tasklist') == tasklist &&
                        el.retrieve('taskid') == taskid;
                });
                if (taskrow) {
                    taskrow.hide();
                }
                this.closeRedBox();
                this.go(this.lastLocation);
                e.stop();
                break;

            case 'kronolithCinternalPMore':
            case 'kronolithCinternalPLess':
            case 'kronolithCtasklistsPMore':
            case 'kronolithCtasklistsPLess':
                var type = id.match(/kronolithC(.*)P/)[1];
                $('kronolithC' + type + 'PBasic').toggle();
                $('kronolithC' + type + 'PAdvanced').toggle();
                e.stop();
                break;

            case 'kronolithCinternalPNone':
            case 'kronolithCinternalPAll':
            case 'kronolithCinternalPG':
            case 'kronolithCinternalPU':
            case 'kronolithCtasklistsPNone':
            case 'kronolithCtasklistsPAll':
            case 'kronolithCtasklistsPG':
            case 'kronolithCtasklistsPU':
                var info = id.match(/kronolithC(.*)P(.*)/);
                this.permsClickHandler(info[1], info[2]);
                break;

            case 'kronolithCinternalPAllShow':
            case 'kronolithCtasklistsPAllShow':
                var type = id.match(/kronolithC(.*)P/)[1];
                this.permsClickHandler(type, 'All');
                break;

            case 'kronolithCinternalPAdvanced':
            case 'kronolithCtasklistsPAdvanced':
                var type = id.match(/kronolithC(.*)P/)[1];
                if (orig.tagName != 'INPUT') {
                    break;
                }
                this.activateAdvancedPerms(type);
                if (orig.name.match(/u_.*||new/)) {
                    this.insertGroupOrUser(type, 'user');
                }
                break;

            case 'kronolithCinternalPUAdd':
            case 'kronolithCinternalPGAdd':
            case 'kronolithCtasklistsPUAdd':
            case 'kronolithCtasklistsPGAdd':
                var info = id.match(/kronolithC(.*)P(.)/);
                this.insertGroupOrUser(info[1], info[2] == 'U' ? 'user' : 'group');
                break;

            case 'kronolithNavDay':
            case 'kronolithNavWeek':
            case 'kronolithNavWorkweek':
            case 'kronolithNavMonth':
            case 'kronolithNavYear':
            case 'kronolithNavAgenda':
                this.go(id.substring(12).toLowerCase() + ':' + this.date.dateString());
                e.stop();
                return;

            case 'kronolithNavTasks':
                this.go('tasks');
                e.stop();
                return;

            case 'kronolithTasksAll':
            case 'kronolithTasksComplete':
            case 'kronolithTasksIncomplete':
            case 'kronolithTasksFuture':
                this.go('tasks:' + id.substring(14).toLowerCase());
                e.stop();
                return;

            case 'kronolithMinicalDate':
                this.go('month:' + orig.retrieve('date'));
                e.stop();
                return;

            case 'kronolith-minical':
                if (orig.id == 'kronolith-minical-prev') {
                    var date = this.parseDate($('kronolithMinicalDate').retrieve('date'));
                    date.previous().month();
                    this.updateMinical(date, date.getMonth() == this.date.getMonth() ? this.view : undefined);
                    e.stop();
                    return;
                }
                if (orig.id == 'kronolith-minical-next') {
                    var date = this.parseDate($('kronolithMinicalDate').retrieve('date'));
                    date.next().month();
                    this.updateMinical(date, date.getMonth() == this.date.getMonth() ? this.view : null);
                    e.stop();
                    return;
                }

                var tmp = orig;
                if (tmp.tagName.toLowerCase() != 'td') {
                    tmp = tmp.up('td');
                }
                if (tmp) {
                    if (tmp.retrieve('weekdate') &&
                        tmp.hasClassName('kronolith-minical-week')) {
                        this.go('week:' + tmp.retrieve('weekdate'));
                    } else if (tmp.retrieve('date') &&
                               !tmp.hasClassName('empty')) {
                        this.go('day:' + tmp.retrieve('date'));
                    }
                }
                e.stop();
                return;

            case 'kronolithEventsDay':
                var date = this.date.clone();
                date.add(Math.round((e.pointerY() - elt.cumulativeOffset().top + elt.up('.kronolithViewBody').scrollTop) / this.daySizes.height * 2) * 30).minutes();
                this.go('event:' + date.toString('yyyyMMddHHmm'));
                e.stop();
                return;

            case 'kronolithViewMonth':
                if (orig.hasClassName('kronolith-first-col')) {
                    var date = orig.retrieve('date');
                    if (date) {
                        this.go('week:' + date);
                        e.stop();
                        return;
                    }
                }
                e.stop();
                return;

            case 'kronolithViewYear':
                var tmp = orig;
                if (tmp.tagName.toLowerCase() != 'td' && tmp.tagName.toLowerCase() != 'th') {
                    tmp = tmp.up('td');
                }
                if (tmp) {
                    if (tmp.retrieve('weekdate') &&
                        tmp.hasClassName('kronolith-minical-week')) {
                        this.go('week:' + tmp.retrieve('weekdate'));
                    } else if (tmp.hasClassName('kronolithMinicalDate')) {
                        this.go('month:' + tmp.retrieve('date'));
                    } else if (tmp.retrieve('date') &&
                               !tmp.hasClassName('empty')) {
                        this.go('day:' + tmp.retrieve('date'));
                    }
                }
                e.stop();
                return;

            case 'kronolithViewAgendaBody':
                var tmp = orig;
                if (tmp.tagName != 'TR') {
                    tmp = tmp.up('tr');
                }
                if (tmp && tmp.retrieve('date')) {
                    this.go('day:' + tmp.retrieve('date'));
                }
                e.stop();
                return;

            case 'kronolithSearchButton':
                this.go('search:' + this.search + ':' + $F('horde-search-input'));
                e.stop();
                break;

            case 'kronolithSearchFuture':
                if (this.search != 'future') {
                    this.go('search:future:' + $F('horde-search-input'));
                }
                e.stop();
                break;

            case 'kronolithSearchPast':
                if (this.search != 'past') {
                    this.go('search:past:' + $F('horde-search-input'));
                }
                e.stop();
                break;

            case 'kronolithSearchAll':
                if (this.search != 'all') {
                    this.go('search:all:' + $F('horde-search-input'));
                }
                e.stop();
                break;
            case 'kronolithEventToTimeslice':
                var params = $H();
                params.set('e', $('kronolithEventId').value);
                params.set('cal', $('kronolithEventCalendar').value);
                params.set('t', $('kronolithEventTimesliceType').value);
                params.set('c', $('kronolithEventTimesliceClient').value);
                HordeCore.doAction('toTimeslice', params);
                break;
            case 'kronolithEventDialog':
            case 'kronolithTaskDialog':
                Horde_Calendar.hideCal();
                return;

            case 'kronolithCalendarDialog':
                if (this.colorPicker) {
                    this.colorPicker.hide();
                }
                return;

            case 'kronolithEditRecurCurrent':
            case 'kronolithEditRecurFuture':
                $('kronolithEventStartDate').setValue(this.orstart);
                $('kronolithEventEndDate').setValue(this.orend);
                if (id == 'kronolithEditRecurCurrent') {
                    this.toggleRecurrence('Exception');
                } else {
                    this.toggleRecurrence(this.lastRecurType);
                }
                return;
            case 'kronolithEditRecurAll':
                this.toggleRecurrence(this.lastRecurType);
                break;
            case 'kronolithEventUrlToggle':
                $('kronolithEventUrlDisplay').hide();
                $('kronolithEventUrl').show();
                e.stop();
                return;
            case 'kronolithCalendarinternalImportButton':
                // Used when user has edit perms to a shared calendar.
                this.calendarImport(elt.up('form'), true);
                break;
            }

            // Caution, this only works if the element has definitely only a
            // single CSS class.
            switch (elt.className) {
            case 'kronolithPrev':
            case 'kronolithNext':
                var newDate = this.date.clone(),
                    offset = elt.className == 'kronolithPrev' ? -1 : 1;
                switch (this.view) {
                case 'day':
                case 'agenda':
                    newDate.add(offset).day();
                    break;
                case 'week':
                case 'workweek':
                    newDate.add(offset).week();
                    break;
                case 'month':
                    newDate.add(offset).month();
                    break;
                case 'year':
                    newDate.add(offset).year();
                    break;
                }
                this.go(this.view + ':' + newDate.dateString());
                e.stop();
                return;

            case 'horde-add':
                this.go('calendar:' + id.replace(/kronolithAdd/, ''));
                e.stop();
                return;

            case 'kronolithTabLink':
                this.openTab(elt);
                e.stop();
                break;

            case 'horde-cancel':
                this.closeRedBox();
                this.resetMap();
                this.go(this.lastLocation);
                e.stop();
                break;

            case 'kronolithEventTag':
                HordeImple.AutoCompleter.kronolithEventTags.addNewItemNode(elt.getText());
                e.stop();
                break;

            case 'kronolithCalendarTag':
                HordeImple.AutoCompleter.kronolithCalendarinternalTags.addNewItemNode(elt.getText());
                e.stop();
                break;

            case 'kronolithTaskTag':
                HordeImple.AutoCompleter.kronolithTaskTags.addNewItemNode(elt.getText());
                e.stop();
                break;

            case 'kronolithEventGeo':
                this.initializeMap(true);
                this.geocode($F('kronolithEventLocation'));
                e.stop();
                break;

            case 'kronolithTaskRow':
                if (elt.retrieve('taskid')) {
                    this.go('task:' + elt.retrieve('tasklist') + ':' + elt.retrieve('taskid'));
                }
                e.stop();
                return;

            case 'horde-resource-edit-000':
            case 'horde-resource-edit-fff':
                this.go('calendar:' + elt.up().retrieve('calendarclass') + '|' + elt.up().retrieve('calendar'));
                e.stop();
                return;

            case 'kronolithMore':
                this.go('day:' + elt.retrieve('date'));
                e.stop();
                return;

            case 'kronolithDatePicker':
                id = elt.readAttribute('id');
                Horde_Calendar.open(id, Date.parseExact($F(id.replace(/Picker$/, 'Date')), Kronolith.conf.date_format));
                e.stop();
                return;

            case 'kronolithColorPicker':
                var input = elt.previous();
                this.colorPicker = new ColorPicker({
                    color: $F(input),
                    offsetParent: elt,
                    update: [[input, 'value'],
                             [input, 'background']]
                });
                e.stop();
                return;
            }

            if (elt.hasClassName('kronolith-event')) {
                if (!Object.isUndefined(elt.retrieve('ajax'))) {
                    this.go(elt.retrieve('ajax'));
                } else {
                    this.go('event:' + elt.retrieve('calendar') + ':' + elt.retrieve('eventid') + ':' + elt.up().retrieve('date'));
                }
                e.stop();
                return;
            } else if (elt.hasClassName('kronolithMonthDay')) {
                if (orig.hasClassName('kronolith-day')) {
                    var date = orig.retrieve('date');
                    if (date) {
                        this.go('day:' + date);
                        e.stop();
                        return;
                    }
                }
                this.go('event:' + elt.retrieve('date'));
                e.stop();
                return;
            } else if (elt.hasClassName('kronolithWeekDay')) {
                this.go('day:' + elt.retrieve('date'));
                e.stop();
                return;
            } else if (elt.hasClassName('kronolithEventsWeek') ||
                       elt.hasClassName('kronolithEventsWorkweek') ||
                       elt.hasClassName('kronolithAllDayContainer')) {
                var date = elt.retrieve('date');
                if (elt.hasClassName('kronolithAllDayContainer')) {
                    date += 'all';
                } else {
                    date = this.parseDate(date);
                    date.add(Math.round((e.pointerY() - elt.cumulativeOffset().top + elt.up('.kronolithViewBody').scrollTop) / (elt.hasClassName('kronolithEventsWeek') ? this.weekSizes.height : this.workweekSizes.height) * 2) * 30).minutes();
                    date = date.toString('yyyyMMddHHmm');
                }
                this.go('event:' + date);
                e.stop();
                return;
            } else if (elt.hasClassName('kronolithTaskCheckbox')) {
                var taskid = elt.up('tr.kronolithTaskRow', 0).retrieve('taskid'),
                    tasklist = elt.up('tr.kronolithTaskRow', 0).retrieve('tasklist');
                this.toggleCompletionClass(taskid);

                HordeCore.doAction('toggleCompletion', {
                    list: tasklist,
                    id: taskid
                }, {
                    callback: function(r) {
                        if (r.toggled) {
                            this.toggleCompletion(tasklist, taskid, r.toggled);
                            if (r.toggled !== true) {
                                this.toggleCompletionClass(taskid);
                            }
                        } else {
                            this.toggleCompletionClass(taskid);
                        }
                    }.bind(this)
                });

                e.stop();
                return;
            } else if (elt.hasClassName('kronolithCalendarSave')) {
                if (!elt.disabled) {
                    elt.disable();
                    if (!this.saveCalendar(elt.up('form'))) {
                        elt.enable();
                    }
                }
                e.stop();
                break;
            } else if (elt.hasClassName('kronolithCalendarContinue')) {
                if (elt.disabled) {
                    e.stop();
                    break;
                }

                elt.disable();
                var form = elt.up('form'),
                    type = form.id.replace(/kronolithCalendarForm/, ''),
                    i = 1;
                while (!$('kronolithCalendar' + type + i).visible()) {
                    i++;
                }
                if (type == 'remote') {
                    var params = { url: $F('kronolithCalendarremoteUrl') };
                    if (i == 1) {
                        if (!$F('kronolithCalendarremoteUrl')) {
                            HordeCore.notify(Kronolith.text.no_url, 'horde.warning');
                            e.stop();
                            break;
                        }

                        HordeCore.doAction('getRemoteInfo', params, {
                            asynchronous: false,
                            callback: function(r) {
                                if (r.success) {
                                    if (r.name) {
                                        $('kronolithCalendarremoteName').setValue(r.name);
                                    }
                                    if (r.desc) {
                                        $('kronolithCalendarremoteDescription').setValue(r.desc);
                                    }
                                    this.calendarNext(type);
                                    this.calendarNext(type);
                                } else if (r.auth) {
                                    this.calendarNext(type);
                                } else {
                                    elt.enable();
                                }
                            }.bind(this)
                        });

                    }
                    if (i == 2) {
                        if ($F('kronolithCalendarremoteUsername')) {
                            params.user = $F('kronolithCalendarremoteUsername');
                            params.password =  $F('kronolithCalendarremotePassword');
                        }

                        HordeCore.doAction('getRemoteInfo', params, {
                            callback: function(r) {
                                if (r.success) {
                                    if (r.name &&
                                        !$F('kronolithCalendarremoteName')) {
                                        $('kronolithCalendarremoteName').setValue(r.name);
                                    }
                                    if (r.desc &&
                                        !$F('kronolithCalendarremoteDescription')) {
                                        $('kronolithCalendarremoteDescription').setValue(r.desc);
                                    }
                                    this.calendarNext(type);
                                } else {
                                    if (r.auth) {
                                        HordeCore.notify(Kronolith.text.wrong_auth, 'horde.warning');
                                    }
                                    elt.enable();
                                }
                            }.bind(this)
                        });
                    }
                    e.stop();
                    break;
                }
                this.calendarNext(type);
                e.stop();
                break;
            } else if (elt.hasClassName('kronolithCalendarDelete')) {
                var form = elt.up('form'),
                    type = form.id.replace(/kronolithCalendarForm/, ''),
                    calendar = $F('kronolithCalendar' + type + 'Id');

                if ((type == 'tasklists' &&
                     !window.confirm(Kronolith.text.delete_tasklist)) ||
                    (type != 'tasklists' &&
                     !window.confirm(Kronolith.text.delete_calendar))) {
                    e.stop();
                    break;
                }

                if (!elt.disabled) {
                    elt.disable();

                    HordeCore.doAction('deleteCalendar', {
                        type: type,
                        calendar: calendar
                    }, {
                        callback: function(r) {
                            if (r.deleted) {
                                this.deleteCalendar(type, calendar);
                            }
                            this.closeRedBox();
                            this.go(this.lastLocation);
                        }.bind(this)
                    });
                }
                e.stop();
                break;
            } else if (elt.hasClassName('kronolithCalendarSubscribe') ||
                       elt.hasClassName('kronolithCalendarUnsubscribe')) {
                var form = elt.up('form');
                this.toggleCalendar($F(form.down('input[name=type]')),
                                    $F(form.down('input[name=calendar]')));
                this.closeRedBox();
                this.go(this.lastLocation);
                e.stop();
                break;
            } else if (elt.tagName == 'INPUT' &&
                       (elt.name == 'event_alarms[]' ||
                        elt.name == 'task[alarm_methods][]')) {
                if (elt.name == 'event_alarms[]') {
                    $('kronolithEventAlarmOn').setValue(1);
                    $('kronolithEventAlarmDefaultOff').setValue(1);
                } else {
                    $('kronolithTaskAlarmOn').setValue(1);
                    $('kronolithTaskAlarmDefaultOff').setValue(1);
                }
                if ($(elt.id + 'Params')) {
                    if (elt.getValue()) {
                        $(elt.id + 'Params').show();
                    } else {
                        $(elt.id + 'Params').hide();
                    }
                }
                break;
            }

            var calClass = elt.retrieve('calendarclass');
            if (calClass) {
                this.toggleCalendar(calClass, elt.retrieve('calendar'));
                e.stop();
                return;
            }

            elt = elt.up();
        }
        // Workaround Firebug bug.
        Prototype.emptyFunction();
    },

    /**
     * Handles date selections from a date picker.
     */
    datePickerHandler: function(e)
    {
        var field = e.element().previous();
        field.setValue(e.memo.toString(Kronolith.conf.date_format));
        this.updateTimeFields(field.identify());
    },

    /**
     * Handles moving an event to a different day in month view and all day
     * events in weekly/daily views.
     */
    onDrop: function(e)
    {
        var drop = e.element(),
            el = e.memo.element;

        if (drop == el.up()) {
            return;
        }

        var lastDate = this.parseDate(el.up().retrieve('date')),
            newDate = this.parseDate(drop.retrieve('date')),
            diff = newDate.subtract(lastDate),
            eventid = el.retrieve('eventid'),
            cal = el.retrieve('calendar'),
            viewDates = this.viewDates(this.date, this.view),
            start = viewDates[0].toString('yyyyMMdd'),
            end = viewDates[1].toString('yyyyMMdd'),
            sig = start + end + (Math.random() + '').slice(2),
            events = this.getCacheForDate(lastDate.toString('yyyyMMdd'), cal),
            attributes = $H({ offDays: diff }),
            event = events.find(function(e) { return e.key == eventid; });

        drop.insert(el);
        this.startLoading(cal, sig);
        if (event.value.r) {
            attributes.set('rday', lastDate);
            attributes.set('cstart', this.cacheStart);
            attributes.set('cend', this.cacheEnd);
        }
        var uatts = {
            cal: cal,
            id: eventid,
            view: this.view,
            sig: sig,
            view_start: start,
            view_end: end,
            att: Object.toJSON(attributes)
        },
        callback = function(r) {
          if (r.events) {
              // Check if this is the still the result of the
              // most current request.
              if (r.sig == this.eventsLoading[r.cal]) {
                  var days;
                  if ((this.view == 'month' &&
                       Kronolith.conf.max_events) ||
                      this.view == 'week' ||
                      this.view == 'workweek' ||
                      this.view == 'day') {
                      days = this.findEventDays(cal, eventid);
                  }
                  this.removeEvent(cal, eventid);
                  if (days && days.length) {
                      this.reRender(days);
                  }
              }
              $H(r.events).each(function(days) {
                  $H(days.value).each(function(event) {
                      if (event.value.c.startsWith('tasks/')) {
                          var tasklist = event.value.c.substr(6),
                              task = event.key.substr(6),
                              taskObject;
                          if (this.tcache.get('incomplete') &&
                              this.tcache.get('incomplete').get(tasklist) &&
                              this.tcache.get('incomplete').get(tasklist).get(task)) {
                              taskObject = this.tcache.get('incomplete').get(tasklist).get(task);
                              taskObject.due = Date.parse(event.value.s);
                              this.tcache.get('incomplete').get(tasklist).set(task, taskObject);
                          }
                      }
                  }, this);
              }, this);
          }
          this.loadEventsCallback(r, false);
          $H(r.events).each(function(days) {
              $H(days.value).each(function(event) {
                  if (event.key == eventid) {
                      this.refreshResources(days.key, cal, eventid, lastDate.toString('yyyyMMdd'), event);
                  }
              }.bind(this))
          }.bind(this));
      }.bind(this);

      if (event.value.mt) {
          $('kronolithEventDiv').hide();
          $('kronolithUpdateDiv').show();
          RedBox.showHtml($('kronolithEventDialog').show());
          this.uatts = uatts;
          this.ucb = callback;
      } else {
          this.doDragDropUpdate(uatts, callback);
      }
    },

    onDragStart: function(e)
    {
        if (this.view == 'month') {
            return;
        }

        var elt = e.element();

        if (elt.hasClassName('kronolithDragger')) {
            elt.up().addClassName('kronolith-selected');
            DragDrop.Drags.getDrag(elt).top = elt.cumulativeOffset().top;
        } else if (elt.hasClassName('kronolithEditable')) {
            elt.addClassName('kronolith-selected').setStyle({ left: 0, width: (this.view == 'week' || this.view == 'workweek') ? '90%' : '95%', zIndex: 1 });
        }

        this.scrollTop = $('kronolithView' + this.view.capitalize())
            .down('.kronolithViewBody')
            .scrollTop;
        this.scrollLast = this.scrollTop;
    },

    onDrag: function(e)
    {
        if (this.view == 'month') {
            return;
        }

        var elt = e.element(),
            drag = DragDrop.Drags.getDrag(elt);
            storage = this.view + 'Sizes',
            step = this[storage].height / 6;

            if (!drag.event) {
                return;
            }

        var event = drag.event.value;

        if (elt.hasClassName('kronolithDragger')) {
            // Resizing the event.
            var div = elt.up(),
                top = drag.ghost.cumulativeOffset().top,
                scrollTop = $('kronolithView' + this.view.capitalize()).down('.kronolithViewBody').scrollTop,
                offset = 0,
                height;

            // Check if view has scrolled since last call.
            if (scrollTop != this.scrollLast) {
                offset = scrollTop - this.scrollLast;
                this.scrollLast = scrollTop;
            }
            if (elt.hasClassName('kronolithDraggerTop')) {
                offset += top - drag.top;
                height = div.offsetHeight - offset;
                div.setStyle({
                    top: (div.offsetTop + offset) + 'px'
                });
                offset = drag.ghost.offsetTop;
                drag.top = top;
            } else {
                offset += top - drag.top;
                height = div.offsetHeight + offset;
                offset = div.offsetTop;
                drag.top = top;
            }
            div.setStyle({
                height: height + 'px'
            });

            this.calculateEventDates(event, storage, step, offset, height);
            drag.innerDiv.update('(' + event.start.toString(Kronolith.conf.time_format) + ' - ' + event.end.toString(Kronolith.conf.time_format) + ') ' + event.t.escapeHTML());
        } else if (elt.hasClassName('kronolithEditable')) {
            // Moving the event.
            if (Object.isUndefined(drag.innerDiv)) {
                drag.innerDiv = drag.ghost.down('.kronolith-event-info');
            }
            if ((this.view == 'week') || (this.view == 'workweek')) {
                var offsetX = Math.round(drag.ghost.offsetLeft / drag.stepX);
                event.offsetDays = offsetX;
                this.calculateEventDates(event, storage, step, drag.ghost.offsetTop, drag.divHeight, event.start.clone().addDays(offsetX), event.end.clone().addDays(offsetX));
            } else {
                event.offsetDays = 0;
                this.calculateEventDates(event, storage, step, drag.ghost.offsetTop, drag.divHeight);
            }
            event.offsetTop = drag.ghost.offsetTop - drag.startTop;
            drag.innerDiv.update('(' + event.start.toString(Kronolith.conf.time_format) + ' - ' + event.end.toString(Kronolith.conf.time_format) + ') ' + event.t.escapeHTML());
            elt.clonePosition(drag.ghost, { offsetLeft: (this.view == 'week' || this.view == 'workweek') ? -2 : 0 });
        }
    },

    onDragEnd: function(e)
    {
        if (this.view == 'month') {
            return;
        }

        if (!e.element().hasClassName('kronolithDragger') &&
            !e.element().hasClassName('kronolithEditable')) {
            return;
        }

        var div = e.element(),
            drag = DragDrop.Drags.getDrag(div),
            event = drag.event;


        if (event.value.al) {
            return;
        }
        var date = drag.midnight,
            storage = this.view + 'Sizes',
            step = this[storage].height / 6,
            dates = this.viewDates(date, this.view),
            start = dates[0].dateString(),
            end = dates[1].dateString(),
            sig = start + end + (Math.random() + '').slice(2),
            element, attributes;

        div.removeClassName('kronolith-selected');
        if (!Object.isUndefined(drag.innerDiv)) {
            this.setEventText(drag.innerDiv, event.value);
        }
        this.startLoading(event.value.calendar, sig);
        if (!Object.isUndefined(event.value.offsetTop)) {
            attributes = $H({ offDays: event.value.offsetDays,
                              offMins: Math.round(event.value.offsetTop / step) * 10 });
            element = div;
        } else if (div.hasClassName('kronolithDraggerTop')) {
            attributes = $H({ start: event.value.start });
            element = div.up();
        } else if (div.hasClassName('kronolithDraggerBottom')) {
            attributes = $H({ end: event.value.end });
            element = div.up();
        } else {
            attributes = $H({ start: event.value.start,
                              end: event.value.end });
            element = div;
        }
        if (event.value.r) {
            attributes.set('rstart', event.value.s);
            attributes.set('rend', event.value.e);
            attributes.set('cstart', this.cacheStart);
            attributes.set('cend', this.cacheEnd);
        }
        element.retrieve('drags').invoke('destroy');
        var uatts = {
            cal: event.value.calendar,
            id: event.key,
            view: this.view,
            sig: sig,
            view_start: start,
            view_end: end,
            att: Object.toJSON(attributes)
        },
        callback = function(r) {
            // Check if this is the still the result of the most current
            // request.
            if (r.events &&
                r.sig == this.eventsLoading[r.cal]) {
                if (event.value.rs) {
                    var d = new Date(event.value.s);
                    this.refreshResources(d.toString('yyyyMMdd'), event.value.calendar, event.key)
                }
                this.removeEvent(event.value.calendar, event.key);
            }
            this.loadEventsCallback(r, false);
        }.bind(this);

        if (event.value.mt) {
            $('kronolithEventDiv').hide();
            $('kronolithUpdateDiv').show();
            RedBox.showHtml($('kronolithEventDialog').show());
            this.uatts = uatts;
            this.ucb = callback;
        } else {
            this.doDragDropUpdate(uatts, callback);
        }
    },

    doDragDropUpdate: function(att, cb)
    {
        HordeCore.doAction('updateEvent', att, {
            callback: cb
        });
    },

    /**
     * Refresh any resource calendars bound to the given just-updated event.
     * Clears the old resource event from UI and cache, and clears the cache
     * for the days of the new event, in order to allow listEvents to refresh
     * the UI.
     *
     * @param  string dt       The current/new date for the event (yyyyMMdd).
     * @param  string cal      The calendar the event exists in.
     * @param  string eventid  The eventid that is changing.
     * @param  string last_dt  The previous date for the event, if known. (yyyyMMdd).
     * @param  object event    The event object (if a new event) dt is ignored.
     *
     */
    refreshResources: function(dt, cal, eventid, last_dt, event)
    {
        var events = this.getCacheForDate(dt, cal),
            update_cals = [], r_dates;

        if (!event) {
            event = events.find(function(e) { return e.key == eventid; });
        }
        if (!dt) {
            dt = new Date(event.value.s);
        } else {
            dt = this.parseDate(dt);
        }
        if (event) {
            $H(event.value.rs).each(function(r) {
                var r_cal = ['resource', r.value.calendar],
                    r_events = this.getCacheForDate(last_dt, r_cal.join('|')),
                    r_event, day, end;

                if (r_events) {
                    r_event = r_events.find(function(e) { return e.value.uid == event.value.uid });
                    if (r_event) {
                        this.removeEvent(r_cal, r_event.key);
                        day = new Date(r_event.value.s);
                        end = new Date(r_event.value.s);
                        while (!day.isAfter(end)) {
                            this.deleteCache(r_cal, null, day.toString('yyyyMMdd'));
                            day.add(1).day();
                        }
                        day = new Date(event.value.s);
                        end = new Date(event.value.e);

                        while (!day.isAfter(end)) {
                            this.deleteCache(r_cal, null, day.toString('yyyyMMdd'));
                            day.add(1).day();
                        }
                    } else {
                        // Don't know the previous date/time so just nuke the cache.
                       this.deleteCache(r_cal);
                    }
                } else {
                    this.deleteCache(r_cal);
                }
                update_cals.push(r_cal);
            }.bind(this));

            if (update_cals.length) {
                dates = this.viewDates(dt, this.view);
                // Ensure we also grab the full length of the events.
                if (dates[0].isAfter(dt)) {
                    dates[0] = dt;
                }
                var dt_end = new Date(event.value.e);
                if (dt_end.isAfter(dates[1])) {
                    dates[1] = dt_end;
                }
                this.loadEvents(dates[0], dates[1], this.view, update_cals);
            }
        }
    },

    editEvent: function(calendar, id, date, title)
    {
        if (this.redBoxLoading) {
            return;
        }
        if (Object.isUndefined(HordeImple.AutoCompleter.kronolithEventTags)) {
            this.editEvent.bind(this, calendar, id, date).defer();
            return;
        }

        this.closeRedBox();
        this.quickClose();
        this.redBoxOnDisplay = RedBox.onDisplay;
        RedBox.onDisplay = function() {
            if (this.redBoxOnDisplay) {
                this.redBoxOnDisplay();
            }
            try {
                $('kronolithEventForm').focusFirstElement();
            } catch(e) {}
            if (Kronolith.conf.maps.driver &&
                $('kronolithEventLinkMap').up().hasClassName('horde-active') &&
                !this.mapInitialized) {

                this.initializeMap();
            }
            RedBox.onDisplay = this.redBoxOnDisplay;
        }.bind(this);
        this.attendees = [];
        this.resources = [];
        this.updateCalendarDropDown('kronolithEventTarget');
        this.toggleAllDay(false);
        this.openTab($('kronolithEventForm').down('.tabset a.kronolithTabLink'));
        this.disableAlarmMethods('Event');
        this.knl.kronolithEventStartTime.markSelected();
        this.knl.kronolithEventEndTime.markSelected();
        $('kronolithEventForm').reset();
        this.resetMap();
        HordeImple.AutoCompleter.kronolithEventAttendees.reset();
        HordeImple.AutoCompleter.kronolithEventTags.reset();
        HordeImple.AutoCompleter.kronolithEventResources.reset();
        if (Kronolith.conf.maps.driver) {
            $('kronolithEventMapLink').hide();
        }
        $('kronolithEventSave').show().enable();
        $('kronolithEventSaveAsNew').show().enable();
        $('kronolithEventDelete').show().enable();
        $('kronolithEventDeleteConfirm').enable();
        $('kronolithEventTarget').show();
        $('kronolithEventTargetRO').hide();
        $('kronolithEventForm').down('.kronolithFormActions .kronolithSeparator').show();
        $('kronolithEventExceptions').clear();
        if (id) {
            // An id passed to this function indicates we are editing an event.
            RedBox.loading();
            var attributes = { cal: calendar, id: id, date: date };
            // Need the current st and et of this instance.
            var events = this.getCacheForDate(date.toString('yyyyMMdd'), calendar);
            if (events) {
                var ev = events.find(function(e) { return e.key == id; });
                if (ev[1].r) {
                    attributes.rsd = ev[1].start.dateString();
                    attributes.red = ev[1].end.dateString();
                }
            }
            HordeCore.doAction('getEvent', attributes, {
                callback: this.editEventCallback.bind(this)
            });
            $('kronolithEventTopTags').update();
        } else {
            // This is a new event.
            HordeCore.doAction('listTopTags', {}, {
                callback: this.topTagsCallback.curry('kronolithEventTopTags', 'kronolithEventTag')
            });
            var d;
            if (date) {
                if (date.endsWith('all')) {
                    date = date.substring(0, date.length - 3);
                    $('kronolithEventAllday').setValue(true);
                    this.toggleAllDay(true);
                }
                d = this.parseDate(date);
            } else {
                d = new Date();
            }
            if (title) {
                $('kronolithEventTitle').setValue(title);
            }
            $('kronolithEventId').clear();
            $('kronolithEventCalendar').clear();
            $('kronolithEventTarget').setValue(Kronolith.conf.default_calendar);
            $('kronolithEventDelete').hide();
            $('kronolithEventStartDate').setValue(d.toString(Kronolith.conf.date_format));
            $('kronolithEventStartTime').setValue(d.toString(Kronolith.conf.time_format));
            this.updateFBDate(d);
            d.add(1).hour();
            this.duration = 60;
            $('kronolithEventEndDate').setValue(d.toString(Kronolith.conf.date_format));
            $('kronolithEventEndTime').setValue(d.toString(Kronolith.conf.time_format));
            $('kronolithEventLinkExport').up('span').hide();
            $('kronolithEventSaveAsNew').hide();
            $('kronolithEventUrlDisplay').hide();
            $('kronolithEventUrl').show();
            this.toggleRecurrence(true, 'None');
            $('kronolithEventEditRecur').hide();
            this.enableAlarm('Event', Kronolith.conf.default_alarm);
            this.redBoxLoading = true;
            RedBox.showHtml($('kronolithEventDialog').show());
        }
    },

    /**
     * Generates ajax request parameters for requests to save events.
     *
     * @return object  An object with request parameters.
     */
    saveEventParams: function()
    {
        var viewDates = this.viewDates(this.date, this.view),
            params = {
                sig: viewDates[0].dateString() + viewDates[1].dateString(),
                view: this.view
            };
        if (this.cacheStart) {
            params.view_start = this.cacheStart.dateString();
            params.view_end = this.cacheEnd.dateString();
        }
        return params;
    },

    /**
     * Submits the event edit form to create or update an event.
     */
    saveEvent: function(asnew)
    {
        this.validateEvent(asnew);
    },

    /**
     * Perform any preliminary checks necessary. doSaveEvent will be called from
     * the callback if checks are successful.
     *
     */
    validateEvent: function(asnew)
    {
        if (this.wrongFormat.size()) {
            HordeCore.notify(Kronolith.text.fix_form_values, 'horde.warning');
            return;
        }

        // Check that there are no conflicts.
        if (Kronolith.conf.has_resources && $F('kronolithEventResourceIds')) {
            HordeCore.doAction(
                'checkResources',
                {
                    s: this.getDate('start').toISOString(),
                    e: this.getDate('end').toISOString(),
                    i: $F('kronolithEventId'),
                    c: $F('kronolithEventCalendar'),
                    r: $F('kronolithEventResourceIds')
                },
                {
                    callback: this.validateEventCallback.curry(asnew).bind(this)
                }
            );
        } else {
            this.validateEventCallback(asnew, {});
        }
    },

    validateEventCallback: function(asnew, r)
    {
        var conflict = false;

        $H(r).each(function(a) {
            // 3 == Kronolith::RESPONSE_DECLINED
            if (a.value == 3) {
                $('kronolithEventDiv').hide();
                $('kronolithConflictDiv').show();
                conflict = true;
                return;
            }
        });
        if (!conflict) {
            this.doSaveEvent(asnew);
        }
    },

    doSaveEvent: function(asnew)
    {
        var cal = $F('kronolithEventCalendar'),
            target = $F('kronolithEventTarget'),
            eventid = $F('kronolithEventId'),
            params;

        if (this.mapInitialized) {
            $('kronolithEventMapZoom').value = this.map.getZoom();
        }

        params = $H($('kronolithEventForm').serialize({ hash: true }))
            .merge(this.saveEventParams());
        params.set('as_new', asnew ? 1 : 0);
        if (this.cacheStart) {
            params.set('cstart', this.cacheStart.toISOString());
            params.set('cend', this.cacheEnd.toISOString());
        }
        HordeImple.AutoCompleter.kronolithEventTags.shutdown();
        $('kronolithEventSave').disable();
        $('kronolithEventSaveAsNew').disable();
        $('kronolithEventDelete').disable();
        this.startLoading(target, params.get('sig'));
        HordeCore.doAction('saveEvent', params, {
            callback: function(r) {
                if (!asnew && r.events && eventid) {
                    this.removeEvent(cal, eventid);
                }
                this.loadEventsCallback(r, false);

                // Refresh bound exceptions
                var calendar = cal.split('|'), refreshed = false;
                $H(r.events).each(function(d) {
                    $H(d.value).each(function(evt) {
                        if (evt.value.bid) {
                            var cache = this.getCacheForDate(this.findEventDays(cal, evt.key, cal));
                            cache.each(function(entry) {
                                if (entry.value.bid == evt.value.bid && evt.value.c != calendar[1]) {
                                    this.removeEvent(cal, entry.key);
                                }
                            }.bind(this));
                        }
                        if (!refreshed && ((evt.key == eventid) || !eventid) && evt.value.rs) {
                            this.refreshResources(null, cal, eventid, false, evt);
                            refreshed = true;
                        }
                    }.bind(this))
                }.bind(this));

                if (r.events) {
                    this.resetMap();
                    this.closeRedBox();
                    this.go(this.lastLocation);
                } else {
                    $('kronolithEventSave').enable();
                    $('kronolithEventSaveAsNew').enable();
                    $('kronolithEventDelete').enable();
                }
                $('kronolithUpdateDiv').hide();
                $('kronolithConflictDiv').hide();
                $('kronolithEventDiv').show();
            }.bind(this)
        });
    },

    quickSaveEvent: function()
    {
        var text = $F('kronolithQuickinsertQ'),
            cal = $F('kronolithQuickinsertCalendars'),
            params;

        params = $H($('kronolithEventForm').serialize({ hash: true }))
            .merge(this.saveEventParams());
        params.set('text', text);
        params.set('cal', cal);

        this.closeRedBox();
        this.startLoading(cal, params.get('sig'));
        HordeCore.doAction('quickSaveEvent', params, {
            callback: function(r) {
                this.loadEventsCallback(r, false);
                if (r.error) {
                    this.editEvent(null, null, null, text);
                } else {
                    $('kronolithQuickinsertQ').value = '';
                }
             }.bind(this)
         });
    },

    /**
     * Closes and resets the quick event form.
     */
    quickClose: function()
    {
        $('kronolithQuickinsertQ').value = '';
        if ($('kronolithQuicktaskQ')) {
            $('kronolithQuicktaskQ').value = '';
        }
        this.closeRedBox();
    },

    topTagsCallback: function(update, tagclass, r)
    {
        $('kronolithEventTabTags').select('label').invoke('show');
        if (!r.tags) {
            $(update).update();
            return;
        }

        var t = new Element('ul', { className: 'horde-tags' });
        r.tags.each(function(tag) {
            if (tag == null) {
                return;
            }
            t.insert(new Element('li', { className: tagclass }).update(tag.escapeHTML()));
        });
        $(update).update(t);
    },

    /**
     * Callback method for showing event forms.
     *
     * @param object r  The ajax response object.
     */
    editEventCallback: function(r)
    {
        if (!r.event) {
            RedBox.close();
            this.go(this.lastLocation);
            return;
        }

        var ev = r.event;

        if (!Object.isUndefined(ev.ln)) {
            this.loadPage(ev.ln);
            this.closeRedBox();
            return;
        }

        /* Basic information */
        $('kronolithEventId').setValue(ev.id);
        $('kronolithEventCalendar').setValue(ev.ty + '|' + ev.c);
        $('kronolithEventTarget').setValue(ev.ty + '|' + ev.c);
        $('kronolithEventTargetRO').update(Kronolith.conf.calendars[ev.ty][ev.c].name.escapeHTML());
        $('kronolithEventTitle').setValue(ev.t);
        $('kronolithEventLocation').setValue(ev.l);
        $('kronolithEventTimezone').setValue(ev.tz);
        if (ev.l && Kronolith.conf.maps.driver) {
            $('kronolithEventMapLink').show();
        }
        if (ev.uhl) {
            $('kronolithEventUrlDisplay').down().update(ev.uhl);
            $('kronolithEventUrlDisplay').show();
            $('kronolithEventUrl').hide();
        }
        else {
            $('kronolithEventUrlDisplay').hide();
            $('kronolithEventUrl').show();
        }

        if (ev.u) {
            $('kronolithEventUrl').setValue(ev.u);
        }

        $('kronolithEventAllday').setValue(ev.al);

        if (ev.r && ev.rsd && ev.red) {
            // Save the original datetime, so we can properly create the
            // exception.
            var osd = Date.parse(ev.rsd + ' ' + ev.st);
            var oed = Date.parse(ev.red + ' ' + ev.et);

            $('kronolithEventRecurOStart').setValue(osd.toString('s'));
            $('kronolithEventRecurOEnd').setValue(oed.toString('s'));

            // ...and put the same value in the form field to replace the
            // date of the initial series.
            $('kronolithEventStartDate').setValue(ev.sd);
            $('kronolithEventEndDate').setValue(ev.ed);
            // Save the current datetime in case we are not editing 'all'
            this.orstart = ev.rsd;
            this.orend = ev.red;
        } else {
            $('kronolithEventStartDate').setValue(ev.sd);
            $('kronolithEventEndDate').setValue(ev.ed);
            $('kronolithEventRecurEnd').clear();
            $('kronolithEventRecurOStart').clear();
            $('kronolithEventRecurOEnd').clear();
            this.orstart = null;
            this.orend = null;
        }

        $('kronolithEventStartTime').setValue(ev.st);
        this.knl.kronolithEventStartTime.setSelected(ev.st);
        this.updateFBDate(Date.parseExact(ev.sd, Kronolith.conf.date_format));
        $('kronolithEventEndTime').setValue(ev.et);
        this.knl.kronolithEventEndTime.setSelected(ev.et);
        this.duration = Math.abs(Date.parse(ev.e).getTime() - Date.parse(ev.s).getTime()) / 60000;
        this.toggleAllDay(ev.al);
        $('kronolithEventStatus').setValue(ev.x);
        $('kronolithEventDescription').setValue(ev.d);
        $('kronolithEventPrivate').setValue(ev.pv);
        $('kronolithEventLinkExport').up('span').show();
        $('kronolithEventExport').href = Kronolith.conf.URI_EVENT_EXPORT.interpolate({ id: ev.id, calendar: ev.c, type: ev.ty });

        /* Alarm */
        if (ev.a) {
            this.enableAlarm('Event', ev.a);
            if (ev.m) {
                $('kronolithEventAlarmDefaultOff').checked = true;
                $H(ev.m).each(function(method) {
                    $('kronolithEventAlarm' + method.key).setValue(1);
                    if ($('kronolithEventAlarm' + method.key + 'Params')) {
                        $('kronolithEventAlarm' + method.key + 'Params').show();
                        $H(method.value).each(function(param) {
                            var input = $('kronolithEventAlarmParam' + param.key);
                            if (input.type == 'radio') {
                                input.up('form').select('input[type=radio]').each(function(radio) {
                                    if (radio.name == input.name &&
                                        radio.value == param.value) {
                                        radio.setValue(1);
                                        throw $break;
                                    }
                                });
                            } else {
                                input.setValue(param.value);
                            }
                        });
                    }
                });
            }
        } else {
            $('kronolithEventAlarmOff').setValue(true);
        }

        /* Recurrence */
        if (ev.r) {
            this.setRecurrenceFields(true, ev.r);
            $('kronolithRecurDelete').show();
            $('kronolithNoRecurDelete').hide();
            $('kronolithEventEditRecur').show();
            this.recurs = true;
        } else if (ev.bid) {
            $('kronolithRecurDelete').hide();
            $('kronolithNoRecurDelete').show();
            $('kronolithEventEditRecur').hide();
            var div = $('kronolithEventRepeatException');
            div.down('span').update(ev.eod);
            this.toggleRecurrence(true, 'Exception');
            this.recurs = false;
        } else {
            $('kronolithRecurDelete').hide();
            $('kronolithNoRecurDelete').show();
            $('kronolithEventEditRecur').hide();
            this.toggleRecurrence(true, 'None');
            this.recurs = false;
        }

        /* Attendees */
        if (!Object.isUndefined(ev.at)) {
            HordeImple.AutoCompleter.kronolithEventAttendees.reset(ev.at.pluck('l'));
            ev.at.each(this.addAttendee.bind(this));
            if (this.fbLoading) {
                $('kronolithFBLoading').show();
            }
        }

        /* Resources */
        if (!Object.isUndefined(ev.rs)) {
            var rs = $H(ev.rs);
            HordeImple.AutoCompleter.kronolithEventResources.reset(rs.values().pluck('name'));
            rs.each(function(r) { this.addResource(r.value, r.key); }.bind(this));
            if (this.fbLoading) {
                $('kronolithResourceFBLoading').show();
            }
        }

        /* Tags */
        HordeImple.AutoCompleter.kronolithEventTags.reset(ev.tg);

        /* Geo */
        if (ev.gl) {
            $('kronolithEventLocationLat').value = ev.gl.lat;
            $('kronolithEventLocationLon').value = ev.gl.lon;
            $('kronolithEventMapZoom').value = Math.max(1, ev.gl.zoom);
        }

        if (!ev.pe) {
            $('kronolithEventSave').hide();
            HordeImple.AutoCompleter.kronolithEventTags.disable();
            $('kronolithEventTabTags').select('label').invoke('hide');
        } else {
            HordeCore.doAction('listTopTags', {}, {
                callback: this.topTagsCallback.curry('kronolithEventTopTags', 'kronolithEventTag')
            });
        }
        if (!ev.pd) {
            $('kronolithEventDelete').hide();
            $('kronolithEventTarget').hide();
            $('kronolithEventTargetRO').show();
        }

        this.setTitle(ev.t);
        this.redBoxLoading = true;
        RedBox.showHtml($('kronolithEventDialog').show());

        /* Hide alarm message for this event. */
        if (r.msgs) {
            r.msgs = r.msgs.reject(function(msg) {
                if (msg.type != 'horde.alarm') {
                    return false;
                }
                var alarm = msg.flags.alarm;
                if (alarm.params && alarm.params.notify &&
                    alarm.params.notify.show &&
                    alarm.params.notify.show.calendar &&
                    alarm.params.notify.show.event &&
                    alarm.params.notify.show.calendar == ev.c &&
                    alarm.params.notify.show.event == ev.id) {
                    return true;
                }
                return false;
            });
        }
    },

    /**
     * Adds an attendee row to the free/busy table.
     *
     * @param object attendee  An attendee object with the properties:
     *                         - e: email address
     *                         - l: the display name of the attendee
     */
    addAttendee: function(attendee)
    {
        if (typeof attendee == 'string') {
            if (attendee.include('@')) {
                HordeCore.doAction('parseEmailAddress', {
                    email: attendee
                }, {
                    callback: function (r) {
                        if (r.email) {
                            this.addAttendee({ e: r.email, l: attendee });
                        }
                    }.bind(this)
                });
                return;
            } else {
                attendee = { l: attendee };
            }
        }

        if (attendee.e) {
            this.attendees.push(attendee);
            this.fbLoading++;
            HordeCore.doAction('getFreeBusy', {
                email: attendee.e
            }, {
                callback: function(r) {
                    this.fbLoading--;
                    if (!this.fbLoading) {
                        $('kronolithFBLoading').hide();
                    }
                    if (!Object.isUndefined(r.fb)) {
                        this.freeBusy.get(attendee.l)[1] = r.fb;
                        this.insertFreeBusy(attendee.l, this.getFBDate());
                    }
                }.bind(this)
            });
        }

        var tr = new Element('tr'), response, i;
        this.freeBusy.set(attendee.l, [ tr ]);
        attendee.r = attendee.r || 1;
        switch (attendee.r) {
            case 1: response = 'None'; break;
            case 2: response = 'Accepted'; break;
            case 3: response = 'Declined'; break;
            case 4: response = 'Tentative'; break;
        }
        tr.insert(new Element('td')
                  .writeAttribute('title', attendee.l)
                  .addClassName('kronolithAttendee' + response)
                  .insert(attendee.e ? attendee.e.escapeHTML() : attendee.l.escapeHTML()));
        for (i = 0; i < 24; i++) {
            tr.insert(new Element('td', { className: 'kronolithFBUnknown' }));
        }
        $('kronolithEventAttendeesList').down('tbody').insert(tr);
    },

    resetFBRows: function()
    {
        this.attendees.each(function(attendee) {
            var row = this.freeBusy.get(attendee.l)[0];
            row.update();

            attendee.r = attendee.r || 1;
            switch (attendee.r) {
                case 1: response = 'None'; break;
                case 2: response = 'Accepted'; break;
                case 3: response = 'Declined'; break;
                case 4: response = 'Tentative'; break;
            }
            row.insert(new Element('td')
                      .writeAttribute('title', attendee.l)
                      .addClassName('kronolithAttendee' + response)
                      .insert(attendee.e ? attendee.e.escapeHTML() : attendee.l.escapeHTML()));
            for (i = 0; i < 24; i++) {
                row.insert(new Element('td', { className: 'kronolithFBUnknown' }));
            }
        }.bind(this));
        this.resources.each(function(resource) {
            var row = this.freeBusy.get(resource)[0],
                tdone = row.down('td');
            row.update();
            row.update(tdone);
            for (i = 0; i < 24; i++) {
                row.insert(new Element('td', { className: 'kronolithFBUnknown' }));
            }
        }.bind(this));
    },

    addResource: function(resource, id)
    {
        var v, response = 1;
        if (!id) {
            // User entered
            this.resourceACCache.choices.each(function(i) {
                if (i.name == resource) {
                    v = i.code;
                    throw $break;
                } else {
                    v = false;
                }
            }.bind(this));
        } else {
            // Populating from an edit event action
            v = id;
            response = resource.response;
            resource = resource.name;
        }

        switch (response) {
            case 1: response = 'None'; break;
            case 2: response = 'Accepted'; break;
            case 3: response = 'Declined'; break;
            case 4: response = 'Tentative'; break;
        }
        var att = {
            'resource': v
        },
        tr, i;
        if (att.resource) {
            this.fbLoading++;
            HordeCore.doAction('getFreeBusy', att, {
                callback: this.addResourceCallback.curry(resource).bind(this)
            });
            tr = new Element('tr');
            this.freeBusy.set(resource, [ tr ]);
            tr.insert(new Element('td')
                .writeAttribute('title', resource)
                .addClassName('kronolithAttendee' + response)
                .insert(resource.escapeHTML()));
            for (i = 0; i < 24; i++) {
                tr.insert(new Element('td', { className: 'kronolithFBUnknown' }));
            }
            $('kronolithEventResourcesList').down('tbody').insert(tr);
            this.resourceACCache.map.set(resource, v);
            $('kronolithEventResourceIds').value = this.resourceACCache.map.values();
        } else {
            HordeCore.notify(Kronolith.text.unknown_resource + ': ' + resource, 'horde.error');
        }
    },

    removeResource: function(resource)
    {
        var row = this.freeBusy.get(resource)[0];
        row.purge();
        row.remove();
        this.resourceACCache.map.unset(resource);
        $('kronolithEventResourceIds').value = this.resourceACCache.map.values();
    },

    addResourceCallback: function(resource, r)
    {
        this.fbLoading--;
        if (!this.fbLoading) {
            $('kronolithResourceFBLoading').hide();
        }
        if (Object.isUndefined(r.fb)) {
            return;
        }
        this.resources.push(resource);
        this.freeBusy.get(resource)[1] = r.fb;
        this.insertFreeBusy(resource);
    },

    /**
     * Removes an attendee row from the free/busy table.
     *
     * @param string attendee  The display name of the attendee.
     */
    removeAttendee: function(attendee)
    {
        var row = this.freeBusy.get(attendee)[0];
        row.purge();
        row.remove();
    },

    normalizeAttendee: function(attendee)
    {
        var pattern = /:(.*);/;
        var match = pattern.exec(attendee);
        if (match) {
           return match[1].split(',');
        }
        return [attendee];
    },

    checkOrganizerAsAttendee: function()
    {
        if (HordeImple.AutoCompleter.kronolithEventAttendees.selectedItems.length == 1 &&
            HordeImple.AutoCompleter.kronolithEventAttendees.selectedItems.first().rawValue != Kronolith.conf.email) {
            // Invite the organizer of this event to the new event.
            HordeImple.AutoCompleter.kronolithEventAttendees.addNewItemNode(Kronolith.conf.email);
            this.addAttendee(Kronolith.conf.email);
        }
    },

    getFBDate: function ()
    {
        var startDate = $('kronolithFBDate').innerHTML.split(' ');
        if (startDate.length > 1) {
            startDate = startDate[1];
        } else {
            startDate = startDate[0];
        }
        return Date.parseExact(startDate, Kronolith.conf.date_format);
    },

    /**
     * Updates rows with free/busy information in the attendees table.
     *
     * @param string attendee  An attendee display name as the free/busy
     *                         identifier.
     * @param date   start     An optinal start date for f/b info. If omitted,
     *                         $('kronolithEventStartDate') is used.
     */
    insertFreeBusy: function(attendee, start)
    {
        if (!$('kronolithEventDialog').visible() ||
            !this.freeBusy.get(attendee)) {
            return;
        }
        var fb = this.freeBusy.get(attendee)[1],
            tr = this.freeBusy.get(attendee)[0],
            td = tr.select('td')[1],
            div = td.down('div'), start;
        if (!fb) {
            return;
        }

        if (!td.getWidth()) {
            this.insertFreeBusy.bind(this, attendee, start).defer();
            return;
        }

        if (div) {
            div.purge();
            div.remove();
        }
        if (!start) {
            start = Date.parseExact($F('kronolithEventStartDate'), Kronolith.conf.date_format);
        }
        var end = start.clone().add(1).days(),
            width = td.getWidth(),
            fbs = this.parseDate(fb.s),
            fbe = this.parseDate(fb.e);


        if (start.isBefore(fbs) || end.isBefore(fbs) || start.isAfter(fbe)) {
            return;
        }

        tr.select('td').each(function(td, i) {
            if (i != 0) {
                td.className = 'kronolithFBFree';
            }
            i++;
        });
        div = new Element('div').setStyle({ position: 'relative', height: td.offsetHeight + 'px' });
        td.insert(div);
        $H(fb.b).each(function(busy) {
            var left, from = Date.parse(busy.key).addSeconds(1),
            to = Date.parse(busy.value).addSeconds(1);
            if (!end.isAfter(from) || to.isBefore(start)) {
                return;
            }
            if (from.isBefore(start)) {
                from = start.clone();
            }
            if (to.isAfter(end)) {
                to = end.clone();
            }
            if (to.getHours() === 0 && to.getMinutes() === 0) {
                to.add(-1).minutes();
            }
            left = from.getHours() + from.getMinutes() / 60;
            div.insert(new Element('div', { className: 'kronolithFBBusy' }).setStyle({ zIndex: 1, top: 0, left: (left * width) + 'px', width: (((to.getHours() + to.getMinutes() / 60) - left) * width) + 'px' }));
        });

    },

    fbStartDateOnChange: function()
    {
        if (!$F('kronolithEventStartDate')) {
          this._checkDate($('kronolithEventStartDate'));
          return;
        }
        this.fbStartDateHandler(Date.parseExact($F('kronolithEventStartDate'), Kronolith.conf.date_format));
    },

    /**
     * @param Date start  The start date.
     */
    fbStartDateHandler: function(start)
    {
        this.updateFBDate(start);
        this.resetFBRows();
        // Need to check visisbility - multiple changes will break the display
        // due to the use of .defer() in insertFreeBusy().
        if ($('kronolithEventTabAttendees').visible()) {
            this.attendeeStartDateHandler(start);
        }
        if ($('kronolithEventTabResources').visible()) {
            this.resourceStartDateHandler(start);
        }
    },

    attendeeStartDateHandler: function(start)
    {
        this.attendees.each(function(attendee) {
            this.insertFreeBusy(attendee.l, start);
        }, this);
    },

    resourceStartDateHandler: function(start)
    {
        this.resources.each(function(resource) {
            this.insertFreeBusy(resource, start);
        }, this);
    },

    nextFreebusy: function()
    {
        this.fbStartDateHandler(this.getFBDate().addDays(1));
    },

    prevFreebusy: function()
    {
        this.fbStartDateHandler(this.getFBDate().addDays(-1));
    },

    /**
     * @start Date object
     */
    updateFBDate: function(start)
    {
        $('kronolithFBDate').update(start.toString('dddd') + ' ' + start.toString(Kronolith.conf.date_format));
        $('kronolithResourceFBDate').update(start.toString('dddd') + ' ' + start.toString(Kronolith.conf.date_format));
    },

    /**
     * Toggles the start and end time fields of the event edit form on and off.
     *
     * @param boolean on  Whether the event is an all-day event, i.e. the time
     *                    fields should be turned off. If not specified, the
     *                    current state is toggled.
     */
    toggleAllDay: function(on)
    {
        var end = this.getDate('end'),
            old = $('kronolithEventStartTimeLabel').getStyle('visibility') == 'hidden';
        if (Object.isUndefined(on)) {
            on = $('kronolithEventStartTimeLabel').getStyle('visibility') == 'visible';
        }
        if (end) {
            if (on) {
                if (end.getHours() == 0 && end.getMinutes() == 0) {
                    end.add(-1).minute();
                }
            } else if (old) {
                end.setHours(23);
                end.setMinutes(59);
            }
            $('kronolithEventEndDate').setValue(end.toString(Kronolith.conf.date_format));
            $('kronolithEventEndTime').setValue(end.toString(Kronolith.conf.time_format));
        }
        $('kronolithEventStartTimeLabel').setStyle({ visibility: on ? 'hidden' : 'visible' });
        $('kronolithEventEndTimeLabel').setStyle({ visibility: on ? 'hidden' : 'visible' });
    },

    /**
     * Enables the alarm in the event or task form and sets the correct value
     * and unit.
     *
     * @param string type    The object type, either 'Event' or 'Task'.
     * @param integer alarm  The alarm time in seconds.
     */
    enableAlarm: function(type, alarm) {
        if (!alarm) {
            return;
        }
        type = 'kronolith' + type + 'Alarm';
        $(type + 'On').setValue(true);
        [10080, 1440, 60, 1].each(function(unit) {
            if (alarm % unit === 0) {
                $(type + 'Value').setValue(alarm / unit);
                $(type + 'Unit').setValue(unit);
                throw $break;
            }
        });
    },

    /**
     * Disables all custom alarm methods in the event form.
     */
    disableAlarmMethods: function(type) {
        $('kronolith' + type + 'TabReminder').select('input').each(function(input) {
            if (input.name == (type == 'Event' ? 'event_alarms[]' : 'task[alarm_methods][]')) {
                input.setValue(0);
                if ($(input.id + 'Params')) {
                    $(input.id + 'Params').hide();
                }
            }
        });
    },

    /**
     * Toggles the recurrence fields of the event and task edit forms.
     *
     * @param boolean event  Whether to use the event form.
     * @param string recur   The recurrence part of the field name, i.e. 'None',
     *                       'Daily', etc.
     */
    toggleRecurrence: function(event, recur)
    {
        var prefix = 'kronolith' + (event ? 'Event' : 'Task');
        if (recur == 'Exception') {
            if (!$(prefix + 'RepeatException').visible()) {
                $(prefix + 'TabRecur').select('div').invoke('hide');
                $(prefix + 'RepeatException').show();
            }
        } else if (recur != 'None') {
            var div = $(prefix + 'Repeat' + recur),
                length = $(prefix + 'RepeatLength');
            this.lastRecurType = recur;
            if (!div.visible()) {
                $(prefix + 'TabRecur').select('div').invoke('hide');
                div.show();
                length.show();
                $(prefix + 'RepeatType').show();
            }
            switch (recur) {
            case 'Daily':
            case 'Weekly':
            case 'Monthly':
            case 'Yearly':
                var recurLower = recur.toLowerCase();
                if (div.down('input[name=recur_' + recurLower + '][value=1]').checked) {
                    div.down('input[name=recur_' + recurLower + '_interval]').disable();
                } else {
                    div.down('input[name=recur_' + recurLower + '_interval]').enable();
                }
                break;
            }

            if (length.down('input[name=recur_end_type][value=date]').checked) {
                $(prefix + 'RecurDate').enable();
                $(prefix + 'RecurPicker').setStyle({ visibility: 'visible' });
            } else {
                $(prefix + 'RecurDate').disable();
                $(prefix + 'RecurPicker').setStyle({ visibility: 'hidden' });
            }
            if (length.down('input[name=recur_end_type][value=count]').checked) {
                $(prefix + 'RecurCount').enable();
            } else {
                $(prefix + 'RecurCount').disable();
            }
        } else {
            $(prefix + 'TabRecur').select('div').invoke('hide');
            $(prefix + 'RepeatType').show();
        }
    },

    /**
     * Fills the recurrence fields of the event and task edit forms.
     *
     * @param boolean event  Whether to use the event form.
     * @param object recur   The recurrence object from the ajax response.
     */
    setRecurrenceFields: function(event, recur)
    {
        var scheme = Kronolith.conf.recur[recur.t],
            schemeLower = scheme.toLowerCase(),
            prefix = 'kronolith' + (event ? 'Event' : 'Task'),
            div = $(prefix + 'Repeat' + scheme);
        $(prefix + 'Link' + scheme).setValue(true);
        if (scheme == 'Monthly' || scheme == 'Yearly') {
            div.down('input[name=recur_' + schemeLower + '_scheme][value=' + recur.t + ']').setValue(true);
        }
        if (scheme == 'Weekly') {
            div.select('input[type=checkbox]').each(function(input) {
                if (input.name == 'weekly[]' &&
                    input.value & recur.d) {
                    input.setValue(true);
                }
            });
        }
        if (recur.i == 1) {
            div.down('input[name=recur_' + schemeLower + '][value=1]').setValue(true);
        } else {
            div.down('input[name=recur_' + schemeLower + '][value=0]').setValue(true);
            div.down('input[name=recur_' + schemeLower + '_interval]').setValue(recur.i);
        }
        if (!Object.isUndefined(recur.e)) {
            $(prefix + 'RepeatLength').down('input[name=recur_end_type][value=date]').setValue(true);
            $(prefix + 'RecurDate').setValue(Date.parse(recur.e).toString(Kronolith.conf.date_format));
        } else if (!Object.isUndefined(recur.c)) {
            $(prefix + 'RepeatLength').down('input[name=recur_end_type][value=count]').setValue(true);
            $(prefix + 'RecurCount').setValue(recur.c);
        } else {
            $(prefix + 'RepeatLength').down('input[name=recur_end_type][value=none]').setValue(true);
        }
        $(prefix + 'Exceptions').setValue(recur.ex || '');
        if ($(prefix + 'Completions')) {
            $(prefix + 'Completions').setValue(recur.co || '');
        }
        this.toggleRecurrence(event, scheme);
    },

    /**
     * Returns the Date object representing the date and time specified in the
     * event form's start or end fields.
     *
     * @param string what  Which fields to parse, either 'start' or 'end'.
     *
     * @return Date  The date object or null if the fields can't be parsed.
     */
    getDate: function(what) {
        var dateElm, timeElm, date, time;
        if (what == 'start') {
            dateElm = 'kronolithEventStartDate';
            timeElm = 'kronolithEventStartTime';
        } else {
            dateElm = 'kronolithEventEndDate';
            timeElm = 'kronolithEventEndTime';
        }
        date = Date.parseExact($F(dateElm), Kronolith.conf.date_format)
            || Date.parse($F(dateElm));
        if (date) {
            time = Date.parseExact($F(timeElm), Kronolith.conf.time_format);
            if (!time) {
                time = Date.parse($F(timeElm));
            }
            if (time) {
                date.setHours(time.getHours());
                date.setMinutes(time.getMinutes());
            }
        }
        return date;
    },

    checkDate: function(e) {
        this._checkDate(e.element());
    },

    _checkDate: function(elm)
    {
        if ($F(elm)) {
            var date = Date.parseExact($F(elm), Kronolith.conf.date_format) || Date.parse($F(elm));
            if (date) {
                elm.setValue(date.toString(Kronolith.conf.date_format));
                this.wrongFormat.unset(elm.id);
            } else {
                HordeCore.notify(Kronolith.text.wrong_date_format.interpolate({ wrong: $F(elm), right: new Date().toString(Kronolith.conf.date_format) }), 'horde.warning');
                this.wrongFormat.set(elm.id, true);
            }
        } else {
            HordeCore.notify(Kronolith.text.wrong_date_format.interpolate({ wrong: $F(elm), right: new Date().toString(Kronolith.conf.date_format) }), 'horde.warning');
            this.wrongFormat.set(elm.id, true);
        }
    },

    /**
     * Attaches a KeyNavList drop down to one of the time fields.
     *
     * @param string|Element field  A time field (id).
     *
     * @return KeyNavList  The drop down list object.
     */
    attachTimeDropDown: function(field)
    {
        var list = [], d = new Date(), time, opts;

        d.setHours(0);
        d.setMinutes(0);
        do {
            time = d.toString(Kronolith.conf.time_format);
            list.push({ l: time, v: time });
            d.add(30).minutes();
        } while (d.getHours() !== 0 || d.getMinutes() !== 0);

        field = $(field);
        opts = {
            list: list,
            domParent: field.up('.kronolithDialog'),
            onChoose: function(value) {
                if (value) {
                    field.setValue(value);
                }
                this.updateTimeFields(field.identify());
            }.bind(this)
        };

        this.knl[field.id] = new KeyNavList(field, opts);

        return this.knl[field.id];
    },

    checkTime: function(e) {
        var elm = e.element();
        if ($F(elm)) {
            var time = Date.parseExact(new Date().toString(Kronolith.conf.date_format) + ' ' + $F(elm), Kronolith.conf.date_format + ' ' + Kronolith.conf.time_format) || Date.parse(new Date().toString('yyyy-MM-dd ') + $F(elm));
            if (time) {
                elm.setValue(time.toString(Kronolith.conf.time_format));
                this.wrongFormat.unset(elm.id);
            } else {
                HordeCore.notify(Kronolith.text.wrong_time_format.interpolate({ wrong: $F(elm), right: new Date().toString(Kronolith.conf.time_format) }), 'horde.warning');
                this.wrongFormat.set(elm.id, true);
            }
        }
    },

    /**
     * Updates the start time in the event form after changing the end time.
     */
    updateStartTime: function(date) {
        var start = this.getDate('start'), end = this.getDate('end');
        if (!start) {
            return;
        }
        if (!date) {
            date = end;
        }
        if (!date) {
            return;
        }
        if (start.isAfter(end)) {
            $('kronolithEventStartDate').setValue(date.toString(Kronolith.conf.date_format));
            $('kronolithEventStartTime').setValue($F('kronolithEventEndTime'));
        }
        this.duration = Math.abs(date.getTime() - start.getTime()) / 60000;
    },

    /**
     * Updates the end time in the event form after changing the start time.
     */
    updateEndTime: function() {
        var date = this.getDate('start');
        if (!date) {
            return;
        }
        date.add(this.duration).minutes();
        $('kronolithEventEndDate').setValue(date.toString(Kronolith.conf.date_format));
        $('kronolithEventEndTime').setValue(date.toString(Kronolith.conf.time_format));
    },

    /**
     * Event handler for scrolling the mouse over the date field.
     *
     * @param Event e       The mouse event.
     * @param string field  The field name.
     */
    scrollDateField: function(e, field) {
        var date = Date.parseExact($F(field), Kronolith.conf.date_format);
        if (!date || (!e.wheelData && !e.detail)) {
            return;
        }
        date.add(e.wheelData > 0 || e.detail < 0 ? 1 : -1).days();
        $(field).setValue(date.toString(Kronolith.conf.date_format));
        switch (field) {
        case 'kronolithEventStartDate':
            this.updateEndTime();
            break;
        case 'kronolithEventEndDate':
            this.updateStartTime(date);
            break;
        }
    },

    /**
     * Event handler for scrolling the mouse over the time field.
     *
     * @param Event e       The mouse event.
     * @param string field  The field name.
     */
    scrollTimeField: function(e, field) {
        var time = Date.parseExact($F(field), Kronolith.conf.time_format) || Date.parse($F(field)),
            newTime, minute;
        if (!time || (!e.wheelData && !e.detail)) {
            return;
        }

        newTime = time.clone();
        newTime.add(e.wheelData > 0 || e.detail < 0 ? 10 : -10).minutes();
        minute = newTime.getMinutes();
        if (minute % 10) {
            if (e.wheelData > 0 || e.detail < 0) {
                minute = minute / 10 | 0;
            } else {
                minute = (minute - 10) / 10 | 0;
            }
            minute *= 10;
            newTime.setMinutes(minute);
        }
        if (newTime.getDate() != time.getDate()) {
            if (newTime.isAfter(time)) {
                newTime = time.clone().set({ hour: 23, minute: 59 });
            } else {
                newTime = time.clone().set({ hour: 0, minute: 0 });
            }
        }

        $(field).setValue(newTime.toString(Kronolith.conf.time_format));
        this.updateTimeFields(field);

        /* Mozilla bug https://bugzilla.mozilla.org/show_bug.cgi?id=502818
         * Need to stop or else multiple scroll events may be fired. We
         * lose the ability to have the mousescroll bubble up, but that is
         * more desirable than having the wrong scrolling behavior. */
        if (Prototype.Browser.Gecko && !e.stop) {
            Event.stop(e);
        }
    },

    /**
     * Updates the time fields of the event dialog after either has been
     * changed.
     *
     * @param string field  The id of the field that has been changed.
     */
    updateTimeFields: function(field)
    {
        switch (field) {
        case 'kronolithEventStartDate':
            this.fbStartDateHandler(Date.parseExact($F(field), Kronolith.conf.date_format));
        case 'kronolithEventStartTime':
            this.updateEndTime();
            break;
        case 'kronolithEventEndDate':
        case 'kronolithEventEndTime':
            this.updateStartTime();
            this.fbStartDateHandler(Date.parseExact($F('kronolithEventStartDate'), Kronolith.conf.date_format));
            break;
        }
    },

    /**
     * Closes a RedBox overlay, after saving its content to the body.
     */
    closeRedBox: function()
    {
        if (!RedBox.getWindow()) {
            return;
        }
        var content = RedBox.getWindowContents();
        if (content) {
            document.body.insert(content.hide());
        }
        RedBox.close();
    },

    // By default, no context onShow action
    contextOnShow: Prototype.emptyFunction,

    // By default, no context onClick action
    contextOnClick: Prototype.emptyFunction,

    // Map
    initializeMap: function(ignoreLL)
    {
        if (this.mapInitialized) {
            return;
        }
        var layers = [];
        if (Kronolith.conf.maps.providers) {
            Kronolith.conf.maps.providers.each(function(l) {
                var p = new HordeMap[l]();
                $H(p.getLayers()).values().each(function(e) {layers.push(e);});
            });
        }

        this.map = new HordeMap.Map[Kronolith.conf.maps.driver]({
            elt: 'kronolithEventMap',
            delayed: true,
            layers: layers,
            markerDragEnd: this.onMarkerDragEnd.bind(this),
            mapClick: this.afterClickMap.bind(this)
        });

        if ($('kronolithEventLocationLat').value && !ignoreLL) {
            var ll = { lat:$('kronolithEventLocationLat').value, lon: $('kronolithEventLocationLon').value };
            // Note that we need to cast the value of zoom to an integer here,
            // otherwise the map display breaks.
            this.placeMapMarker(ll, true, $('kronolithEventMapZoom').value - 0);
        }
        //@TODO: check for Location field - and if present, but no lat/lon value, attempt to
        // geocode it.
        this.map.display();
        this.mapInitialized = true;
    },

    resetMap: function()
    {
        this.mapInitialized = false;
        $('kronolithEventLocationLat').value = null;
        $('kronolithEventLocationLon').value = null;
        $('kronolithEventMapZoom').value = null;
        if (this.mapMarker) {
            this.map.removeMarker(this.mapMarker, {});
            this.mapMarker = null;
        }
        if (this.map) {
            this.map.destroy();
            this.map = null;
        }
    },

    /**
     * Callback for handling marker drag end.
     *
     * @param object r  An object that implenents a getLonLat() method to obtain
     *                  the new location of the marker.
     */
    onMarkerDragEnd: function(r)
    {
        var ll = r.getLonLat();
        $('kronolithEventLocationLon').value = ll.lon;
        $('kronolithEventLocationLat').value = ll.lat;
        var gc = new HordeMap.Geocoder[Kronolith.conf.maps.geocoder](this.map.map, 'kronolithEventMap');
        gc.reverseGeocode(ll, this.onReverseGeocode.bind(this), this.onGeocodeError.bind(this) );
    },

    /**
     * Callback for handling a reverse geocode request.
     *
     * @param array r  An array of objects containing the results. Each object in
     *                 the array is {lat:, lon:, address}
     */
    onReverseGeocode: function(r)
    {
        if (!r.length) {
            return;
        }
        $('kronolithEventLocation').value = r[0].address;
    },

    onGeocodeError: function(r)
    {
        $('kronolithEventGeo_loading_img').toggle();
        HordeCore.notify(Kronolith.text.geocode_error + ' ' + r, 'horde.error');
    },

    /**
     * Callback for geocoding calls.
     */
    onGeocode: function(r)
    {
        $('kronolithEventGeo_loading_img').toggle();
        r = r.shift();
        if (r.precision) {
            zoom = r.precision * 2;
        } else {
            zoom = null;
        }
        this.ensureMap(true);
        this.placeMapMarker({ lat: r.lat, lon: r.lon }, true, zoom);
    },

    geocode: function(a) {
        if (!a) {
            return;
        }
        $('kronolithEventGeo_loading_img').toggle();
        var gc = new HordeMap.Geocoder[Kronolith.conf.maps.geocoder](this.map.map, 'kronolithEventMap');
        gc.geocode(a, this.onGeocode.bind(this), this.onGeocodeError);
    },

    /**
     * Place the event marker on the map, at point ll, ensuring it exists.
     * Optionally center the map on the marker and zoom. Zoom only honored if
     * center is set, and if center is set, but zoom is null, we zoomToFit().
     *
     */
    placeMapMarker: function(ll, center, zoom)
    {
        if (!this.mapMarker) {
            this.mapMarker = this.map.addMarker(
                    ll,
                    { draggable: true },
                    {
                        context: this,
                        dragend: this.onMarkerDragEnd
                    });
        } else {
            this.map.moveMarker(this.mapMarker, ll);
        }

        if (center) {
            this.map.setCenter(ll, zoom);
            if (!zoom) {
                this.map.zoomToFit();
            }
        }
        $('kronolithEventLocationLon').value = ll.lon;
        $('kronolithEventLocationLat').value = ll.lat;
    },

    /**
     * Remove the event marker from the map. Called after clearing the location
     * field.
     */
    removeMapMarker: function()
    {
        if (this.mapMarker) {
            this.map.removeMarker(this.mapMarker, {});
            $('kronolithEventLocationLon').value = null;
            $('kronolithEventLocationLat').value = null;
        }

        this.mapMarker = false;
    },

    /**
     * Ensures the map tab is visible and sets UI elements accordingly.
     */
    ensureMap: function(ignoreLL)
    {
        if (!this.mapInitialized) {
            this.initializeMap(ignoreLL);
        }
        var dialog = $('kronolithEventForm');
        dialog.select('.kronolithTabsOption').invoke('hide');
        dialog.select('.tabset li').invoke('removeClassName', 'horde-active');
        $('kronolithEventTabMap').show();
        $('kronolithEventLinkMap').up().addClassName('horde-active');
    },

    /**
     * Callback that gets called after a new marker has been placed on the map
     * due to a single click on the map.
     *
     * @return object o  { lonlat: }
     */
    afterClickMap: function(o)
    {
        this.placeMapMarker(o.lonlat, false);
        var gc = new HordeMap.Geocoder[Kronolith.conf.maps.geocoder](this.map.map, 'kronolithEventMap');
        gc.reverseGeocode(o.lonlat, this.onReverseGeocode.bind(this), this.onGeocodeError.bind(this) );
    },

    /* Onload function. */
    onDomLoad: function()
    {
        var dateFields, timeFields;

        /* Initialize the starting page. */
        var tmp = location.hash;
        if (!tmp.empty() && tmp.startsWith('#')) {
            tmp = (tmp.length == 1) ? '' : tmp.substring(1);
        }
        if (tmp.empty()) {
            this.updateView(this.date, Kronolith.conf.login_view);
            $('kronolithView' + Kronolith.conf.login_view.capitalize()).show();
        }
        HordeCore.doAction('listCalendars', {}, { callback: this.initialize.bind(this, tmp) });

        RedBox.onDisplay = function() {
            this.redBoxLoading = false;
        }.bind(this);
        RedBox.duration = this.effectDur;

        $('kronolithEventStartDate', 'kronolithEventEndDate', 'kronolithTaskDueDate').compact().invoke('observe', 'blur', this.checkDate.bind(this));
        var timeFields = $('kronolithEventStartTime', 'kronolithEventEndTime', 'kronolithTaskDueTime').compact();
        timeFields.invoke('observe', 'blur', this.checkTime.bind(this));
        timeFields.each(function(field) {
            var dropDown = this.attachTimeDropDown(field);
            field.observe('click', function() { dropDown.show(); });
        }, this);
        $('kronolithEventStartDate', 'kronolithEventStartTime').invoke('observe', 'change', this.updateEndTime.bind(this));
        $('kronolithEventEndDate', 'kronolithEventEndTime').invoke('observe', 'change', function() { this.updateStartTime(); }.bind(this));

        if (Kronolith.conf.has_tasks) {
            $('kronolithTaskDueDate', 'kronolithTaskDueTime').compact().invoke('observe', 'focus', this.setDefaultDue.bind(this));
            $('kronolithTaskList').observe('change', function() {
                this.updateTaskParentDropDown($F('kronolithTaskList'));
                this.updateTaskAssigneeDropDown($F('kronolithTaskList'));
            }.bind(this));
        }

        document.observe('keydown', KronolithCore.keydownHandler.bindAsEventListener(KronolithCore));
        document.observe('keyup', KronolithCore.keyupHandler.bindAsEventListener(KronolithCore));
        document.observe('click', KronolithCore.clickHandler.bindAsEventListener(KronolithCore));
        document.observe('dblclick', KronolithCore.clickHandler.bindAsEventListener(KronolithCore, true));

        // Mouse wheel handler.
        dateFields = [ 'kronolithEventStartDate', 'kronolithEventEndDate' ];
        timeFields = [ 'kronolithEventStartTime', 'kronolithEventEndTime' ];
        if (Kronolith.conf.has_tasks) {
            dateFields.push('kronolithTaskDueDate');
            timeFields.push('kronolithTaskDueTime');
        }
        dateFields.each(function(field) {
            $(field).observe(Prototype.Browser.Gecko ? 'DOMMouseScroll' : 'mousewheel', this.scrollDateField.bindAsEventListener(this, field));
        }, this);
        timeFields.each(function(field) {
            $(field).observe(Prototype.Browser.Gecko ? 'DOMMouseScroll' : 'mousewheel', this.scrollTimeField.bindAsEventListener(this, field));
        }, this);

        $('kronolithEventStartDate').observe('change', this.fbStartDateOnChange.bind(this));
        $('kronolithFBDatePrev').observe('click', this.prevFreebusy.bind(this));
        $('kronolithFBDateNext').observe('click', this.nextFreebusy.bind(this));
        $('kronolithResourceFBDatePrev').observe('click', this.prevFreebusy.bind(this));
        $('kronolithResourceFBDateNext').observe('click', this.nextFreebusy.bind(this));

        this.updateMinical(this.date);
    },

    initialize: function(location, r)
    {
        Kronolith.conf.calendars = r.calendars;
        this.updateCalendarList();
        HordeSidebar.refreshEvents();
        $('kronolithLoadingCalendars').hide();
        $('kronolithMenuCalendars').show();
        this.initialized = true;

        /* Initialize the starting page. */
        if (!location.empty()) {
            this.go(decodeURIComponent(location));
        } else {
            this.go(Kronolith.conf.login_view);
        }

        /* Start polling. */
        new PeriodicalExecuter(function()
            {
                HordeCore.doAction('poll');
                $(kronolithGotoToday).update(Date.today().toString(Kronolith.conf.date_format));
            },
            60
        );
    }

};

/* Initialize global event handlers. */
document.observe('dom:loaded', KronolithCore.onDomLoad.bind(KronolithCore));
document.observe('DragDrop2:drag', KronolithCore.onDrag.bindAsEventListener(KronolithCore));
document.observe('DragDrop2:drop', KronolithCore.onDrop.bindAsEventListener(KronolithCore));
document.observe('DragDrop2:end', KronolithCore.onDragEnd.bindAsEventListener(KronolithCore));
document.observe('DragDrop2:start', KronolithCore.onDragStart.bindAsEventListener(KronolithCore));
document.observe('Horde_Calendar:select', KronolithCore.datePickerHandler.bindAsEventListener(KronolithCore));
document.observe('FormGhost:reset', KronolithCore.searchReset.bindAsEventListener(KronolithCore));
document.observe('FormGhost:submit', KronolithCore.searchSubmit.bindAsEventListener(KronolithCore));
document.observe('HordeCore:showNotifications', KronolithCore.showNotification.bindAsEventListener(KronolithCore));
if (Prototype.Browser.IE) {
    $('kronolithBody').observe('selectstart', Event.stop);
}

/* Extend AJAX exception handling. */
HordeCore.onException = HordeCore.onException.wrap(KronolithCore.onException.bind(KronolithCore));
