package net.ranides.assira.concurrent;

import net.ranides.assira.events.Event;
import net.ranides.assira.events.EventDispatcher;
import net.ranides.assira.events.EventHandler;
import net.ranides.assira.events.EventRouter;

import java.time.Duration;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * It is builder object designed to construct complex tasks.
 *
 * Constructed task can execute action or send event to specified router.
 * You can specify router by reference or by name.
 *
 * Non-periodic tasks will call only "start" and "cancel" handlers.
 *
 * You can construct periodic task which will call "start", "finish", "cancel" and "interval" handlers.
 *
 * Periodic tasks can repeat specified number of times or infinitely.
 *
 * You can specificy router name as EventHandler.UI or EventHandler.UI_WAIT, then you can schedule action to be
 * executed inside AWT thread (using SwingInvoeker)
 */
public class TaskBuilder {

    private EventRouter router;

    private String routerName;

    private Duration delay;

    private Duration interval;

    private Integer repeat;

    private Consumer<Integer> actionInterval;

    private Consumer<Integer> actionStart;

    private Consumer<Integer> actionFinish;

    private Consumer<Integer> actionCancel;

    private Function<Integer, Event> eventInterval;

    private Function<Integer, Event> eventStart;

    private Function<Integer, Event> eventFinish;

    private Function<Integer, Event> eventCancel;

    /**
     * Creates new builder
     */
    public TaskBuilder() {
        // do nothing
    }

    /**
     * Defines target router executing task
     *
     * @param router router
     * @return this
     */
    public TaskBuilder router(EventRouter router) {
        this.router = router;
        return this;
    }

    /**
     * Defines target router executing task
     *
     * @param router router
     * @return this
     */
    public TaskBuilder router(String router) {
        this.routerName = router;
        return this;
    }

    /**
     * Defines delay time for delayed, repeatable or lazy task
     *
     * @param millis millis
     * @return this
     */
    public TaskBuilder delay(long millis) {
        return delay(Duration.ofMillis(millis));
    }

    /**
     * Defines delay time for delayed, repeatable or lazy task
     *
     * @param duration duration
     * @return this
     */
    public TaskBuilder delay(Duration duration) {
        this.delay = duration;
        return this;
    }

    /**
     * Defines interval time for repeatable task.
     * If builder does not define number of repeats, it will be repeated infinitely.
     *
     * @param interval interval
     * @return this
     */
    public TaskBuilder interval(long interval) {
        return interval(Duration.ofMillis(interval));
    }

    /**
     * Defines interval time for repeatable task.
     * If builder does not define number of repeats, it will be repeated infinitely.
     *
     * @param interval interval
     * @return this
     */
    public TaskBuilder interval(Duration interval) {
        this.interval = interval;
        return this;
    }

    /**
     * Defines how many times task should be executed
     *
     * @param repeat repeat
     * @return this
     */
    public TaskBuilder repeat(int repeat) {
        this.repeat = repeat;
        return this;
    }

    /**
     * Defines code to be executed at the begining of task.
     * If task is repeatable, this code will be invoked only once.
     *
     * @param action action
     * @return this
     */
    public TaskBuilder start(Runnable action) {
        this.actionStart = (v) -> action.run();
        return this;
    }

    /**
     * Defines event to be emitted at the begining of task.
     * If task is repeatable, this event will be emitted only once.
     *
     * @param action action
     * @return this
     */
    public TaskBuilder start(Supplier<Event> action) {
        this.eventStart = (v) -> action.get();
        return this;
    }

    /**
     * Defines code to be executed at the end of task.
     * If task is repeatable, this code will be invoked only once, after last repetition.
     *
     * @param action action
     * @return this
     */
    public TaskBuilder finish(Runnable action) {
        this.actionFinish = (v) -> action.run();
        return this;
    }

    /**
     * Defines event to be emitted at the end of task.
     * If task is repeatable, this event will be emitted only once, after last repetition.
     *
     * @param action action
     * @return this
     */
    public TaskBuilder finish(Supplier<Event> action) {
        this.eventFinish = (v) -> action.get();
        return this;
    }

    /**
     * Defines code executed by repeatable task.
     * Action will receive iteration number.
     *
     * @param action action
     * @return this
     */
    public TaskBuilder interval(Consumer<Integer> action) {
        this.actionInterval = action;
        return this;
    }

    /**
     * Defines code executed by repeatable task.
     *
     * @param action action
     * @return this
     */
    public TaskBuilder interval(Runnable action) {
        this.actionInterval = (v) -> action.run();
        return this;
    }

    /**
     * Defines event to be emitted by repeatable task.
     * Event generator will receive iteration number.
     *
     * @param action action
     * @return this
     */
    public TaskBuilder interval(Function<Integer, Event> action) {
        this.eventInterval = action;
        return this;
    }

    /**
     * Defines event to be emitted by repeatable task.
     *
     * @param action action
     * @return this
     */
    public TaskBuilder interval(Supplier<Event> action) {
        this.eventInterval = (v) -> action.get();
        return this;
    }

    /**
     * Defines code to be executed if task was cancelled before execution.
     * If task is repeatable, this code will be executed, if cancelation took place
     * before last iteration or finish callback.
     *
     * @param action action
     * @return this
     */
    public TaskBuilder cancel(Runnable action) {
        this.actionCancel = (v) -> action.run();
        return this;
    }

    /**
     * Defines event to be emitted if task was cancelled before execution.
     * If task is repeatable, this event will be emitted, if cancelation took place
     * before last iteration or finish callback.
     *
     * @param action action
     * @return this
     */
    public TaskBuilder cancel(Supplier<Event> action) {
        this.eventCancel = (v) -> action.get();
        return this;
    }

    /**
     * Creates new cancelable TimerTask, which is already in started state.
     *
     * @return TimerTask
     */
    public TimerTask run() {

        initRouter();

        actionStart = initDispatch(actionStart, eventStart);
        actionCancel = initDispatch(actionCancel, eventCancel);
        actionInterval = initDispatch(actionInterval, eventInterval);
        actionFinish = initDispatch(actionFinish, eventFinish);

        initIntervalStart();

        boolean awt = EventHandler.UI.equals(routerName) || EventHandler.UI_WAIT.equals(routerName);
        boolean snc = EventHandler.UI_WAIT.equals(routerName);
        if(awt) {
            actionStart = initUI(actionStart, snc);
            actionFinish = initUI(actionFinish, snc);
            actionCancel = initUI(actionCancel, snc);
            actionInterval = initUI(actionInterval, snc);
        }

        return newTimer();
    }

    private void initRouter() {
        if(routerName != null) {
            if(router != null) {
                throw new IllegalStateException("Both router and router name is specified in invoker");
            }
            router = EventDispatcher.find(routerName).orElse(null);
        }
    }

    private Consumer<Integer> initDispatch(Consumer<Integer> action, Function<Integer, Event> event) {
        if(event == null) {
            return action;
        }
        if(action != null) {
            throw new IllegalStateException("Both action and event supplier is specified in invoker");
        }
        if(router == null) {
            throw new IllegalStateException("Event supplier is defined, but event dispatcher not");
        }
        return (v) -> router.signalEvent( event.apply(v) );
    }

    private void initIntervalStart() {
        if(interval == null) {
            if(actionInterval != null) {
                throw new IllegalStateException("Task has defined repeat interval but not interval action");
            }
        }
        if(actionInterval == null) {
            if(interval != null) {
                throw new IllegalStateException("Task has defined repeat action but not repeat interval");
            }
        }
    }

    private static Consumer<Integer> initUI(Consumer<Integer> action, boolean sync) {
        if(action == null) {
            return null;
        }
        return (v) -> SwingInvoker.run(sync, () -> action.accept(v));
    }

    private TimerTask newTimer() {
        if(actionInterval != null) {
            return TaskInvoker.repeat(toMillis(delay), toMillis(interval), new IntervalTask(repeat, actionStart, actionCancel, actionFinish, actionInterval));
        } else {
            return TaskInvoker.later(toMillis(delay), new LaterTask(actionStart, actionCancel, actionFinish));
        }
    }

    private static long toMillis(Duration duration) {
        return duration!=null ? duration.toMillis() : 0;
    }

    private static class LaterTask extends TimerTask {

        protected final Consumer<Integer> rStart;

        protected final Consumer<Integer> rCancel;

        protected final Consumer<Integer> rFinish;

        public LaterTask(Consumer<Integer> rStart, Consumer<Integer> rCancel, Consumer<Integer> rFinish) {
            this.rStart = rStart;
            this.rCancel = rCancel;
            this.rFinish = rFinish;
        }

        @Override
        public void run() {
            rStart.accept(0);
            if(rFinish != null) {
                rFinish.accept(0);
            }
        }

        @Override
        public boolean cancel() {
            if(super.cancel()) {
                if(rCancel!=null) {
                    rCancel.accept(0);
                }
                return true;
            } else {
                return false;
            }
        }

    }

    private static class IntervalTask extends LaterTask {

        private final AtomicInteger counter = new AtomicInteger(0);

        private final Integer max;

        protected final Consumer<Integer> rFinish;

        protected final Consumer<Integer> rInterval;

        public IntervalTask(Integer max, Consumer<Integer> rStart, Consumer<Integer> rCancel, Consumer<Integer> rFinish, Consumer<Integer> rInterval) {
            super(rStart, rCancel, rFinish);
            this.max = max;
            this.rFinish = rFinish;
            this.rInterval = rInterval;
        }

        @Override
        public void run() {
            int now = counter.incrementAndGet();
            if(now == 1 && rStart!=null) {
                rStart.accept(0);
            }

            rInterval.accept(rStart!=null ? now-1 : now);

            if(max != null && now == max) {
                super.cancel();
                if(rFinish != null) {
                    rFinish.accept(now);
                }
            }
        }

    }

}
