private static long Compute(ScheduleSpec spec, long afterTimeInMillis, TimeZoneInfo timeZone)
        {
            while (true)
            {
                DateTimeEx after;

                if (spec.OptionalTimeZone != null)
                {
                    try
                    {
                        timeZone = TimeZoneHelper.GetTimeZoneInfo(spec.OptionalTimeZone);
                    }
                    catch (TimeZoneNotFoundException)
                    {
                        // this behavior ensures we are consistent with Java, but IMO, it's bad behavior...
                        // basically, if the timezone is not found, we default to UTC.
                        timeZone = TimeZoneInfo.Utc;
                    }

                    after = new DateTimeEx(
                        afterTimeInMillis.TimeFromMillis(timeZone),
                        timeZone);
                }
                else
                {
                    after = new DateTimeEx(
                        afterTimeInMillis.TimeFromMillis(timeZone),
                        timeZone);
                }

                var result = new ScheduleCalendar {
                    Milliseconds = after.Millisecond
                };

                ICollection <int> minutesSet = spec.UnitValues.Get(ScheduleUnit.MINUTES);
                ICollection <int> hoursSet   = spec.UnitValues.Get(ScheduleUnit.HOURS);
                ICollection <int> monthsSet  = spec.UnitValues.Get(ScheduleUnit.MONTHS);
                ICollection <int> secondsSet = null;

                bool isSecondsSpecified = false;

                if (spec.UnitValues.ContainsKey(ScheduleUnit.SECONDS))
                {
                    isSecondsSpecified = true;
                    secondsSet         = spec.UnitValues.Get(ScheduleUnit.SECONDS);
                }

                if (isSecondsSpecified)
                {
                    result.Second = NextValue(secondsSet, after.Second);
                    if (result.Second == -1)
                    {
                        result.Second = NextValue(secondsSet, 0);
                        after.AddMinutes(1);
                    }
                }

                result.Minute = NextValue(minutesSet, after.Minute);
                if (result.Minute != after.Minute)
                {
                    result.Second = NextValue(secondsSet, 0);
                }
                if (result.Minute == -1)
                {
                    result.Minute = NextValue(minutesSet, 0);
                    after.AddHours(1);
                }

                result.Hour = NextValue(hoursSet, after.Hour);
                if (result.Hour != after.Hour)
                {
                    result.Second = NextValue(secondsSet, 0);
                    result.Minute = NextValue(minutesSet, 0);
                }
                if (result.Hour == -1)
                {
                    result.Hour = NextValue(hoursSet, 0);
                    after.AddDays(1, DateTimeMathStyle.Java);
                }

                // This call may change second, minute and/or hour parameters
                // They may be reset to minimum values if the day rolled
                result.DayOfMonth = DetermineDayOfMonth(spec, after, result);

                bool dayMatchRealDate = false;
                while (!dayMatchRealDate)
                {
                    if (CheckDayValidInMonth(timeZone, result.DayOfMonth, after.Month, after.Year))
                    {
                        dayMatchRealDate = true;
                    }
                    else
                    {
                        after.AddMonths(1, DateTimeMathStyle.Java);
                    }
                }

                int currentMonth = after.Month;
                result.Month = NextValue(monthsSet, currentMonth);
                if (result.Month != currentMonth)
                {
                    result.Second     = NextValue(secondsSet, 0);
                    result.Minute     = NextValue(minutesSet, 0);
                    result.Hour       = NextValue(hoursSet, 0);
                    result.DayOfMonth = DetermineDayOfMonth(spec, after, result);
                }
                if (result.Month == -1)
                {
                    result.Month = NextValue(monthsSet, 0);
                    after.AddYears(1);
                }

                // Perform a last valid date check, if failing, try to compute a new date based on this altered after date
                int year = after.Year;
                if (!CheckDayValidInMonth(timeZone, result.DayOfMonth, result.Month, year))
                {
                    afterTimeInMillis = after.TimeInMillis;
                    continue;
                }

                return(GetTime(result, after.Year, spec.OptionalTimeZone, timeZone));
            }
        }
        /// <summary>
        /// Determine the next valid day of month based on the given specification of valid days in month and
        /// valid days in week. If both days in week and days in month are supplied, the days are OR-ed.
        /// </summary>
        /// <param name="spec"></param>
        /// <param name="after"></param>
        /// <param name="result"></param>
        /// <returns></returns>

        private static int DetermineDayOfMonth(ScheduleSpec spec, DateTimeEx after, ScheduleCalendar result)
        {
            ICollection <Int32> daysOfMonthSet = spec.UnitValues.Get(ScheduleUnit.DAYS_OF_MONTH);
            ICollection <Int32> daysOfWeekSet  = spec.UnitValues.Get(ScheduleUnit.DAYS_OF_WEEK);
            ICollection <Int32> secondsSet     = spec.UnitValues.Get(ScheduleUnit.SECONDS);
            ICollection <Int32> minutesSet     = spec.UnitValues.Get(ScheduleUnit.MINUTES);
            ICollection <Int32> hoursSet       = spec.UnitValues.Get(ScheduleUnit.HOURS);

            int dayOfMonth;

            // If days of week is a wildcard, just go by days of month
            if (spec.OptionalDayOfMonthOperator != null || spec.OptionalDayOfWeekOperator != null)
            {
                var isWeek = false;
                var op     = spec.OptionalDayOfMonthOperator;
                if (spec.OptionalDayOfMonthOperator == null)
                {
                    op     = spec.OptionalDayOfWeekOperator;
                    isWeek = true;
                }

                // may return the current day or a future day in the same month,
                // and may advance the "after" date to the next month
                int currentYYMMDD = GetTimeYYYYMMDD(after);
                IncreaseAfterDayOfMonthSpecialOp(op.Operator, op.Day, op.Month, isWeek, after);
                int rolledYYMMDD = GetTimeYYYYMMDD(after);

                // if rolled then reset time portion
                if (rolledYYMMDD > currentYYMMDD)
                {
                    result.Second = (NextValue(secondsSet, 0));
                    result.Minute = (NextValue(minutesSet, 0));
                    result.Hour   = (NextValue(hoursSet, 0));
                    return(after.GetFieldValue(DateTimeFieldEnum.DAY_OF_MONTH));
                }
                // rolling backwards is not allowed
                else if (rolledYYMMDD < currentYYMMDD)
                {
                    throw new IllegalStateException("Failed to evaluate special date op, rolled date less then current date");
                }
                else
                {
                    var work = new DateTimeEx(after);
                    work.SetFieldValue(DateTimeFieldEnum.SECOND, result.Second);
                    work.SetFieldValue(DateTimeFieldEnum.MINUTE, result.Minute);
                    work.SetFieldValue(DateTimeFieldEnum.HOUR_OF_DAY, result.Hour);
                    if (work <= after)
                    {    // new date is not after current date, so bump
                        after.AddUsingField(DateTimeFieldEnum.DAY_OF_MONTH, 1);
                        result.Second = NextValue(secondsSet, 0);
                        result.Minute = NextValue(minutesSet, 0);
                        result.Hour   = NextValue(hoursSet, 0);
                        IncreaseAfterDayOfMonthSpecialOp(op.Operator, op.Day, op.Month, isWeek, after);
                    }
                    return(after.GetFieldValue(DateTimeFieldEnum.DAY_OF_MONTH));
                }
            }
            else if (daysOfWeekSet == null)
            {
                dayOfMonth = NextValue(daysOfMonthSet, after.Day);
                if (dayOfMonth != after.Day)
                {
                    result.Second = NextValue(secondsSet, 0);
                    result.Minute = NextValue(minutesSet, 0);
                    result.Hour   = NextValue(hoursSet, 0);
                }
                if (dayOfMonth == -1)
                {
                    dayOfMonth = NextValue(daysOfMonthSet, 0);
                    after.AddMonths(1, DateTimeMathStyle.Java);
                }
            }
            // If days of weeks is not a wildcard and days of month is a wildcard, go by days of week only
            else if (daysOfMonthSet == null)
            {
                // Loop to find the next day of month that works for the specified day of week values
                while (true)
                {
                    dayOfMonth = after.Day;
                    int dayOfWeek = (int)after.DayOfWeek;

                    // TODO
                    //
                    // Check the DayOfWeek logic in this section.  The former code reads something
                    // like the following:
                    //
                    // Calendar.Get(after, SupportClass.CalendarManager.DAY_OF_WEEK) - 1;
                    //
                    // Java calendars are one based which means that subtracting one makes them
                    // zero-based.  CLR DateTimes are zero-based so there should be no need to
                    // tweak the dates to make this work.

                    // If the day matches neither the day of month nor the day of week
                    if (!daysOfWeekSet.Contains(dayOfWeek))
                    {
                        result.Second = NextValue(secondsSet, 0);
                        result.Minute = NextValue(minutesSet, 0);
                        result.Hour   = NextValue(hoursSet, 0);
                        after.AddDays(1, DateTimeMathStyle.Java);
                    }
                    else
                    {
                        break;
                    }
                }
            }
            // Both days of weeks and days of month are not a wildcard
            else
            {
                // Loop to find the next day of month that works for either day of month  OR   day of week
                while (true)
                {
                    dayOfMonth = after.Day;
                    int dayOfWeek = (int)after.DayOfWeek;

                    // TODO
                    //
                    // See my discussion above about day of week conversion

                    // If the day matches neither the day of month nor the day of week
                    if ((!daysOfWeekSet.Contains(dayOfWeek)) && (!daysOfMonthSet.Contains(dayOfMonth)))
                    {
                        result.Second = NextValue(secondsSet, 0);
                        result.Minute = NextValue(minutesSet, 0);
                        result.Hour   = NextValue(hoursSet, 0);
                        after.AddDays(1, DateTimeMathStyle.Java);
                    }
                    else
                    {
                        break;
                    }
                }
            }

            return(dayOfMonth);
        }