<?php

require_once KRONOLITH_BASE . '/lib/Driver.php';
require_once KRONOLITH_BASE . '/lib/Event.php';

define('KRONOLITH_RECUR_NONE',          0);
define('KRONOLITH_RECUR_DAILY',         1);
define('KRONOLITH_RECUR_WEEKLY',        2);
define('KRONOLITH_RECUR_DAY_OF_MONTH',  3);
define('KRONOLITH_RECUR_WEEK_OF_MONTH', 4);
define('KRONOLITH_RECUR_YEARLY',        5);

define('KRONOLITH_MONDAY',    0);
define('KRONOLITH_TUESDAY',   1);
define('KRONOLITH_WEDNESDAY', 2);
define('KRONOLITH_THURSDAY',  3);
define('KRONOLITH_FRIDAY',    4);
define('KRONOLITH_SATURDAY',  5);
define('KRONOLITH_SUNDAY',    6);

define('KRONOLITH_MASK_SUNDAY',    1);
define('KRONOLITH_MASK_MONDAY',    2);
define('KRONOLITH_MASK_TUESDAY',   4);
define('KRONOLITH_MASK_WEDNESDAY', 8);
define('KRONOLITH_MASK_THURSDAY',  16);
define('KRONOLITH_MASK_FRIDAY',    32);
define('KRONOLITH_MASK_SATURDAY',  64);
define('KRONOLITH_MASK_WEEKDAYS',  62);
define('KRONOLITH_MASK_WEEKEND',   65);
define('KRONOLITH_MASK_ALLDAYS',   127);

define('KRONOLITH_JANUARY',    1);
define('KRONOLITH_FEBRUARY',   2);
define('KRONOLITH_MARCH',      3);
define('KRONOLITH_APRIL',      4);
define('KRONOLITH_MAY',        5);
define('KRONOLITH_JUNE',       6);
define('KRONOLITH_JULY',       7);
define('KRONOLITH_AUGUST',     8);
define('KRONOLITH_SEPTEMBER',  9);
define('KRONOLITH_OCTOBER',   10);
define('KRONOLITH_NOVEMBER',  11);
define('KRONOLITH_DECEMBER',  12);

define('KRONOLITH_DELETE_CATEGORY', 106);
define('KRONOLITH_RENAME_CATEGORY', 107);
define('KRONOLITH_ADD_CATEGORY', 108);

/**
 * The Kronolith:: class provides functionality common to all of
 * Kronolith.
 *
 * $Horde: kronolith/lib/Kronolith.php,v 1.148 2003/07/15 15:19:53 chuck Exp $
 *
 * @author  Chuck Hagenbuch <chuck@horde.org>
 * @version $Revision: 1.148 $
 * @since   Kronolith 0.1
 * @package kronolith
 */
class Kronolith {

    function dateObject($date = null)
    {
        $obj = new stdClass();
        $obj->year  = null;
        $obj->month = null;
        $obj->mday  = null;
        $obj->hour  = null;
        $obj->min   = null;
        $obj->sec   = null;

        if (is_array($date) || is_object($date)) {
            foreach ($date as $key => $val) {
                $obj->$key = $val;
            }
        }

        return $obj;
    }

    /**
     * Returns all the events that happen each day within a time period
     *
     * @param object $startDate    The start of the time range.
     * @param object $endDate      The end of the time range.
     * @param array  $calendars    The calendars to check for events.
     *
     * @return array  The events happening in this time period.
     */
    function listEventIds($startDate, $endDate, $calendars)
    {
        global $calendar;

        $eventIds = array();
        foreach ($calendars as $cal) {
            if ($calendar->getCalendar() != $cal) {
                $calendar->close();
                $calendar->open($cal);
            }
            $eventIds[$cal] = $calendar->listEvents($startDate, $endDate);
        }

        return $eventIds;
    }

    /**
     * Returns all the alarms active right on $date.
     *
     * @param object $date         The start of the time range.
     * @param array  $calendars    The calendars to check for events.
     *
     * @return array  The alarms active on $date.
     */
    function listAlarms($date, $calendars)
    {
        global $calendar;

        $alarms = array();
        foreach ($calendars as $cal) {
            if ($calendar->getCalendar() != $cal) {
                $calendar->close();
                $calendar->open($cal);
            }
            $alarms[$cal] = $calendar->listAlarms($date);
        }

        return $alarms;
    }

    /**
     * Returns an event object for an event on a remote calendar.
     *
     * This is kind of a temorary solution until we can have multiple drivers
     * in use at the same time.
     *
     * @param $url      The url of the remote calendar.
     * @param $eventID  The index of the event on the remote calenar.
     *
     * @return object Kronolith_Event   The event object.
     */
    function getRemoteEventObject($url, $eventID)
    {
        global $calendar;

        if (!array_key_exists('remote_events', $_SESSION['kronolith'])) {
            $_SESSION['kronolith']['remote_events'] = array();
        }

        if (!array_key_exists($url, $_SESSION['kronolith']['remote_events'])) {
            $options['method'] = 'GET';
            $options['timeout'] = 5;

            require_once 'HTTP/Request.php';
            $http = &new HTTP_Request($url, $options);
            @$http->sendRequest();
            if ($http->getResponseCode() != 200) {
                return PEAR::raiseError(sprintf(_("Could not open %s."), $url));
            }
            $_SESSION['kronolith']['remote_events'][$url] = $http->getResponseBody();

            /* Log fetch at DEBUG level */
            Horde::logMessage(sprintf('Retrieved remote calendar for %s: url = "%s"',
                                      Auth::getAuth(), $url), __FILE__, __LINE__, PEAR_LOG_DEBUG);
        }

        $vcs = &Horde_Data::singleton('icalendar');
        $result = $vcs->importData($_SESSION['kronolith']['remote_events'][$url]);
        if (!is_a($result, 'PEAR_Error')) {
            $event = &$calendar->getEventObject();
            $event->fromHash($vcs->toHash($eventID));
            $event->remoteCal = $url;
            $event->eventIndex = $eventID;
            return $event;
        }
        return false;
    }

    /**
     * Returns all the events from a remote calendar.
     *
     * @param string $url       The url of the remote calendar.
     *
     */
    function listRemoteEvents($url)
    {
        global $calendar;

        if (!array_key_exists('remote_events', $_SESSION['kronolith'])) {
            $_SESSION['kronolith']['remote_events'] = array();
        }

        if (!array_key_exists($url, $_SESSION['kronolith']['remote_events'])) {
            $options['method'] = 'GET';
            $options['timeout'] = 5;

            require_once 'HTTP/Request.php';
            $http = &new HTTP_Request($url, $options);
            @$http->sendRequest();
            if ($http->getResponseCode() != 200) {
                return PEAR::raiseError(sprintf(_("Could not open %s."), $url));
            }
            $_SESSION['kronolith']['remote_events'][$url] = $http->getResponseBody();

            /* Log fetch at DEBUG level */
            Horde::logMessage(sprintf('Retrieved remote calendar for %s: url = "%s"',
                                      Auth::getAuth(), $url), __FILE__, __LINE__, PEAR_LOG_DEBUG);
        }

        $events = array();
        $vcs = &Horde_Data::singleton('icalendar');
        $result = $vcs->importData($_SESSION['kronolith']['remote_events'][$url]);
        if (!is_a($result, 'PEAR_Error')) {
            $iMax = $vcs->count();
            for ($i = 0; $i < $iMax; $i++) {
                $event = &$calendar->getEventObject();
                $event->fromHash($vcs->toHash($i));
                $event->remoteCal = $url;
                $event->eventIndex = $i;
                $events[] = $event;
            }
        }
        return $events;
    }

    /**
     * Returns all the events that happen each day within a time period
     *
     * @param mixed   $startDate       The start of the time range. Either a unix
     *                                 timestamp, or a Kronolith date object.
     * @param mixed   $endDate         The end of the time range. Either a unix
     *                                 timestamp, or a Kronolith date object.
     * @param array   $calendars       The calendars to check for events.
     * @param boolean $showRecurrence  Return every instance of a recurring event?
     *                                 If false, will only return recurring events
     *                                 once inside the $startDate - $endDate range.
     *
     * @return array  The events happening in this time period.
     */
    function listEvents($startDate = null, $endDate = null, $calendars = null, $showRecurrence = true)
    {
        global $calendar, $prefs, $registry;

        if (!isset($startDate)) {
            $startDate = Kronolith::timestampToObject(time());
        } elseif (!is_object($startDate)) {
            $startDate = Kronolith::timestampToObject($startDate);
        }
        if (!isset($endDate)) {
            $endDate = Kronolith::timestampToObject(time());
        } elseif (!is_object($endDate)) {
            $endDate = Kronolith::timestampToObject($endDate);
        }
        if (!isset($calendars)) {
            $calendars = $GLOBALS['display_calendars'];
        }

        $eventIds = Kronolith::listEventIds($startDate, $endDate, $calendars);

        $startOfPeriodTimestamp = mktime(0, 0, 0, $startDate->month, $startDate->mday, $startDate->year);
        $endOfPeriodTimestamp = mktime(23, 59, 59, $endDate->month, $endDate->mday, $endDate->year);
        $daysInPeriod = Date_Calc::dateDiff($startDate->mday, $startDate->month, $startDate->year, $endDate->mday, $endDate->month, $endDate->year);

        $results = array();
        foreach ($eventIds as $cal => $events) {
            if ($calendar->getCalendar() != $cal) {
                $calendar->close();
                $calendar->open($cal);
            }
            foreach ($events as $id) {
                // We MUST fetch each event right before getting its
                // recurrences; this is due to the way MCAL
                // works. MCAL's nextRecurrence() function gives you
                // the next recurrence for the event most recently
                // fetched. So if you fetch all events and then loop
                // through them, every recurrence you get will be for
                // the last event that you fetched.
                $event = &$calendar->getEventObject($id);

                if (!$event->hasRecurType(KRONOLITH_RECUR_NONE) && $showRecurrence) {
                    /* Recurring Event */

                    if ($event->getStartTimestamp() < $startOfPeriodTimestamp) {
                        // The first time the event happens was before the
                        // period started. Start searching for recurrences
                        // from the start of the period.
                        $next = array('year' => $startDate->year, 'month' => $startDate->month, 'mday' => $startDate->mday);
                    } else {
                        // The first time the event happens is in the
                        // range; unless there is an exception for
                        // this ocurrence, add it.
                        if (!$event->hasException($event->getStartDate('Y'),
                                                  $event->getStartDate('n'),
                                                  $event->getStartDate('j'))) {
                            $results[$event->getStartDatestamp()][$id] = $event;
                        }

                        // Start searching for recurrences from the day
                        // after it starts.
                        $next = array('year' => $event->getStartDate('Y'), 'month' => $event->getStartDate('n'), 'mday' => $event->getStartDate('j') + 1);
                    }

                    // Add all recurences of the event.
                    $next = $event->nextRecurrence($next);
                    while ($next !== false && (Kronolith::compareDates($next, $endDate) <= 0)) {
                        if (!$event->hasException($next->year, $next->month, $next->mday)) {
                            $results[Kronolith::objectToDatestamp($next)][$id] = $event;
                        }
                        $next = $event->nextRecurrence(array('year' => $next->year,
                                                             'month' => $next->month,
                                                             'mday' => $next->mday + 1,
                                                             'hour' => $next->hour,
                                                             'min' => $next->min,
                                                             'sec' => $next->sec));
                    }
                } else {
                    /* Event only occurs once. */

                    // Work out what day it starts on.
                    if ($event->getStartTimestamp() < $startOfPeriodTimestamp) {
                        // It started before the beginning of the period.
                        $eventStartStamp = $startOfPeriodTimestamp;
                    } else {
                        $eventStartStamp = $event->getStartTimestamp();
                    }

                    // Work out what day it ends on.
                    if ($event->getEndTimestamp() >= $endOfPeriodTimestamp) {
                        // Ends after the end of the period.
                        $eventEndStamp = $endOfPeriodTimestamp;
                    } else {
                        // If the event doesn't end at 12am set the
                        // end date to the current end date. If it
                        // ends at 12am and does not end at the same
                        // time that it starts (0 duration), set the
                        // end date to the previous day's end date.
                        if ($event->getEndDate('G') != 0 ||
                            $event->getEndDate('i') != 0 ||
                            $event->getStartTimestamp() == $event->getEndTimestamp()) {
                            $eventEndStamp = $event->getEndTimestamp();
                        } else {
                            $eventEndStamp = mktime(23, 59, 59,
                                                    $event->getEndDate('n'),
                                                    $event->getEndDate('j') - 1,
                                                    $event->getEndDate('Y'));
                        }
                    }

                    // Add the event to all the days it covers.
                    $i = date('j', $eventStartStamp);
                    $loopStamp = mktime(0, 0, 0,
                                        date('n', $eventStartStamp),
                                        $i,
                                        date('Y', $eventStartStamp));
                    while ($loopStamp <= $eventEndStamp) {
                        if (!($event->isAllDay() && $loopStamp == $eventEndStamp)) {
                            $results[$loopStamp][$id] = $event;
                        }
                        $loopStamp = mktime(0, 0, 0,
                                            date('n', $eventStartStamp),
                                            ++$i,
                                            date('Y', $eventStartStamp));
                    }
                }
            }
        }

        /* Nag Tasks. */
        if ($prefs->getValue('show_tasks') &&
            (Auth::getAuth() || $registry->allowGuests($registry->hasMethod('tasks/list')))) {
            $taskList = $registry->call('tasks/list');
            $dueEndStamp = mktime(0, 0, 0, $endDate->month, $endDate->mday + 1, $endDate->year);
            if (!is_a($taskList, 'PEAR_Error')) {
                foreach ($taskList as $task) {
                    if ($task['due'] >= $startOfPeriodTimestamp && $task['due'] < $dueEndStamp) {
                        $event = &$calendar->getEventObject();
                        $event->setTitle(sprintf(_("Due: %s"), $task['name']));
                        $event->taskID = $task['task_id'];
                        $event->tasklistID = $task['tasklist_id'];
                        $event->setStartTimestamp($task['due']);
                        $event->setEndTimestamp($task['due'] + 1);
                        $dayStamp = mktime(0, 0, 0,
                                           date('n', $task['due']),
                                           date('j', $task['due']),
                                           date('Y', $task['due']));
                        $results[$dayStamp]['_task' . $task['task_id']] = $event;
                    }
                }
            }
        }

        /* Moment Meetings. */
        if ($prefs->getValue('show_meetings') &&
            (Auth::getAuth() || $registry->allowGuests($registry->hasMethod('meeting/list')))) {
            $meetingList = $registry->call('meeting/list');
            if (!is_a($meetingList, 'PEAR_Error')) {
                foreach ($meetingList as $meeting) {
                    if ($meeting['end'] > $startOfPeriodTimestamp &&
                        $meeting['start'] < $endOfPeriodTimestamp) {
                        $event = &$calendar->getEventObject();
                        $event->meetingID = $meeting['id'];
                        $event->setTitle(sprintf(_("Meeting: %s"), $meeting['title']));
                        $event->setStartTimestamp($meeting['start']);
                        $event->setEndTimestamp($meeting['end']);
                        $dayStamp = mktime(0, 0, 0,
                                           date('n', $meeting['start']),
                                           date('j', $meeting['start']),
                                           date('Y', $meeting['start']));
                        $results[$dayStamp]['_meeting' . $meeting['id']] = $event;
                    }
                }
            }
        }

        /* Remote Calendars */
        foreach ($GLOBALS['display_remote_calendars'] as $url) {
            $events = Kronolith::listRemoteEvents($url);
            foreach ($events as $event) {
                // Event is not in period
                if ($event->getEndTimestamp() < $startOfPeriodTimestamp ||
                    $event->getStartTimestamp() > $endOfPeriodTimestamp) {
                    continue;
                }

                // Work out what day it starts on.
                if ($event->getStartTimestamp() < $startOfPeriodTimestamp) {
                    // It started before the beginning of the period.
                    $eventStartDay = date('j', $startOfPeriodTimestamp);
                } else {
                    $eventStartDay = $event->getStartDate('j');
                }

                // Work out what day it ends on.
                if ($event->getEndTimestamp() >= $endOfPeriodTimestamp) {
                    // Ends after the end of the period.
                    $eventEndDay = date('j', $endOfPeriodTimestamp);
                } else {
                    // If the event doesn't end at 12am set the end
                    // date to the current end date. If it ends at
                    // 12am set the end date to the previous days end
                    // date.
                    if ($event->getEndDate('G') != 0 ||
                        $event->getEndDate('i') != 0) {
                        $eventEndDay = $event->getEndDate('j');
                    } else {
                        $eventEndDay = $event->getEndDate('j') - 1;
                    }
                }

                // Add the event to all the days it covers.
                // TODO: this needs to handle the month (or year) wrapping.
                for ($i = $eventStartDay; $i <= $eventEndDay; $i++) {
                    $dayStamp = mktime(0, 0, 0,
                                       $event->getStartDate('n'),
                                       $i,
                                       $event->getStartDate('Y'));

                    $results[$dayStamp]['_remote' . $url . $startOfPeriodTimestamp . $i] = $event;
                }
            }
        }

        foreach ($results as $day => $devents) {
            if (count($devents)) {
                uasort($devents, array('Kronolith', '_sortEventStartTime'));
                $results[$day] = $devents;
            }
        }

        return $results;
    }

    /**
     * Used with usort() to sort events based on their start times.
     * This function ignores the date component so recuring events can
     * be sorted correctly on a per day basis.
     */
    function _sortEventStartTime($a, $b)
    {
        return ((int)date('Gis', $a->startTimestamp) - (int)date('Gis', $b->startTimestamp));
    }

    function _sortEvents($a, $b)
    {
        return $a->startTimestamp - $b->startTimestamp;
    }

    function secondsToString($seconds)
    {
        $hours = floor($seconds / 3600);
        $minutes = ($seconds / 60) % 60;

        if ($hours > 1) {
            if ($minutes == 0) {
                return sprintf(_("%d hours"), $hours);
            } else if ($minutes == 1) {
                return sprintf(_("%d hours, %d minute"), $hours, $minutes);
            } else {
                return sprintf(_("%d hours, %d minutes"), $hours, $minutes);
            }
        } else if ($hours == 1) {
            if ($minutes == 0) {
                return sprintf(_("%d hour"), $hours);
            } else if ($minutes == 1) {
                return sprintf(_("%d hour, %d minute"), $hours, $minutes);
            } else {
                return sprintf(_("%d hour, %d minutes"), $hours, $minutes);
            }
        } else {
            if ($minutes == 0) {
                return _("no time");
            } else if ($minutes == 1) {
                return sprintf(_("%d minute"), $minutes);
            } else {
                return sprintf(_("%d minutes"), $minutes);
            }
        }
    }

    function recurToString($type)
    {
        switch ($type) {

        case KRONOLITH_RECUR_NONE:
            return _("Recurs not");
        case KRONOLITH_RECUR_DAILY:
            return _("Recurs daily");
        case KRONOLITH_RECUR_WEEKLY:
            return _("Recurs weekly");
        case KRONOLITH_RECUR_DAY_OF_MONTH:
        case KRONOLITH_RECUR_WEEK_OF_MONTH:
            return _("Recurs monthly");
        case KRONOLITH_RECUR_YEARLY:
            return _("Recurs yearly");

        }
    }

    /**
     * Returns week of the year, first Monday is first day of first week
     *
     * @param optional string $day    in format DD
     * @param optional string $month  in format MM
     * @param optional string $year   in format CCYY
     *
     * @return integer $week_number
     */
    function weekOfYear($day = null, $month = null, $year = null)
    {
        global $prefs;

        if (!isset($year)) $year = date('Y');
        if (!isset($month)) $month = date('n');
        if (!isset($day)) $day = date('j');
        if (!$prefs->getValue('week_start_monday') && Kronolith::dayOfWeek($year, $month, $day) == KRONOLITH_SUNDAY) {
            $day++;
        }

        $dayOfYear = Kronolith::dayOfYear($year, $month, $day);
        $dayOfWeek = Kronolith::dayOfWeek($year, $month, $day);
        $dayOfWeekJan1 = Kronolith::dayOfWeek($year, 1, 1);

        if ($dayOfYear <= 7 - $dayOfWeekJan1 && $dayOfWeekJan1 > 3 ) {
            if ($dayOfWeekJan1 == 4 || ($dayOfWeekJan1 == 5 && Kronolith::isLeapYear($year - 1))) {
                return '53';
            } else {
                return '52';
            }
        }

        if (Kronolith::isLeapYear($year)) {
            $daysInYear = 366;
        } else {
            $daysInYear = 365;
        }

        if ($daysInYear - $dayOfYear < 3 - $dayOfWeek) {
            return 1;
        }

        $WeekNumber = floor(($dayOfYear + (6 - $dayOfWeek) + $dayOfWeekJan1) / 7);
        if ($dayOfWeekJan1 > 3) { $WeekNumber -= 1; }

        return $WeekNumber;
    }

    /**
     * Return the number of weeks in the given year (52 or 53).
     *
     * @param optional integer $year  The year to count the number of weeks in.
     *
     * @return integer $numWeeks      The number of weeks in $year.
     */
    function weeksInYear($year = null)
    {
        if (!isset($year)) $year = date('Y');

        // Find the last Thursday of the year.
        $day = 31;
        while (date('w', mktime(0, 0, 0, 12, $day, $year)) != 4) {
            $day--;
        }
        return Kronolith::weekOfYear($day, 12, $year);
    }

    /**
     * Returns the day of the year (1-366) that corresponds to the
     * first day of the given week.
     *
     * @param int week        The week of the year to find the first day of.
     *
     * @return int dayOfYear  The day of the year of the first day of the given week.
     */
    function firstDayOfWeek($week = null, $year = null)
    {
        if (!isset($year)) $year = date('Y');
        if (!isset($week)) $week = Kronolith::weekOfYear(null, null, $year);

        $start = Kronolith::dayOfWeek($year, 1, 1);
        if ($start > 3) $start -= 7;
        return ((($week * 7) - (7 + $start)) + 1);
    }

    /**
     * Find the number of days in the given month.
     *
     * @param string month in format MM, default current local month
     *
     * @return int number of days
     */
    function daysInMonth($month = null, $year = null)
    {
        if (!isset($month)) {
            $month = date('n');
        }

        switch ($month) {
        case 2:
            if (!isset($year)) {
                $year = date('Y');
            }
            return Kronolith::isLeapYear($year) ? 29 : 28;
            break;

        case 4:
        case 6:
        case 9:
        case 11:
            return 30;
            break;

        default:
            return 31;
        }
    }

    function isLeapYear($year = null)
    {
        if (!isset($year)) {
            $year = date('Y');
        } else {
            if (strlen($year) != 4) {
                return false;
            }
            if (preg_match('/\D/', $year)) {
                return false;
            }
        }

        return (($year % 4 == 0 && $year % 100 != 0) || $year % 400 == 0);
    }

    /**
     * Return the day of the week (0 = Monday, 6 = Sunday) of the
     * given date.
     *
     * @param optional int $year
     * @param optional int $month
     * @param optional int $day
     *
     * @return int The day of the week.
     */
    function dayOfWeek($year = null, $month = null, $day = null)
    {
        if (!isset($year))  $year  = date('Y');
        if (!isset($month)) $month = date('n');
        if (!isset($day))   $day   = date('d');

        if ($month > 2) {
            $month -= 2;
        } else {
            $month += 10;
            $year--;
        }

        $day = (floor((13 * $month - 1) / 5) +
                $day + ($year % 100) +
                floor(($year % 100) / 4) +
                floor(($year / 100) / 4) - 2 *
                floor($year / 100) + 77);

        $weekday_number = (($day - 7 * floor($day / 7))) - 1;
        if ($weekday_number == -1) {
            // Wrap check.
            $weekday_number = 6;
        }

        return $weekday_number;
    }

    function dayOfYear($year = null, $month = null, $day = null)
    {
        if (!isset($year)) {
            $year = date('Y');
        }
        if (!isset($month)) {
            $month = date('n');
        }
        if (!isset($day)) {
            $day = date('d');
        }

        $days = 0;
        for ($i = 1; $i < $month; $i++) {
            $days += Kronolith::daysInMonth($i, $year);
        }
        $days += $day;

        return $days;
    }

    function compareDates($first, $second)
    {
        $first = Kronolith::dateObject($first);
        $second = Kronolith::dateObject($second);

        if ($first->year - $second->year != 0) {
            return $first->year - $second->year;
        } elseif ($first->month - $second->month != 0) {
            return $first->month - $second->month;
        } elseif ($first->mday - $second->mday != 0) {
            return $first->mday - $second->mday;
        } elseif ($first->hour - $second->hour != 0) {
            return $first->hour - $second->hour;
        } elseif ($first->min - $second->min != 0) {
            return $first->min - $second->min;
        } else {
            return $first->sec - $second->sec;
        }
    }

    function dateDiff($start, $end, $split = false)
    {
        $start = Kronolith::dateObject($start);
        $end = Kronolith::dateObject($end);

        $res = new stdClass();
        $res->year = $end->year - $start->year;
        $res->month = $end->month - $start->month;
        $res->mday = $end->mday - $start->mday;
        $res->hour = $end->hour - $start->hour;
        $res->min = $end->min - $start->min;
        $res->sec = $end->sec - $start->sec;
        if (!$split) {
            $res->month += $res->year * 12;
            $res->mday = Date_Calc::dateDiff($start->mday, $start->month, $start->year, $end->mday, $end->month, $end->year);
            $res->hour += $res->mday * 24;
            $res->min += $res->hour * 60;
            $res->sec += $res->min * 60;
        }

        return $res;
    }

    function correctDate($date)
    {
        if (isset($date->sec)) {
            while ($date->sec > 59) {
                $date->min++;
                $date->sec -= 60;
            }
        }
        if (isset($date->min)) {
            while ($date->min > 59) {
                $date->hour++;
                $date->min -= 60;
            }
        }
        if (isset($date->hour)) {
            while ($date->hour > 23) {
                $date->mday++;
                $date->hour -= 24;
            }
        }
        if (isset($date->mday)) {
            while ($date->mday > Date_Calc::daysInMonth($date->month, $date->year)) {
                $date->month++;
                $date->mday -= Date_Calc::daysInMonth($date->month - 1, $date->year);
            }
        }
        if (isset($date->month)) {
            while ($date->month > 12) {
                $date->year++;
                $date->month -= 12;
            }
        }

        return $date;
    }

    function timestampToObject($timestamp)
    {
        $res = new stdClass();
        list($res->hour, $res->min, $res->sec, $res->mday, $res->month, $res->year) = explode('/', date('H/i/s/j/n/Y', $timestamp));
        return $res;
    }

    function timestampToArray($timestamp)
    {
        $obj = Kronolith::timestampToObject($timestamp);

        return array('hour' => $obj->hour,
                     'min' => $obj->min,
                     'sec' => $obj->sec,
                     'month' => $obj->month,
                     'mday' => $obj->mday,
                     'year' => $obj->year);
    }

    function objectToTimestamp($obj)
    {
        return @mktime($obj->hour, $obj->min, $obj->sec, $obj->month, $obj->mday, $obj->year);
    }

    function objectToDatestamp($obj)
    {
        return @mktime(0, 0, 0, $obj->month, $obj->mday, $obj->year);
    }

    function arrayToTimestamp($arr)
    {
        return @mktime($arr['hour'], $arr['min'], $arr['sec'], $arr['month'], $arr['mday'], $arr['year']);
    }

    /**
     * Builds the HTML for a event category widget.
     *
     * @param string  $name       The name of the widget.
     * @param integer $selected   (optional) The default category.
     * @param boolean $newheader  (optional) Include a new category option.
     *
     * @return string       The HTML <select> widget.
     */
    function buildCategoryWidget($name, $selected = false, $newheader = false)
    {
        $html = "<select id=\"$name\" name=\"$name\">";

        if ($newheader) {
            $html .= '<option value="*new*">' . _("New Category") . "</option>\n";
            $html .= '<option value="">----</option>' . "\n";
        }

        foreach (Kronolith::listCategories() as $id => $name) {
            $html .= "<option value=\"$id\"";
            $html .= ($id == $selected && $selected !== false) ? ' selected="selected">' : '>';
            $html .= $name . "</option>\n";
        }
        $html .= '</select>';

        return $html;
    }

    /**
     * Builds the category legend for the displayed calendars.
     *
     * @return string  The HTML table for the legend.
     */
    function buildCategoryLegend()
    {
        if (count($GLOBALS['display_calendars']) == 0) {
            return '';
        }

        $colors = Kronolith::categoryColors();
        $html = '<tr><td class="smallheader">';
        $html .= '<table border="0" cellpadding="0" cellspacing="4">';
        foreach (Kronolith::listCalendars() as $id => $cal) {
            if (in_array($id, $GLOBALS['display_calendars'])) {
                $categories = Kronolith::listCategories($id);
                $html .= '<tr><td>' . $cal->getShareName() . ':</td>';
                foreach ($categories as $catKey => $catName) {
                    $categoryColor = isset($colors[$id][$catKey]) ? $colors[$id][$catKey] : '#ccccff';
                    $html .= '<td class="month-eventbox" width="15" style="background-color: ' . $categoryColor . '; ';
                    $html .= 'border-color: ' . Kronolith::borderColor($categoryColor) . '">';
                    $html .= '&nbsp;</td>';
                    $html .= '<td style="width: 60;">' . $catName . '</td>' . "\n";
                }
                $html .= '</tr>' . "\n";
            }
        }
        $html .= '</table></td></tr>' . "\n";
        return $html;
    }

    /**
     * List all calendars a user has access to.
     *
     * @param optional boolean $owneronly  Only return calenders that this
     *                                     user owns? Defaults to false.
     * @param optional integer $permission The permission to filter calendars by.
     *
     * @return array  The calendar list.
     */
    function listCalendars($owneronly = false, $permission = _PERMS_SHOW)
    {
        $calendars = $GLOBALS['shares']->listShares(Auth::getAuth(), $permission, $owneronly);
        if (is_a($calendars, 'PEAR_Error')) {
            Horde::logMessage($calendars, __FILE__, __LINE__, PEAR_LOG_ERR);
            return array();
        }

        return $calendars;
    }

    /**
     * List a user's categories
     *
     * @return array A list of categories.
     */
    function listCategories($calendar = null)
    {
        global $prefs;

        static $catString, $categories;

        $cur = $GLOBALS['shares']->getPrefByShare('event_categories', $calendar);
        if (is_null($catString) || $catString != $cur) {
            $categories = array(0 => _("Unfiled"));

            $catString = $cur;
            if (empty($catString)) {
                return $categories;
            }

            $cats = explode('|', $catString);
            foreach ($cats as $cat) {
                list($key, $val) = explode(':', $cat);
                $categories[$key] = $val;
            }
        }

        asort($categories);
        return $categories;
    }

    /**
     * Add a new category
     *
     * @param string  $name     The name of the category to add.
     *
     * @return integer          A valid category id, 0 on failure or
     *                          the new category's id on success.
     */
    function addCategory($name)
    {
        global $prefs;

        if ($prefs->isLocked('event_categories') || empty($name)) {
            return 0;
        }

        $categories = Kronolith::listCategories();
        if (in_array($name, $categories)) {
            return 0;
        }

        $categories[] = $name;
        unset($categories[0]);

        $cats = array();
        $key = 0;
        foreach ($categories as $key => $cat) {
            $cat = array($key, $cat);
            $cats[] = implode(':', $cat);
        }

        $catString = implode('|', $cats);
        $prefs->setValue('event_categories', $catString);

        return $key;
    }

    /**
     * Delete a category
     *
     * @param integer   $categoryID The id of the category to remove.
     *
     * @return boolean              True on success, false on failure.
     */
    function deleteCategory($categoryID)
    {
        global $prefs;

        $categories = Kronolith::listCategories();

        if ($prefs->isLocked('event_categories') ||
            !array_key_exists($categoryID, $categories)) {
            return false;
        }

        unset($categories[0]);
        unset($categories[$categoryID]);

        $cats = array();
        foreach ($categories as $key => $cat) {
            $cat = array($key, $cat);
            $cats[] = implode(':', $cat);
        }

        $catString = implode('|', $cats);
        $prefs->setValue('event_categories', $catString);

        return true;
    }

    /**
     * Rename a category
     *
     * @param integer   $categoryID The id of the category to remove.
     * @param string    $name       The new name of the category.
     *
     * @return boolean              True on success, false on failure.
     */
    function renameCategory($categoryID, $name)
    {
        global $prefs;

        $categories = Kronolith::listCategories();

        if ($prefs->isLocked('event_categories') ||
            empty($name) ||
            !array_key_exists($categoryID, $categories)) {
            return false;
        }

        unset($categories[0]);
        $categories[$categoryID] = $name;

        $cats = array();
        foreach ($categories as $key => $cat) {
            $cat = array($key, $cat);
            $cats[] = implode(':', $cat);
        }

        $catString = implode('|', $cats);
        $prefs->setValue('event_categories', $catString);

        return true;
    }

    /**
     * Returns the highlight colors for the categories
     *
     * @return array A list of colors, key matches listCategories keys.
     */
    function categoryColors()
    {
        global $prefs, $registry;

        static $colorString, $colors;
        if (!is_array($colors)) {
            $colors = array();
            $colorString = array();
        }

        foreach ($GLOBALS['display_calendars'] as $calendar_id) {
            $_colorString = $GLOBALS['shares']->getPrefByShare('event_colors', $calendar_id);
            if (!array_key_exists($calendar_id, $colors) || $colorString[$calendar_id] != $_colorString) {
                $colors[$calendar_id] = array(0 => '#ccccff');
                $colorString[$calendar_id] = $_colorString;
                $cols = explode('|', $colorString[$calendar_id]);
                foreach ($cols as $col) {
                    if (!empty($col)) {
                        list($key, $val) = explode(':', $col);
                        $colors[$calendar_id][$key] = $val;
                    }
                }
            }
        }

        return $colors;
    }

    /**
     * Returns the string matching the given category ID.
     *
     * @param integer $categoryID     The category ID to look up.
     * @param string  $calendar       The calendar the category belongs to.
     *
     * @return string       The formatted category string.
     */
    function formatCategory($categoryID = 0, $calendar = null)
    {
        $categories = Kronolith::listCategories($calendar);
        return isset($categories[$categoryID]) ?
            $categories[$categoryID] :
            $categories[0];
    }

    /**
     * Calculate the border (darker) version of a color.
     *
     * @param string $color   An HTML color, e.g.: ffffcc.
     *
     * @return string  A darker html color.
     */
    function borderColor($color)
    {
        return Horde_Image::modifyColor($color, -0x44);
    }

    /**
     * Calculate the highlight (lighter) version of a color.
     *
     * @param string $color   An HTML color, e.g.: ffffcc.
     *
     * @return string  A lighter html color.
     */
    function highlightColor($color)
    {
        return Horde_Image::modifyColor($color, 0x22);
    }

    function isFreeBusyUrl()
    {
        $path = $_SERVER['PHP_SELF'];
        if (!empty($_SERVER['PATH_INFO'])) {
            $path = str_replace($_SERVER['PATH_INFO'], '', $path);
        }
        return (boolean)(basename($path) == 'fb.php');
    }

    /**
     * Generate the free/busy text for $calendar. Cache it for at
     * least an hour, as well.
     *
     * @access public
     *
     * @param string  $calendar    The calendar to view free/busy slots for.
     * @param integer $startstamp  The start of the time period to retrieve.
     * @param integer $endstamp    The end of the time period to retrieve.
     * @param boolean $returnObj   (optional) Default false. Return a vFreebusy
     *                             Object instead of text.
     *
     * @return string  The free/busy text.
     */
    function generateFreeBusy($cal, $startstamp = null, $endstamp = null, $returnObj = false)
    {
        global $shares;

        require_once HORDE_BASE . '/lib/Identity.php';
        require_once HORDE_BASE . '/lib/iCalendar.php';
        require_once KRONOLITH_BASE . '/lib/version.php';

        // Fetch the appropriate share and check permissions.
        $share = &$shares->getShare($cal);
        if (is_a($share, 'PEAR_Error')) {
            return '';
        }

        // Default the start date to today
        if (is_null($startstamp)) {
            $month = date('n');
            $year = date('Y');
            $day = date('j');

            $startstamp = mktime(0, 0, 0, $month, $day, $year);
        }

        // Default the end date to the start date + freebusy_days.
        if (is_null($endstamp) || $endstamp < $startstamp) {
            $month = date('n', $startstamp);
            $year = date('Y', $startstamp);
            $day = date('j', $startstamp);

            $endstamp = mktime(0, 0, 0, $month,
                               $day + $shares->getPrefByShare('freebusy_days', $share), $year);
        }

        // Get the Identity for the owner of the share.
        $identity = &new Identity($share->getOwner());
        $email = $identity->getValue('from_addr');
        $cn = $identity->getValue('fullname');

        // Fetch events.
        $startDate = Kronolith::timestampToObject($startstamp);
        $endDate = Kronolith::timestampToObject($endstamp);
        $busy = Kronolith::listEvents($startDate, $endDate, array($cal));

        // Create the new iCalendar.
        $vCal = &new Horde_iCalendar();
        $vCal->setAttribute('PRODID', '-//The Horde Project//Kronolith ' . KRONOLITH_VERSION . '//EN');
        $vCal->setAttribute('METHOD', 'PUBLISH');

        // Create new vFreebusy.
        $vFb = &Horde_iCalendar::newComponent('vfreebusy', $vCal);
        $params = array();
        if (!empty($cn)) {
            $params['CN'] = $cn;
        }
        if (!empty($email)) {
            $vFb->setAttribute('ORGANIZER', 'MAILTO:' . $email, $params);
        } else {
            $vFb->setAttribute('ORGANIZER', '', $params);
        }

        $vFb->setAttribute('DTSTAMP', time());
        $vFb->setAttribute('DTSTART', $startDate);
        $vFb->setAttribute('DTEND', $endDate);
        $vFb->setAttribute('URL', Horde::applicationUrl('fb.php?c=' . $cal, true, -1));

        // Add all the busy periods.
        foreach ($busy as $day => $events) {
            foreach ($events as $event) {
                $start = $event->getStartTimestamp();
                $end = $event->getEndTimestamp();
                $duration = $end - $start;

                // Make sure that we're using the current date for
                // recurring events.
                if (!$event->hasRecurType(KRONOLITH_RECUR_NONE)) {
                    $startThisDay = mktime($event->getStartDate('G'),
                                           $event->getStartDate('i'),
                                           $event->getStartDate('s'),
                                           date('n', $day),
                                           date('j', $day),
                                           date('Y', $day));
                } else {
                    $startThisDay = $event->getStartTimestamp();
                }
                $vFb->addBusyPeriod('BUSY', $startThisDay, null, $duration);
            }
        }

        // Remove the overlaps.
        $vFb->simplify();
        $vCal->addComponent($vFb);

        // Return the vFreebusy object if requested.
        if ($returnObj) {
            return $vFb;
        }

        // Generate the vCal file.
        $fb = $vCal->exportvCalendar();
        return $fb;
    }

    function menu()
    {
        global $conf, $notification, $calendar, $registry, $shares,
            $prefs, $browser, $display_calendars,
            $display_remote_calendars, $print_link;
        require_once HORDE_BASE . '/lib/Menu.php';

        $timestamp = Horde::getFormData('timestamp');
        if (!$timestamp) {
            $year = Horde::getFormData('year', date('Y'));
            $month = Horde::getFormData('month', date('m'));
            $day = Horde::getFormData('mday', date('d'));
            if ($week = Horde::getFormData('week')) {
                $month = 1;
                $day = Kronolith::firstDayOfWeek($week, $year);
            }
            $hour = date('H');
            $min = date('i');
            $timestamp = mktime($hour, $min - ($min % 5), 0, $month, $day, $year);
        }
        $append = "?timestamp=$timestamp";

        $calendars = Kronolith::listCalendars();
        $remote_calendars = unserialize($prefs->getValue('remote_cals'));

        require KRONOLITH_TEMPLATES . '/menu/menu.inc';

        /* Include the JavaScript for the help system. */
        Help::javascript();

        // Get any alarms in the next hour.
        $_now = time();
        $_alarmList = Kronolith::listAlarms(Kronolith::timestampToObject($_now), $GLOBALS['display_calendars']);
        $_messages = array();
        foreach ($_alarmList as $_alarmCal => $_alarmEvents) {
            if (count($_alarmCal) && $calendar->getCalendar() != $_alarmCal) {
                $calendar->close();
                $calendar->open($_alarmCal);
            }
            foreach ($_alarmEvents as $_alarmEventId) {
                $_alarmEvent = $calendar->getEventObject($_alarmEventId);

                // Make sure the event is actually occurring if it's a
                // repeating event.
                if ($_alarmEvent->hasException(date('Y', $_now), date('n', $_now), date('j', $_now))) {
                    continue;
                }

                $differential = $_alarmEvent->getStartTimestamp($_now) - $_now;
                $_key = $_alarmEvent->getStartTimestamp();
                while (isset($_messages[$_key])) {
                    $_key++;
                }
                if ($differential >= -60 && $differential < 60) {
                    $_messages[$_key] = array(sprintf(_("%s is starting now."), $_alarmEvent->getTitle()), 'kronolith.alarm');
                } elseif ($differential < 0 && $_now <= $_alarmEvent->getEndTimestamp()) {
                    $_messages[$_key] = array(sprintf(_("%s is in progress."), $_alarmEvent->getTitle()), 'kronolith.event');
                } elseif ($differential >= 60 && $differential < 7200) {
                    $_messages[$_key] = array(sprintf(_("%s starts in %s"), $_alarmEvent->getTitle(), Kronolith::secondsToString($differential)), 'kronolith.alarm');
                }
            }
        }

        ksort($_messages);
        foreach ($_messages as $message) {
            $notification->push($message[0], $message[1]);
        }

        // Check here for guest calendars so that we don't get
        // multiple messages after redirects, etc.
        if (!Auth::getAuth() && !count(Kronolith::listCalendars())) {
            $notification->push(_("No calendars are available to guests."));
        }

        // Display all notifications.
        $notification->notify();
    }

}
