package com.github.emmanueltouzery.crony;

import java.time.Duration;
import java.time.ZonedDateTime;

import javaslang.Function1;
import javaslang.collection.Stream;

/**
 * Retrieve information regarding the execution dates of
 * a cron specification.
 */
public class CronExecution {

    /**
     * Get an infinite stream of the next execution dates after a date you give
     * for a cron specification.
     * You can use {@link javaslang.collection.Stream#take} to retrieve a limited
     * number of elements.
     * @param cron the cron specification
     * @param base the date to start from
     * @return an infinite stream of upcoming execution dates.
     */
    public static Stream<ZonedDateTime> getNextExecutionDates(Cron cron, ZonedDateTime base) {
        return Stream
            .iterate(base, d -> getNextExecutionDate(cron, d))
            .drop(1);
    }

    /**
     * Get the next execution date for a cron specification after the date you give
     * @param cron the cron specification
     * @param base the date to start from
     * @return the next execution date
     */
    public static ZonedDateTime getNextExecutionDate(Cron cron, ZonedDateTime base) {
        return getExecutionDateDirection(cron, base, true);
    }

    /**
     * Get an infinite stream of the previous execution dates before a date you give
     * for a cron specification.
     * You can use {@link javaslang.collection.Stream#take} to retrieve a limited
     * number of elements.
     * @param cron the cron specification
     * @param base the date to start from
     * @return an infinite stream of previous execution dates.
     */
    public static Stream<ZonedDateTime> getPreviousExecutionDates(Cron cron, ZonedDateTime base) {
        return Stream
            .iterate(base, d -> getPreviousExecutionDate(cron, d))
            .drop(1);
    }

    /**
     * Get the previous execution date for a cron specification before the date you give
     * @param cron the cron specification
     * @param base the date to start from
     * @return the previous execution date
     */
    public static ZonedDateTime getPreviousExecutionDate(Cron cron, ZonedDateTime base) {
        return getExecutionDateDirection(cron, base, false);
    }

    /**
     * Calculates the gap between the date you give and the closest execution date
     * for the cron specification you supply (either before or after the date you give).
     * The calculated gap will always be positive. So for instance if the previous
     * execution is 2 minutes before the date you give, and the next one is 2 hours
     * after, the duration returned will be two minutes (not minus two minutes).
     * @param cron the cron specification
     * @param base the date to consider
     * @return the gap between the date you give and the closest execution time
     */
    public static Duration gapToClosestExecution(Cron cron, ZonedDateTime base) {
        Duration duration1 = Duration.between(getPreviousExecutionDate(cron, base), base);
        Duration duration2 = Duration.between(base, getNextExecutionDate(cron, base));
        return (duration1.getSeconds() < duration2.getSeconds()) ? duration1 : duration2;
    }

    private static ZonedDateTime getExecutionDateDirection(
        Cron cron, ZonedDateTime date, boolean forward) {
        int increment = forward ? 1 : -1;
        Function1<ZonedDateTime, ZonedDateTime> newDayReset = forward
            ? d -> d.withHour(0).withMinute(0)
            : d -> d.withHour(23).withMinute(59);

        // if we are right on an execution date right now, we'll return the next one.
        date = date.plusMinutes(increment);

        // first find the next matching day
        while (!isDayMatch(cron, date)) {
            date = newDayReset.apply(date.plusDays(increment));
        }
        // now find the next matching hour
        while (!cron.hourSpec.isMatch(date)) {
            date = date.plusHours(increment);
        }
        // and now the minute
        while (!cron.isMatch(date)) {
            date = date.plusMinutes(increment);
        }
        return date;
    }

    private static boolean isDayMatch(Cron cron, ZonedDateTime datetime) {
        return cron.dayOfMonthSpec.isMatch(datetime) &&
            cron.monthSpec.isMatch(datetime) &&
            cron.dayOfWeekSpec.isMatch(datetime);
    }
}

@@ -3363,24 +3363,25 @@
 1 : -1;%0A
+%0A
         
 Function
@@ -3376,168 +3376,42 @@
     
-Function1%3CZonedDateTime, ZonedDateTime%3E newDayReset = forward%0A            ? d -%3E d.withHour(0).withMinute(0)%0A            : d -%3E d.withHour(23).withMinute(59);%0A%0A
+if (isDayMatch(cron, date)) %7B%0A    
     
@@ -3492,32 +3492,36 @@
 xt one.%0A        
+    
 date = date.plus
@@ -3539,16 +3539,259 @@
 rement);
+%0A        %7D else %7B%0A            // will have to go to another day: reset the time.%0A            date = forward%0A                ? date.plusDays(1).withHour(0).withMinute(0)%0A                : date.plusDays(-1).withHour(23).withMinute(59);%0A        %7D
 %0A%0A      
@@ -3893,26 +3893,8 @@
 e = 
-newDayReset.apply(
 date
@@ -3913,17 +3913,16 @@
 crement)
-)
 ;%0A      

clean up in the cron execution date calculating algorithm.