/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package schedule;

import java.util.Calendar;
import java.util.Date;
import java.util.Locale;

/**
 * Purpose of this class is to provide iterator returning exact dates at which
 * various tasks and actions can be scheduled. This iterator allows scheduling
 * task at most once per day, as it enables user to provide wildcards matching
 * any day of month or week. However, hour and minute of execution must be
 * entered exactly. This class returns values with accuracy in minutes, i.e
 * seconds and milliseconds of date returned is allways 0.
 *
 * Number of day is as follows: Monday is 1, Tuesday is 2 and so on with Sunday
 * as day 7.
 * 
 * @author Marcel Duris
 * @see schedule.ISchedulable
 */
public class TaskTime implements ISchedulable {

    private int dayOfMonth;
    private int dayOfWeek;
    private int hour;
    private int minute;
    private Calendar calendar;

    /**
     * Basic constructor which takes values representing day of month or day
     * in week, hour and minute of time, when task should run. Task must run
     * at least every month, which is long enough and at most once per day.
     *
     * Wildcard matching any day of month or week can be passed as arguments.
     * This wildcard is ITask.ANYTIME constant. This wildcard cannot be used
     * to specify hour or minute of execution.
     *
     * At least one parameter passed to this function must be wildcard, i.e
     * user cannot specify both day in month and day in week, when task should
     * run, although ability to run tasks at Friday 13 is appealing.
     *
     * @param dayOfMonth number of day in month, wildcard can be used
     * @param dayOfWeek number of day in week, wildcard can be used
     * @param hour exact hour of execution in 24 hour per day format
     * @param minute exact minute of execution
     */
    public TaskTime(int dayOfMonth, int dayOfWeek, int hour, int minute) {
        if (dayOfMonth != ANYTIME && dayOfWeek != ANYTIME) {
            throw new IllegalArgumentException("Can not set both day of month and day of week.");
        }

        calendar = Calendar.getInstance(Locale.ROOT);
        this.dayOfMonth = dayOfMonth;
        this.dayOfWeek = dayOfWeek;
        this.hour = hour;
        this.minute = minute;

        setTime();
    }

    /**
     * This function takes current time and modifies days, hours and minutes so
     * it matches parameters passed to constructor. This time can be in future
     * or in past. Value is stored in internal calendar object.
     */
    private void setTime() {
        calendar.setTime(new Date());

        if (dayOfMonth != ANYTIME) {
            calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth);
        }

        if (dayOfWeek != ANYTIME) {
            calendar.set(Calendar.DAY_OF_WEEK, dayOfWeek);
        }

        if (hour != ANYTIME) {
            calendar.set(Calendar.HOUR_OF_DAY, hour);
        }

        calendar.set(Calendar.MINUTE, minute);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
    }

    /**
     * Returns date of next execution of given task.
     * 
     * @return date of next run of task
     */
    public Date nextDue() {
        setTime();

        //If now is too late...
        if (calendar.getTime().before(new Date())) {
            if (hour == ANYTIME) {
                setHour();
            }

            if (calendar.getTime().before(new Date())) {
                //if setting hour didn't help, we set day
                setDay();
            }

            if (calendar.getTime().before(new Date())) {
                // If anything else fails, next month will do
                setMonth();
            }
        }

        return calendar.getTime();
    }

    /**
     * Checks whether date passed to function is valid date when task can be
     * run.
     *
     * @param date date to checks
     * @return true only if date matches configured parameters
     */
    public boolean accepts(Date date) {
        Calendar c = Calendar.getInstance();
        c.setTime(date);
        c.set(Calendar.SECOND, 0);
        c.set(Calendar.MILLISECOND, 0);

        setTime();

        return c.compareTo(calendar) == 0;
    }

    /**
     * Tries to set next admissible hour to run task.
     */
    private void setHour() {
        /* we can try to set hour to following hour only if we stay at current
         * day or next day is allowed
         */
        if ((dayOfMonth == ANYTIME && dayOfWeek == ANYTIME) ||
                calendar.get(Calendar.HOUR_OF_DAY) < 23) {
            calendar.add(Calendar.HOUR_OF_DAY, 1);
        }
    }

    /**
     * Tries to set next admissible day to run task.
     */
    private void setDay() {
        if (dayOfMonth == ANYTIME) {
            /* either day of week or day of month must be set to any, or we
             * can't set next day
             */
            if (dayOfWeek == ANYTIME) {
                //choose either one or seven days
                calendar.add(Calendar.DAY_OF_MONTH, 1);
            } else {
                calendar.add(Calendar.DAY_OF_MONTH, 7);
            }
        }
        if (hour == ANYTIME) {
            calendar.set(Calendar.HOUR_OF_DAY, 0);
        }
    }

    /**
     * Sets time of next run to next month.
     */
    private void setMonth() {
        calendar.add(Calendar.MONTH, 1);
        if (dayOfMonth == ANYTIME) {
            calendar.set(Calendar.DAY_OF_MONTH, 1);
        } else {
            calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth);
        }
        if (dayOfWeek != ANYTIME) {
            calendar.set(Calendar.DAY_OF_WEEK, dayOfWeek);
        }
        if (hour == ANYTIME) {
            calendar.set(Calendar.HOUR_OF_DAY, 0);
        }
    }
}
