package net.ranides.assira.concurrent;

import lombok.experimental.UtilityClass;
import net.ranides.assira.functional.special.CancelableRunnable;
import net.ranides.assira.functional.special.CancelableConsumer;

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;

/**
 * Utility methods scheduling tasks using global instance of java.util.Timer
 */
@UtilityClass
public class TaskInvoker {

    static final class Li {

        static final Timer TIMER = new Timer("Assira TimerUtils", true);

    }

    /**
     * Executes action after "delay" milliseconds.
     * Created TimerTask can be canceled.
     *
     * @param delay delay in ms
     * @param action action
     * @return TimerTask
     */
    public static TimerTask later(long delay, Runnable action) {
        return later(delay, asTimerTask(action));
    }

    /**
     * Executes action after "delay" milliseconds.
     * Returns TimerTask passed as an argument.
     * Returned TimerTask can be canceled.
     *
     * @param delay delay
     * @param task task
     * @return TimerTask
     */
    public static TimerTask later(long delay, TimerTask task) {
        Li.TIMER.schedule(task, delay);
        return task;
    }

    /**
     * Executes action periodically, in every "period" milliseconds, starting after "delay".
     * Created TimerTask can be canceled.
     *
     * @param delay delay
     * @param period period
     * @param action action
     * @return TimerTask
     */
    public static TimerTask repeat(long delay, long period, Runnable action) {
        return repeat(delay, period, asTimerTask(action));
    }

    /**
     * Executes action periodically, in every "period" milliseconds, starting after "delay".
     * Returns TimerTask passed as an argument.
     * Returned TimerTask can be canceled.
     *
     * @param delay delay
     * @param period period
     * @param task task
     * @return TimerTask
     */
    public static TimerTask repeat(long delay, long period, TimerTask task) {
        Li.TIMER.schedule(task, delay, period);
        return task;
    }

    /**
     * Creates object which can be used to execute action after "delay" milliseconds.
     * If the same action is scheduled more than one time, it will cancel previous calls.
     * Returned object can be used to execute or cancel action.
     *
     * Example usage:
     *      action = lazy(1000, () -> code...)
     *
     *      action.run()
     *      ...
     *      action.run()
     *
     * Effect will be dependent on delay between "run" calls.
     *
     * It is guaranteed that "code..." will be executed ~500ms after last "run".
     *
     * Other "run" calls can cause execution of code, only if delay between them is bigger than 500ms
     * If not, "code..." execution will be canceled and just moved 500ms into the future.
     *
     * @param delay delay in ms
     * @param action action
     * @return CancelableRunnable
     */
    public static CancelableRunnable lazy(long delay, Runnable action) {
        return new LazyRunnable(delay, action);
    }

    /**
     * Creates object which can be used to execute action after "delay" milliseconds.
     * If the same action is scheduled more than one time, it will cancel previous calls.
     * Returned object can be used to execute or cancel action.
     *
     * Example usage:
     *      action = lazy(1000, () -> code...)
     *
     *      action.run()
     *      ...
     *      action.run()
     *
     * Effect will be dependent on delay between "run" calls.
     *
     * It is guaranteed that "code..." will be executed ~500ms after last "run".
     *
     * Other "run" calls can cause execution of code, only if delay between them is bigger than 500ms
     * If not, "code..." execution will be canceled and just moved 500ms into the future.
     *
     * @param delay delay in ms
     * @param action action
     * @return CancelableRunnable
     */
    public static <T> CancelableConsumer<T> lazy(long delay, Consumer<T> action) {
        return new LazyConsumer<>(delay, action);
    }

    private static TimerTask asTimerTask(Runnable action) {
        return new TimerTask() {
            @Override
            public void run() {
                action.run();
            }
        };
    }

    private static class LazyRunnable implements CancelableRunnable {

        private final AtomicReference<TimerTask> task = new AtomicReference<>();

        private volatile boolean finished;

        private volatile boolean canceled;

        private final long delay;

        private final Runnable action;

        public LazyRunnable(long delay, Runnable action) {
            this.delay = delay;
            this.action = action;
        }

        @Override
        public void run() {
            task.getAndUpdate(prev -> {
                if(prev != null) {
                    prev.cancel();
                }
                this.canceled = false;
                this.finished = false;
                return later(delay, this::call);
            });
        }

        private void call() {
            try {
                action.run();
            } finally {
                task.set(null);
                finished = true;
                canceled = true;
            }
        }

        @Override
        public boolean cancel() {
            if(this.canceled || this.finished) {
                return false;
            }
            task.getAndUpdate(prev -> {
                if(prev != null) {
                    prev.cancel();
                    this.canceled = true;
                    this.finished = false;
                }
                return null;
            });
            return true;
        }

        @Override
        public boolean isDone() {
            return this.finished;
        }

        @Override
        public boolean isCanceled() {
            return this.canceled;
        }
    }

    private static class LazyConsumer<T> implements CancelableConsumer<T> {

        private final AtomicReference<TimerTask> task = new AtomicReference<>();

        private volatile boolean finished;

        private volatile boolean canceled;

        private final long delay;

        private final Consumer<T> action;

        public LazyConsumer(long delay, Consumer<T> action) {
            this.delay = delay;
            this.action = action;
        }

        @Override
        public void accept(T argument) {
            task.getAndUpdate(prev -> {
                if(prev != null) {
                    prev.cancel();
                }
                this.canceled = false;
                this.finished = false;
                return later(delay, () -> call(argument));
            });
        }

        private void call(T argument) {
            try {
                action.accept(argument);
            } finally {
                task.set(null);
                finished = true;
                canceled = true;
            }
        }

        @Override
        public boolean cancel() {
            if(this.canceled || this.finished) {
                return false;
            }
            task.getAndUpdate(prev -> {
                if(prev != null) {
                    prev.cancel();
                    this.canceled = true;
                    this.finished = false;
                }
                return null;
            });
            return true;
        }

        @Override
        public boolean isDone() {
            return this.finished;
        }

        @Override
        public boolean isCanceled() {
            return this.canceled;
        }
    }

}
