/*
 * RHQ Management Platform
 * Copyright 2010-2011, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This program 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 version 2 of the License.
 *
 * This program 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. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.rhq.enterprise.gui.coregui.client.components.trigger;

import java.util.Date;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.TreeSet;

import com.smartgwt.client.types.Visibility;
import com.smartgwt.client.widgets.HTMLFlow;
import com.smartgwt.client.widgets.Img;
import com.smartgwt.client.widgets.events.ClickEvent;
import com.smartgwt.client.widgets.events.ClickHandler;
import com.smartgwt.client.widgets.form.DynamicForm;
import com.smartgwt.client.widgets.form.fields.DateTimeItem;
import com.smartgwt.client.widgets.form.fields.FormItem;
import com.smartgwt.client.widgets.form.fields.FormItemIcon;
import com.smartgwt.client.widgets.form.fields.RadioGroupItem;
import com.smartgwt.client.widgets.form.fields.SpacerItem;
import com.smartgwt.client.widgets.form.fields.TextItem;
import com.smartgwt.client.widgets.form.fields.events.ChangedEvent;
import com.smartgwt.client.widgets.form.fields.events.ChangedHandler;
import com.smartgwt.client.widgets.form.fields.events.IconClickEvent;
import com.smartgwt.client.widgets.form.fields.events.IconClickHandler;

import com.smartgwt.client.widgets.tab.Tab;
import com.smartgwt.client.widgets.tab.TabSet;
import org.rhq.core.domain.common.JobTrigger;
import org.rhq.enterprise.gui.coregui.client.CoreGUI;
import org.rhq.enterprise.gui.coregui.client.components.form.DurationItem;
import org.rhq.enterprise.gui.coregui.client.components.form.EnhancedDynamicForm;
import org.rhq.enterprise.gui.coregui.client.components.form.TimeUnit;
import org.rhq.enterprise.gui.coregui.client.components.form.UnitType;
import org.rhq.enterprise.gui.coregui.client.util.message.Message;
import org.rhq.enterprise.gui.coregui.client.util.selenium.LocatableDynamicForm;
import org.rhq.enterprise.gui.coregui.client.util.selenium.LocatableVLayout;

/**
 * A SmartGWT widget that provides the ability to create a new {@link JobTrigger job trigger}, or view or edit an
 * existing job trigger.
 *
 * @author Ian Springer
 */
public class JobTriggerEditor extends LocatableVLayout {

    // Field Names
    private static final String FIELD_MODE = "mode";
    private static final String FIELD_REPEAT_INTERVAL = "repeatInterval";
    private static final String FIELD_REPEAT_DURATION = "repeatDuration";
    private static final String FIELD_END_TIME = "endTime";
    private static final String FIELD_START_TYPE = "startType";
    private static final String FIELD_START_TIME = "startTime";
    private static final String FIELD_START_DELAY = "startDelay";
    private static final String FIELD_RECURRENCE_TYPE = "recurrenceType";
    private static final String FIELD_CRON_EXPRESSION = "cronExpression";

    private JobTrigger jobTrigger;
    private boolean isReadOnly;

    private LocatableVLayout calendarModeLayout;
    private LocatableVLayout cronModeLayout;

    private EnhancedDynamicForm modeForm;
    private EnhancedDynamicForm calendarTypeForm;
    private DynamicForm laterForm;
    private DynamicForm repeatForm;
    private LocatableDynamicForm cronForm;

    // These flags allow us to determine the trigger type.
    private boolean isCronMode;
    private boolean isStartLater;
    private boolean isRecurring;
    private boolean isRepeatDuration;
    private boolean isEndTime;
    private boolean isStartDelay;
    private boolean isStartTime;

    /**
     * Create a new job trigger.
     *
     * @param locatorId
     */
    public JobTriggerEditor(String locatorId, boolean isReadOnly) {
        super(locatorId);

        this.isReadOnly = isReadOnly;
    }

    /**
     * View or edit an existing job trigger.
     *
     * @param locatorId
     * @param jobTrigger
     */
    public JobTriggerEditor(String locatorId, JobTrigger jobTrigger) {
        super(locatorId);

        this.jobTrigger = jobTrigger;
        this.isReadOnly = true;
    }

    public void setJobTrigger(JobTrigger jobTrigger) {
        this.jobTrigger = jobTrigger;
        this.isReadOnly = true;
        if (isDrawn()) {
            refresh();
        }
    }

    private void refresh() {
        if (this.jobTrigger != null) {
            FormItem modeItem = this.modeForm.getItem(FIELD_MODE);
            if (this.jobTrigger.getRecurrenceType() == JobTrigger.RecurrenceType.CRON_EXPRESSION) {
                modeItem.setValue("cron");
                changeMode("cron");
                this.cronForm.setValue("cronExpression", this.jobTrigger.getCronExpression());
            } else {
                modeItem.setValue("calendar");
                this.calendarTypeForm.hide();
                changeMode("calendar");

                FormItem startTypeItem = this.laterForm.getItem(FIELD_START_TYPE);
                startTypeItem.setValue("on");
                DurationItem startDelayItem = (DurationItem) this.laterForm.getItem(FIELD_START_DELAY);
                FormItem startTimeItem = this.laterForm.getField(FIELD_START_TIME);
                changeStartType("on", startDelayItem, startTimeItem);
                startTimeItem.setValue(this.jobTrigger.getStartDate());

                FormItem calendarTypeItem = this.calendarTypeForm.getField("calendarType");
                if (this.jobTrigger.getRecurrenceType() == JobTrigger.RecurrenceType.REPEAT_INTERVAL) {
                    calendarTypeItem.setValue("laterAndRepeat");
                    changeCalendarType("laterAndRepeat");

                    DurationItem repeatIntervalItem = (DurationItem) this.repeatForm.getItem(FIELD_REPEAT_INTERVAL);
                    repeatIntervalItem.setValue(this.jobTrigger.getRepeatInterval().intValue(), UnitType.TIME);

                    FormItem endTimeItem = this.repeatForm.getField(FIELD_END_TIME);
                    DurationItem repeatDurationItem = (DurationItem) this.repeatForm.getItem(FIELD_REPEAT_DURATION);
                    FormItem recurrenceTypeItem = this.repeatForm.getField(FIELD_RECURRENCE_TYPE);
                    if (this.jobTrigger.getRepeatCount() != null) {
                        recurrenceTypeItem.setValue("for");
                        changeRecurrenceType("for", endTimeItem, repeatDurationItem);
                        repeatDurationItem.setValue(this.jobTrigger.getRepeatCount(), UnitType.ITERATIONS);
                    } else if (this.jobTrigger.getEndDate() != null) {
                        recurrenceTypeItem.setValue("until");
                        changeRecurrenceType("until", endTimeItem, repeatDurationItem);
                        endTimeItem.setValue(this.jobTrigger.getEndDate());
                    } else {
                        recurrenceTypeItem.setValue("indefinitely");
                        changeRecurrenceType("indefinitely", endTimeItem, repeatDurationItem);
                    }
                } else {
                    calendarTypeItem.setValue("later");
                    changeCalendarType("later");
                }
            }
        }
    }

    @Override
    protected void onDraw() {
        super.onDraw();

        this.modeForm = new EnhancedDynamicForm(extendLocatorId("ModeForm"), this.isReadOnly);
        this.modeForm.setNumCols(3);
        this.modeForm.setColWidths("140", "220", "*");

        RadioGroupItem modeItem = new RadioGroupItem(FIELD_MODE, MSG.widget_jobTriggerEditor_field_mode());
        LinkedHashMap<String, String> modeValueMap = new LinkedHashMap<String, String>();
        modeValueMap.put("calendar", MSG.widget_jobTriggerEditor_value_calendar());
        modeValueMap.put("cron", MSG.widget_jobTriggerEditor_value_cronExpression());
        modeItem.setValueMap(modeValueMap);
        modeItem.setVertical(false);
        modeItem.setShowTitle(true);
        modeItem.setValue("calendar");

        this.modeForm.setFields(modeItem);
        addMember(this.modeForm);

        this.calendarModeLayout = new LocatableVLayout(extendLocatorId("CalendarModeLayout"));

        this.calendarTypeForm = new EnhancedDynamicForm(extendLocatorId("CalendarTypeForm"), this.isReadOnly);

        RadioGroupItem calendarTypeItem = new RadioGroupItem("calendarType");
        calendarTypeItem.setWidth(440);
        calendarTypeItem.setShowTitle(false);
        LinkedHashMap<String, String> calendarTypeValueMap = new LinkedHashMap<String, String>();
        calendarTypeValueMap.put("now", MSG.widget_jobTriggerEditor_value_now());
        calendarTypeValueMap.put("nowAndRepeat", MSG.widget_jobTriggerEditor_value_nowAndRepeat());
        calendarTypeValueMap.put("later", MSG.widget_jobTriggerEditor_value_later());
        calendarTypeValueMap.put("laterAndRepeat", MSG.widget_jobTriggerEditor_value_laterAndRepeat());
        calendarTypeItem.setValueMap(calendarTypeValueMap);
        calendarTypeItem.setVertical(false);
        calendarTypeItem.setValue("now");

        this.calendarTypeForm.setFields(calendarTypeItem);
        
        this.calendarModeLayout.addMember(this.calendarTypeForm);
        addMember(this.calendarModeLayout);

        this.cronModeLayout = new LocatableVLayout(extendLocatorId("CronModeLayout"));
        this.cronModeLayout.setVisible(false);

        this.cronForm = new LocatableDynamicForm(this.cronModeLayout.extendLocatorId("Form"));

        TextItem cronExpressionItem = new TextItem(FIELD_CRON_EXPRESSION,
                MSG.widget_jobTriggerEditor_field_cronExpression());
        cronExpressionItem.setRequired(true);
        cronExpressionItem.setWidth(340);

        this.cronForm.setFields(cronExpressionItem);

        this.cronModeLayout.addMember(this.cronForm);

        final TabSet cronHelpTabSet = new TabSet();
        cronHelpTabSet.setWidth100();
        cronHelpTabSet.setHeight(200);
        Img closeIcon = new Img("[SKIN]/headerIcons/close.png", 16, 16);
        closeIcon.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                cronHelpTabSet.hide();
            }
        });
        cronHelpTabSet.setTabBarControls(closeIcon);

        Tab formatTab = new Tab(MSG.widget_jobTriggerEditor_tab_format());
        HTMLFlow formatPane = new HTMLFlow();
        formatPane.setWidth100();
        formatPane.setContents("<p>A cron expression is a string comprised of 6 or 7 fields separated by white space. Fields can contain any of the\n" +
                "allowed values, along with various combinations of the allowed special characters for that field. The fields are as\n" +
                "follows:</p>\n" +
                "<table cellpadding=\"3\" cellspacing=\"1\">\n" +
                "    <tbody>\n" +
                "\n" +
                "        <tr>\n" +
                "            <th>Field Name</th>\n" +
                "            <th>Mandatory</th>\n" +
                "            <th>Allowed Values</th>\n" +
                "            <th>Allowed Special Characters</th>\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "\n" +
                "            <td>Seconds</td>\n" +
                "            <td>YES</td>\n" +
                "\n" +
                "            <td>0-59</td>\n" +
                "            <td>, - * /</td>\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "\n" +
                "            <td>Minutes</td>\n" +
                "            <td>YES</td>\n" +
                "            <td>0-59</td>\n" +
                "\n" +
                "            <td>, - * /</td>\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "\n" +
                "            <td>Hours</td>\n" +
                "            <td>YES</td>\n" +
                "            <td>0-23</td>\n" +
                "            <td>, - * /</td>\n" +
                "\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "\n" +
                "            <td>Day of month</td>\n" +
                "            <td>YES</td>\n" +
                "            <td>1-31</td>\n" +
                "            <td>, - * ? / L W<br clear=\"all\" />\n" +
                "            </td>\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "\n" +
                "            <td>Month</td>\n" +
                "            <td>YES</td>\n" +
                "            <td>1-12 or JAN-DEC</td>\n" +
                "            <td>, - * /</td>\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "\n" +
                "            <td>Day of week</td>\n" +
                "\n" +
                "            <td>YES</td>\n" +
                "            <td>1-7 or SUN-SAT</td>\n" +
                "            <td>, - * ? / L #</td>\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "\n" +
                "            <td>Year</td>\n" +
                "            <td>NO</td>\n" +
                "\n" +
                "            <td>empty, 1970-2099</td>\n" +
                "            <td>, - * /</td>\n" +
                "        </tr>\n" +
                "    </tbody>\n" +
                "\n" +
                "</table>\n" +
                "<p>So cron expressions can be as simple as this: <tt>&#42; * * * ? *</tt><br />\n" +
                "or more complex, like this: <tt>0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010</tt></p>\n" +
                "\n" +
                "<h2><a name=\"CronTriggersTutorial-Specialcharacters\"></a>Special Characters</h2>\n" +
                "\n" +
                "<ul>\n" +
                "    <li><tt><b>&#42;</b></tt> (<em>\"all values\"</em>) - used to select all values within a field. For example, \"*\"\n" +
                "    in the minute field means <em>\"every minute\"</em>.</li>\n" +
                "\n" +
                "</ul>\n" +
                "\n" +
                "\n" +
                "<ul>\n" +
                "    <li><tt><b>?</b></tt> (<em>\"no specific value\"</em>) - useful when you need to specify something in one of the\n" +
                "    two fields in which the character is allowed, but not the other. For example, if I want my trigger to fire on a\n" +
                "    particular day of the month (say, the 10th), but don't care what day of the week that happens to be, I would put\n" +
                "    \"10\" in the day-of-month field, and \"?\" in the day-of-week field. See the examples below for clarification.</li>\n" +
                "\n" +
                "</ul>\n" +
                "\n" +
                "\n" +
                "<ul>\n" +
                "    <li><tt><b>&#45;</b></tt> &#45; used to specify ranges. For example, \"10-12\" in the hour field means <em>\"the\n" +
                "    hours 10, 11 and 12\"</em>.</li>\n" +
                "\n" +
                "</ul>\n" +
                "\n" +
                "\n" +
                "<ul>\n" +
                "    <li><tt><b>,</b></tt> &#45; used to specify additional values. For example, \"MON,WED,FRI\" in the day-of-week\n" +
                "    field means <em>\"the days Monday, Wednesday, and Friday\"</em>.</li>\n" +
                "\n" +
                "</ul>\n" +
                "\n" +
                "\n" +
                "<ul>\n" +
                "\n" +
                "    <li><tt><b>/</b></tt> &#45; used to specify increments. For example, \"0/15\" in the seconds field means <em>\"the\n" +
                "    seconds 0, 15, 30, and 45\"</em>. And \"5/15\" in the seconds field means <em>\"the seconds 5, 20, 35, and 50\"</em>. You can\n" +
                "    also specify '/' after the '<b>' character - in this case '</b>' is equivalent to having '0' before the '/'. '1/3'\n" +
                "    in the day-of-month field means <em>\"fire every 3 days starting on the first day of the month\"</em>.</li>\n" +
                "\n" +
                "</ul>\n" +
                "\n" +
                "<ul>\n" +
                "    <li><tt><b>L</b></tt> (<em>\"last\"</em>) - has different meaning in each of the two fields in which it is\n" +
                "    allowed. For example, the value \"L\" in the day-of-month field means <em>\"the last day of the month\"</em> &#45; day\n" +
                "    31 for January, day 28 for February on non-leap years. If used in the day-of-week field by itself, it simply means\n" +
                "    \"7\" or \"SAT\". But if used in the day-of-week field after another value, it means <em>\"the last xxx day of the\n" +
                "    month\"</em> &#45; for example \"6L\" means <em>\"the last friday of the month\"</em>. When using the 'L' option, it is\n" +
                "    important not to specify lists, or ranges of values, as you'll get confusing results.</li>\n" +
                "\n" +
                "</ul>\n" +
                "\n" +
                "\n" +
                "<ul>\n" +
                "    <li><tt><b>W</b></tt> (<em>\"weekday\"</em>) - used to specify the weekday (Monday-Friday) nearest the given day.\n" +
                "    As an example, if you were to specify \"15W\" as the value for the day-of-month field, the meaning is: <em>\"the\n" +
                "    nearest weekday to the 15th of the month\"</em>. So if the 15th is a Saturday, the trigger will fire on Friday the 14th.\n" +
                "    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\n" +
                "    Tuesday the 15th. However if you specify \"1W\" as the value for day-of-month, and the 1st is a Saturday, the trigger\n" +
                "    will fire on Monday the 3rd, as it will not 'jump' over the boundary of a month's days. The 'W' character can only\n" +
                "    be specified when the day-of-month is a single day, not a range or list of days.\n" +
                "        <div class=\"tip\">\n" +
                "            The 'L' and 'W' characters can also be combined in the day-of-month field to yield 'LW', which\n" +
                "            translates to <em>\"last weekday of the month\"</em>.\n" +
                "        </div>\n" +
                "\n" +
                "    </li>\n" +
                "\n" +
                "    <li><tt><b>&#35;</b></tt> &#45; used to specify \"the nth\" XXX day of the month. For example, the value of \"6#3\"\n" +
                "    in the day-of-week field means <em>\"the third Friday of the month\"</em> (day 6 = Friday and \"#3\" = the 3rd one in\n" +
                "    the month). Other examples: \"2#1\" = the first Monday of the month and \"4#5\" = the fifth Wednesday of the month. Note\n" +
                "    that if you specify \"#5\" and there is not 5 of the given day-of-week in the month, then no firing will occur that\n" +
                "    month.\n" +
                "        <div class=\"tip\">\n" +
                "            The legal characters and the names of months and days of the week are not case sensitive. <tt>MON</tt>\n" +
                "            is the same as <tt>mon</tt>.\n" +
                "        </div>\n" +
                "\n" +
                "    </li>\n" +
                "</ul>" +
                "<h2><a name=\"CronTriggersTutorial-Notes\"></a>Notes</h2>\n" +
                "\n" +
                "<ul>\n" +
                "    <li>Support for specifying both a day-of-week and a day-of-month value is not complete (you must currently use\n" +
                "    the '?' character in one of these fields).</li>\n" +
                "    <li>Be careful when setting fire times between mid-night and 1:00 AM - \"daylight savings\" can cause a skip or a\n" +
                "    repeat depending on whether the time moves back or jumps forward.</li>\n" +
                "\n" +
                "</ul>");
        formatTab.setPane(formatPane);

        Tab examplesTab = new Tab(MSG.widget_jobTriggerEditor_tab_examples());
        HTMLFlow examplesPane = new HTMLFlow();
        examplesPane.setWidth100();
        examplesPane.setContents("<table cellpadding=\"3\" cellspacing=\"1\">\n" +
                "    <tbody>\n" +
                "        <tr>\n" +
                "            <th>Expression</th>\n" +
                "\n" +
                "            <th>Meaning</th>\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "            <td><tt>0 0 12 * * ?</tt></td>\n" +
                "\n" +
                "            <td>Fire at 12pm (noon) every day</td>\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "\n" +
                "            <td><tt>0 15 10 ? * *</tt></td>\n" +
                "            <td>Fire at 10:15am every day</td>\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "            <td><tt>0 15 10 * * ?</tt></td>\n" +
                "\n" +
                "            <td>Fire at 10:15am every day</td>\n" +
                "\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "            <td><tt>0 15 10 * * ? *</tt></td>\n" +
                "            <td>Fire at 10:15am every day</td>\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "            <td><tt>0 15 10 * * ? 2005</tt></td>\n" +
                "\n" +
                "            <td>Fire at 10:15am every day during the year 2005</td>\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "            <td><tt>0 * 14 * * ?</tt></td>\n" +
                "            <td>Fire every minute starting at 2pm and ending at 2:59pm, every day</td>\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "\n" +
                "            <td><tt>0 0/5 14 * * ?</tt></td>\n" +
                "\n" +
                "            <td>Fire every 5 minutes starting at 2pm and ending at 2:55pm, every day</td>\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "            <td><tt>0 0/5 14,18 * * ?</tt></td>\n" +
                "            <td>Fire every 5 minutes starting at 2pm and ending at 2:55pm, AND fire every 5\n" +
                "            minutes starting at 6pm and ending at 6:55pm, every day</td>\n" +
                "\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "            <td><tt>0 0-5 14 * * ?</tt></td>\n" +
                "\n" +
                "            <td>Fire every minute starting at 2pm and ending at 2:05pm, every day</td>\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "            <td><tt>0 10,44 14 ? 3 WED</tt></td>\n" +
                "\n" +
                "            <td>Fire at 2:10pm and at 2:44pm every Wednesday in the month of March.</td>\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "            <td><tt>0 15 10 ? * MON-FRI</tt></td>\n" +
                "\n" +
                "            <td>Fire at 10:15am every Monday, Tuesday, Wednesday, Thursday and Friday</td>\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "\n" +
                "            <td><tt>0 15 10 15 * ?</tt></td>\n" +
                "            <td>Fire at 10:15am on the 15th day of every month</td>\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "            <td><tt>0 15 10 L * ?</tt></td>\n" +
                "\n" +
                "            <td>Fire at 10:15am on the last day of every month</td>\n" +
                "\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "            <td><tt>0 15 10 ? * 6L</tt></td>\n" +
                "            <td>Fire at 10:15am on the last Friday of every month</td>\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "            <td><tt>0 15 10 ? * 6L</tt></td>\n" +
                "\n" +
                "            <td>Fire at 10:15am on the last Friday of every month</td>\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "            <td><tt>0 15 10 ? * 6L 2002-2005</tt></td>\n" +
                "            <td>Fire at 10:15am on every last friday of every month during the years 2002,\n" +
                "            2003, 2004 and 2005</td>\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "\n" +
                "            <td><tt>0 15 10 ? * 6#3</tt></td>\n" +
                "\n" +
                "            <td>Fire at 10:15am on the third Friday of every month</td>\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "            <td><tt>0 0 12 1/5 * ?</tt></td>\n" +
                "            <td>Fire at 12pm (noon) every 5 days every month, starting on the first day of the\n" +
                "            month.</td>\n" +
                "\n" +
                "        </tr>\n" +
                "        <tr>\n" +
                "            <td><tt>0 11 11 11 11 ?</tt></td>\n" +
                "\n" +
                "            <td>Fire every November 11th at 11:11am.</td>\n" +
                "        </tr>\n" +
                "    </tbody>\n" +
                "</table>");
        examplesTab.setPane(examplesPane);

        cronHelpTabSet.addTab(formatTab);
        cronHelpTabSet.addTab(examplesTab);

        cronHelpTabSet.setVisible(false);

        FormItemIcon helpIcon = new FormItemIcon();
        helpIcon.setSrc("[SKIN]/actions/help.png");
        cronExpressionItem.setIcons(helpIcon);
        cronExpressionItem.addIconClickHandler(new IconClickHandler() {
            public void onIconClick(IconClickEvent event) {
                cronHelpTabSet.show();
            }
        });

        this.cronModeLayout.addMember(cronHelpTabSet);
        addMember(this.cronModeLayout);

        modeItem.addChangedHandler(new ChangedHandler() {
            public void onChanged(ChangedEvent event) {
                String mode = (String) event.getValue();
                changeMode(mode);
            }
        });

        this.laterForm = createLaterForm();
        this.calendarModeLayout.addMember(this.laterForm);

        this.repeatForm = createRepeatForm();
        this.calendarModeLayout.addMember(this.repeatForm);

        calendarTypeItem.addChangedHandler(new ChangedHandler() {
            public void onChanged(ChangedEvent event) {
                String calendarType = (String)event.getValue();
                changeCalendarType(calendarType);
            }
        });

        refresh();
    }

    private void changeCalendarType(String calendarType) {
        if (calendarType.equals("now")) {
            this.isStartLater = false;
            this.isRecurring = false;
        } else if (calendarType.equals("nowAndRepeat")) {
            this.isStartLater = false;
            this.isRecurring = true;

            FormItem repeatIntervalItem = repeatForm.getItem(FIELD_REPEAT_INTERVAL);
            repeatIntervalItem.setTitle(MSG.widget_jobTriggerEditor_field_repeatInterval_now());
            repeatIntervalItem.redraw();
        } else if (calendarType.equals("later")) {
            this.isStartLater = true;
            this.isRecurring = false;
        } else {
            // value.equals("laterAndRepeat")
            this.isStartLater = true;
            this.isRecurring = true;

            FormItem repeatIntervalItem = repeatForm.getItem(FIELD_REPEAT_INTERVAL);
            repeatIntervalItem.setTitle(MSG.widget_jobTriggerEditor_field_repeatInterval_later());
            repeatIntervalItem.redraw();
        }
        laterForm.setVisibility(this.isStartLater ? Visibility.VISIBLE : Visibility.HIDDEN);
        repeatForm.setVisibility(this.isRecurring ? Visibility.VISIBLE : Visibility.HIDDEN);
    }

    private void changeMode(String mode) {
        if (mode.equals("calendar")) {
            this.isCronMode = false;
            calendarModeLayout.show();
            cronModeLayout.hide();
        } else {
            // cron expression mode
            this.isCronMode = true;
            calendarModeLayout.hide();
            cronModeLayout.show();
        }
    }

    private DynamicForm createRepeatForm() {
        final EnhancedDynamicForm repeatForm = new EnhancedDynamicForm(extendLocatorId("RepeatForm"), this.isReadOnly);
        repeatForm.setNumCols(6);
        repeatForm.setColWidths(140, 130, 130, 130, 130);

        TreeSet<TimeUnit> supportedUnits = new TreeSet<TimeUnit>();
        supportedUnits.add(TimeUnit.SECONDS);
        supportedUnits.add(TimeUnit.MINUTES);
        supportedUnits.add(TimeUnit.HOURS);
        supportedUnits.add(TimeUnit.DAYS);
        supportedUnits.add(TimeUnit.WEEKS);
        supportedUnits.add(TimeUnit.MONTHS);
        supportedUnits.add(TimeUnit.YEARS);
        DurationItem repeatIntervalItem = new DurationItem(FIELD_REPEAT_INTERVAL,
                MSG.widget_jobTriggerEditor_field_repeatInterval_now(),
                supportedUnits, false, this.isReadOnly, repeatForm);
        repeatIntervalItem.setRequired(true);
        repeatIntervalItem.setContextualHelp(MSG.widget_jobTriggerEditor_fieldHelp_repeatInterval());

        RadioGroupItem recurrenceTypeItem = new RadioGroupItem(FIELD_RECURRENCE_TYPE);
        recurrenceTypeItem.setRequired(true);
        recurrenceTypeItem.setShowTitle(false);
        LinkedHashMap<String, String> recurrenceTypeValueMap = new LinkedHashMap<String, String>();
        recurrenceTypeValueMap.put("for", MSG.widget_jobTriggerEditor_value_for());
        recurrenceTypeValueMap.put("until", MSG.widget_jobTriggerEditor_value_until());
        recurrenceTypeValueMap.put("indefinitely", MSG.widget_jobTriggerEditor_value_indefinitely());
        recurrenceTypeItem.setValueMap(recurrenceTypeValueMap);

        supportedUnits = new TreeSet<TimeUnit>();
        supportedUnits.add(TimeUnit.SECONDS);
        supportedUnits.add(TimeUnit.MINUTES);
        supportedUnits.add(TimeUnit.HOURS);
        supportedUnits.add(TimeUnit.DAYS);
        supportedUnits.add(TimeUnit.WEEKS);
        supportedUnits.add(TimeUnit.MONTHS);
        supportedUnits.add(TimeUnit.YEARS);
        final DurationItem repeatDurationItem = new DurationItem(FIELD_REPEAT_DURATION, null,
                supportedUnits, true, this.isReadOnly, repeatForm);
        repeatDurationItem.setShowTitle(false);
        repeatDurationItem.setVisible(false);
        repeatDurationItem.setContextualHelp(MSG.widget_jobTriggerEditor_fieldHelp_repeatDuration());

        final DateTimeItem endTimeItem = createDateTimeItem(FIELD_END_TIME);
        endTimeItem.setShowTitle(false);
        endTimeItem.setVisible(false);

        SpacerItem spacerItem = new SpacerItem();

        recurrenceTypeItem.addChangedHandler(new ChangedHandler() {
            public void onChanged(ChangedEvent event) {
                String recurrenceType = (String)event.getValue();
                changeRecurrenceType(recurrenceType, endTimeItem, repeatDurationItem);
            }
        });

        repeatForm.setFields(repeatIntervalItem, recurrenceTypeItem, repeatDurationItem, endTimeItem, spacerItem);
        repeatForm.setVisible(false);

        return repeatForm;
    }

    private void changeRecurrenceType(String recurrenceType, FormItem endTimeItem, DurationItem repeatDurationItem) {
        if (recurrenceType.equals("for")) {
            this.isEndTime = false;
            this.isRepeatDuration = true;
        } else if (recurrenceType.equals("until")) {
            this.isEndTime = true;
            this.isRepeatDuration = false;
        } else {
            // indefinite
            this.isEndTime = false;
            this.isRepeatDuration = false;
        }

        endTimeItem.setRequired(this.isEndTime);
        if (this.isEndTime) {
            endTimeItem.show();
        } else {
            endTimeItem.hide();
        }
        repeatDurationItem.setRequired(this.isRepeatDuration);
        if (this.isRepeatDuration) {
            repeatDurationItem.show();
        } else {
            repeatDurationItem.hide();
        }
    }

    private DynamicForm createLaterForm() {
        final EnhancedDynamicForm laterForm = new EnhancedDynamicForm(extendLocatorId("LaterForm"), this.isReadOnly);
        laterForm.setNumCols(4);
        laterForm.setColWidths(140, 130, 130);

        RadioGroupItem startTypeItem = new RadioGroupItem(FIELD_START_TYPE, MSG.widget_jobTriggerEditor_field_startType());
        LinkedHashMap<String, String> startTypeValueMap = new LinkedHashMap<String, String>();
        startTypeValueMap.put("on", MSG.widget_jobTriggerEditor_value_on());
        startTypeValueMap.put("in", MSG.widget_jobTriggerEditor_value_in());
        startTypeItem.setValueMap(startTypeValueMap);
        startTypeItem.setShowTitle(true);

        final DateTimeItem startTimeItem = createDateTimeItem(FIELD_START_TIME);

        TreeSet<TimeUnit> supportedUnits = new TreeSet<TimeUnit>();
        supportedUnits.add(TimeUnit.SECONDS);
        supportedUnits.add(TimeUnit.MINUTES);
        supportedUnits.add(TimeUnit.HOURS);
        supportedUnits.add(TimeUnit.DAYS);
        supportedUnits.add(TimeUnit.WEEKS);
        supportedUnits.add(TimeUnit.MONTHS);
        supportedUnits.add(TimeUnit.YEARS);
        final DurationItem startDelayItem = new DurationItem(FIELD_START_DELAY, null,
                supportedUnits, false, this.isReadOnly, laterForm);
        startDelayItem.setShowTitle(false);
        startDelayItem.setVisible(false);
        startDelayItem.setContextualHelp(MSG.widget_jobTriggerEditor_fieldHelp_startDelay());

        SpacerItem spacerItem = new SpacerItem();

        startTypeItem.addChangedHandler(new ChangedHandler() {
            public void onChanged(ChangedEvent event) {
                String startType = (String)event.getValue();
                changeStartType(startType, startDelayItem, startTimeItem);
            }
        });

        laterForm.setFields(startTypeItem, startTimeItem, startDelayItem, spacerItem);
        laterForm.setVisible(false);

        return laterForm;
    }

    private void changeStartType(String startType, DurationItem startDelayItem, FormItem startTimeItem) {
        if (startType.equals("on")) {
            this.isStartDelay = false;
            this.isStartTime = true;
        } else {
            // value.equals("in")
            this.isStartDelay = true;
            this.isStartTime = false;
        }
        startDelayItem.setRequired(this.isStartDelay);
        if (this.isStartDelay) {
            startDelayItem.show();
        } else {
            startDelayItem.hide();
        }
        startTimeItem.setRequired(this.isStartTime);
        if (this.isStartTime) {
            startTimeItem.show();
        } else {
            startTimeItem.hide();
        }
    }

    /**
     * Returns the start time, if one was specified, otherwise returns null to indicate an immediate start.
     *
     * @return the start time, if one was specified, otherwise returns null to indicate an immediate start
     */
    public Date getStartTime() {
        if (!this.isStartLater) {
            return null;
        }
        Date startTime;
        if (this.isStartDelay) {
            // start delay - compute start time
            DurationItem startDelayItem = (DurationItem) this.laterForm.getItem(FIELD_START_DELAY);
            long delay = startDelayItem.getValueAsLong() * 1000;
            long startTimestamp = System.currentTimeMillis() + delay;
            startTime = new Date(startTimestamp);
        } else {
            // start time
            DateTimeItem startTimeItem = (DateTimeItem)this.laterForm.getField(FIELD_START_TIME);
            startTime = startTimeItem.getValueAsDate();
        }
        return startTime;
    }

    public Long getRepeatInterval() {
        Long intervalMillis;
        if (this.isRecurring) {
            DurationItem repeatInterval = (DurationItem) this.repeatForm.getItem(FIELD_REPEAT_INTERVAL);
            intervalMillis = repeatInterval.getValueAsLong() * 1000;
        } else {
            intervalMillis = null;
        }
        return intervalMillis;
    }

    public Integer getRepeatCount() {
        Integer repetitions;
        if (this.isRecurring) {
            if (this.isRepeatDuration) {
                DurationItem repeatDurationItem = (DurationItem) this.repeatForm.getItem(FIELD_REPEAT_DURATION);
                if (repeatDurationItem.getUnitType() == UnitType.ITERATIONS) {
                    // n repetitions
                    repetitions = repeatDurationItem.getValueAsInteger();
                } else {
                    // n units of time - compute end time
                    repetitions = null;
                }
            } else {
                repetitions = null;
            }
        } else {
            repetitions = null;
        }
        return repetitions;
    }

    public Date getEndTime() {
        Date endTime;
        if (this.isRecurring) {
            if (this.isRepeatDuration) {
                DurationItem repeatDurationItem = (DurationItem) this.repeatForm.getItem(FIELD_REPEAT_DURATION);
                if (repeatDurationItem.getUnitType() == UnitType.ITERATIONS) {
                    // n repetitions
                    endTime = null;
                } else {
                    // n units of time - compute end time
                    long delay = repeatDurationItem.getValueAsLong() * 1000;
                    long endTimestamp = System.currentTimeMillis() + delay;
                    endTime = new Date(endTimestamp);
                }
            } else if (this.isEndTime) {
                DateTimeItem endTimeItem = (DateTimeItem)this.repeatForm.getField(FIELD_END_TIME);
                endTime = endTimeItem.getValueAsDate();
            } else {
                endTime = null;
            }
        } else {
            endTime = null;
        }
        return endTime;
    }

    public String getCronExpression() {
        return this.cronForm.getValueAsString(FIELD_CRON_EXPRESSION);
    }

    public boolean validate() {
        // TODO (ips, 01/12/11): Use custom validators to do the startTime / endTime validation instead of the code
        //                       below; that way field-specific validation errors will be used, rather than messages in
        //                       the message bar.
        boolean isValid = true;
        Date currentTime = new Date();
        Date startTime = getStartTime();
        Date endTime = getEndTime();
        if (this.isStartLater) {
            isValid = isValid && this.laterForm.validate();
            if (startTime != null) {
                if (startTime.before(currentTime)) {
                    Message message = new Message(MSG.widget_jobTriggerEditor_message_startTimeMustBeInFuture(),
                            Message.Severity.Error,
                            EnumSet.of(Message.Option.Transient));
                    CoreGUI.getMessageCenter().notify(message);
                    isValid = false;
                }
                if (this.isRecurring && endTime != null) {
                    if (endTime.before(startTime)) {
                        Message message = new Message(MSG.widget_jobTriggerEditor_message_endTimeMustBeAfterStartTime(),
                                Message.Severity.Error,
                                EnumSet.of(Message.Option.Transient));
                        CoreGUI.getMessageCenter().notify(message);
                        isValid = false;
                    }
                }
            }
        }
        if (this.isRecurring) {
            isValid = isValid && this.repeatForm.validate();
            if (endTime != null) {
                if (endTime.before(currentTime)) {
                    Message message = new Message(MSG.widget_jobTriggerEditor_message_endTimeMustBeAfterStartTime(),
                            Message.Severity.Error,
                            EnumSet.of(Message.Option.Transient));
                    CoreGUI.getMessageCenter().notify(message);
                    isValid = false;
                }
            }
        }
        return isValid;
    }

    private static DateTimeItem createDateTimeItem(String name) {
        final DateTimeItem dateTimeItem = new DateTimeItem(name);
        dateTimeItem.setEnforceDate(true);
        dateTimeItem.setCenturyThreshold(99);
        dateTimeItem.setShowTitle(false);
        dateTimeItem.setVisible(false);
        dateTimeItem.setStartDate(new Date());
        dateTimeItem.setUseMask(true);
        dateTimeItem.setShowHint(true);
        return dateTimeItem;
    }

}
