
/*---------------------------------------------------------------------------*/
/* time_occurrence.cpp                                                       */
/* copyright (c) innovaphone 2018                                            */
/*                                                                           */
/*---------------------------------------------------------------------------*/

#include <platform.h>

#include "sdk/common/interface/time.h"
#include "sdk/common/interface/timezone.h"

#include "sdk/common/lib/time_occurrence.h"

#define RESULT_ARRAY_BLOCK_SIZE             10

#define ONE_DAY_MS                          86400000 // 24 * 60 * 60 * 1000
#define ONE_WEEK_MS                         604800000 // ONE_DAY_MS * 7

#define DN_SUNDAY       0
#define DN_MONDAY       1
#define DN_TUESDAY      2
#define DN_WEDNESDAY    3
#define DN_THURSDAY     4
#define DN_FRIDAY       5
#define DN_SATURDAY     6
#define DN_DAY          7
#define DN_WEEK_DAY     8
#define DN_WEEKEND_DAY  9


// There is no define for yearly, because that will be done by multiplying the yearly interval with 12 and using the monthly calculation.
#define RT_NONE     0
#define RT_DAILY    1
#define RT_WEEKLY   2
#define RT_MONTHLY  3


/*---------------------------------------------------------------------------*/
/* Helper functions                                                          */
/*---------------------------------------------------------------------------*/

// Little function to test, if only one day is given, so if only one bit is set. To test it, the nice power of 2
// code is used. See http://www.graphics.stanford.edu/~seander/bithacks.html for the code, and a lot of
// other useful bit-magic :-).

inline bool IsPowerOf2(word val)
{
    return val != 0 && !(val & (val - 1));
}


/*---------------------------------------------------------------------------*/
/* class TimeOccurrence                                                      */
/*---------------------------------------------------------------------------*/

TimeOccurrence::TimeOccurrence()
{
    this->recurringType = RT_NONE;
    this->start = 0;
    this->end = 0;
    this->days = 0;
    this->dayNum = 0;
    this->interval = 0;
    this->month = RM_NO_MONTH;
    this->maxOccurrences = 0;
    this->weekDays = RD_WEEK_DAYS;
    this->weekEndDays = RD_WEEKEND_DAYS;
    this->resultCount = 0;
    this->results = nullptr;
    this->resultArraySize = 0;
    this->timeZone = nullptr;
    this->startTimeIsDST = false;
}


TimeOccurrence::~TimeOccurrence()
{
    free(this->results);
}

void TimeOccurrence::SetStartTimeTimeZone(ITimeZone * tz)
{
    this->timeZone = tz;
}


void TimeOccurrence::SetWeekDays(word weekDays)
{
    ASSERT(weekDays < RD_DAY, "TimeOccurrence::SetWeekDays(): RD_DAY, RD_WEEK_DAY or RD_WEEKEND_DAY must not be passed to SetWeekDays()");
    this->weekDays = weekDays;
}


void TimeOccurrence::SetWeekEndDays(word weekEndDays)
{
    ASSERT(weekDays < RD_DAY, "TimeOccurrence::SetWeekEndDays(): RD_DAY, RD_WEEK_DAY or RD_WEEKEND_DAY must not be passed to SetWeekEndDays()");
    this->weekEndDays = weekEndDays;
}


long64 TimeOccurrence::AdjustCalculatedResult(long64 calculatedTime, class ITimeZone * tz)
{
    if (tz == nullptr) tz = this->timeZone;
    if (tz != nullptr && tz->DSTDiff() != 0) {
        bool calcIsDST = tz->IsDST(calculatedTime);
        if (this->startTimeIsDST != calcIsDST) {
            // So the calculated time must be adjusted, depending on whether the first occurrence is in DST or not.
            // So if the appointment was created out of DST but the calculated first occurrence is, we need
            // to remove the DST value. And if the appointment was created during DST, and the calculated
            // result is not in DST, the DST value must be added.
            if (this->startTimeIsDST) calculatedTime += tz->DSTDiff();
            else calculatedTime -= tz->DSTDiff();
        }
    }
    return calculatedTime;
}


void TimeOccurrence::PrepareDayList()
{
    if (this->days == 0 || this->days >= RD_DAY) return;

    struct {
        recurring_days_t day;
        int dayNum;
    } dayList[] = { { RD_SUNDAY, DN_SUNDAY }, { RD_MONDAY, DN_MONDAY }, { RD_TUESDAY, DN_TUESDAY }, { RD_WEDNESDAY, DN_WEDNESDAY },
    { RD_THURSDAY, DN_THURSDAY }, { RD_FRIDAY, DN_FRIDAY }, { RD_SATURDAY, DN_SATURDAY } };

    this->daysSetCount = 0;
    memset(&daysSetNum, -1, sizeof(daysSetNum));

    for (auto d : dayList) {
        if ((d.day & this->days) != 0) {
            this->daysSetNum[this->daysSetCount++] = d.dayNum;
        }
    }
}


void TimeOccurrence::Prepare()
{
    this->PrepareDayList();
    this->startTimeIsDST = this->timeZone == nullptr || this->timeZone->DSTDiff() == 0 ? false : this->timeZone->IsDST(this->start);

    // If there is a max-occurrence, the end-date must be calculated...
    if (this->maxOccurrences < RD_NO_LIMIT) this->end = this->CalculateLastOccurrence();
}


long64 TimeOccurrence::DoCalculateFirstOccurrence(long64 ts)
{
    this->Calculate(ts, RD_NO_END_DATE, 1);
    long64 result = this->results[0];
    this->resultCount = 0;
    return result;
}


void TimeOccurrence::SetDailyMaster(long64 start, long64 end, dword interval, dword maxOccurrences)
{
    if (maxOccurrences != RD_NO_LIMIT && end != RD_NO_END_DATE) {
        ASSERT(false, "TimeOccurrence::SetWeeklyMaster(): Setting end AND maxOccurrences is ambiguous!");
    }
    ASSERT(interval > 0, "TimeOccurrence::SetDailyMaster(): interval must not be 0");

    this->recurringType = RT_DAILY;
    this->start = start;
    this->end = end;
    this->interval = interval;
    this->maxOccurrences = maxOccurrences;
}


void TimeOccurrence::SetWeeklyMaster(dword days, long64 start, long64 end, dword interval, dword maxOccurrences)
{
    if (maxOccurrences != RD_NO_LIMIT && end != RD_NO_END_DATE) {
        ASSERT(false, "TimeOccurrence::SetWeeklyMaster(): Setting end AND maxOccurrences is ambiguous!");
    }
    ASSERT((days & RD_DAY) == 0, "TimeOccurrence::SetWeeklyMaster(): RD_DAY is invalid for SetWeeklyMaster()!");
    ASSERT(days != RD_NO_DAY, "TimeOccurrence::SetWeeklyMaster(): days must not be RD_NO_DAY (0)");
    ASSERT(interval > 0, "TimeOccurrence::SetWeeklyMaster(): interval must not be 0");

    this->recurringType = RT_WEEKLY;
    this->days = days;
    this->start = start;
    this->end = end;
    this->interval = interval;
    this->maxOccurrences = maxOccurrences;
}


void TimeOccurrence::SetMonthlyMaster(dword dayNum, recurring_days_t day, long64 start, long64 end, dword interval, dword maxOccurrences)
{
    if (maxOccurrences != RD_NO_LIMIT && end != RD_NO_END_DATE) {
        ASSERT(false, "TimeOccurrence::SetWeeklyMaster(): Setting end AND maxOccurrences is ambiguous!");
    }

    ASSERT(day != RD_NO_DAY, "TimeOccurrence::SetMonthlyMaster(): day must not be RD_NO_DAY (0).");
    ASSERT(IsPowerOf2(day), "TimeOccurrence::SetRelativeMonthlyMaster(): Function called with more than one day as recurring_days_t value!");
    ASSERT(interval > 0, "TimeOccurrence::SetMonthlyMaster(): interval must not be 0");

    this->recurringType = RT_MONTHLY;
    this->dayNum = dayNum;
    this->days = day;
    this->start = start;
    this->end = end;
    this->interval = interval;
    this->maxOccurrences = maxOccurrences;

    // Need to reset to be reusable.
    this->month = RM_NO_MONTH;
}


void TimeOccurrence::SetYearlyMaster(dword dayNum, recurring_days_t day, recurring_month_t month, long64 start, long64 end, dword interval, dword maxOccurrences)
{
    if (maxOccurrences != RD_NO_LIMIT && end != RD_NO_END_DATE) {
        ASSERT(false, "TimeOccurrence::SetWeeklyMaster(): Setting end AND maxOccurrences is ambiguous!");
    }

    ASSERT(day != RD_NO_DAY, "TimeOccurrence::SetYearlyMaster(): day must not be RD_NO_DAY (0).");
    ASSERT(IsPowerOf2(day), "TimeOccurrence::SetRelativeYearlyMaster(): Function called with more than one day as recurring_days_t value!");
    ASSERT(interval > 0, "TimeOccurrence::SetYearlyMaster(): interval must not be 0");

    this->start = start;
    this->end = end;
    this->month = month;
    this->recurringType = RT_MONTHLY;
    this->interval = interval * 12;
    this->maxOccurrences = maxOccurrences;
    this->days = day;
    this->dayNum = dayNum;
}


void TimeOccurrence::Calculate(long64 timePeriodStart, long64 timePeriodEnd, dword maxResults)
{
    ASSERT(this->recurringType != RT_NONE, "TimeOccurrence::Calculate(): One of the Set*Master() functions must be called before calling Calculate()!");
    if (maxResults > UINT16_MAX) maxResults = UINT16_MAX;

    this->resultCount = 0;
    if ((timePeriodStart < this->end && timePeriodEnd > this->start) || maxResults == 0) {
        this->Prepare();
        this->CalculateOccurrences(timePeriodStart, timePeriodEnd, maxResults, nullptr);
    }
}


void TimeOccurrence::Calculate(UTimeOccurrence * user, long64 timePeriodStart, long64 timePeriodEnd)
{
    ASSERT(this->recurringType != RT_NONE, "TimeOccurrence::Calculate(): One of the Set*Master() functions must be called before calling Calculate()!");
    ASSERT(user != nullptr, "TimeOccurrence::Calculate(): Overloaded function that takes UTimeOccurrence pointer as parameter must not be called with null pointer!");
    if (timePeriodStart < this->end && timePeriodEnd > this->start) {
        this->Prepare();
        this->CalculateOccurrences(timePeriodStart, timePeriodEnd, 0, user);
    }
}


void TimeOccurrence::CalculateOccurrences(long64 tpStart, long64 tpEnd, word maxResults, UTimeOccurrence * user)
{
    switch (this->recurringType)
    {
    case RT_DAILY:
        this->CalculateDailyOccurrences(tpStart, tpEnd, maxResults, user);
        break;

    case RT_WEEKLY:
        this->CalculateWeeklyOccurrences(tpStart, tpEnd, maxResults, user);
        break;

    case RT_MONTHLY:
        this->CalculateMonthlyOccurrences(tpStart, tpEnd, maxResults, user);
        break;

    case RT_NONE:
        ASSERT(false, "TimeOccurrence::CalculateOccurrences(): Called while this->recurringType == RT_NONE - this should not happen!");
        break;
    }
}


bool TimeOccurrence::ProcessResult(long64 value, word maxResults, UTimeOccurrence * user)
{
    value = this->AdjustCalculatedResult(value, this->timeZone);
    return user != nullptr ? user->TimeOccurrenceResult(this, value) : this->ArrayPut(value, maxResults);
}


bool TimeOccurrence::ArrayPut(long64 value, word maxResults)
{
    if (this->resultCount + 1 > this->resultArraySize) {
        this->resultArraySize += RESULT_ARRAY_BLOCK_SIZE;
        this->results = static_cast<long64 *>(realloc(this->results, this->resultArraySize * sizeof(long64)));
    }

    this->results[this->resultCount++] = value;
    return this->resultCount != maxResults;
}


long64 TimeOccurrence::Result(size_t index)
{
    ASSERT(index < this->resultCount, "TimeOccurrence::Result(): given index ist out of range");
    return this->results[index];
}


void TimeOccurrence::CalculateDailyOccurrences(long64 tpStart, long64 tpEnd, word maxResults, UTimeOccurrence * user)
{
    if (this->start > tpEnd || this->end < tpStart) return;

    long64 theStart;
    long64 rmStart = ITime::RemoveTime(this->start);
    long64 timePart = this->start - rmStart;

    if (rmStart >= tpStart) theStart = rmStart;
    else {
        theStart = ITime::RemoveTime(tpStart);
        int diff = ((theStart - rmStart) / ONE_DAY_MS) % this->interval;
        int toAdd = diff == 0 ? 0 : this->interval - diff;
        theStart += long64(toAdd) * ONE_DAY_MS;

        // The calculation can lead to a first occurrence before the timestamp given in tpStart. In that case,
        // directly jump to the next one.
        if (theStart + timePart < tpStart) theStart += ONE_DAY_MS * this->interval;
    }

    bool cont = true;
    while (cont) {
        long64 result = theStart + timePart;

        if (result > tpEnd || result > this->end) {
            cont = false;
        }
        else {
            cont = this->ProcessResult(result, maxResults, user);
            if (cont) theStart += ONE_DAY_MS * this->interval;
        }
    }
}


void TimeOccurrence::CalculateWeeklyOccurrences(long64 tpStart, long64 tpEnd, word maxResults, UTimeOccurrence * user)
{
    if (this->start > tpEnd || this->end < tpStart) return;

    long64 startWOTime = ITime::RemoveTime(this->start);
    long64 timePart = this->start - startWOTime;

    // At first calculate the differences to add for each day to calculate
    long64 timeDiffs[8];
    int curDay = 0;
    int last = 0;
    for (int i = 0; i < this->daysSetCount; ++i) {
        timeDiffs[i] = long64(this->daysSetNum[i] - last) * ONE_DAY_MS;
        last = this->daysSetNum[i];
    }

    // The diff from the last day to the end of the week.
    timeDiffs[this->daysSetCount] = long64(7 - this->daysSetNum[this->daysSetCount - 1]) * ONE_DAY_MS;

    // Get the start for the calculation.
    long64 theStart;
    long64 minStartValue;
    if (this->start > tpStart) {
        minStartValue = this->start;
        theStart = startWOTime;
    }
    else {
        minStartValue = tpStart;
        long64 tpStartWOTime = ITime::RemoveTime(tpStart);
        theStart = tpStartWOTime;// -long64(tm.tmWDay) * ONE_DAY_MS;
        // Make sure that the potential first occurrence will be at or after the given start date.
        if (theStart + timePart < minStartValue) theStart += ONE_DAY_MS;
    }

    // Jump to the first occurrence that the same or newer than the start time for the
    // calculation. If the the is the same as the last on in the list, the time part
    // defines, weather the day counts the next one still needs to be used.
    time_tm_t tm;
    ITime::GetTimeStruct(theStart, &tm);
    int maxDay = this->daysSetNum[this->daysSetCount - 1];
    if (tm.tmWDay > maxDay) {
        theStart += (this->daysSetNum[0] - tm.tmWDay + 7) * ONE_DAY_MS;
    }
    else {
        for (int i = 0; i < this->daysSetCount; ++i)
        {
            curDay = i;
            if (this->daysSetNum[i] == tm.tmWDay) {
                break;
            }
            else if (this->daysSetNum[i] > tm.tmWDay) {
                theStart += (this->daysSetNum[i] - tm.tmWDay) * ONE_DAY_MS;
                break;
            }
        }
    }

    //while (theStart + timePart < minStartValue) theStart += timeDiffs[curDay++];

    // Adjust for interval, if nescesarry.
    if (this->interval > 1) {
        long64 weeks = (theStart - startWOTime) / ONE_WEEK_MS;
        long64 rest = weeks % this->interval;
        if (rest > 0) theStart += (this->interval - rest) * ONE_WEEK_MS;
    }

    if ((theStart + timePart) < minStartValue) theStart = minStartValue; // theStart = startWOTime + timeDiffs[0] + ONE_WEEK_MS;

    bool cont = true;
    while (cont) {
        long64 curStart = theStart + timePart;
        if (curStart > tpEnd || curStart > this->end) cont = false;
        else cont = this->ProcessResult(curStart, maxResults, user);

        ++curDay;
        if (curDay == this->daysSetCount) {
            theStart += timeDiffs[curDay] + long64(this->interval - 1) * ONE_WEEK_MS; // Jump to the begin of the next week
            curDay = 0;
        }
        
        theStart += timeDiffs[curDay];
    }
}


void TimeOccurrence::CalculateMonthlyOccurrences(long64 tpStart, long64 tpEnd, word maxResults, UTimeOccurrence * user)
{
    if (this->start > tpEnd || this->end < tpStart) return;

    long64 theStart = ITime::RemoveTime(this->start);
    long64 timePart = this->start - theStart;
    if (this->start < tpStart) {
        // If the time of the series start is before the time of tpStart,
        // increment the start value by one day (because the first occurrence that
        // will be calculated is outside the given time frame)
        theStart = ITime::RemoveTime(tpStart);
        if (timePart < tpStart - theStart) theStart += ONE_DAY_MS;
    }

    time_tm_t tm;
    ITime::GetTimeStruct(theStart, &tm);
    int startMonth = tm.tmMon;
    // At first west adjust the month, if there is a monthly interval set.
    // If there is an interval AND a month defined, we must take care for
    // the month to be the active one. This is the case for yearly occurrences
    // (which will be handled as monthyl occurrences with a fixed month and
    // an interval of 12 * x (while x is a value starting from 1).
    if (this->month != RM_NO_MONTH) {
        bool monthPassed = startMonth > this->month;
        tm.tmMon = startMonth = this->month + (monthPassed ? this->interval : 0);
        if (this->days == RD_DAY) tm.tmMDay = this->dayNum;
    }
    else if (this->interval > 1 && theStart + timePart > this->start) {
        time_tm_t tmStart;
        ITime::GetTimeStruct(this->start, &tmStart);

        int diff = (tm.tmYear - tmStart.tmYear) * 12 - (tmStart.tmMon - tm.tmMon);
        diff %= this->interval;
        int toAdd = diff == 0 ? 0 : this->interval - diff;

        tm.tmMon += toAdd;
    }

    if (this->days == RD_DAY && tm.tmMDay > this->dayNum && tm.tmMon == startMonth ) {
        // Special handling for absolut days. If the day already passed and tm still holds the day and year of the given start
        // time, jump to the next possible month.
        tm.tmMDay = this->dayNum;
        tm.tmMon += interval;
    }

    bool cont = true;
    while (cont) {
        if (this->days != RD_DAY) this->SetRelativeDay(&tm);
        else {
            // Make sure that the max. days for a month will be respected for absolute recurring masters
            int maxDaysInMonth = ITime::GetDaysOfMonth(tm.tmMon, tm.tmYear + 1900);
            tm.tmMDay = this->dayNum > maxDaysInMonth ? maxDaysInMonth : this->dayNum;
        }

        long64 curStart = ITime::TimeStructToMilliseconds(&tm) + timePart;

        if (curStart > tpEnd || curStart > this->end) {
            cont = false;
        }
        else {
            // Ok, for the monthly relative occurrence, there is a problem. The relative occurrence is something like "the third
            // Monday in Februrary". Well, sadly we need to calculate that first to know, if the result of the calculation lies 
            // before the time stamp given in tpStart (which will be the case if tpStart and the first occurrence are very close
            // and in the same month). So the only solution here: discared what we have calculated.
            if (curStart >= tpStart) cont = this->ProcessResult(curStart, maxResults, user);
            tm.tmMon += this->interval;
        }
    }
}


void TimeOccurrence::SetRelativeDay(time_tm_t * tm)
{
    // There a several was to calculate the relative day:
    //
    // - x. day (by day name) in the month (e. g. 3. Wednesday in January)
    // - x. Weekday in the month (depending on the config witch day is a week day or not)
    // - x. Weekend-Day in the month (depending on the config, too)
    // - x. day in the month (which is aktually the same as the absolute date - e. G. 3. January; this one
    //                        is a little bit wierd and must be supported to be compatible with MS Exchange)
    //
    int tmpMon, tmpMaxDays, tmpYear;

    // The good part - because there can be a set of day names OR the flags RD_DAY, RD_WEEKDAY and RD_WEEKEND_DAY
    // (the flags will ALWAYS stand alone!), a switch-case can be used...
    switch (this->days) {
    case RD_DAY:
        tm->tmMDay = this->dayNum > 31 ? 31 : this->dayNum;
        tmpYear = tm->tmMon / 12;
        tmpMon = tm->tmMon - (tmpYear * 12);
        tmpMaxDays = ITime::GetDaysOfMonth(tmpMon, tmpYear + tm->tmYear + 1900);
        if (tm->tmMDay > tmpMaxDays) tm->tmMDay = tmpMaxDays;
        break;

    case RD_WEEK_DAY:
        this->SetRelativeWeekDay(tm, this->weekDays);
        break;

    case RD_WEEKEND_DAY:
        this->SetRelativeWeekDay(tm, this->weekEndDays);
        break;

    default:
        this->SetRelativeAnyDay(tm);
    }

    // Is there need to add the intervall because the month had been changed?
    //ITime::NormalizeTimeStruct(tm);
    //if (tm->tmMon != oldVal) tm->tmMon += this->interval - 1;
}


void TimeOccurrence::SetRelativeWeekDay(time_tm_t * tm, word days)
{
    static int day_list[] = { RD_SUNDAY, RD_MONDAY, RD_TUESDAY, RD_WEDNESDAY, RD_THURSDAY, RD_FRIDAY, RD_SATURDAY };

    // Start at the first day of the month (month and year is already set when this function will be called)
    tm->tmMDay = 1;
    ITime::NormalizeTimeStruct(tm);

    int daysFound = 0;
    int lastDayFound = 0;
    int curday = tm->tmWDay;
    int daysOfMonth = ITime::GetDaysOfMonth(tm->tmMon, tm->tmYear);

    // That's probably not the most elegant and performant way, but it's works, its flexible and simple.
    for (int i = 0; i < daysOfMonth; i++) {
        if ((days & day_list[curday]) != 0) {
            ++daysFound;
            lastDayFound = i;
            if (daysFound == this->dayNum) break;
        }

        ++curday;
        if (curday == 7) curday = 0;
    }

    tm->tmMDay = lastDayFound + 1;
}


void TimeOccurrence::SetRelativeAnyDay(time_tm_t * tm)
{
    ASSERT(this->daysSetCount != 0, "TimeOccurrence::SetRelativeAnyDay(): daysSetNum must not be 0 when SetRelativeDay() is called");

    // When this function is called, the year or month of tm had been changed. So at first we need to normalize the
    // structure to update all fields. Before that, the month day will be set to 1 to simplyfy the following calculations
    tm->tmMDay = 1;
    ITime::NormalizeTimeStruct(tm);

    // Now search the first occurrence of the given day. If the first of the week is the searched one, everything
    // is already fine. If not the searched day still follows. But get them is simple: substract the number of the
    // searched day from the number of the first one. If the result is negativ, only add 7 (because the searched day
    // is in the next week).
    int dayToSet = this->daysSetNum[this->daysSetCount - 1];
    int dayDelta = 0;
    if (dayToSet != tm->tmWDay) {
        dayDelta = dayToSet - tm->tmWDay;
        if (dayDelta < 0) dayDelta += 7;
    }

    // Now we have the first occurrence. Adjust the value to the occurrence number to look for.
    dayDelta += (this->dayNum - 1) * 7;

    tm->tmMDay += dayDelta;
    int maxDays = ITime::GetDaysOfMonth(tm->tmMon, tm->tmYear);
    if (tm->tmMDay > maxDays) {
        int diff = tm->tmMDay - maxDays;
        int rest = diff % 7;
        if (rest > 0) diff += 7 - rest;
        tm->tmMDay -= diff;
    }
}


long64 TimeOccurrence::CalculateWeeklyEnd(long64 startDate, dword numOfOccurrences)
{
    this->PrepareDayList();
    ASSERT(this->daysSetCount != 0, "TimeOccurrence::CalcWeeklyEnd(): daysSetCount must not be 0 when CalcWeeklyEnd() is called");

    time_tm_t tm;
    ITime::GetTimeStruct(startDate, &tm);

    // At first calc the number of occurrences until the end of the week. For each occurrence, decrement the max number,
    // because they already passed.
    dword daysInRestOfWeek = 0;
    for (int i = 0; i < this->daysSetCount; ++i) {
        if (this->daysSetNum[i] >= tm.tmWDay) ++daysInRestOfWeek;
    }

    if (numOfOccurrences <= daysInRestOfWeek) {
        int lastDay = this->daysSetNum[numOfOccurrences - 1];
        int diff = lastDay - tm.tmWDay;
        return startDate + (diff * ONE_DAY_MS);
    }

    int dayDelta = 7 - tm.tmWDay;
    numOfOccurrences -= daysInRestOfWeek;

    long64 result = startDate + long64(dayDelta * ONE_DAY_MS) + long64(this->interval - 1) * ONE_WEEK_MS;

    int weeksPassed = numOfOccurrences / this->daysSetCount;
    result += long64(weeksPassed) * ONE_WEEK_MS;

    int diff = numOfOccurrences - (this->daysSetCount * weeksPassed);
    if (diff > 0) {
        dayDelta = this->daysSetNum[diff - 1];
        result += long64(weeksPassed * (this->interval - 1)) * ONE_WEEK_MS + long64(dayDelta) * ONE_DAY_MS;
    }
    else {
        result += long64((weeksPassed - 1) * (this->interval - 1)) * ONE_WEEK_MS;
        result -= long64(7 - this->daysSetNum[this->daysSetCount - 1]) * ONE_DAY_MS;
    }

    return result;
}


long64 TimeOccurrence::CalculateLastOccurrence(dword numOfOccurrences)
{
    dword maxOcc = numOfOccurrences == 0 ? this->maxOccurrences : numOfOccurrences;
    if (this->end == RD_NO_END_DATE && maxOcc == RD_NO_LIMIT) return RD_NO_END_DATE;
    if (maxOcc == 0) return 0;

    time_tm_t tm;
    long64 startDate = ITime::RemoveTime(this->start);
    long64 timePart = this->start - startDate;
    long64 result = 0;
    
    switch (this->recurringType)
    {
    case RT_DAILY:
        result = startDate + (long64(this->interval * (maxOcc - 1)) * ONE_DAY_MS);
        break;

    case RT_WEEKLY:
        result = this->CalculateWeeklyEnd(startDate, maxOcc);
        break;

    case RT_MONTHLY:
        this->PrepareDayList();
        ITime::GetTimeStruct(startDate, &tm);
        tm.tmMon += (maxOcc - 1) * this->interval;
        this->SetRelativeDay(&tm);
        result = ITime::TimeStructToMilliseconds(&tm);
        break;

    default:
        ASSERT(false, "Unexpected value for this->type");
    }

    return result + timePart;
}


void TimeOccurrence::NormalizeSeriesStart()
{
    // For daily occurrences, there is nothing to adjust, because the series start date is the first correct occurrence.
    if (this->recurringType == RT_DAILY) return;

    time_tm_t tm;
    int tmpDay = 0;
    long64 startDate = ITime::RemoveTime(this->start);
    long64 timePart = this->start - startDate;
    long64 result = 0;

    if (this->end == RD_NO_END_DATE && this->maxOccurrences == 0) return;

    switch (this->recurringType)
    {
    case RT_WEEKLY:
        this->PrepareDayList();
        ITime::GetTimeStruct(startDate, &tm);
        if (tm.tmWDay > this->daysSetNum[this->daysSetCount - 1]) tmpDay = this->daysSetNum[0];
        else {
            for (int i = 0; i < this->daysSetCount; ++i) {
                tmpDay = this->daysSetNum[i];
                if (tmpDay >= tm.tmWDay) break;
            }
        } 

        tmpDay -= tm.tmWDay;
        if (tmpDay < 0) tmpDay += (7 * this->interval);
        result = startDate + tmpDay * ONE_DAY_MS;
        result += timePart;
        break;

    case RT_MONTHLY:
        this->PrepareDayList();
        ITime::GetTimeStruct(startDate, &tm);
        this->SetRelativeDay(&tm);
        result = ITime::TimeStructToMilliseconds(&tm) + timePart;
        // Make sure that the first occurrence is not before the current start time.
        if (result < this->start) {
            long64 curStart = this->start;
            this->start = result;
            result = this->DoCalculateFirstOccurrence(curStart);
        }
        break;

    default:
        ASSERT(false, "Unexpected value for this->type");
    }

    this->start = result;
}

long64 TimeOccurrence::CalculateFirstOccurrence(long64 timePeriodStart)
{
    // timePeriodStart set to 0 means, that the caller whants the first occurrence which is the adjusted series
    // time start.
    long64 result;
    this->NormalizeSeriesStart();
    result = timePeriodStart == 0 ? this->start : this->DoCalculateFirstOccurrence(timePeriodStart);
    if (timePeriodStart == 0) result = this->start;

    return result;
}
