/**
 * Provides a parser and evaluator for unix-like cron expressions. Cron expressions provide the
 * ability to specify complex time combinations such as "At 8:00am every Monday through
 * Friday" or "At 1:30am every last Friday of the month".
 *
 * <p>Cron expressions are comprised of 6 required fields and one optional field separated by white
 * space. The fields respectively are described as follows:
 *
 * <table cellspacing="8">
 * <tr>
 * <th align="left">Field Name</th>
 * <th align="left">&nbsp;</th>
 * <th align="left">Allowed Values</th>
 * <th align="left">&nbsp;</th>
 * <th align="left">Allowed Special Characters</th>
 * </tr>
 * <tr>
 * <td align="left"><code>Seconds</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>0-59</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>, - * /</code></td>
 * </tr>
 * <tr>
 * <td align="left"><code>Minutes</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>0-59</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>, - * /</code></td>
 * </tr>
 * <tr>
 * <td align="left"><code>Hours</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>0-23</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>, - * /</code></td>
 * </tr>
 * <tr>
 * <td align="left"><code>Day-of-month</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>1-31</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>, - * ? / L W C</code></td>
 * </tr>
 * <tr>
 * <td align="left"><code>Month</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>1-12 or JAN-DEC</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>, - * /</code></td>
 * </tr>
 * <tr>
 * <td align="left"><code>Day-of-Week</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>1-7 or SUN-SAT</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>, - * ? / L #</code></td>
 * </tr>
 * <tr>
 * <td align="left"><code>Year (Optional)</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>empty, 1970-2099</code></td>
 * <td align="left">&nbsp;</th>
 * <td align="left"><code>, - * /</code></td>
 * </tr>
 * </table>
 *
 * <p>The '*' character is used to specify all values. For example, &quot;*&quot; in the minute
 * field means &quot;every minute&quot;.
 *
 * <p>The '?' character is allowed for the day-of-month and day-of-week fields. It is used to
 * specify 'no specific value'. This is useful when you need to specify something in one of the two
 * fileds, but not the other.
 *
 * <p>The '-' character is used to specify ranges For example &quot;10-12&quot; in the hour field
 * means &quot;the hours 10, 11 and 12&quot;.
 *
 * <p>The ',' character is used to specify additional values. For example &quot;MON,WED,FRI&quot; in
 * the day-of-week field means &quot;the days Monday, Wednesday, and Friday&quot;.
 *
 * <p>The '/' character is used to specify increments. For example &quot;0/15&quot; in the seconds
 * field means &quot;the seconds 0, 15, 30, and 45&quot;. And &quot;5/15&quot; in the seconds field
 * means &quot;the seconds 5, 20, 35, and 50&quot;. Specifying '*' before the '/' is equivalent to
 * specifying 0 is the value to start with. Essentially, for each field in the expression, there is
 * a set of numbers that can be turned on or off. For seconds and minutes, the numbers range from 0
 * to 59. For hours 0 to 23, for days of the month 0 to 31, and for months 1 to 12. The
 * &quot;/&quot; character simply helps you turn on every &quot;nth&quot; value in the given set.
 * Thus &quot;7/6&quot; in the month field only turns on month &quot;7&quot;, it does NOT mean every
 * 6th month, please note that subtlety.
 *
 * <p>The 'L' character is allowed for the day-of-month and day-of-week fields. This character is
 * short-hand for &quot;last&quot;, but it has different meaning in each of the two fields. For
 * example, the value &quot;L&quot; in the day-of-month field means &quot;the last day of the
 * month&quot; - day 31 for January, day 28 for February on non-leap years. If used in the
 * day-of-week field by itself, it simply means &quot;7&quot; or &quot;SAT&quot;. But if used in the
 * day-of-week field after another value, it means &quot;the last xxx day of the month&quot; - for
 * example &quot;6L&quot; means &quot;the last friday of the month&quot;. When using the 'L' option,
 * it is important not to specify lists, or ranges of values, as you'll get confusing results.
 *
 * <p>The 'W' character is allowed for the day-of-month field. This character is used to specify the
 * weekday (Monday-Friday) nearest the given day. As an example, if you were to specify
 * &quot;15W&quot; as the value for the day-of-month field, the meaning is: &quot;the nearest
 * weekday to the 15th of the month&quot;. So if the 15th is a Saturday, the trigger will fire on
 * Friday the 14th. If the 15th is a Sunday, the trigger will fire on Monday the 16th. If the 15th
 * is a Tuesday, then it will fire on Tuesday the 15th. However if you specify &quot;1W&quot; as the
 * value for day-of-month, and the 1st is a Saturday, the trigger will fire on Monday the 3rd, as it
 * will not 'jump' over the boundary of a month's days. The 'W' character can only be specified when
 * the day-of-month is a single day, not a range or list of days.
 *
 * <p>The 'L' and 'W' characters can also be combined for the day-of-month expression to yield 'LW',
 * which translates to &quot;last weekday of the month&quot;.
 *
 * <p>The '#' character is allowed for the day-of-week field. This character is used to specify
 * &quot;the nth&quot; XXX day of the month. For example, the value of &quot;6#3&quot; in the
 * day-of-week field means the third Friday of the month (day 6 = Friday and &quot;#3&quot; = the
 * 3rd one in the month). Other examples: &quot;2#1&quot; = the first Monday of the month and
 * &quot;4#5&quot; = the fifth Wednesday of the month. Note that if you specify &quot;#5&quot; and
 * there is not 5 of the given day-of-week in the month, then no firing will occur that month.
 *
 * <p>
 * <!--The 'C' character is allowed for the day-of-month and day-of-week fields.
 * This character is short-hand for "calendar". This means values are
 * calculated against the associated calendar, if any. If no calendar is
 * associated, then it is equivalent to having an all-inclusive calendar. A
 * value of "5C" in the day-of-month field means "the first day included by the
 * calendar on or after the 5th". A value of "1C" in the day-of-week field
 * means "the first day included by the calendar on or after sunday".-->
 *
 * <p>The legal characters and the names of months and days of the week are not case sensitive.
 *
 * <p><b>NOTES:</b>
 *
 * <ul>
 *   <li>Support for specifying both a day-of-week and a day-of-month value is not complete (you'll
 *       need to use the '?' character in on of these fields).
 * </ul>
 *
 * @author Sharada Jambula, James House
 * @author Contributions from Mads Henderson
 * @author Refactoring from CronTrigger to CronExpression by Aaron Craven
 */
public class CronExpression {

  private static final WidgetsLocalizedMessages MSGS =
      WidgetsLocalizedMessagesSingleton.getInstance().getMessages();
  private static final long serialVersionUID = 12423409423L;

  protected static final int SECOND = 0;

  protected static final int MINUTE = 1;

  protected static final int HOUR = 2;

  protected static final int DAY_OF_MONTH = 3;

  protected static final int MONTH = 4;

  protected static final int DAY_OF_WEEK = 5;

  protected static final int YEAR = 6;

  protected static final int ALL_SPEC_INT = 99; // '*'

  protected static final int NO_SPEC_INT = 98; // '?'

  protected static final Integer ALL_SPEC = new Integer(ALL_SPEC_INT);

  protected static final Integer NO_SPEC = new Integer(NO_SPEC_INT);

  protected static Map monthMap = new HashMap(20);

  protected static Map dayMap = new HashMap(60);

  static {
    monthMap.put("JAN", new Integer(0));
    monthMap.put("FEB", new Integer(1));
    monthMap.put("MAR", new Integer(2));
    monthMap.put("APR", new Integer(3));
    monthMap.put("MAY", new Integer(4));
    monthMap.put("JUN", new Integer(5));
    monthMap.put("JUL", new Integer(6));
    monthMap.put("AUG", new Integer(7));
    monthMap.put("SEP", new Integer(8));
    monthMap.put("OCT", new Integer(9));
    monthMap.put("NOV", new Integer(10));
    monthMap.put("DEC", new Integer(11));

    dayMap.put("SUN", new Integer(1));
    dayMap.put("MON", new Integer(2));
    dayMap.put("TUE", new Integer(3));
    dayMap.put("WED", new Integer(4));
    dayMap.put("THU", new Integer(5));
    dayMap.put("FRI", new Integer(6));
    dayMap.put("SAT", new Integer(7));
  }

  private String cronExpression = null;

  protected transient TreeSet seconds;

  protected transient TreeSet minutes;

  protected transient TreeSet hours;

  protected transient TreeSet daysOfMonth;

  protected transient TreeSet months;

  protected transient TreeSet daysOfWeek;

  protected transient TreeSet years;

  protected transient boolean lastdayOfWeek = false;

  protected transient int nthdayOfWeek = 0;

  protected transient boolean lastdayOfMonth = false;

  protected transient boolean nearestWeekday = false;

  protected transient boolean calendardayOfWeek = false;

  protected transient boolean calendardayOfMonth = false;

  protected transient boolean expressionParsed = false;

  /**
   * Constructs a new <CODE>CronExpression</CODE> based on the specified parameter.
   *
   * @param cronExpression String representation of the cron expression the new object should
   *     represent
   * @throws java.text.ParseException if the string expression cannot be parsed into a valid <CODE>
   *     CronExpression</CODE>
   */
  public CronExpression(String cronExpression) throws ParseException {
    if (cronExpression == null) {
      throw new IllegalArgumentException(MSGS.cronExpressionNull());
    }

    this.cronExpression = cronExpression;

    buildExpression(cronExpression.toUpperCase());
  }

  /**
   * Returns the string representation of the <CODE>CronExpression</CODE>
   *
   * @return a string representation of the <CODE>CronExpression</CODE>
   */
  public String toString() {
    return cronExpression;
  }

  /**
   * Indicates whether the specified cron expression can be parsed into a valid cron expression
   *
   * @param cronExpression the expression to evaluate
   * @return a boolean indicating whether the given expression is a valid cron expression
   */
  public static boolean isValidExpression(String cronExpression) {

    try {
      new CronExpression(cronExpression);
    } catch (ParseException pe) {
      return false;
    }

    return true;
  }

  ////////////////////////////////////////////////////////////////////////////
  //
  // Expression Parsing Functions
  //
  ////////////////////////////////////////////////////////////////////////////

  protected void buildExpression(String expression) throws ParseException {
    expressionParsed = true;

    try {

      if (seconds == null) seconds = new TreeSet();
      if (minutes == null) minutes = new TreeSet();
      if (hours == null) hours = new TreeSet();
      if (daysOfMonth == null) daysOfMonth = new TreeSet();
      if (months == null) months = new TreeSet();
      if (daysOfWeek == null) daysOfWeek = new TreeSet();
      if (years == null) years = new TreeSet();

      int exprOn = SECOND;

      String[] exprsTok = expression.split(" |\\t");

      for (int i = 0; i < exprsTok.length; i++) {
        if (exprOn > YEAR) {
          break;
        }
        String expr = exprsTok[i];
        String[] vtok = expr.split(",");
        for (int j = 0; j < vtok.length; j++) {
          String v = vtok[j];
          storeExpressionVals(0, v, exprOn);
        }
        exprOn++;
      }

      if (exprOn <= DAY_OF_WEEK)
        throw new ParseException(MSGS.cronUnexpectedEndOfExpression(), expression.length());

      if (exprOn <= YEAR) storeExpressionVals(0, "*", YEAR);

    } catch (ParseException pe) {
      throw pe;
    } catch (Exception e) {
      throw new ParseException(MSGS.cronIllegalExpressionFormat(e.toString()), 0);
    }
  }

  protected int storeExpressionVals(int pos, String s, int type) throws ParseException {
    int incr = 0;
    int i = skipWhiteSpace(pos, s);
    if (i >= s.length()) return i;
    char c = s.charAt(i);
    if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW"))) {
      String sub = s.substring(i, i + 3);
      int sval = -1;
      int eval = -1;
      if (type == MONTH) {
        sval = getMonthNumber(sub) + 1;
        if (sval < 0) {
          throw new ParseException(MSGS.cronInvalidMonthValue(sub), i);
        }
        if (s.length() > i + 3) {
          c = s.charAt(i + 3);
          if (c == '-') {
            i += 4;
            sub = s.substring(i, i + 3);
            eval = getMonthNumber(sub) + 1;
            if (eval < 0) {
              throw new ParseException(MSGS.cronInvalidMonthValue(sub), i);
            }
          }
        }
      } else if (type == DAY_OF_WEEK) {
        sval = getDayOfWeekNumber(sub);
        if (sval < 0) {
          throw new ParseException(MSGS.cronInvalidDOWValue(sub), i);
        }
        if (s.length() > i + 3) {
          c = s.charAt(i + 3);
          if (c == '-') {
            i += 4;
            sub = s.substring(i, i + 3);
            eval = getDayOfWeekNumber(sub);
            if (eval < 0) {
              throw new ParseException(MSGS.cronInvalidDOWValue(sub), i);
            }
            if (sval > eval) {
              throw new ParseException(
                  MSGS.cronInvalidDOWSequence(Integer.toString(sval), Integer.toString(eval)), i);
            }

          } else if (c == '#') {
            try {
              i += 4;
              nthdayOfWeek = Integer.parseInt(s.substring(i));
              if (nthdayOfWeek < 1 || nthdayOfWeek > 5) throw new Exception();
            } catch (Exception e) {
              throw new ParseException(MSGS.cronIllegalHashFollowingNumeric(), i);
            }
          } else if (c == 'L') {
            lastdayOfWeek = true;
            i++;
          }
        }

      } else {
        throw new ParseException(MSGS.cronIllegalCharactersForPosition(sub), i);
      }
      if (eval != -1) {
        incr = 1;
      }
      addToSet(sval, eval, incr, type);
      return (i + 3);
    }

    if (c == '?') {
      i++;
      if ((i + 1) < s.length() && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) {
        throw new ParseException(
            MSGS.cronIllegalCharacterAfter("?", String.valueOf(s.charAt(i))), i);
      }
      if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) {
        throw new ParseException(MSGS.cronIllegalQuestionMark(), i);
      }
      if (type == DAY_OF_WEEK && !lastdayOfMonth) {
        int val = ((Integer) daysOfMonth.last()).intValue();
        if (val == NO_SPEC_INT) {
          throw new ParseException(MSGS.cronIllegalQuestionMark(), i);
        }
      }

      addToSet(NO_SPEC_INT, -1, 0, type);
      return i;
    }

    if (c == '*' || c == '/') {
      if (c == '*' && (i + 1) >= s.length()) {
        addToSet(ALL_SPEC_INT, -1, incr, type);
        return i + 1;
      } else if (c == '/'
          && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s.charAt(i + 1) == '\t')) {
        throw new ParseException(MSGS.cronIllegalSlash(), i);
      } else if (c == '*') {
        i++;
      }
      c = s.charAt(i);
      if (c == '/') { // is an increment specified?
        i++;
        if (i >= s.length()) {
          throw new ParseException(MSGS.cronUnexpectedEndOfString(), i);
        }

        incr = getNumericValue(s, i);

        i++;
        if (incr > 10) i++;
        if (incr > 59 && (type == SECOND || type == MINUTE)) {
          throw new ParseException(MSGS.cronIllegalIncrement("60", Integer.toString(incr)), i);
        } else if (incr > 23 && (type == HOUR)) {
          throw new ParseException(MSGS.cronIllegalIncrement("24", Integer.toString(incr)), i);
        } else if (incr > 31 && (type == DAY_OF_MONTH)) {
          throw new ParseException(MSGS.cronIllegalIncrement("31", Integer.toString(incr)), i);
        } else if (incr > 7 && (type == DAY_OF_WEEK)) {
          throw new ParseException(MSGS.cronIllegalIncrement("7", Integer.toString(incr)), i);
        } else if (incr > 12 && (type == MONTH)) {
          throw new ParseException(MSGS.cronIllegalIncrement("12", Integer.toString(incr)), i);
        }
      } else incr = 1;

      addToSet(ALL_SPEC_INT, -1, incr, type);
      return i;
    } else if (c == 'L') {
      i++;
      if (type == DAY_OF_MONTH) lastdayOfMonth = true;
      if (type == DAY_OF_WEEK) addToSet(7, 7, 0, type);
      if (type == DAY_OF_MONTH && s.length() > i) {
        c = s.charAt(i);
        if (c == 'W') {
          nearestWeekday = true;
          i++;
        }
      }
      return i;
    } else if (c >= '0' && c <= '9') {
      int val = Integer.parseInt(String.valueOf(c));
      i++;
      if (i >= s.length()) {
        addToSet(val, -1, -1, type);
      } else {
        c = s.charAt(i);
        if (c >= '0' && c <= '9') {
          ValueSet vs = getValue(val, s, i);
          val = vs.value;
          i = vs.pos;
        }
        i = checkNext(i, s, val, type);
        return i;
      }
    } else {
      throw new ParseException(MSGS.cronUnexpectedCharacter(String.valueOf(c)), i);
    }

    return i;
  }

  protected int checkNext(int pos, String s, int val, int type) throws ParseException {
    int end = -1;
    int i = pos;

    if (i >= s.length()) {
      addToSet(val, end, -1, type);
      return i;
    }

    char c = s.charAt(pos);

    if (c == 'L') {
      if (type == DAY_OF_WEEK) {
        lastdayOfWeek = true;
      } else {
        throw new ParseException(MSGS.cronOptionIsNotValidHere("L", Integer.toString(i)), i);
      }
      TreeSet set = getSet(type);
      set.add(new Integer(val));
      i++;
      return i;
    }

    if (c == 'W') {
      if (type == DAY_OF_MONTH) {
        nearestWeekday = true;
      } else {
        throw new ParseException(MSGS.cronOptionIsNotValidHere("W", Integer.toString(i)), i);
      }
      TreeSet set = getSet(type);
      set.add(new Integer(val));
      i++;
      return i;
    }

    if (c == '#') {
      if (type != DAY_OF_WEEK) {
        throw new ParseException(MSGS.cronOptionIsNotValidHere("#", Integer.toString(i)), i);
      }
      i++;
      try {
        nthdayOfWeek = Integer.parseInt(s.substring(i));
        if (nthdayOfWeek < 1 || nthdayOfWeek > 5) {
          throw new Exception();
        }
      } catch (Exception e) {
        throw new ParseException(MSGS.cronIllegalHashFollowingNumeric(), i);
      }

      TreeSet set = getSet(type);
      set.add(new Integer(val));
      i++;
      return i;
    }

    if (c == 'C') {
      if (type == DAY_OF_WEEK) {
        calendardayOfWeek = true;
      } else if (type == DAY_OF_MONTH) {
        calendardayOfMonth = true;
      } else {
        throw new ParseException(MSGS.cronOptionIsNotValidHere("C", Integer.toString(i)), i);
      }
      TreeSet set = getSet(type);
      set.add(new Integer(val));
      i++;
      return i;
    }

    if (c == '-') {
      i++;
      c = s.charAt(i);
      int v = Integer.parseInt(String.valueOf(c));
      end = v;
      i++;
      if (i >= s.length()) {
        addToSet(val, end, 1, type);
        return i;
      }
      c = s.charAt(i);
      if (c >= '0' && c <= '9') {
        ValueSet vs = getValue(v, s, i);
        int v1 = vs.value;
        end = v1;
        i = vs.pos;
      }
      if (i < s.length() && ((c = s.charAt(i)) == '/')) {
        i++;
        c = s.charAt(i);
        int v2 = Integer.parseInt(String.valueOf(c));
        i++;
        if (i >= s.length()) {
          addToSet(val, end, v2, type);
          return i;
        }
        c = s.charAt(i);
        if (c >= '0' && c <= '9') {
          ValueSet vs = getValue(v2, s, i);
          int v3 = vs.value;
          addToSet(val, end, v3, type);
          i = vs.pos;
          return i;
        } else {
          addToSet(val, end, v2, type);
          return i;
        }
      } else {
        addToSet(val, end, 1, type);
        return i;
      }
    }

    if (c == '/') {
      i++;
      c = s.charAt(i);
      int v2 = Integer.parseInt(String.valueOf(c));
      i++;
      if (i >= s.length()) {
        addToSet(val, end, v2, type);
        return i;
      }
      c = s.charAt(i);
      if (c >= '0' && c <= '9') {
        ValueSet vs = getValue(v2, s, i);
        int v3 = vs.value;
        addToSet(val, end, v3, type);
        i = vs.pos;
        return i;
      } else {
        throw new ParseException(MSGS.cronUnexpectedCharacterAfterSlash(String.valueOf(c)), i);
      }
    }

    addToSet(val, end, 0, type);
    i++;
    return i;
  }

  public String getCronExpression() {
    return cronExpression;
  }

  protected int skipWhiteSpace(int i, String s) {
    for (; i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t'); i++) ;

    return i;
  }

  protected int findNextWhiteSpace(int i, String s) {
    for (; i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != '\t'); i++) ;

    return i;
  }

  protected void addToSet(int val, int end, int incr, int type) throws ParseException {
    TreeSet set = getSet(type);

    if (type == SECOND || type == MINUTE) {
      if ((val < 0 || val > 59 || end > 59) && (val != ALL_SPEC_INT)) {
        throw new ParseException(MSGS.cronInvalidMinuteSecondValue(), -1);
      }
    } else if (type == HOUR) {
      if ((val < 0 || val > 23 || end > 23) && (val != ALL_SPEC_INT)) {
        throw new ParseException(MSGS.cronInvalidHourValue(), -1);
      }
    } else if (type == DAY_OF_MONTH) {
      if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT) && (val != NO_SPEC_INT)) {
        throw new ParseException(MSGS.cronInvalidDayOfMonthValue(), -1);
      }
    } else if (type == MONTH) {
      if ((val < 1 || val > 12 || end > 12) && (val != ALL_SPEC_INT)) {
        throw new ParseException(MSGS.cronInvalidMonthValueGeneral(), -1);
      }
    } else if (type == DAY_OF_WEEK) {
      if ((val == 0 || val > 7 || end > 7) && (val != ALL_SPEC_INT) && (val != NO_SPEC_INT)) {
        throw new ParseException(MSGS.cronInvalidDayOfWeekValue(), -1);
      }
    }

    if ((incr == 0 || incr == -1) && val != ALL_SPEC_INT) {
      if (val != -1) {
        set.add(new Integer(val));
      } else {
        set.add(NO_SPEC);
      }
      return;
    }

    int startAt = val;
    int stopAt = end;

    if (val == ALL_SPEC_INT && incr <= 0) {
      incr = 1;
      set.add(ALL_SPEC); // put in a marker, but also fill values
    }

    if (type == SECOND || type == MINUTE) {
      if (stopAt == -1) {
        stopAt = 59;
      }
      if (startAt == -1 || startAt == ALL_SPEC_INT) {
        startAt = 0;
      }
    } else if (type == HOUR) {
      if (stopAt == -1) {
        stopAt = 23;
      }
      if (startAt == -1 || startAt == ALL_SPEC_INT) {
        startAt = 0;
      }
    } else if (type == DAY_OF_MONTH) {
      if (stopAt == -1) {
        stopAt = 31;
      }
      if (startAt == -1 || startAt == ALL_SPEC_INT) {
        startAt = 1;
      }
    } else if (type == MONTH) {
      if (stopAt == -1) {
        stopAt = 12;
      }
      if (startAt == -1 || startAt == ALL_SPEC_INT) {
        startAt = 1;
      }
    } else if (type == DAY_OF_WEEK) {
      if (stopAt == -1) {
        stopAt = 7;
      }
      if (startAt == -1 || startAt == ALL_SPEC_INT) {
        startAt = 1;
      }
    } else if (type == YEAR) {
      if (stopAt == -1) {
        stopAt = 2099;
      }
      if (startAt == -1 || startAt == ALL_SPEC_INT) {
        startAt = 1970;
      }
    }

    for (int i = startAt; i <= stopAt; i += incr) {
      set.add(new Integer(i));
    }
  }

  protected TreeSet getSet(int type) {
    switch (type) {
      case SECOND:
        return seconds;
      case MINUTE:
        return minutes;
      case HOUR:
        return hours;
      case DAY_OF_MONTH:
        return daysOfMonth;
      case MONTH:
        return months;
      case DAY_OF_WEEK:
        return daysOfWeek;
      case YEAR:
        return years;
      default:
        return null;
    }
  }

  protected ValueSet getValue(int v, String s, int i) {
    char c = s.charAt(i);
    String s1 = String.valueOf(v);
    while (c >= '0' && c <= '9') {
      s1 += c;
      i++;
      if (i >= s.length()) {
        break;
      }
      c = s.charAt(i);
    }
    ValueSet val = new ValueSet();
    if (i < s.length()) {
      val.pos = i;
    } else {
      val.pos = i + 1;
    }
    val.value = Integer.parseInt(s1);
    return val;
  }

  protected int getNumericValue(String s, int i) {
    int endOfVal = findNextWhiteSpace(i, s);
    String val = s.substring(i, endOfVal);
    return Integer.parseInt(val);
  }

  protected int getMonthNumber(String s) {
    Integer integer = (Integer) monthMap.get(s);

    if (integer == null) {
      return -1;
    }

    return integer.intValue();
  }

  protected int getDayOfWeekNumber(String s) {
    Integer integer = (Integer) dayMap.get(s);

    if (integer == null) {
      return -1;
    }

    return integer.intValue();
  }
}
/** @author Steven Barkdull */
public class ScheduleEditor extends VerticalPanel implements IChangeHandler {

  public static enum ENDS_TYPE {
    TIME,
    DURATION
  }

  public static class DurationValues {
    public int days = 0;

    public int hours = 0;

    public int minutes = 0;
  }

  public static enum TIME {
    MILLISECOND(1),
    SECOND(MILLISECOND.time * 1000),
    MINUTE(SECOND.time * 60),
    HOUR(MINUTE.time * 60),
    DAY(HOUR.time * 24);

    private long time;

    TIME(long time) {
      this.time = time;
    }

    public long getTime() {
      return this.time;
    }
  }

  private static final WidgetsLocalizedMessages MSGS =
      WidgetsLocalizedMessagesSingleton.getInstance().getMessages();

  protected static final String SCHEDULE_LABEL = "schedule-label"; // $NON-NLS-1$

  protected static final String SCHEDULE_EDITOR_CAPTION_PANEL =
      "schedule-editor-caption-panel"; //$NON-NLS-1$

  public enum ScheduleType {
    RUN_ONCE(0, MSGS.runOnce()),
    SECONDS(1, MSGS.seconds()),
    MINUTES(2, MSGS.minutes()),
    HOURS(3, MSGS.hours()),
    DAILY(4, MSGS.daily()),
    WEEKLY(5, MSGS.weekly()),
    MONTHLY(6, MSGS.monthly()),
    YEARLY(7, MSGS.yearly()),
    CRON(8, MSGS.cron());

    private ScheduleType(int value, String name) {
      this.value = value;
      this.name = name;
    }

    private final int value;

    private final String name;

    private static ScheduleType[] scheduleValue = {
      RUN_ONCE, SECONDS, MINUTES, HOURS, DAILY, WEEKLY, MONTHLY, YEARLY, CRON
    };

    public int value() {
      return value;
    }

    public String toString() {
      return name;
    }

    public static ScheduleType get(int idx) {
      return scheduleValue[idx];
    }

    public static int length() {
      return scheduleValue.length;
    }

    public static ScheduleType stringToScheduleType(String strSchedule) throws EnumException {
      for (ScheduleType v : EnumSet.range(ScheduleType.RUN_ONCE, ScheduleType.CRON)) {
        if (v.toString().equals(strSchedule)) {
          return v;
        }
      }
      throw new EnumException(MSGS.invalidTemporalValue(scheduleValue.toString()));
    }
  } /* end enum */

  private RunOnceEditor runOnceEditor = null;

  private RecurrenceEditor recurrenceEditor = null;

  private CronEditor cronEditor = null;

  // TODO sbarkdull, can this be static?
  private Map<ScheduleType, Panel> scheduleTypeMap = new HashMap<ScheduleType, Panel>();

  private Map<TemporalValue, ScheduleType> temporalValueToScheduleTypeMap =
      createTemporalValueToScheduleTypeMap();

  private Map<ScheduleType, TemporalValue> scheduleTypeToTemporalValueMap =
      createScheduleTypeMapToTemporalValue();

  private ListBox scheduleCombo = null;

  private ICallback<IChangeHandler> onChangeHandler = null;

  private boolean isBlockoutDialog = false;

  private TimePicker startTimePicker = null;

  private TimePicker blockoutEndTimePicker = null;

  private Widget startTimePanel = null;

  private RadioButton endTimeRadioButton = null;

  private RadioButton durationRadioButton = null;

  private ListBox daysListBox = null;

  private ListBox hoursListBox = null;

  private ListBox minutesListBox = null;

  protected Button blockoutCheckButton = new Button(MSGS.viewBlockoutTimes());

  protected ListBox timeZonePicker = null;

  public ScheduleEditor(ScheduleDialogType type) {
    super();
    isBlockoutDialog = (type == ScheduleDialogType.BLOCKOUT);
    startTimePicker = new TimePicker();

    setStylePrimaryName("scheduleEditor"); // $NON-NLS-1$

    scheduleCombo = createScheduleCombo();
    Label l = new Label(MSGS.recurrenceColon());
    l.setStyleName(SCHEDULE_LABEL);
    add(l);
    add(scheduleCombo);

    SimplePanel hspacer = new SimplePanel();
    hspacer.setWidth("100px"); // $NON-NLS-1$

    if (!isBlockoutDialog) {
      startTimePanel = createStartTimePanel();
      add(startTimePanel);
    } else {

      // Blockout End TimePicker
      blockoutEndTimePicker = new TimePicker();
      blockoutEndTimePicker.setHour("01"); // $NON-NLS-1$
      blockoutEndTimePicker.setMinute("00"); // $NON-NLS-1$
      blockoutEndTimePicker.setTimeOfDay(TimeUtil.TimeOfDay.PM);

      // Blockout End Caption Panel
      blockoutEndTimePicker.getElement().getStyle().setDisplay(Display.NONE);

      final String[] daysList = new String[365];
      final String[] hoursList = new String[24];
      final String[] minutesList = new String[60];

      // Populate list
      for (Integer i = 0; i < 365; i++) {
        String iStr = i.toString();
        daysList[i] = iStr;

        if (i < 60) {
          minutesList[i] = iStr;
          if (i < 24) {
            hoursList[i] = iStr;
          }
        }
      }

      // Units of time Drop Down
      daysListBox = new ListBox();
      daysListBox.getElement().setId("daysListBox"); // $NON-NLS-1$
      populateListItems(daysListBox, daysList, 0, 365);

      final Label daysLabel = new Label(MSGS.dayOrDays());
      daysLabel.getElement().setAttribute("for", daysListBox.getElement().getId()); // $NON-NLS-1$

      hoursListBox = new ListBox();
      hoursListBox.getElement().setId("hoursListBox"); // $NON-NLS-1$
      populateListItems(hoursListBox, hoursList, 0, 24);

      final Label hoursLabel = new Label(MSGS.hourOrHours());
      hoursLabel.getElement().setAttribute("for", hoursListBox.getElement().getId()); // $NON-NLS-1$

      minutesListBox = new ListBox();
      minutesListBox.getElement().setId("minutesListBox"); // $NON-NLS-1$
      populateListItems(minutesListBox, minutesList, 0, 60);

      final Label minutesLabel = new Label(MSGS.minuteOrMinutes());
      minutesLabel
          .getElement()
          .setAttribute("for", minutesListBox.getElement().getId()); // $NON-NLS-1$

      final HorizontalPanel durationPanel = new HorizontalPanel();
      durationPanel.setVerticalAlignment(VerticalPanel.ALIGN_MIDDLE);
      durationPanel.setSpacing(blockoutEndTimePicker.getSpacing());
      durationPanel.add(daysListBox);
      durationPanel.add(daysLabel);
      durationPanel.add(hoursListBox);
      durationPanel.add(hoursLabel);
      durationPanel.add(minutesListBox);
      durationPanel.add(minutesLabel);

      // Bind change handler
      this.scheduleCombo.addChangeHandler(
          new ChangeHandler() {

            @Override
            public void onChange(ChangeEvent event) {
              String scheduleType = scheduleCombo.getItemText(scheduleCombo.getSelectedIndex());

              if (ScheduleType.RUN_ONCE.toString().equals(scheduleType)) {
                show(
                    true,
                    daysListBox,
                    daysLabel,
                    hoursListBox,
                    hoursLabel,
                    minutesListBox,
                    minutesLabel);

                populateListItems(daysListBox, daysList, 0, 365);
                populateListItems(hoursListBox, hoursList, 0, 24);
                populateListItems(minutesListBox, minutesList, 0, 60);

              } else if (ScheduleType.HOURS.toString().equals(scheduleType)) {
                hide(true, daysListBox, daysLabel, hoursListBox, hoursLabel);
                show(true, minutesListBox, minutesLabel);

                populateListItems(minutesListBox, minutesList, 0, 60);

              } else if (ScheduleType.DAILY.toString().equals(scheduleType)) {
                hide(true, daysListBox, daysLabel);
                show(true, hoursListBox, hoursLabel, minutesListBox, minutesLabel);

                populateListItems(hoursListBox, hoursList, 0, 24);
                populateListItems(minutesListBox, minutesList, 0, 60);

              } else if (ScheduleType.WEEKLY.toString().equals(scheduleType)) {
                show(
                    true,
                    daysListBox,
                    daysLabel,
                    hoursListBox,
                    hoursLabel,
                    minutesListBox,
                    minutesLabel);

                populateListItems(daysListBox, daysList, 0, 7);
                populateListItems(hoursListBox, hoursList, 0, 24);
                populateListItems(minutesListBox, minutesList, 0, 60);

              } else if (ScheduleType.MONTHLY.toString().equals(scheduleType)) {
                show(
                    true,
                    daysListBox,
                    daysLabel,
                    hoursListBox,
                    hoursLabel,
                    minutesListBox,
                    minutesLabel);

                populateListItems(daysListBox, daysList, 0, 28);
                populateListItems(hoursListBox, hoursList, 0, 24);
                populateListItems(minutesListBox, minutesList, 0, 60);

              } else if (ScheduleType.YEARLY.toString().equals(scheduleType)) {
                show(
                    true,
                    daysListBox,
                    daysLabel,
                    hoursListBox,
                    hoursLabel,
                    minutesListBox,
                    minutesLabel);

                populateListItems(daysListBox, daysList, 0, 365);
                populateListItems(hoursListBox, hoursList, 0, 24);
                populateListItems(minutesListBox, minutesList, 0, 60);
              }
            }
          });

      /*
       * Radio Buttons for duration
       */
      this.durationRadioButton =
          new RadioButton("durationRadioGroup", "durationRadioButton"); // $NON-NLS-1$ //$NON-NLS-2$
      this.durationRadioButton.setText(MSGS.duration());
      this.durationRadioButton.setValue(Boolean.TRUE);
      this.durationRadioButton.addClickHandler(
          new ClickHandler() {

            @Override
            public void onClick(ClickEvent event) {
              blockoutEndTimePicker.getElement().getStyle().setDisplay(Display.NONE);
              durationPanel.getElement().getStyle().clearDisplay();
            }
          });

      this.endTimeRadioButton =
          new RadioButton("durationRadioGroup", "endTimeRadioButton"); // $NON-NLS-1$ //$NON-NLS-2$
      this.endTimeRadioButton.setText(MSGS.endTime());
      this.endTimeRadioButton.addClickHandler(
          new ClickHandler() {

            @Override
            public void onClick(ClickEvent event) {
              blockoutEndTimePicker.getElement().getStyle().clearDisplay();
              durationPanel.getElement().getStyle().setDisplay(Display.NONE);
            }
          });

      // Radio Buttons Panel
      HorizontalPanel radioButtonsPanel = new HorizontalPanel();
      radioButtonsPanel.setVerticalAlignment(VerticalPanel.ALIGN_MIDDLE);
      radioButtonsPanel.add(this.durationRadioButton);
      radioButtonsPanel.add(this.endTimeRadioButton);

      // Ends Panel
      VerticalPanel endsPanel = new VerticalPanel();
      endsPanel.add(radioButtonsPanel);
      endsPanel.add(blockoutEndTimePicker);
      endsPanel.add(durationPanel);

      // Blockout period
      CaptionPanel blockoutStartCaptionPanel = new CaptionPanel(MSGS.startTime());
      HorizontalPanel blockoutStartPanel = new HorizontalPanel();
      blockoutStartPanel.add(getStartTimePicker());
      timeZonePicker = new ListBox();
      timeZonePicker.setStyleName("timeZonePicker");
      timeZonePicker.setVisibleItemCount(1);
      blockoutStartPanel.add(timeZonePicker);
      timeZonePicker.getElement().getParentElement().getStyle().setPaddingTop(5, Unit.PX);

      blockoutStartCaptionPanel.add(blockoutStartPanel);
      populateTimeZonePicker();

      // Ends Caption Panel
      CaptionPanel endCaptionPanel = new CaptionPanel(MSGS.endsCaptionTitle());
      endCaptionPanel.add(endsPanel);

      VerticalPanel blockoutPanel = new VerticalPanel();
      blockoutPanel.setWidth("100%"); // $NON-NLS-1$
      blockoutPanel.add(blockoutStartCaptionPanel);
      blockoutPanel.add(endCaptionPanel);

      add(blockoutPanel);
    }

    VerticalPanel vp = new VerticalPanel();
    vp.setWidth("100%"); // $NON-NLS-1$
    add(vp);
    setCellHeight(vp, "100%"); // $NON-NLS-1$

    runOnceEditor = new RunOnceEditor(startTimePicker);
    vp.add(runOnceEditor);
    scheduleTypeMap.put(ScheduleType.RUN_ONCE, runOnceEditor);
    runOnceEditor.setVisible(true);

    recurrenceEditor = new RecurrenceEditor(startTimePicker);
    vp.add(recurrenceEditor);
    scheduleTypeMap.put(ScheduleType.SECONDS, recurrenceEditor);
    scheduleTypeMap.put(ScheduleType.MINUTES, recurrenceEditor);
    scheduleTypeMap.put(ScheduleType.HOURS, recurrenceEditor);
    scheduleTypeMap.put(ScheduleType.DAILY, recurrenceEditor);
    scheduleTypeMap.put(ScheduleType.WEEKLY, recurrenceEditor);
    scheduleTypeMap.put(ScheduleType.MONTHLY, recurrenceEditor);
    scheduleTypeMap.put(ScheduleType.YEARLY, recurrenceEditor);
    recurrenceEditor.setVisible(false);

    cronEditor = new CronEditor();
    scheduleTypeMap.put(ScheduleType.CRON, cronEditor);
    cronEditor.setVisible(false);

    if (!isBlockoutDialog) {
      vp.add(cronEditor);

      VerticalPanel blockoutButtonPanel = new VerticalPanel();
      blockoutButtonPanel.setWidth("100%"); // $NON-NLS-1$
      // blockoutButtonPanel.setHeight("30%");
      blockoutButtonPanel.setHorizontalAlignment(VerticalPanel.ALIGN_CENTER);
      blockoutButtonPanel.setVerticalAlignment(VerticalPanel.ALIGN_MIDDLE);

      // We want to add a button to check for blockout conflicts
      blockoutCheckButton.setStyleName("pentaho-button"); // $NON-NLS-1$
      blockoutCheckButton.getElement().setId("blockout-check-button"); // $NON-NLS-1$
      blockoutCheckButton.setVisible(false);

      hspacer.setHeight("50px"); // $NON-NLS-1$
      blockoutButtonPanel.add(hspacer);
      blockoutButtonPanel.add(blockoutCheckButton);

      vp.add(hspacer);
      add(blockoutButtonPanel);
    }

    configureOnChangeHandler();
  }

  private void show(boolean applyToParent, UIObject... objs) {
    for (UIObject obj : objs) {
      Element ele = obj.getElement();
      if (applyToParent) {
        ele = ele.getParentElement();
      }
      ele.getStyle().clearDisplay();
    }
  }

  private void hide(boolean applyToParent, UIObject... objs) {
    for (UIObject obj : objs) {
      Element ele = obj.getElement();
      if (applyToParent) {
        ele = ele.getParentElement();
      }
      ele.getStyle().setDisplay(Display.NONE);
    }
  }

  private void populateListItems(ListBox listBox, String[] arr, int startIndex, int howMany) {

    // Clear items
    listBox.clear();

    // Add itesm
    int endIndex = startIndex + howMany;
    for (int i = startIndex; i < endIndex; i++) {
      listBox.addItem(arr[i]);
    }
  }

  private void populateTimeZonePicker() {

    String url = GWT.getHostPageBaseURL() + "api/system/timezones"; // $NON-NLS-1$
    RequestBuilder timeZonesRequest = new RequestBuilder(RequestBuilder.GET, url);
    timeZonesRequest.setHeader("accept", "application/json"); // $NON-NLS-1$ //$NON-NLS-2$
    timeZonesRequest.setHeader("If-Modified-Since", "01 Jan 1970 00:00:00 GMT");
    try {
      timeZonesRequest.sendRequest(
          null,
          new RequestCallback() {

            @Override
            public void onResponseReceived(Request request, Response response) {
              timeZonePicker.clear();
              String responseText = response.getText();
              JSONValue value = JSONParser.parseLenient(responseText);
              JSONObject object = value.isObject();
              value = object.get("timeZones");
              JSONValue serverTZvalue = object.get("serverTzId");
              JSONString serverTZIdString = serverTZvalue.isString();
              String serverTZId = serverTZIdString.stringValue();
              object = value.isObject();
              value = object.get("entry");
              JSONArray timeZonesJSONArray = value.isArray();
              for (int i = 0; i < timeZonesJSONArray.size(); i++) {
                JSONValue entryValue = timeZonesJSONArray.get(i);
                JSONObject entryObject = entryValue.isObject();
                JSONValue keyValue = entryObject.get("key");
                JSONValue theValue = entryObject.get("value");
                String key = keyValue.isString().stringValue();
                String valueForKey = theValue.isString().stringValue();
                timeZonePicker.addItem(valueForKey, key);
              }
              for (int i = 0; i < timeZonePicker.getItemCount(); i++) {
                if (timeZonePicker.getValue(i).equalsIgnoreCase(serverTZId)) {
                  timeZonePicker.setSelectedIndex(i);
                  break;
                }
              }
            }

            @Override
            public void onError(Request request, Throwable exception) {
              // TODO Auto-generated method stub

            }
          });
    } catch (RequestException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }

  public ListBox getTimeZonePicker() {
    return timeZonePicker;
  }

  public void setBlockoutButtonHandler(final ClickHandler handler) {
    blockoutCheckButton.addClickHandler(handler);
  }

  public Button getBlockoutCheckButton() {
    return blockoutCheckButton;
  }

  public TimePicker getStartTimePicker() {
    return startTimePicker;
  }

  public TimePicker getBlockoutEndTimePicker() {
    return blockoutEndTimePicker;
  }

  public ENDS_TYPE getBlockoutEndsType() {
    return this.durationRadioButton.getValue() ? ENDS_TYPE.DURATION : ENDS_TYPE.TIME;
  }

  public DurationValues getDurationValues() {
    DurationValues vals = new DurationValues();

    String displayNone = Display.NONE.getCssName();

    // Days
    if (!displayNone.equals(this.daysListBox.getElement().getStyle().getDisplay())) {
      vals.days =
          Integer.parseInt(this.daysListBox.getItemText(this.daysListBox.getSelectedIndex()));
    }

    // Hours
    if (!displayNone.equals(this.hoursListBox.getElement().getStyle().getDisplay())) {
      vals.hours =
          Integer.parseInt(this.hoursListBox.getItemText(this.hoursListBox.getSelectedIndex()));
    }

    // Minutes
    if (!displayNone.equals(this.minutesListBox.getElement().getStyle().getDisplay())) {
      vals.minutes =
          Integer.parseInt(this.minutesListBox.getItemText(this.minutesListBox.getSelectedIndex()));
    }

    return vals;
  }

  public void setDurationFields(long duration) {

    long remainder = duration;

    long days = remainder / TIME.DAY.getTime();
    remainder -= days * TIME.DAY.getTime();

    long hours = remainder / TIME.HOUR.getTime();
    remainder -= hours * TIME.HOUR.getTime();

    long minutes = remainder / TIME.MINUTE.getTime();

    this.daysListBox.setSelectedIndex(new Long(days).intValue());
    this.hoursListBox.setSelectedIndex(new Long(hours).intValue());
    this.minutesListBox.setSelectedIndex(new Long(minutes).intValue());

    // Set valid end time if range is within 24hrs
    if (duration < TIME.DAY.getTime()) {
      boolean isPM = hours >= 12;
      this.blockoutEndTimePicker.setHour(new Long(hours + (isPM ? -12 : 0)).toString());
      this.blockoutEndTimePicker.setMinute(new Long(minutes).toString());
      this.blockoutEndTimePicker.setTimeOfDay(isPM ? TimeOfDay.PM : TimeOfDay.AM);
    }
  }

  protected Widget createStartTimePanel() {
    CaptionPanel startTimeGB = new CaptionPanel(MSGS.startTime());
    startTimeGB.setStyleName(SCHEDULE_EDITOR_CAPTION_PANEL);

    startTimeGB.add(getStartTimePicker());

    return startTimeGB;
  }

  public void reset(Date now) {
    runOnceEditor.reset(now);
    recurrenceEditor.reset(now);
    cronEditor.reset(now);

    setScheduleType(ScheduleType.RUN_ONCE);
  }

  public String getCronString() {
    switch (getScheduleType()) {
      case RUN_ONCE:
        return null;
      case SECONDS: // fall through
      case MINUTES: // fall through
      case HOURS: // fall through
      case DAILY: // fall through
      case WEEKLY: // fall through
      case MONTHLY: // fall through
      case YEARLY:
        return recurrenceEditor.getCronString();
      case CRON:
        return cronEditor.getCronString();
      default:
        throw new RuntimeException(MSGS.invalidRunType(getScheduleType().toString()));
    }
  }

  /**
   * @param cronStr
   * @throws CronParseException if cronStr is not a valid CRON string.
   */
  public void setCronString(String cronStr) throws CronParseException {

    // Try original simplistic parser...
    CronParser cp = new CronParser(cronStr);
    String recurrenceStr = null;
    try {
      recurrenceStr = cp.parseToRecurrenceString(); // throws CronParseException
    } catch (CronParseException e) {
      if (!CronExpression.isValidExpression(cronStr)) { // Parse with proper expression parser
        throw e;
      }
      recurrenceStr = null; // valid cronstring, not parse-able to recurrence string
    }

    if (null != recurrenceStr) {
      recurrenceEditor.inititalizeWithRecurrenceString(recurrenceStr);
      TemporalValue tv = recurrenceEditor.getTemporalState();
      ScheduleType rt = temporalValueToScheduleType(tv);
      setScheduleType(rt);
    } else {
      // its a cron string that cannot be parsed into a recurrence string, switch to cron string
      // editor.
      setScheduleType(ScheduleType.CRON);
    }

    cronEditor.setCronString(cronStr);
  }

  /**
   * @return null if the selected schedule does not support repeat-in-seconds, otherwise return the
   *     number of seconds between schedule execution.
   * @throws RuntimeException if the temporal value is invalid. This condition occurs as a result of
   *     programmer error.
   */
  public Long getRepeatInSecs() throws RuntimeException {
    return recurrenceEditor.getRepeatInSecs();
  }

  public void setRepeatInSecs(Integer repeatInSecs) {
    recurrenceEditor.inititalizeWithRepeatInSecs(repeatInSecs);
    TemporalValue tv = recurrenceEditor.getTemporalState();
    ScheduleType rt = temporalValueToScheduleType(tv);
    setScheduleType(rt);
  }

  private ListBox createScheduleCombo() {
    final ScheduleEditor localThis = this;
    ListBox lb = new ListBox();
    lb.setVisibleItemCount(1);
    // lb.setStyleName("scheduleCombo"); //$NON-NLS-1$
    lb.addChangeHandler(
        new ChangeHandler() {

          @Override
          public void onChange(ChangeEvent event) {
            localThis.handleScheduleChange();
          }
        });

    // add all schedule types to the combobox
    for (ScheduleType schedType : EnumSet.range(ScheduleType.RUN_ONCE, ScheduleType.CRON)) {
      if (!isBlockoutDialog
          || (schedType != ScheduleType.CRON
              && schedType != ScheduleType.SECONDS
              && schedType != ScheduleType.MINUTES
              && schedType != ScheduleType.HOURS)) {
        lb.addItem(schedType.toString());
      }
    }
    lb.setItemSelected(0, true);

    return lb;
  }

  public ScheduleType getScheduleType() {
    String selectedValue = scheduleCombo.getValue(scheduleCombo.getSelectedIndex());
    return ScheduleType.stringToScheduleType(selectedValue);
  }

  public void setScheduleType(ScheduleType scheduleType) {
    int itemCount = scheduleCombo.getItemCount();
    for (int i = 0; i < itemCount; i++) {
      String itemText = scheduleCombo.getItemText(i);
      if (itemText.equals(scheduleType.toString())) {
        scheduleCombo.setSelectedIndex(i);
      }
    }
    selectScheduleTypeEditor(scheduleType);
  }

  /**
   * NOTE: should only ever be used by validators. This is a backdoor into this class that shouldn't
   * be here, do not use this method unless you are validating.
   *
   * @return DateRangeEditor
   */
  public RecurrenceEditor getRecurrenceEditor() {
    return recurrenceEditor;
  }

  /**
   * NOTE: should only ever be used by validators. This is a backdoor into this class that shouldn't
   * be here, do not use this method unless you are validating.
   *
   * @return DateRangeEditor
   */
  public CronEditor getCronEditor() {
    return cronEditor;
  }

  /**
   * NOTE: should only ever be used by validators. This is a backdoor into this class that shouldn't
   * be here, do not use this method unless you are validating.
   *
   * @return DateRangeEditor
   */
  public RunOnceEditor getRunOnceEditor() {
    return runOnceEditor;
  }

  public void setStartTime(String startTime) {
    runOnceEditor.setStartTime(startTime);
    recurrenceEditor.setStartTime(startTime);
  }

  public void setBlockoutEndTime(String endTime) {
    blockoutEndTimePicker.setTime(endTime);
  }

  public String getStartTime() {
    switch (getScheduleType()) {
      case RUN_ONCE:
        return runOnceEditor.getStartTime();
      case SECONDS: // fall through
      case MINUTES: // fall through
      case HOURS: // fall through
      case DAILY: // fall through
      case WEEKLY: // fall through
      case MONTHLY: // fall through
      case YEARLY:
        return recurrenceEditor.getStartTime();
      case CRON:
        return cronEditor.getStartTime();
      default:
        throw new RuntimeException(MSGS.invalidRunType(getScheduleType().toString()));
    }
  }

  public void setStartDate(Date startDate) {
    runOnceEditor.setStartDate(startDate);
    recurrenceEditor.setStartDate(startDate);
    cronEditor.setStartDate(startDate);
  }

  public Date getStartDate() {
    switch (getScheduleType()) {
      case RUN_ONCE:
        Date startDate = runOnceEditor.getStartDate();
        String startTime = runOnceEditor.getStartTime();
        String[] times = startTime.split(":"); // $NON-NLS-1$
        int hour = Integer.parseInt(times[0]);
        int minute = Integer.parseInt(times[1]);
        if (startTime.indexOf("PM") >= 0) { // $NON-NLS-1$
          hour += 12;
        }

        startDate.setHours(hour);
        startDate.setMinutes(minute);
        startDate.setSeconds(0);
        return startDate;
      case SECONDS: // fall through
      case MINUTES: // fall through
      case HOURS: // fall through
      case DAILY: // fall through
      case WEEKLY: // fall through
      case MONTHLY: // fall through
      case YEARLY:
        return recurrenceEditor.getStartDate();
      case CRON:
        return cronEditor.getStartDate();
      default:
        throw new RuntimeException(MSGS.invalidRunType(getScheduleType().toString()));
    }
  }

  public void setEndDate(Date endDate) {
    recurrenceEditor.setEndDate(endDate);
    cronEditor.setEndDate(endDate);
  }

  public Date getEndDate() {
    switch (getScheduleType()) {
      case RUN_ONCE:
        return null;
      case SECONDS: // fall through
      case MINUTES: // fall through
      case HOURS: // fall through
      case DAILY: // fall through
      case WEEKLY: // fall through
      case MONTHLY: // fall through
      case YEARLY:
        return recurrenceEditor.getEndDate();
      case CRON:
        return cronEditor.getEndDate();
      default:
        throw new RuntimeException(MSGS.invalidRunType(getScheduleType().toString()));
    }
  }

  public void setNoEndDate() {
    recurrenceEditor.setNoEndDate();
    cronEditor.setNoEndDate();
  }

  public void setEndBy() {
    cronEditor.setEndBy();
    recurrenceEditor.setEndBy();
  }

  private void handleScheduleChange() throws EnumException {
    ScheduleType schedType = getScheduleType();
    selectScheduleTypeEditor(schedType);
  }

  private void selectScheduleTypeEditor(ScheduleType scheduleType) {
    // if we are switching to cron type, then hide the start time panel
    if ((isBlockoutDialog == false) && (startTimePanel != null)) {
      if (scheduleType == ScheduleType.CRON) {
        startTimePanel.setVisible(false);
      } else {
        startTimePanel.setVisible(true);
      }
    }

    // hide all panels
    for (Map.Entry<ScheduleType, Panel> me : scheduleTypeMap.entrySet()) {
      me.getValue().setVisible(false);
    }
    // show the selected panel
    Panel p = scheduleTypeMap.get(scheduleType);
    p.setVisible(true);

    TemporalValue tv = scheduleTypeToTemporalValue(scheduleType);
    if (null != tv) {
      // force the recurrence editor to display the appropriate ui
      recurrenceEditor.setTemporalState(tv);
    }
  }

  private static Map<TemporalValue, ScheduleType> createTemporalValueToScheduleTypeMap() {
    Map<TemporalValue, ScheduleType> m = new HashMap<TemporalValue, ScheduleType>();

    m.put(TemporalValue.SECONDS, ScheduleType.SECONDS);
    m.put(TemporalValue.MINUTES, ScheduleType.MINUTES);
    m.put(TemporalValue.HOURS, ScheduleType.HOURS);
    m.put(TemporalValue.DAILY, ScheduleType.DAILY);
    m.put(TemporalValue.WEEKLY, ScheduleType.WEEKLY);
    m.put(TemporalValue.MONTHLY, ScheduleType.MONTHLY);
    m.put(TemporalValue.YEARLY, ScheduleType.YEARLY);

    return m;
  }

  private static Map<ScheduleType, TemporalValue> createScheduleTypeMapToTemporalValue() {
    Map<ScheduleType, TemporalValue> m = new HashMap<ScheduleType, TemporalValue>();

    m.put(ScheduleType.SECONDS, TemporalValue.SECONDS);
    m.put(ScheduleType.MINUTES, TemporalValue.MINUTES);
    m.put(ScheduleType.HOURS, TemporalValue.HOURS);
    m.put(ScheduleType.DAILY, TemporalValue.DAILY);
    m.put(ScheduleType.WEEKLY, TemporalValue.WEEKLY);
    m.put(ScheduleType.MONTHLY, TemporalValue.MONTHLY);
    m.put(ScheduleType.YEARLY, TemporalValue.YEARLY);

    return m;
  }

  private ScheduleType temporalValueToScheduleType(TemporalValue tv) {
    return temporalValueToScheduleTypeMap.get(tv);
  }

  private TemporalValue scheduleTypeToTemporalValue(ScheduleType st) {
    return scheduleTypeToTemporalValueMap.get(st);
  }

  public void setOnChangeHandler(ICallback<IChangeHandler> handler) {
    this.onChangeHandler = handler;
  }

  protected void changeHandler() {
    if (null != onChangeHandler) {
      onChangeHandler.onHandle(this);
    }
  }

  private void configureOnChangeHandler() {
    final ScheduleEditor localThis = this;

    ICallback<IChangeHandler> handler =
        new ICallback<IChangeHandler>() {
          @Override
          public void onHandle(IChangeHandler o) {
            localThis.changeHandler();
          }
        };

    ChangeHandler changeHandler =
        new ChangeHandler() {
          @Override
          public void onChange(ChangeEvent event) {
            localThis.changeHandler();
          }
        };

    ClickHandler clickHandler =
        new ClickHandler() {

          @Override
          public void onClick(ClickEvent event) {
            localThis.changeHandler();
          }
        };

    scheduleCombo.addChangeHandler(changeHandler);
    runOnceEditor.setOnChangeHandler(handler);
    recurrenceEditor.setOnChangeHandler(handler);
    cronEditor.setOnChangeHandler(handler);

    if (daysListBox != null) {
      this.daysListBox.addChangeHandler(changeHandler);
    }
    if (hoursListBox != null) {
      this.hoursListBox.addChangeHandler(changeHandler);
    }
    if (minutesListBox != null) {
      this.minutesListBox.addChangeHandler(changeHandler);
    }

    if (this.startTimePicker != null) {
      startTimePicker.setOnChangeHandler(handler);
    }
    if (this.blockoutEndTimePicker != null) {
      this.blockoutEndTimePicker.setOnChangeHandler(handler);
    }

    if (this.durationRadioButton != null) {
      this.durationRadioButton.addClickHandler(clickHandler);
    }
    if (this.endTimeRadioButton != null) {
      this.endTimeRadioButton.addClickHandler(clickHandler);
    }
  }

  public boolean isBlockoutDialog() {
    return isBlockoutDialog;
  }
}
/** @author Steven Barkdull */
@SuppressWarnings("deprecation")
public class DateRangeEditor extends CaptionPanel implements IChangeHandler {

  private static final WidgetsLocalizedMessages MSGS =
      WidgetsLocalizedMessagesSingleton.getInstance().getMessages();
  private static final String SCHEDULE_EDITOR_CAPTION_PANEL =
      "schedule-editor-caption-panel"; //$NON-NLS-1$

  private static final String END_DATE_RB_GROUP = "end-date-group"; // $NON-NLS-1$
  private static final String END_DATE_PICKER = "end-date-picker"; // $NON-NLS-1$
  private static final String START_DATE_PICKER = "start-date-picker"; // $NON-NLS-1$

  private DatePickerEx startDatePicker = null;
  private EndDatePanel endDatePanel = null;

  private ErrorLabel startLabel = null;
  private ICallback<IChangeHandler> onChangeHandler = null;
  private static int uniqueInstanceNumber = 0;

  public DateRangeEditor(Date date) {

    super(MSGS.rangeOfRecurrence());
    this.addStyleName(SCHEDULE_EDITOR_CAPTION_PANEL);
    uniqueInstanceNumber += 1;

    HorizontalPanel outerHP = new HorizontalPanel();
    add(outerHP);

    HorizontalPanel hp = new HorizontalPanel();
    Label l = new Label(MSGS.startLabel());
    l.setStyleName("startLabel"); // $NON-NLS-1$
    hp.add(l);
    DefaultFormat format = new DefaultFormat(DateTimeFormat.getShortDateFormat());
    startDatePicker = new DatePickerEx(format);
    startDatePicker.getDatePicker().setStyleName(START_DATE_PICKER);
    hp.add(startDatePicker.getDatePicker());
    startLabel = new ErrorLabel(hp);
    outerHP.add(startLabel);

    endDatePanel = new EndDatePanel(date);
    outerHP.add(endDatePanel);

    reset(date);
    configureOnChangeHandler();
  }

  public void setStartDateError(String errorMsg) {
    startLabel.setErrorMsg(errorMsg);
  }

  public Date getStartDate() {
    return startDatePicker.getSelectedDate();
  }

  public void setStartDate(Date d) {
    startDatePicker.getDatePicker().setValue(d);
  }

  public Date getEndDate() {
    return endDatePanel.getDate();
  }

  public void setEndDate(Date d) {
    endDatePanel.setDate(d);
  }

  public void reset(Date d) {
    startDatePicker.getDatePicker().setValue(d);
    endDatePanel.reset(d);
  }

  public void setNoEndDate() {
    endDatePanel.setNoEndDate();
  }

  public boolean isEndBy() {
    return endDatePanel.isEndBy();
  }

  public void setEndBy() {
    endDatePanel.setEndBy();
  }

  public boolean isNoEndDate() {
    return endDatePanel.isNoEndDate();
  }

  public void setEndByError(String errorMsg) {
    endDatePanel.setEndByError(errorMsg);
  }

  public void setOnChangeHandler(ICallback<IChangeHandler> handler) {
    this.onChangeHandler = handler;
  }

  private void changeHandler() {
    if (null != onChangeHandler) {
      onChangeHandler.onHandle(this);
    }
  }

  private void configureOnChangeHandler() {
    final DateRangeEditor localThis = this;

    ICallback<IChangeHandler> handler =
        new ICallback<IChangeHandler>() {
          public void onHandle(IChangeHandler o) {
            localThis.changeHandler();
          }
        };

    startDatePicker.setOnChangeHandler(handler);
    endDatePanel.setOnChangeHandler(handler);
  }

  private class EndDatePanel extends VerticalPanel implements IChangeHandler {

    private DatePickerEx endDatePicker = null;
    private RadioButton noEndDateRb = null;
    private RadioButton endByRb = null;
    private ErrorLabel endByLabel = null;
    private ICallback<IChangeHandler> onChangeHandler = null;

    public EndDatePanel(Date date) {
      final EndDatePanel localThis = this;

      noEndDateRb =
          new RadioButton(END_DATE_RB_GROUP + uniqueInstanceNumber, MSGS.noEndDateLabel());
      noEndDateRb.setStyleName("recurrenceRadioButton"); // $NON-NLS-1$
      noEndDateRb.setValue(true);
      add(noEndDateRb);
      HorizontalPanel hp = new HorizontalPanel();
      add(hp);

      HorizontalPanel endByPanel = new HorizontalPanel();
      endByRb = new RadioButton(END_DATE_RB_GROUP + uniqueInstanceNumber, MSGS.endByLabel());
      endByRb.setStyleName("recurrenceRadioButton"); // $NON-NLS-1$
      endByPanel.add(endByRb);
      DefaultFormat format = new DefaultFormat(DateTimeFormat.getShortDateFormat());
      endDatePicker = new DatePickerEx(format);
      endDatePicker.getDatePicker().setStyleName(END_DATE_PICKER);
      endDatePicker.getDatePicker().setEnabled(false);
      endByPanel.add(endDatePicker.getDatePicker());
      endByLabel = new ErrorLabel(endByPanel);
      hp.add(endByLabel);

      noEndDateRb.addClickListener(
          new ClickListener() {
            public void onClick(Widget sender) {
              localThis.endDatePicker.getDatePicker().setEnabled(false);
            }
          });

      endByRb.addClickListener(
          new ClickListener() {
            public void onClick(Widget sender) {
              localThis.endDatePicker.getDatePicker().setEnabled(true);
            }
          });
      reset(date);
      configureOnChangeHandler();
    }

    public void reset(Date d) {
      setNoEndDate();
      endDatePicker.getDatePicker().setValue(d);
    }

    @SuppressWarnings("unused")
    public DatePickerEx getEndDatePicker() {
      return endDatePicker;
    }

    public void setNoEndDate() {
      endByRb.setValue(false);
      noEndDateRb.setValue(true);
      endDatePicker.getDatePicker().setEnabled(false);
    }

    public boolean isEndBy() {
      return endByRb.getValue();
    }

    public void setEndBy() {
      noEndDateRb.setValue(false);
      endByRb.setValue(true);
      endDatePicker.getDatePicker().setEnabled(true);
    }

    public boolean isNoEndDate() {
      return noEndDateRb.getValue();
    }

    public Date getDate() {
      return isEndBy() ? endDatePicker.getSelectedDate() : null;
    }

    public void setDate(Date d) {
      endDatePicker.getDatePicker().setValue(d);
    }

    public void setEndByError(String errorMsg) {
      endByLabel.setErrorMsg(errorMsg);
    }

    public void setOnChangeHandler(ICallback<IChangeHandler> handler) {
      this.onChangeHandler = handler;
    }

    private void changeHandler() {
      if (null != onChangeHandler) {
        onChangeHandler.onHandle(this);
      }
    }

    private void configureOnChangeHandler() {
      final EndDatePanel localThis = this;

      ICallback<IChangeHandler> handler =
          new ICallback<IChangeHandler>() {
            public void onHandle(IChangeHandler o) {
              localThis.changeHandler();
            }
          };
      KeyboardListener keyboardListener =
          new KeyboardListener() {
            public void onKeyDown(Widget sender, char keyCode, int modifiers) {}

            public void onKeyPress(Widget sender, char keyCode, int modifiers) {}

            public void onKeyUp(Widget sender, char keyCode, int modifiers) {
              localThis.changeHandler();
            }
          };

      ClickListener clickListener =
          new ClickListener() {
            public void onClick(Widget sender) {
              localThis.changeHandler();
            }
          };

      endDatePicker.setOnChangeHandler(handler);
      noEndDateRb.addClickListener(clickListener);
      noEndDateRb.addKeyboardListener(keyboardListener);
      endByRb.addClickListener(clickListener);
      endByRb.addKeyboardListener(keyboardListener);
    }
  } // end EndDatePanel
}