/*
 * Copyright (c) 1998-2012 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Reza Rahman
 */
package com.caucho.config.timer;

import java.util.LinkedList;
import java.util.List;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicReference;

import com.caucho.config.ConfigException;
import com.caucho.config.types.Trigger;
import com.caucho.util.L10N;
import com.caucho.util.QDate;

/**
 * Implements a cron-style trigger. This trigger is primarily intended for the
 * EJB calendar style timer service functionality.
 */
// TODO Is this class getting too large? Maybe separate into a
// parser/lexer/interpreter sub-component? Also, is parsing better done/more
// readable/maintainable via creating compiler style grammar tokens instead of
// direct String processing?
public class CronTrigger implements Trigger {
  private static final L10N L = new L10N(CronTrigger.class);
  // Order of search is important in the token maps.
  private static final String [][] MONTH_TOKEN_MAP = { { "january", "1" },
      { "february", "2" }, { "march", "3" }, { "april", "4" }, { "may", "5" },
      { "june", "6" }, { "july", "7" }, { "august", "8" },
      { "september", "9" }, { "october", "10" }, { "november", "11" },
      { "december", "12" }, { "jan", "1" }, { "feb", "2" }, { "mar", "3" },
      { "apr", "4" }, { "jun", "6" }, { "jul", "7" }, { "aug", "8" },
      { "sep", "9" }, { "oct", "10" }, { "nov", "11" }, { "dec", "12" } };
  private static final String [][] DAY_OF_WEEK_TOKEN_MAP = { { "sunday", "0" },
      { "monday", "1" }, { "tuesday", "2" }, { "wednesday", "3" },
      { "thursday", "4" }, { "friday", "5" }, { "saturday", "6" },
      { "sun", "0" }, { "mon", "1" }, { "tue", "2" }, { "wed", "3" },
      { "thu", "4" }, { "fri", "5" }, { "sat", "6" } };
  private static final String [][] RELATIVE_DAY_OF_WEEK_TOKEN_MAP = {
      { "last", "-0" }, { "1st", "1" }, { "2nd", "2" }, { "3rd", "3" },
      { "4th", "4" }, { "5th", "5" } };

  private AtomicReference<QDate> _localCalendar = new AtomicReference<QDate>();

  private boolean [] _seconds;
  private boolean [] _minutes;
  private boolean [] _hours;
  private boolean _isDaysFilterRelative;
  private boolean [] _days;
  private String _daysFilter;
  private boolean [] _months;
  private boolean [] _daysOfWeek;
  private YearsFilter _yearsFilter;

  private TimeZone _timezone = TimeZone.getTimeZone("GMT");

  private long _start = -1;
  private long _end = -1;

  /**
   * Creates new cron trigger.
   * 
   * @param cronExpression
   *          The cron expression to create the trigger from.
   * @param start
   *          The date the trigger should begin firing, in milliseconds. -1
   *          indicates that no start date should be enforced.
   * @param end
   *          The date the trigger should end firing, in milliseconds. -1
   *          indicates that no end date should be enforced.
   * @param string
   */
  public CronTrigger(final CronExpression cronExpression, final long start,
      final long end, TimeZone timezone)
  {
    if (cronExpression.getSecond() != null) {
      _seconds = parseRange(cronExpression.getSecond(), 0, 59, false);
    }

    if (cronExpression.getMinute() != null) {
      _minutes = parseRange(cronExpression.getMinute(), 0, 59, false);
    }

    if (cronExpression.getHour() != null) {
      _hours = parseRange(cronExpression.getHour(), 0, 23, false);
    }

    if (cronExpression.getDayOfWeek() != null) {
      _daysOfWeek = parseRange(
          tokenizeDayOfWeek(cronExpression.getDayOfWeek()), 0, 7, false);
    }

    if (_daysOfWeek[7]) { // Both 0 and 7 are Sunday, as in UNIX cron.
      _daysOfWeek[0] = _daysOfWeek[7];
    }

    if (cronExpression.getDayOfMonth() != null) {
      _daysFilter = tokenizeDayOfMonth(cronExpression.getDayOfMonth());
      _days = parseRange(_daysFilter, 1, 31, true);
    }

    if (cronExpression.getMonth() != null) {
      _months = parseRange(tokenizeMonth(cronExpression.getMonth()), 1, 12,
          false);
    }

    if (cronExpression.getYear() != null) {
      _yearsFilter = parseYear(cronExpression.getYear());
    }

    if (timezone != null) {
      _timezone = timezone;
    }

    _start = start;
    _end = end;
  }

  private String tokenizeDayOfWeek(String dayOfWeek)
  {
    dayOfWeek = tokenize(dayOfWeek, DAY_OF_WEEK_TOKEN_MAP);

    return dayOfWeek;
  }

  private String tokenizeDayOfMonth(String dayOfMonth)
  {
    dayOfMonth = tokenize(dayOfMonth, RELATIVE_DAY_OF_WEEK_TOKEN_MAP);
    dayOfMonth = tokenize(dayOfMonth, DAY_OF_WEEK_TOKEN_MAP);

    return dayOfMonth;
  }

  private String tokenizeMonth(String month)
  {
    month = tokenize(month, MONTH_TOKEN_MAP);

    return month;
  }

  private String tokenize(String value, String [][] tokenMap)
  {
    // TODO The String processing is more resource intensive than necessary. See
    // if StringBuilder can work with regex?

    for (int i = 0; i < tokenMap.length; i++) {
      value = value.replaceAll("(?i)" + tokenMap[i][0], tokenMap[i][1]);
    }

    return value;
  }

  /**
   * parses a range, following cron rules.
   */
  // TODO This does not handle extra spaces between tokens, should it?
  private boolean [] parseRange(String range, int rangeMin, int rangeMax,
      boolean parseDayOfMonth) throws ConfigException
  {
    boolean [] values = new boolean[rangeMax + 1];

    int i = 0;
    while (i < range.length()) {
      char character = range.charAt(i);

      int min = 0;
      int max = 0;
      int increment = 1;

      if (character == '*') {
        min = rangeMin;
        max = rangeMax;
        i++;
      } else if ('0' <= character && character <= '9') {
        for (; i < range.length() && '0' <= (character = range.charAt(i))
            && character <= '9'; i++) {
          min = 10 * min + character - '0';
        }

        if (i < range.length() && character == '-') {
          for (i++; i < range.length() && '0' <= (character = range.charAt(i))
              && character <= '9'; i++) {
            max = 10 * max + character - '0';
          }
        } else if (parseDayOfMonth && (i < range.length())
            && (character == ' ')) {
          // This is just for further parsing validation, the filter value
          // cannot be processed right now.
          _isDaysFilterRelative = true; // This is the Nth weekday case.

          int dayOfWeek = 0;

          for (i++; i < range.length() && '0' <= (character = range.charAt(i))
              && character <= '9'; i++) {
            dayOfWeek = 10 * dayOfWeek + character - '0';
          }

          if ((dayOfWeek < 0) || (dayOfWeek > 6)) {
            throw new ConfigException(L.l(
                "'{0}' is an illegal cron range (day of week out of range)",
                range));
          }

          if ((min < 1) || (min > 5)) {
            throw new ConfigException(L.l(
                "'{0}' is an illegal cron range (invalid day of week)", range));
          }

          if (i < range.length()) {
            if ((i < (range.length() - 1)) && (character == ',')) {
              i++;
            } else {
              throw new ConfigException(L.l(
                  "'{0}' is an illegal cron range (invalid syntax)", range));
            }
          }

          continue;
        } else {
          max = min;
        }
      } else {
        if (parseDayOfMonth && (character == '-')) { // This is a -N days of
          // month case.
          _isDaysFilterRelative = true;

          // This is just for further parsing validation, the filter value
          // cannot be processed right now.

          int dayOfMonth = 0;

          for (i++; i < range.length() && '0' <= (character = range.charAt(i))
              && character <= '9'; i++) {
            // Don't need to do anything, evaluation will be done later, just
            // need to validate parsing for now.
            dayOfMonth = 10 * dayOfMonth + character - '0';
          }

          if ((dayOfMonth < 0) || (dayOfMonth > 30)) {
            throw new ConfigException(L.l(
                "'{0}' is an illegal cron range (day of month out of range)",
                range));
          }

          if ((i < range.length()) && ((character = range.charAt(i)) == '/')) {
            increment = 0;

            for (i++; i < range.length()
                && '0' <= (character = range.charAt(i)) && character <= '9'; i++) {
              increment = 10 * increment + character - '0';
            }

            if ((increment < rangeMin) && (increment > rangeMax)) {
              throw new ConfigException(
                  L
                      .l(
                          "'{0}' is an illegal cron range (increment value out of range)",
                          range));
            }
          }

          // The case of the last (-0) weekday
          if ((i < range.length()) && ((character = range.charAt(i)) == ' ')) {
            // Just need to do validation parsing, evaluation will be done
            // later.
            int dayOfWeek = 0;

            for (i++; i < range.length()
                && '0' <= (character = range.charAt(i)) && character <= '9'; i++) {
              dayOfWeek = 10 * dayOfWeek + character - '0';
            }

            if ((dayOfWeek < 0) || (dayOfWeek > 6)) {
              throw new ConfigException(L.l(
                  "'{0}' is an illegal cron range (day of week out of range)",
                  range));
            }

            if (min != 0) {
              throw new ConfigException(L.l(
                  "'{0}' is an illegal cron range (invalid syntax)", range));
            }
          }

          if (i < range.length()) {
            if ((i < (range.length() - 1)) && (character == ',')) {
              i++;
            } else {
              throw new ConfigException(L.l(
                  "'{0}' is an illegal cron range (invalid syntax)", range));
            }
          }

          continue;
        } else {
          throw new ConfigException(L.l(
              "'{0}' is an illegal cron range (invalid syntax)", range));
        }
      }

      if (min < rangeMin) {
        throw new ConfigException(L.l(
            "'{0}' is an illegal cron range (min value is too small)", range));
      } else if (max > rangeMax) {
        throw new ConfigException(L.l(
            "'{0}' is an illegal cron range (max value is too large)", range));
      }

      if ((i < range.length()) && ((character = range.charAt(i)) == '/')) {
        increment = 0;

        for (i++; i < range.length() && '0' <= (character = range.charAt(i))
            && character <= '9'; i++) {
          increment = 10 * increment + character - '0';
        }

        if (min == max) { // This is in the form of N/M, where N is the interval
          // start.
          max = rangeMax;
        }

        if ((increment < rangeMin) && (increment > rangeMax)) {
          throw new ConfigException(L.l(
              "'{0}' is an illegal cron range (increment value out of range)",
              range));
        }
      }

      if (i < range.length()) {
        if ((i < (range.length() - 1)) && (character == ',')) {
          i++;
        } else {
          throw new ConfigException(L.l(
              "'{0}' is an illegal cron range (invalid syntax)", range));
        }
      }

      for (; min <= max; min += increment) {
        values[min] = true;
      }
    }

    return values;
  }

  private YearsFilter parseYear(String year)
  {
    YearsFilter yearsFilter = new YearsFilter();

    int i = 0;
    while (i < year.length()) {
      YearsFilterValue filterValue = new YearsFilterValue();

      char character = year.charAt(i);

      if (character == '*') {
        filterValue.setAnyYear(true);
        i++;
      } else if ('0' <= character && character <= '9') {
        int startYear = 0;

        for (; i < year.length() && '0' <= (character = year.charAt(i))
            && character <= '9'; i++) {
          startYear = 10 * startYear + character - '0';
        }

        filterValue.setStartYear(startYear);

        if (i < year.length() && character == '-') {
          int endYear = 0;

          for (i++; i < year.length() && '0' <= (character = year.charAt(i))
              && character <= '9'; i++) {
            endYear = 10 * endYear + character - '0';
          }

          filterValue.setEndYear(endYear);
        } else {
          filterValue.setEndYear(startYear);
        }
      } else {
        throw new ConfigException(L.l("'{0}' is an illegal cron range", year));
      }

      if ((i < year.length()) && ((character = year.charAt(i)) == '/')) {
        filterValue.setAnyYear(false);
        int increment = 0;

        for (i++; i < year.length() && '0' <= (character = year.charAt(i))
            && character <= '9'; i++) {
          increment = 10 * increment + character - '0';
        }

        if (increment == 0) {
          throw new ConfigException(L.l("'{0}' is an illegal cron range", year));
        } else {
          filterValue.setIncrement(increment);
        }

        if (filterValue.getStartYear() == filterValue.getEndYear()) {
          // This is in the form of N/M, where N is the interval start.
          filterValue.setEndYear(Integer.MAX_VALUE);
        }
      }

      yearsFilter.addFilterValue(filterValue);

      if (i < year.length()) {
        if ((i < (year.length() - 1)) && (character == ',')) {
          i++;
        } else {
          throw new ConfigException(L.l("'{0}' is an illegal cron range", year));
        }
      }
    }

    return yearsFilter;
  }

  /**
   * Gets the next time this trigger should be fired.
   * 
   * @param now
   *          The current time.
   * @return The next time this trigger should be fired.
   */
  @Override
  public long nextTime(long now)
  {
    // Jump to start time.
    if ((_start != -1) && (now < _start)) {
      now = _start;
    }

    QDate calendar = allocateCalendar();

    // Round up to seconds.
    long time = now + 1000 - now % 1000;

    calendar.setGMTTime(time);

    QDate nextTime = getNextTime(calendar);

    if (nextTime != null) {
      time = nextTime.getGMTTime();
      time -= _timezone.getRawOffset(); // Adjust for time zone specification.
    } else {
      time = Long.MAX_VALUE; // This trigger is inactive.
    }

    // Check for end date
    if ((_end != -1) && (time > _end)) {
      time = Long.MAX_VALUE; // This trigger is inactive.
    }

    freeCalendar(calendar);

    if (now < time)
      return time;
    else
      return nextTime(now + 3600000L); // Daylight savings time.
  }

  private QDate getNextTime(QDate currentTime)
  {
    int year = _yearsFilter.getNextMatch(currentTime.getYear());

    if (year == -1) {
      return null;
    } else {
      if (year > currentTime.getYear()) {
        currentTime.setSecond(0);
        currentTime.setMinute(0);
        currentTime.setHour(0);
        currentTime.setDayOfMonth(1);
        currentTime.setMonth(0); // The QDate implementation uses 0 indexed
        // months, but cron does not.
        currentTime.setYear(year);
      }

      QDate nextTime = getNextTimeInYear(currentTime);

      int count = 0;

      // Don't look more than approximately five years ahead.
      while ((count < 5) && (nextTime == null)) {
        count++;
        year++;
        year = _yearsFilter.getNextMatch(year);

        if (year == -1) {
          return null;
        } else {
          currentTime.setSecond(0);
          currentTime.setMinute(0);
          currentTime.setHour(0);
          currentTime.setDayOfMonth(1);
          currentTime.setMonth(0); // The QDate implementation uses 0 indexed
          // months, but cron does not.
          currentTime.setYear(year);

          nextTime = getNextTimeInYear(currentTime);
        }
      }

      return nextTime;
    }
  }

  private QDate getNextTimeInYear(QDate currentTime)
  {
    int month = getNextMatch(_months, (currentTime.getMonth() + 1));

    if (month == -1) {
      return null;
    } else {
      if (month > (currentTime.getMonth() + 1)) {
        currentTime.setSecond(0);
        currentTime.setMinute(0);
        currentTime.setHour(0);
        currentTime.setDayOfMonth(1);
        currentTime.setMonth(month - 1);
      }

      QDate nextTime = getNextTimeInMonth(currentTime);

      while ((month < _months.length) && (nextTime == null)) {
        month++;
        month = getNextMatch(_months, month);

        if (month == -1) {
          return null;
        } else {
          currentTime.setSecond(0);
          currentTime.setMinute(0);
          currentTime.setHour(0);
          currentTime.setDayOfMonth(1);
          currentTime.setMonth(month - 1);

          nextTime = getNextTimeInMonth(currentTime);
        }
      }

      return nextTime;
    }
  }

  private QDate getNextTimeInMonth(QDate currentTime)
  {
    // If the days filter is relative to particular months, the days map should
    // be re-calculated.
    if (_isDaysFilterRelative) {
      calculateDays(currentTime);
    }

    // Note, QDate uses a 1 indexed weekday, while cron uses a 0 indexed
    // weekday.
    int day = getNextDayMatch(currentTime.getDayOfMonth(), (currentTime
        .getDayOfWeek() - 1), currentTime.getDayOfMonth(), currentTime
        .getDaysInMonth());

    if (day == -1) {
      return null;
    } else {
      if (day > currentTime.getDayOfMonth()) {
        currentTime.setSecond(0);
        currentTime.setMinute(0);
        currentTime.setHour(0);
        currentTime.setDayOfMonth(day);
      }

      QDate nextTime = getNextTimeInDay(currentTime);

      if (nextTime == null) {
        day++;
        // Note, QDate uses a 1 indexed weekday, while cron uses a 0 indexed
        // weekday.
        day = getNextDayMatch(currentTime.getDayOfMonth(), (currentTime
            .getDayOfWeek() - 1), day, currentTime.getDaysInMonth());

        if (day == -1) {
          return null;
        } else {
          currentTime.setSecond(0);
          currentTime.setMinute(0);
          currentTime.setHour(0);
          currentTime.setDayOfMonth(day);

          return getNextTimeInDay(currentTime);
        }
      }

      return nextTime;
    }
  }

  private void calculateDays(QDate currentTime)
  {
    _days = new boolean[currentTime.getDaysInMonth() + 1];

    int i = 0;
    while (i < _daysFilter.length()) {
      char character = _daysFilter.charAt(i);

      int min = 0;
      int max = min;
      int increment = 1;

      if (character == '*') {
        min = 1;
        max = currentTime.getDaysInMonth();
        i++;
      } else if ('0' <= character && character <= '9') {
        for (; i < _daysFilter.length()
            && '0' <= (character = _daysFilter.charAt(i)) && character <= '9'; i++) {
          min = 10 * min + character - '0';
        }

        if (i < _daysFilter.length() && character == '-') {
          for (i++; i < _daysFilter.length()
              && '0' <= (character = _daysFilter.charAt(i)) && character <= '9'; i++) {
            max = 10 * max + character - '0';
          }
        } else if ((i < _daysFilter.length()) && (character == ' ')) {
          // This is the Nth weekday case.
          int dayOfWeek = 0;

          for (i++; i < _daysFilter.length()
              && '0' <= (character = _daysFilter.charAt(i)) && character <= '9'; i++) {
            dayOfWeek = 10 * dayOfWeek + character - '0';
          }

          int n = min;
          min = 1;
          int monthStartDayofWeek = ((currentTime.getDayOfWeek() - 1)
              - ((currentTime.getDayOfMonth() - min) % 7) + 7) % 7;

          min = min + ((dayOfWeek - monthStartDayofWeek + 7) % 7);

          min = min + ((n - 1) * 7);

          max = min;
        } else {
          max = min;
        }
      } else if (character == '-') { // This is a -N days from end of month
        // case.
        for (i++; i < _daysFilter.length()
            && '0' <= (character = _daysFilter.charAt(i)) && character <= '9'; i++) {
          min = 10 * min + character - '0';
        }

        min = currentTime.getDaysInMonth() - min;

        // The case of the last (-0) weekday case.
        if ((i < _daysFilter.length())
            && ((character = _daysFilter.charAt(i)) == ' ')) {
          int dayOfWeek = 0;

          for (i++; i < _daysFilter.length()
              && '0' <= (character = _daysFilter.charAt(i)) && character <= '9'; i++) {
            dayOfWeek = 10 * dayOfWeek + character - '0';
          }

          min = 1;
          int monthStartDayofWeek = ((currentTime.getDayOfWeek() - 1)
              - ((currentTime.getDayOfMonth() - min) % 7) + 7) % 7;

          min = min + ((dayOfWeek - monthStartDayofWeek + 7) % 7);

          // This is an integer division.
          min = min + (((currentTime.getDaysInMonth() - min) / 7) * 7);
        }

        max = min;
      }

      if ((i < _daysFilter.length())
          && ((character = _daysFilter.charAt(i)) == '/')) {
        for (i++; i < _daysFilter.length()
            && '0' <= (character = _daysFilter.charAt(i)) && character <= '9'; i++) {
          increment = 10 * increment + character - '0';
        }

        if (min == max) { // This is in the form of N/M, where N is the interval
          // start and the end is the max value.
          max = currentTime.getDaysInMonth();
        }
      }

      for (int day = min; ((day > 0) && (day <= max) && (day <= currentTime
          .getDaysInMonth())); day += increment) {
        _days[day] = true;
      }

      if (character == ',') {
        i++;
      }
    }
  }

  private int getNextDayMatch(int initialDayOfMonth, int initialDayOfWeek,
      int day, int daysInMonth)
  {
    while (day <= daysInMonth) {
      day = getNextMatch(_days, day, (daysInMonth + 1));

      if (day == -1) {
        return -1;
      }

      int dayOfWeek = ((initialDayOfWeek + ((day - initialDayOfMonth) % 7)) % 7);
      int nextDayOfWeek = getNextMatch(_daysOfWeek, dayOfWeek);

      if (nextDayOfWeek == dayOfWeek) {
        return day;
      } else if (nextDayOfWeek == -1) {
        day += (7 - dayOfWeek);
      } else {
        day += (nextDayOfWeek - dayOfWeek);
      }
    }

    return -1;
  }

  private QDate getNextTimeInDay(QDate currentTime)
  {
    int hour = getNextMatch(_hours, currentTime.getHour());

    if (hour == -1) {
      return null;
    } else {
      if (hour > currentTime.getHour()) {
        currentTime.setSecond(0);
        currentTime.setMinute(0);
        currentTime.setHour(hour);
      }

      QDate nextTime = getNextTimeInHour(currentTime);

      if (nextTime == null) {
        hour++;
        hour = getNextMatch(_hours, hour);

        if (hour == -1) {
          return null;
        } else {
          currentTime.setSecond(0);
          currentTime.setMinute(0);
          currentTime.setHour(hour);

          return getNextTimeInHour(currentTime);
        }
      }

      return nextTime;
    }
  }

  private QDate getNextTimeInHour(QDate currentTime)
  {
    int minute = getNextMatch(_minutes, currentTime.getMinute());

    if (minute == -1) {
      return null;
    } else {
      if (minute > currentTime.getMinute()) {
        currentTime.setSecond(0);
        currentTime.setMinute(minute);
      }

      QDate nextTime = getNextTimeInMinute(currentTime);

      if (nextTime == null) {
        minute++;
        minute = getNextMatch(_minutes, minute);

        if (minute == -1) {
          return null;
        } else {
          currentTime.setSecond(0);
          currentTime.setMinute(minute);

          return getNextTimeInMinute(currentTime);
        }
      }

      return nextTime;
    }
  }

  private QDate getNextTimeInMinute(QDate currentTime)
  {
    int second = getNextMatch(_seconds, currentTime.getSecond());

    if (second == -1) {
      return null;
    } else {
      currentTime.setSecond(second);

      return currentTime;
    }
  }

  private int getNextMatch(boolean [] range, int start)
  {
    return getNextMatch(range, start, range.length);
  }

  private int getNextMatch(boolean [] range, int start, int end)
  {
    for (int match = start; match < end; match++) {
      if (range[match]) {
        return match;
      }
    }

    return -1;
  }

  private QDate allocateCalendar()
  {
    QDate calendar = _localCalendar.getAndSet(null);

    if (calendar == null) {
      calendar = QDate.createLocal();
    }

    return calendar;
  }

  private void freeCalendar(QDate cal)
  {
    _localCalendar.set(cal);
  }

  private class YearsFilter {
    private List<YearsFilterValue> _filterValues = new LinkedList<YearsFilterValue>();
    private boolean _anyYear = false;
    private int _endYear = 0;

    private void addFilterValue(YearsFilterValue filterValue)
    {
      if (filterValue.isAnyYear()) {
        _anyYear = true;
      } else if (filterValue.getEndYear() > _endYear) {
        _endYear = filterValue.getEndYear();
      }

      _filterValues.add(filterValue);
    }

    private int getNextMatch(int year)
    {
      if (_anyYear) {
        return year;
      }

      while (year <= _endYear) {
        for (YearsFilterValue filterValue : _filterValues) {
          if ((year >= filterValue.getStartYear())
              && (year <= filterValue.getEndYear())
              && ((year % filterValue.getIncrement()) == 0)) {
            return year;
          }
        }

        year++;
      }

      return -1;
    }
  }

  private class YearsFilterValue {
    private boolean _anyYear = false;
    private int _startYear = 0;
    private int _endYear = 0;
    private int _increment = 1;

    public boolean isAnyYear()
    {
      return _anyYear;
    }

    public void setAnyYear(boolean anyYear)
    {
      _anyYear = anyYear;
    }

    public int getStartYear()
    {
      return _startYear;
    }

    public void setStartYear(int startYear)
    {
      _startYear = startYear;
    }

    public int getEndYear()
    {
      return _endYear;
    }

    public void setEndYear(int endYear)
    {
      _endYear = endYear;
    }

    public void setIncrement(int increment)
    {
      _increment = increment;
    }

    public int getIncrement()
    {
      return _increment;
    }
  }
}