<?php
/**
 * @package Horde_Kolab
 *
 * $Horde: framework/Kolab/Kolab/Freebusy.php,v 1.3.2.9 2008/04/06 06:28:50 wrobel Exp $
 */

/** We need the Kolab library for connecting to the IMAP storage. */
require_once 'Horde/Kolab.php';

/** We need the Date library for event handling. */
require_once 'Horde/Date.php';

/** We need the Recurrence library for recurrence handling. */
require_once 'Horde/Date/Recurrence.php';

/** Event status - Taken from Kronolith*/
define('KRONOLITH_STATUS_NONE', 0);
define('KRONOLITH_STATUS_TENTATIVE', 1);
define('KRONOLITH_STATUS_CONFIRMED', 2);
define('KRONOLITH_STATUS_CANCELLED', 3);
define('KRONOLITH_STATUS_FREE', 4);

/**
 * The Horde_Kolab_Freebusy class provides a library for quickly
 * generating free/busy information from the Kolab IMAP data.
 * 
 * This class is a merged result from the Kolab free/busy package and
 * the Horde::Kronolith free/busy driver.
 *
 * $Horde: framework/Kolab/Kolab/Freebusy.php,v 1.3.2.9 2008/04/06 06:28:50 wrobel Exp $
 *
 * Copyright 2007-2008 The Horde Project (http://www.horde.org/)
 *
 * @author  Gunnar Wrobel <wrobel@pardus.de>
 * @author  Chuck Hagenbuch <chuck@horde.org>
 * @author  Steffen Hansen <steffen@klaralvdalens-datakonsult.se>
 * @package Horde_Kolab
 */
class Horde_Kolab_Freebusy {

    /**
     * Our Kolab server connection.
     *
     * @var Kolab
     */
    var $_kolab = null;

    /**
     * Shortcut to the imap connection
     *
     * @var Kolab_IMAP
     */
    var $_store;

    /**
     * The folder we are generating free/busy information for
     *
     * @var string
     */
    var $_folder;

    /**
     * Is this folder relevant only for users or admins?
     *
     * @var string
     */
    var $_relevance;

    /**
     * Folder ACLs
     *
     * @var string
     */
    var $_acl;

    /**
     * Folder extended attributes ACL
     *
     * @var string
     */
    var $_xacl;

    /**
     * Initialize the free/busy handler.
     *
     * @return Horde_Kolab_Freebusy  The initialized free/busy class
     */
    function Horde_Kolab_Freebusy()
    {
        /* Pretend that we are Kronolith */
        $this->_kolab = &new Kolab('kronolith');
    }

    /**
     * Connect to IMAP.
     *
     * This function has been derived from the synchronize() function
     * in the Kolab driver for Kronolith.
     *
     * @param string $folder         The folder to generate free/busy data for.
     */
    function connect($folder)
    {
        // Connect to the Kolab backend
        $result = $this->_kolab->open($folder, 1);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }
        $this->_store = &$this->_kolab->_storage;
        if (!$this->_store->_imap->exists($folder)) {
            return PEAR::raiseError(sprintf(_("Folder %s does not exist!"), $folder));
        }
        $type = $this->_store->getMailboxType($folder);
        if ($type != 'event') {
            return PEAR::raiseError(sprintf(_("Folder %s has type \"%s\" not \"event\"!"), 
                                            $folder, $type));
        }
        $this->_folder = $folder;
    }

    /**
     * Lists all events in the time range, optionally restricting
     * results to only events with alarms.
     *
     * Taken from the Kolab driver for Kronolith.
     *
     * @param Horde_Date $startInterval  Start of range date object.
     * @param Horde_Date $endInterval    End of range data object.
     *
     * @return array  Events in the given time range.
     */
    function listEvents($startDate = null, $endDate = null)
    {
        $objects = $this->_store->getObjects();
        if (is_a($objects, 'PEAR_Error')) {
            return $objects;
        }

        if (is_null($startDate)) {
            $startDate = &new Horde_Date(array('mday' => 1, 'month' => 1, 'year' => 0000));
        }
        if (is_null($endDate)) {
            $endDate = &new Horde_Date(array('mday' => 31, 'month' => 12, 'year' => 9999));
        }
        $startts = $startDate->timestamp();
        $endts = $endDate->timestamp();

        $result = array();

        foreach($objects as $object) {
            /* check if event period intersects with given period */
            if (!(($object['start-date'] > $endts) || 
                  ($object['end-date'] < $startts))) {
                $event = &new Kolab_Event($object);
                $result[] = $event;
                continue;
            }

            /* do recurrence expansion if not keeping anyway */
            if (isset($object['recurrence'])) {
                $event = &new Kolab_Event($object);
                $next = $event->recurrence->nextRecurrence($startDate);
                while ($next !== false && 
                       $event->recurrence->hasException($next->year, $next->month, $next->mday)) {
                    $next->mday++;
                    $next = $event->recurrence->nextRecurrence($next);
                }

                if ($next !== false) {
                    $duration = $next->timestamp() - $event->start->timestamp();
                    $next_end = &new Horde_Date($event->end->timestamp() + $duration);

                    if ((!(($endDate->compareDateTime($next) < 0) || 
                           ($startDate->compareDateTime($next_end) > 0)))) {
                        $result[] = $event;
                    }

                }
            }
        }

        return $result;
    }

    function getRelevance() {

        /* cached? */
        if (isset($this->_relevance)) {
            return $this->_relevance;
        }

        $annotation = $this->_store->_imap->getAnnotation('/vendor/kolab/incidences-for',
                                                          'value.shared',
                                                          $this->_folder);
        if (is_a($annotation, 'PEAR_Error')) {
            return $annotation;
        }

        if (empty($annotation)) {
            Horde::logMessage(sprintf('No relevance value found for %s', $this->_folder),
                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
            $this->_relevance = 'admins';
        } else {
            Horde::logMessage(sprintf('Relevance for %s is %s', $this->_folder, $annotation),
                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
            $this->_relevance = $annotation;
        }
        return $this->_relevance;
    }

    function getACL() {

        /* cached? */
        if (isset($this->_acl)) {
            return $this->_acl;
        }

        $acl = $this->_store->_imap->getACL($this->_folder);
        if (is_a($acl, 'PEAR_Error')) {
            return $acl;
        }

        if (empty($acl)) {
            Horde::logMessage(sprintf('No ACL found for %s', $this->_folder),
                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
            $this->_acl = '';
        } else {
            Horde::logMessage(sprintf('ACL for %s is %s', 
                                      $this->_folder, serialize($acl)),
                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
            $this->_acl = $acl;
        }
        return $this->_acl;
    }

    function getExtendedACL() {

        /* cached? */
        if (isset($this->_xacl)) {
            return $this->_xacl;
        }

        $annotation = $this->_store->_imap->getAnnotation('/vendor/kolab/xfb-readable',
                                                          'value.shared',
                                                          $this->_folder);
        if (is_a($annotation, 'PEAR_Error')) {
            return $annotation;
        }

        if (empty($annotation)) {
            Horde::logMessage(sprintf('No extended ACL value found for %s', $this->_folder),
                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
            $this->_xacl = '';
        } else {
            Horde::logMessage(sprintf('Extended ACL for %s is %s', $this->_folder, $annotation),
                              __FILE__, __LINE__, PEAR_LOG_DEBUG);
            $this->_xacl = $annotation;
        }
        return $this->_xacl;
    }

    /**
     * Generates the free/busy text for $calendar. Cache it for at least an
     * hour, as well.
     *
     * @param integer $startstamp     The start of the time period to retrieve.
     * @param integer $endstamp       The end of the time period to retrieve.
     * @param integer $fbpast         The number of days that free/busy should
     *                                be calculated for the past
     * @param integer $fbfuture       The number of days that free/busy should
     *                                be calculated for the future
     * @param string  $user           Set organizer to this user.
     * @param string  $cn             Set the common name of this user.
     *
     * @return Horde_iCalendar  The iCal object or a PEAR error.
     */
    function &generate($startstamp = null, $endstamp = null,
                       $fbpast = 0, $fbfuture = 60,
                       $user = null, $cn = null)
    {
        /* Get the iCalendar library at this point */
        require_once 'Horde/iCalendar.php';

        /* 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 - $fbpast, $year);
        }

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

            $endstamp = mktime(0, 0, 0,
                               $month,
                               $day + $fbfuture,
                               $year);
        }

        Horde::logMessage(sprintf('Creating free/busy information from %s to %s',
                                  $startstamp, $endstamp), __FILE__, __LINE__,
                          PEAR_LOG_DEBUG);

        /* Fetch events. */
        $startDate = new Horde_Date($startstamp);
        $endDate = new Horde_Date($endstamp);
        $events = $this->listEvents($startDate, $endDate);
        if (is_a($events, 'PEAR_Error')) {
            return $events;
        }

        /* Create the new iCalendar. */
        $vCal = new Horde_iCalendar();
        $vCal->setAttribute('PRODID', '-//proko2//freebusy 1.0//EN');
        $vCal->setAttribute('METHOD', 'PUBLISH');

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

        $vFb->setAttribute('DTSTAMP', $_SERVER['REQUEST_TIME']);
        $vFb->setAttribute('DTSTART', $startstamp);
        $vFb->setAttribute('DTEND', $endstamp);
        // URL is not required, so out it goes...
        //$vFb->setAttribute('URL', Horde::applicationUrl('fb.php?u=' . $share->get('owner'), true, -1));

        /* Add all the busy periods. */
        foreach ($events as $event) {
            if ($event->hasStatus(KRONOLITH_STATUS_FREE)) {
                continue;
            }

            $duration = $event->end->timestamp() - $event->start->timestamp();
            $extra = array('X-UID'      => base64_encode($event->eventID),
                           'X-SUMMARY'  => base64_encode($event->private ? '' : $event->title),
                           'X-LOCATION' => base64_encode($event->private ? '' : $event->location));

            /* Make sure that we're using the current date for recurring
             * events. */
            if ($event->recurs()) {
                $startThisDay = mktime($event->start->hour,
                                       $event->start->min,
                                       $event->start->sec,
                                       date('n', $day),
                                       date('j', $day),
                                       date('Y', $day));
            } else {
                $startThisDay = $event->start->timestamp($extra);
            }
            if (!$event->recurs()) {
                $vFb->addBusyPeriod('BUSY', $startThisDay, null, $duration, $extra);
            } else {
                $next = $event->recurrence->nextRecurrence($startDate);
                while ($next) {
                    if ($endDate->compareDateTime($next) < 0) {
                        break;
                    }
                    if (!$event->recurrence->hasException($next->year, $next->month, $next->mday)) {
                        $vFb->addBusyPeriod('BUSY', $next->timestamp(), null, $duration, $extra);
                    }
                    $next->mday++;
                    $next = $event->recurrence->nextRecurrence($next);
                }
            }
        }

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

        return $vCal;
    }
}

class Kolab_Event {

    /**
     * The driver unique identifier for this event.
     *
     * @var string
     */
    var $eventID = null;

    /**
     * The start time of the event.
     *
     * @var Horde_Date
     */
    var $start;

    /**
     * The end time of the event.
     *
     * @var Horde_Date
     */
    var $end;

    /**
     * The title of this event.
     *
     * @var string
     */
    var $title = '';

    /**
     * The location this event occurs at.
     *
     * @var string
     */
    var $location = '';

    /**
     * Whether the event is private.
     *
     * @var boolean
     */
    var $private = false;

    function Kolab_Event($event) 
    {
        $this->eventID = $event['uid'];

        $this->start = new Horde_Date($event['start-date']);
        $this->end = new Horde_Date($event['end-date']);

        if (isset($event['summary'])) {
            $this->title = $event['summary'];
        }

        if (isset($event['location'])) {
            $this->location = $event['location'];
        }

        if ($event['sensitivity'] == 'private' || $event['sensitivity'] == 'confidential') {
            $this->private = true;
        }

        if (isset($event['show-time-as'])) {
            switch ($event['show-time-as']) {
                case 'free':
                    $this->status = KRONOLITH_STATUS_FREE;
                    break;

                case 'tentative':
                    $this->status = KRONOLITH_STATUS_TENTATIVE;
                    break;

                case 'busy':
                case 'outofoffice':
                default:
                    $this->status = KRONOLITH_STATUS_CONFIRMED;
            }
        } else {
            $this->status = KRONOLITH_STATUS_CONFIRMED;
        }

        // Recurrence
        if (isset($event['recurrence'])) {
            $this->recurrence = new Horde_Date_Recurrence($this->start);
            $this->recurrence->fromHash($event['recurrence']);
        }

    }

    /**
     * Returns whether this event is a recurring event.
     *
     * @return boolean  True if this is a recurring event.
     */
    function recurs()
    {
        return isset($this->recurrence) &&
            !$this->recurrence->hasRecurType(HORDE_DATE_RECUR_NONE);
    }

    /**
     * Sets the global UID for this event.
     *
     * @param string $uid  The global UID for this event.
     */
    function setUID($uid)
    {
        $this->_uid = $uid;
    }

    /**
     * Checks whether the events status is the same as the specified value.
     *
     * @param integer $status  The status value to check against.
     *
     * @return boolean  True if the events status is the same as $status.
     */
    function hasStatus($status)
    {
        return ($status == $this->status);
    }

}
