package tc.oc.commons.core.scheduler;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.inject.Inject;

import com.google.common.collect.ImmutableSet;
import java.time.Duration;
import java.time.Instant;
import tc.oc.commons.core.concurrent.CatchingRunnable;
import tc.oc.commons.core.exception.ExceptionHandler;
import tc.oc.commons.core.logging.Loggers;
import tc.oc.commons.core.plugin.PluginScoped;
import tc.oc.commons.core.util.StackTrace;

/**
 * Represents a disposable {@link Task} scheduler.
 *
 * This mostly acts as a wrapper for the Bukkit/Bungee schedulers, however
 * it adds additional functionality such as debouncing, phased registration,
 * and universal cancellation.
 */
@PluginScoped
public class Scheduler {

    protected final Logger logger;
    private final SchedulerBackend<Object> backend;
    private final ExceptionHandler exceptionHandler;

    private final Set<AbstractTask> registered = new HashSet<>();
    private final Set<AbstractTask> started = new HashSet<>();

    private final Set<Class<?>> skipTraceClasses = ImmutableSet.of(getClass(), State.class, Unstarted.class, Cancelled.class, Running.class);

    private State state;

    @Inject protected Scheduler(Loggers loggers, SchedulerBackend backend, ExceptionHandler exceptionHandler) {
        this(loggers, backend, exceptionHandler, true);
    }

    public Scheduler(Loggers loggers, SchedulerBackend backend, ExceptionHandler exceptionHandler, boolean started) {
        this.exceptionHandler = exceptionHandler;
        this.logger = loggers.get(getClass());
        this.backend = backend;

        state = started ? new Running() : new Unstarted();
    }

    protected Task register(Task.Parameters parameters, Runnable runnable) {
        return register(parameters, runnable, new StackTrace(skipTraceClasses));
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "{registered=" + registered + ", started=" + started + ", state=" + state.getClass().getSimpleName() + "}";
    }

    public Task createTask(Runnable task) {
        return register(Task.Parameters.fromDuration(null, null), task);
    }

    public Task createDelayedTask(Duration delay, Runnable task) {
        return register(Task.Parameters.fromDuration(delay, null), task);
    }

    public Task createDelayedTask(long delay, Runnable task) {
        return register(Task.Parameters.fromTicks(delay, null), task);
    }

    public Task createDelayedTask(Instant when, Runnable task) {
        final Instant now = Instant.now();
        if(when.isAfter(Instant.now())) {
            return createDelayedTask(Duration.between(now, when), task);
        } else {
            return createTask(task);
        }
    }

    public Task createRepeatingTask(Duration interval, Runnable task) {
        return register(Task.Parameters.fromDuration(Duration.ZERO, interval), task);
    }

    public Task createRepeatingTask(long interval, Runnable task) {
        return register(Task.Parameters.fromTicks(0L, interval), task);
    }

    public Task createRepeatingTask(Duration delay, Duration interval, Runnable task) {
        return register(Task.Parameters.fromDuration(delay, interval), task);
    }

    public Task createRepeatingTask(long delay, long interval, Runnable task) {
        return register(Task.Parameters.fromTicks(delay, interval), task);
    }

    /**
     * Create a new {@link ReusableTask} with no schedule
     */
    public ReusableTask createReusableTask(Runnable runnable) {
        return new ReusableTask(this, new CatchingRunnable(exceptionHandler, runnable, new StackTrace(skipTraceClasses)));
    }

    public DebouncedTask createDebouncedTask(Duration delay, Runnable runnable) {
        return new DebouncedTask(this, delay, runnable);
    }

    public DebouncedTask createDebouncedTask(Runnable runnable) {
        return new DebouncedTask(this, runnable);
    }


    // Keep all synchronized methods below

    /**
     * Start all registered {@link Task}s from now on.
     */
    synchronized public void start() {
        state.start();
    }

    /**
     * Permanently disable this scheduler and cancel all {@link Task}s in the past and future.
     */
    synchronized public void cancel() {
        state.cancel();
    }

    synchronized protected Task register(Task.Parameters parameters, Runnable runnable, @Nullable StackTrace trace) {
        return state.register(parameters, new CatchingRunnable(exceptionHandler, runnable, trace));
    }

    synchronized protected boolean isTaskQueued(AbstractTask task) {
        return state.isTaskQueued(task);
    }

    synchronized protected boolean isTaskRunning(AbstractTask task) {
        return state.isTaskRunning(task);
    }

    synchronized protected void cancelTask(AbstractTask task) {
        state.cancelTask(task);
    }

    /**
     * Run the given task only if there is no instance of that task's class already scheduled or running.
     * @return The handle of the newly scheduled task, if it was scheduled, otherwise the handle of the existing task.
     */
    synchronized public Task debounceTask(Runnable runnable) {
        for(Task task : state.pendingTasks()) {
            if(task.isPending() && task.getRunnable().getClass().isInstance(runnable)) {
                return task;
            }
        }
        return createTask(runnable);
    }

    protected Object startTask(AbstractTask task) {
        final Runnable runnable;
        if(task.getParameters().isRepeating()) {
            runnable = task.getRunnable();
        } else {
            runnable = () -> {
                try {
                    task.getRunnable().run();
                } finally {
                    synchronized(Scheduler.this) {
                        started.remove(task);
                    }
                }
            };
        }
        final Object backendTask = backend.startTask(task.getParameters(), runnable);
        task.setRunning(backendTask);
        synchronized(this) {
            started.add(task);
        }
        return backendTask;
    }

    private abstract class State {
        void start() {};
        void cancel() {};
        void cancelTask(AbstractTask task) {}

        abstract Iterable<? extends Task> pendingTasks();
        abstract boolean isTaskQueued(AbstractTask task);
        abstract boolean isTaskRunning(AbstractTask task);

        DisposableTask register(Task.Parameters parameters, CatchingRunnable runnable) {
            return new DisposableTask(Scheduler.this, parameters, runnable);
        }
    }

    private class Unstarted extends State {
        @Override
        void start() {
            registered.forEach(Scheduler.this::startTask);
            registered.clear();
            state = new Running();
        }

        @Override
        void cancel() {
            registered.clear();
            state = new Cancelled();
        }

        @Override
        DisposableTask register(Task.Parameters parameters, CatchingRunnable runnable) {
            final DisposableTask task = super.register(parameters, runnable);
            registered.add(task);
            return task;
        }

        @Override
        void cancelTask(AbstractTask task) {
            registered.remove(task);
        }

        @Override
        boolean isTaskQueued(AbstractTask task) {
            return registered.contains(task);
        }

        @Override
        boolean isTaskRunning(AbstractTask task) {
            return false;
        }

        @Override
        public Iterable<? extends Task> pendingTasks() {
            return registered;
        }
    }

    private class Running extends State {
        @Override
        void cancel() {
            ImmutableSet.copyOf(started).forEach(Task::cancel);
            started.clear();
            state = new Cancelled();
        }

        @Override
        DisposableTask register(Task.Parameters parameters, CatchingRunnable runnable) {
            final DisposableTask task = super.register(parameters, runnable);
            startTask(task);
            return task;
        }

        @Override
        boolean isTaskQueued(AbstractTask task) {
            return task.backend != null && backend.isTaskQueued(task.backend);
        }

        @Override
        boolean isTaskRunning(AbstractTask task) {
            return task.backend != null && backend.isTaskRunning(task.backend);
        }

        @Override
        void cancelTask(AbstractTask task) {
            started.remove(task);
            if(task.backend != null) {
                backend.cancelTask(task.backend);
            }
            task.setCancelled();
        }

        @Override
        public Iterable<? extends Task> pendingTasks() {
            return started
                .stream()
                .filter(Task::isPending)
                .collect(Collectors.toSet());
        }
    }

    private class Cancelled extends State {
        @Override
        void start() {
            throw new IllegalStateException("Scheduler has already been cancelled");
        }

        @Override
        DisposableTask register(Task.Parameters parameters, CatchingRunnable runnable) {
            final DisposableTask task = super.register(parameters, runnable);
            task.setCancelled();
            return task;
        }

        @Override
        boolean isTaskQueued(AbstractTask task) {
            return false;
        }

        @Override
        boolean isTaskRunning(AbstractTask task) {
            return false;
        }

        @Override
        public Iterable<Task> pendingTasks() {
            return Collections.emptySet();
        }
    }
}