<?php

/**
 * Horde Kronolith driver for the Kolab IMAP server.
 * Based on code by Adriaan Putter <kougom@yahoo.com>
 *
 * Copyright (C) 2003 Code Fusion, cc.
 * Written by Stuart Bingė <s.binge@codefusion.co.za>
 *
 * Created 2003-08-25
 * Last Modified 2003-08-29 by Stuart Bingė
 */

require_once(HORDE_BASE . '/lib/Kolab.php');
require_once(HORDE_BASE . '/lib/iCalendar.php');

class Kronolith_Driver_kolab extends Kronolith_Driver {

  var $params = array();
  var $_folder;
  var $_server;
  var $imap;

  function Kronolith_Driver_kolab($params = array())
  {
    $this->type = 'kolab';
    $this->params = $params;
    $this->_folder = $params['folder'];
    $this->_server = $params['server'];
  }

  function open($calendar)
  {
    $this->_calendar = $calendar;
    $this->imap = Kolab::OpenImapConnection($this->_server, $this->_folder);
    if (!$this->imap) return PEAR::raiseError('Unable to open IMAP connection.');
    return true;
  }

  function close()
  {
    Kolab::CloseIMAPConnection($this->imap);
  }

  function delete($calendar)
  {
    // nothing for now...
  }

  function listEvents($startDate = null, $endDate = null)
  {
    $events = array();

    $msgs = Kolab::GetMessageList($this->imap);
    $cal = new Horde_iCalendar;
    for ($i = 0; $i < count($msgs); $i++)
    {
      $body = imap_body($this->imap, $msgs[$i]);
      $cal->_components = array();
      $cal->parsevCalendar($body);

      $components = $cal->getComponents();
      $ispresent = false;

      foreach ($components as $component)
      {
        if (!is_a($component, 'Horde_iCalendar_vevent')) continue;

        $startattr = $component->getAttribute('DTSTART');
        if (is_array($startattr) || is_a($startattr, 'PEAR_Error')) continue;

        $start = getdate($startattr);
        if (!$ispresent &&
            $start['year'] >= $startDate->year && $start['year'] <= $endDate->year &&
            $start['mon'] >= $startDate->month && $start['mon'] <= $endDate->month &&
            $start['mday'] >= $startDate->mday && $start['mday'] <= $endDate->mday)
        {
          array_push($events, $component->getAttribute('UID'));
          $ispresent = true;
        }
      }
    }

    return $events;
  }

  function listAlarms($date)
  {
    $events = $this->listEvents($date, $date);
    $alarms = array();

    $cal = new Horde_iCalendar;
    foreach ($events as $event)
    {
      $matches = imap_search($this->imap, "SUBJECT $event");
      if (!is_array($matches) || count($matches) < 1) continue;

      $body = imap_body($this->imap, $matches[0]);
      $cal->parsevCalendar($body);
      $components = $cal->getComponents();

      foreach ($components as $component)
      {
        if (!is_a($component, 'Horde_iCalendar_vevent')) continue;

        $subcomps = $component->getComponents();
        foreach ($subcomps as $subcomp)
        {
          if (!is_a($subcomp, 'Horde_iCalendar_valarm')) continue;

          $ts = Kronolith::objectToTimestamp($date);

          $dateattr = $component->getAttribute('DTSTART');
          if (is_array($dateattr) || is_a($dateattr, 'PEAR_Error')) continue;

          $offset = $subcomp->getAttribute("TRIGGER");
          $diff = $ts - $dateattr;
          if ($diff >= $offset && $diff <= 0)
            array_push($alarms, $event);
        }
      }
    }

    return $alarms;
  }

  function &getEventObject($eventID = null)
  {
    if (!is_null($eventID)) {
      $cal = new Horde_iCalendar;
      $matches = imap_search($this->imap, "SUBJECT $eventID");
      if (!is_array($matches) || count($matches) < 1) return false;
      $body = imap_body($this->imap, $matches[0]);
      $headers = Kolab::GetMessageHeaders($this->imap, $matches[0]);
      $cat = Kolab::GetHeaderValue($headers, X_HEAD_CAT, 0);
      $cal->parsevCalendar($body); 
      $components = $cal->getComponents();

      foreach ($components as $component)
      {
        if (!is_a($component, 'Horde_iCalendar_vevent')) continue;

        $obj = new Kronolith_Event_kolab($this, $component);
        $obj->category = $cat;
        return $obj;
      }

      return false;
    } else {
      return new Kronolith_Event_kolab($this);
    }
  }

  function saveEvent($event)
  {
    // $event should be a Kronolith_Event_kolab

    list($user, $pass) = Kolab::GetAuthentication();
    $mailbox = Kolab::MailboxURI($this->_server, $this->_folder);

    $eventID = $event->getID();

    $cal = new Horde_iCalendar;
    $alobj = NULL;
    if (!is_null($eventID))
    {
      $matches = imap_search($this->imap, "SUBJECT $eventID");
      if (!is_array($matches) || count($matches) < 1) return false;
      $body = imap_body($this->imap, $matches[0]);
      imap_delete($this->imap, $matches[0]);
      imap_expunge($this->imap);
      $cal->parsevCalendar($body);

      $evobj =& $cal->findComponent('VEVENT');
      if (!is_null($evobj))
        $alobj =& $evobj->findComponent('VALARM');
    }
    else
    {
      $eventID = uniqid('kronolith-');
      $evobj =& $cal->addNewComponent('VEVENT');
      $evobj->setAttribute('CREATED', time());
      $evobj->setAttribute('UID', $eventID);
    }

    if (is_null($evobj)) return PEAR::raiseError("Unable to locate event object for event $eventID");

    $evobj->setAttribute('SUMMARY', $event->getTitle());
    $evobj->setAttribute('DESCRIPTION', $event->getDescription());
    $evobj->setAttribute('LOCATION', $event->getLocation());
    $evobj->setAttribute('LAST-MODIFIED', time());
    $evobj->setAttribute('ORGANIZER', $user);//$event->getCreatorID());
    $evobj->setAttribute('DTSTART', $event->getStartTimestamp());
    $evobj->setAttribute('DTEND', $event->getEndTimestamp());

    $trigger = $event->getAlarm();
    if ($trigger != 0)
    {
      if (is_null($alobj))
        $alobj =& $evobj->addNewComponent('VALARM');

      $alobj->setAttribute('TRIGGER', $trigger);
    }

    $body = str_replace("\n", MIME_NL, $cal->exportvCalendar());

    if (!Kolab::AddMessage($this->imap, $mailbox, $user, 'text/calendar', $body, 'Kronolith', array(
        'Subject' => $eventID,
        X_HEAD_CAT => $event->getCategory()
      )))
      return PEAR::raiseError('Unable to add task.');

    return true;
  }

    function nextRecurrence($eventID, $afterDate, $weekstart = KRONOLITH_SUNDAY)
    {
        $event =& $this->getEventObject($eventID);
        if (is_a($event, 'PEAR_Error')) return $event;

        $afterDate = Kronolith::dateObject($afterDate);

        if (Kronolith::compareDates($event->start, $afterDate) > 0) {
            return $event->start;
        }

        $event->recurEnd->hour = 23;
        $event->recurEnd->min  = 59;
        $event->recurEnd->sec  = 59;

        switch ($event->getRecurType()) {

        case KRONOLITH_RECUR_DAILY:
            $diff = Kronolith::dateDiff($event->start, $afterDate);
            $recur = (ceil($diff->mday / $event->recurInterval)) * $event->recurInterval;
            $next = $event->start;
            list($next->mday, $next->month, $next->year) = explode('/', Date_Calc::daysToDate(Date_Calc::dateToDays($next->mday, $next->month, $next->year) + $recur, '%e/%m/%Y'));
            if (Kronolith::compareDates($next, $event->recurEnd) <= 0 &&
                Kronolith::compareDates($next, $afterDate) >= 0) {
                return $next;
            }
            break;

        case KRONOLITH_RECUR_WEEKLY:
            list($start_week->mday, $start_week->month, $start_week->year) = explode('/', Date_Calc::beginOfWeek($event->start->mday, $event->start->month, $event->start->year, '%e/%m/%Y'));
            $start_week->hour = $event->start->hour;
            $start_week->min = $event->start->min;
            $start_week->sec = $event->start->sec;
            list($after_week->mday, $after_week->month, $after_week->year) = explode('/', Date_Calc::beginOfWeek($afterDate->mday, $afterDate->month, $afterDate->year, '%e/%m/%Y'));
            $after_week_end = $after_week;
            $after_week_end->mday += 7;
            $after_week_end = Kronolith::correctDate($after_week_end);
            $diff = Kronolith::dateDiff($start_week, $after_week);
            $recur = $diff->mday + $diff->mday % ($event->recurInterval * 7);
            $next = $start_week;
            list($next->mday, $next->month, $next->year) = explode('/', Date_Calc::daysToDate(Date_Calc::dateToDays($next->mday, $next->month, $next->year) + $recur, '%e/%m/%Y'));
            while (Kronolith::compareDates($next, $afterDate) < 0 && Kronolith::compareDates($next, $after_week_end) < 0) {
                $next->mday++;
                $next = Kronolith::correctDate($next);
            }
            if (Kronolith::compareDates($next, $event->recurEnd) <= 0) {
                if (Kronolith::compareDates($next, $after_week_end) >= 0) {
                    return $this->nextRecurrence($eventID, $after_week_end);
                }
                while (!$event->recurOnDay((int)pow(2, (int)Date_Calc::dayOfWeek($next->mday, $next->month, $next->year))) && Kronolith::compareDates($next, $after_week_end) < 0) {
                    $next->mday++;
                    $next = Kronolith::correctDate($next);
                }
                if (Kronolith::compareDates($next, $event->recurEnd) <= 0) {
                    if (Kronolith::compareDates($next, $after_week_end) >= 0) {
                        return $this->nextRecurrence($eventID, $after_week_end);
                    } else {
                        return Kronolith::dateObject($next);
                    }
                }
            }
            break;

        case KRONOLITH_RECUR_DAY_OF_MONTH:
            $diff = Kronolith::dateDiff($event->start, $afterDate);
            $recur = $diff->month + $diff->month % $event->recurInterval;
            $next = $event->start;
            $next->month += $recur;
            $next = Kronolith::correctDate($next);
            if (Kronolith::compareDates($next, $event->recurEnd) <= 0 &&
                Kronolith::compareDates($next, $afterDate) >= 0) {
                return $next;
            }
            break;

        case KRONOLITH_RECUR_WEEK_OF_MONTH:
            $week = ceil($event->start->mday / 7);
            $wday = date('w', Kronolith::objectToTimestamp($event->start));

            $diff = Kronolith::dateDiff($event->start, $afterDate);
            $recur = $diff->month + $diff->month % $event->recurInterval;
            $next = $event->start;
            $next->month += $recur;
            $next = Kronolith::correctDate($next);
            $next->mday = $week*7;
            $next_wday = date('w', Kronolith::objectToTimestamp($next));
            if ($next_wday > $wday) {
                $next->mday -= ($next_wday - $wday);
            } elseif ($next_wday < $wday) {
                $next->mday -= ($next_wday + (7 - $wday));
            }

            // Correct the data and return it if w/in the requested range
            $next = Kronolith::correctDate($next);
            if (Kronolith::compareDates($next, $event->recurEnd) <= 0 &&
                Kronolith::compareDates($next, $afterDate) >= 0) {
                return $next;
            }
            break;

        case KRONOLITH_RECUR_YEARLY:
            $diff = Kronolith::dateDiff($event->start, $afterDate);
            $recur = $diff->year + $diff->year % $event->recurInterval;
            $next = $event->start;
            $next->year += $recur;
            if (Kronolith::compareDates($next, $event->recurEnd) <= 0 &&
                Kronolith::compareDates($next, $afterDate) >= 0) {
                return $next;
            }
            break;

        }

        return false;
    }

    function deleteEvent($eventID)
    {
        $matches = imap_search($this->imap, "SUBJECT $eventID");
        if (!is_array($matches) || count($matches) < 1) return false;
        imap_delete($this->imap, $matches[0]);
        imap_expunge($this->imap);
    }
}

class Kronolith_Event_kolab extends Kronolith_Event {

  function toDriver()
  {
    // Nothing - all this is done in the drivers' saveEvent() function
  }

  function fromDriver($icalEvent)
  {
    // $icalEvent should be a Horde_iCalendar_vevent

    $tmp = $icalEvent->getAttribute('SUMMARY');
    if (!is_array($tmp) && !is_a($tmp, 'PEAR_Error'))
      $this->title = $tmp;

    $tmp = $icalEvent->getAttribute('DESCRIPTION');
    if (!is_array($tmp) && !is_a($tmp, 'PEAR_Error'))
      $this->description = $tmp;

    $tmp = $icalEvent->getAttribute('LOCATION');
    if (!is_array($tmp) && !is_a($tmp, 'PEAR_Error'))
      $this->location = $tmp;

    $tmp = $icalEvent->getAttribute('ORGANIZER');
    if (!is_array($tmp) && !is_a($tmp, 'PEAR_Error'))
      $this->creatorID = $tmp;

    $tmp = $icalEvent->getAttribute('DTSTART');
    if (!is_array($tmp) && !is_a($tmp, 'PEAR_Error'))
      {
        $this->startTimestamp = $tmp;
        $this->start = Kronolith::timestampToObject($this->startTimestamp);
      }

    $tmp = $icalEvent->getAttribute('DTEND');
    if (!is_array($tmp) && !is_a($tmp, 'PEAR_Error'))
      {
        $this->endTimestamp = $tmp;
        $this->end = Kronolith::timestampToObject($this->endTimestamp);
      }

    $this->durMin = ($this->endTimestamp - $this->startTimestamp) / 60;

    $this->eventID = $icalEvent->getAttribute('UID');

        $tmp = $icalEvent->getAttribute('RRULE');
        if (!is_a($tmp, 'PEAR_Error'))
        {
            preg_match_all('/([^;=]*)=?([^;]*);?/', $tmp, $rmatches);

            for ($i = 0; $i < count($rmatches[2]); $i++)
            {
                switch ($rmatches[1][$i])
                {
                    case 'FREQ':
                        $freq = $rmatches[2][$i];

                        switch ($freq)
                        {
                            case 'DAILY':
                                $this->recurType = KRONOLITH_RECUR_DAILY;
                                break;
                            case 'WEEKLY':
                                $this->recurType = KRONOLITH_RECUR_WEEKLY;
                                break;
                            case 'MONTHLY':
                                $this->recurType = KRONOLITH_RECUR_DAY_OF_MONTH;
                                break;
                            case 'YEARLY':
                                $this->recurType = KRONOLITH_RECUR_YEARLY;
                                break;
                            default:
                                $this->recurType = KRONOLITH_RECUR_NONE;
                        }
                        break;

                    case 'UNTIL':
                        $until = $rmatches[2][$i];
                        $this->recurEndTimestamp = $icalEvent->_parseDateTime($until);
                        $this->recurEnd = Kronolith::timestampToObject($this->recurEndTimestamp);
                        break;

                    case 'INTERVAL':
                        $interval = $rmatches[2][$i];
                        $this->recurInterval = $interval;
                        break;

                    case 'COUNT':
                        $this->recurEndTimestamp = Kolab::countToUntil($this->startTimestamp, $rmatches[2][$i], $this->recurType);
                        $this->recurEnd = Kronolith::timestampToObject($this->recurEndTimestamp);
                        break;
                }
            }
        }

    $subcomps = $icalEvent->getComponents();
    foreach ($subcomps as $subcomp)
    {
      if (!is_a($subcomp, 'Horde_iCalendar_valarm')) continue;

      $tmp = $subcomp->getAttribute('TRIGGER');
      if (!is_array($tmp) && !is_a($tmp, 'PEAR_Error'))
        $this->alarm = $tmp;
    }

    $this->initialized = true;
  }
}
