/**
  * returns next occurrence _ignoring exceptions_ or NULL if there is none/not computable
  * 
  * NOTE: an ongoing event during $from [start, end[ is considered as next 
  * NOTE: for previous events on ongoing event is considered as previous
  *  
  * NOTE: computing the next occurrence of an open end rrule can be dangerous, as it might result
  *       in a endless loop. Therefore we only make a limited number of attempts before giving up.
  * 
  * @param  Calendar_Model_Event         $_event
  * @param  Tinebase_Record_RecordSet    $_exceptions
  * @param  Tinebase_DateTime            $_from
  * @param  Int                          $_which
  * @return Calendar_Model_Event|NULL
  */
 public static function computeNextOccurrence($_event, $_exceptions, $_from, $_which = 1)
 {
     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
         Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' $from = ' . $_from->toString());
     }
     if ($_which === 0 || $_event->dtstart >= $_from && $_event->dtend > $_from) {
         return $_event;
     }
     $freqMap = array(self::FREQ_DAILY => Tinebase_DateTime::MODIFIER_DAY, self::FREQ_WEEKLY => Tinebase_DateTime::MODIFIER_WEEK, self::FREQ_MONTHLY => Tinebase_DateTime::MODIFIER_MONTH, self::FREQ_YEARLY => Tinebase_DateTime::MODIFIER_YEAR);
     $rrule = new Calendar_Model_Rrule(NULL, TRUE);
     $rrule->setFromString($_event->rrule);
     $from = clone $_from;
     $until = clone $from;
     $interval = $_which * $rrule->interval;
     // we don't want to compute ourself
     $ownEvent = clone $_event;
     $ownEvent->setRecurId($_event->getId());
     $exceptions = clone $_exceptions;
     $exceptions->addRecord($ownEvent);
     $recurSet = new Tinebase_Record_RecordSet('Calendar_Model_Event');
     if ($_from->isEarlier($_event->dtstart)) {
         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' from is ealier dtstart -> given event is next occurrence');
         }
         return $_event;
     }
     $rangeDate = $_which > 0 ? $until : $from;
     if (!isset($freqMap[$rrule->freq])) {
         if (Tinebase_Core::isLogLevel(Zend_Log::ERR)) {
             Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' Invalid RRULE:' . print_r($rrule->toArray(), true));
         }
         throw new Calendar_Exception('Invalid freq in RRULE: ' . $rrule->freq);
     }
     $rangeDate->add($interval, $freqMap[$rrule->freq]);
     $attempts = 0;
     if ($_event->rrule_until instanceof DateTime && Tinebase_Core::isLogLevel(Zend_Log::TRACE)) {
         Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Event rrule_until: ' . $_event->rrule_until->toString());
     }
     while (TRUE) {
         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) {
             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' trying to find next occurrence from ' . $from->toString());
         }
         if ($_event->rrule_until instanceof DateTime && $from->isLater($_event->rrule_until)) {
             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' passed rrule_until -> no further occurrences');
             }
             return NULL;
         }
         $until = $_event->rrule_until instanceof DateTime && $until->isLater($_event->rrule_until) ? clone $_event->rrule_until : $until;
         $recurSet->merge(self::computeRecurrenceSet($_event, $exceptions, $from, $until));
         $attempts++;
         // NOTE: computeRecurrenceSet also returns events during $from in some cases, but we need
         // to events later than $from.
         $recurSet = $recurSet->filter(function ($event) use($from) {
             return $event->dtstart >= $from;
         });
         if (count($recurSet) >= abs($_which)) {
             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " found next occurrence after {$attempts} attempt(s)");
             }
             break;
         }
         if ($attempts > count($exceptions) + 5) {
             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " could not find the next occurrence after {$attempts} attempts, giving up");
             }
             return NULL;
         }
         $from->add($interval, $freqMap[$rrule->freq]);
         $until->add($interval, $freqMap[$rrule->freq]);
     }
     $recurSet->sort('dtstart', $_which > 0 ? 'ASC' : 'DESC');
     $nextOccurrence = $recurSet[abs($_which) - 1];
     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
         Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' $nextOccurrence->dtstart = ' . $nextOccurrence->dtstart->toString());
     }
     return $nextOccurrence;
 }