package com.boydti.fawe.util;

import com.boydti.fawe.Fawe;
import com.boydti.fawe.config.Settings;
import com.boydti.fawe.object.FaweQueue;
import com.boydti.fawe.object.RunnableVal;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import javax.annotation.Nullable;

public abstract class TaskManager {

    public static TaskManager IMP;

    private ForkJoinPool pool = new ForkJoinPool();

    /**
     * Run a repeating task on the main thread
     *
     * @param r
     * @param interval in ticks
     * @return
     */
    public abstract int repeat(final Runnable r, final int interval);

    /**
     * Run a repeating task asynchronously
     *
     * @param r
     * @param interval in ticks
     * @return
     */
    public abstract int repeatAsync(final Runnable r, final int interval);

    /**
     * Run a task asynchronously
     *
     * @param r
     */
    public abstract void async(final Runnable r);

    /**
     * Run a task on the main thread
     *
     * @param r
     */
    public abstract void task(final Runnable r);

    /**
     * Get the public ForkJoinPool<br>
     * - ONLY SUBMIT SHORT LIVED TASKS<br>
     * - DO NOT USE SLEEP/WAIT/LOCKS IN ANY SUBMITTED TASKS<br>
     *
     * @return
     */
    public ForkJoinPool getPublicForkJoinPool() {
        return pool;
    }

    /**
     * Run a buch of tasks in parallel using the shared thread pool
     *
     * @param runnables
     */
    public void parallel(Collection<Runnable> runnables) {
//        if (!Fawe.get().isJava8()) {
//            ExecutorCompletionService c = new ExecutorCompletionService(pool);
//            for (Runnable run : runnables) {
//                c.submit(run, null);
//            }
//            try {
//                for (int i = 0; i < runnables.size(); i++) {
//                    c.take();
//                }
//            } catch (Exception e) {
//                e.printStackTrace();
//            }
//            return;
//        }
        for (Runnable run : runnables) {
            pool.submit(run);
        }
        pool.awaitQuiescence(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
    }

    /**
     * Run a bunch of tasks in parallel
     *
     * @param runnables  The tasks to run
     * @param numThreads Number of threads (null = config.yml parallel threads)
     */
    @Deprecated
    public void parallel(Collection<Runnable> runnables, @Nullable Integer numThreads) {
        if (runnables == null) {
            return;
        }
        if (numThreads == null) {
            numThreads = Settings.IMP.QUEUE.PARALLEL_THREADS;
        }
        if (numThreads <= 1) {
            for (Runnable run : runnables) {
                if (run != null) {
                    run.run();
                }
            }
            return;
        }
        int numRuns = runnables.size();
        int amountPerThread = 1 + numRuns / numThreads;
        final Runnable[][] split = new Runnable[numThreads][amountPerThread];
        Thread[] threads = new Thread[numThreads];
        int i = 0;
        int j = 0;
        for (Runnable run : runnables) {
            split[i][j] = run;
            if (++i >= numThreads) {
                i = 0;
                j++;
            }
        }
        for (i = 0; i < threads.length; i++) {
            final Runnable[] toRun = split[i];
            Thread thread = threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < toRun.length; j++) {
                        Runnable run = toRun[j];
                        if (run != null) {
                            run.run();
                        }
                    }
                }
            });
            thread.start();
        }
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    /**
     * Disable async catching for a specific task
     *
     * @param queue
     * @param run
     */
    public void runUnsafe(FaweQueue queue, Runnable run) {
        queue.startSet(true);
        try {
            run.run();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        queue.endSet(true);
    }

    /**
     * Run a task on the current thread or asynchronously
     * - If it's already the main thread, it will jst call run()
     *
     * @param r
     * @param async
     */
    public void taskNow(final Runnable r, boolean async) {
        if (async) {
            async(r);
        } else if (r != null) {
            r.run();
        }
    }

    /**
     * Run a task as soon as possible on the main thread
     * - Non blocking if not calling from the main thread
     *
     * @param r
     */
    public void taskNowMain(final Runnable r) {
        if (r == null) {
            return;
        }
        if (Fawe.isMainThread()) {
            r.run();
        } else {
            task(r);
        }
    }

    /**
     * Run a task as soon as possible not on the main thread
     *
     * @param r
     * @see com.boydti.fawe.Fawe#isMainThread()
     */
    public void taskNowAsync(final Runnable r) {
        taskNow(r, Fawe.isMainThread());
    }

    /**
     * Run a task on the main thread at the next tick or now async
     *
     * @param r
     * @param async
     */
    public void taskSoonMain(final Runnable r, boolean async) {
        if (async) {
            async(r);
        } else {
            task(r);
        }
    }


    /**
     * Run a task later on the main thread
     *
     * @param r
     * @param delay in ticks
     */
    public abstract void later(final Runnable r, final int delay);

    /**
     * Run a task later asynchronously
     *
     * @param r
     * @param delay in ticks
     */
    public abstract void laterAsync(final Runnable r, final int delay);

    /**
     * Cancel a task
     *
     * @param task
     */
    public abstract void cancel(final int task);

    /**
     * Break up a task and run it in fragments of 5ms.<br>
     * - Each task will run on the main thread.<br>
     *
     * @param objects  - The list of objects to run the task for
     * @param task     - The task to run on each object
     * @param whenDone - When the object task completes
     * @param <T>
     */
    public <T> void objectTask(Collection<T> objects, final RunnableVal<T> task, final Runnable whenDone) {
        final Iterator<T> iterator = objects.iterator();
        task(new Runnable() {
            @Override
            public void run() {
                long start = System.currentTimeMillis();
                boolean hasNext;
                while ((hasNext = iterator.hasNext()) && System.currentTimeMillis() - start < 5) {
                    task.value = iterator.next();
                    task.run();
                }
                if (!hasNext) {
                    later(whenDone, 1);
                } else {
                    later(this, 1);
                }
            }
        });
    }

    /**
     * Quickly run a task on the main thread, and wait for execution to finish:<br>
     * - Useful if you need to access something from the Bukkit API from another thread<br>
     * - Usualy wait time is around 25ms<br>
     *
     * @param function
     * @param <T>
     * @return
     */
    public <T> T sync(final RunnableVal<T> function) {
        return sync(function, Integer.MAX_VALUE);
    }

    public <T> T sync(final Supplier<T> function) {
        return sync(function, Integer.MAX_VALUE);
    }

    public void wait(AtomicBoolean running, int timout) {
        try {
            long start = System.currentTimeMillis();
            synchronized (running) {
                while (running.get()) {
                    running.wait(timout);
                    if (running.get() && System.currentTimeMillis() - start > Settings.IMP.QUEUE.DISCARD_AFTER_MS) {
                        new RuntimeException("FAWE is taking a long time to execute a task (might just be a symptom): ").printStackTrace();
                        Fawe.debug("For full debug information use: /fawe threads");
                    }
                }
            }
        } catch (InterruptedException e) {
            MainUtil.handleError(e);
        }
    }

    public void notify(AtomicBoolean running) {
        running.set(false);
        synchronized (running) {
            running.notifyAll();
        }
    }

    public <T> T syncWhenFree(final RunnableVal<T> function) {
        return syncWhenFree(function, Integer.MAX_VALUE);
    }

    public void taskWhenFree(Runnable run) {
        if (Fawe.isMainThread()) {
            run.run();
        } else {
            SetQueue.IMP.addTask(run);
        }
    }

    /**
     * Run a task on the main thread when the TPS is high enough, and wait for execution to finish:<br>
     * - Useful if you need to access something from the Bukkit API from another thread<br>
     * - Usualy wait time is around 25ms<br>
     *
     * @param function
     * @param timeout  - How long to wait for execution
     * @param <T>
     * @return
     */
    public <T> T syncWhenFree(final RunnableVal<T> function, int timeout) {
        if (Fawe.isMainThread()) {
            function.run();
            return function.value;
        }
        final AtomicBoolean running = new AtomicBoolean(true);
        RunnableVal<RuntimeException> run = new RunnableVal<RuntimeException>() {
            @Override
            public void run(RuntimeException value) {
                try {
                    function.run();
                } catch (RuntimeException e) {
                    this.value = e;
                } catch (Throwable neverHappens) {
                    MainUtil.handleError(neverHappens);
                } finally {
                    running.set(false);
                }
                synchronized (function) {
                    function.notifyAll();
                }
            }
        };
        SetQueue.IMP.addTask(run);
        try {
            synchronized (function) {
                while (running.get()) {
                    function.wait(timeout);
                }
            }
        } catch (InterruptedException e) {
            MainUtil.handleError(e);
        }
        if (run.value != null) {
            throw run.value;
        }
        return function.value;
    }

    /**
     * Quickly run a task on the main thread, and wait for execution to finish:<br>
     * - Useful if you need to access something from the Bukkit API from another thread<br>
     * - Usualy wait time is around 25ms<br>
     *
     * @param function
     * @param timeout  - How long to wait for execution
     * @param <T>
     * @return
     */
    public <T> T sync(final RunnableVal<T> function, int timeout) {
        return sync((Supplier<T>) function, timeout);
    }

    public <T> T sync(final Supplier<T> function, int timeout) {
        if (Fawe.isMainThread()) {
            return function.get();
        }
        final AtomicBoolean running = new AtomicBoolean(true);
        RunnableVal<Object> run = new RunnableVal<Object>() {
            @Override
            public void run(Object value) {
                try {
                    this.value = function.get();
                } catch (RuntimeException e) {
                    this.value = e;
                } catch (Throwable neverHappens) {
                    MainUtil.handleError(neverHappens);
                } finally {
                    running.set(false);
                }
                synchronized (function) {
                    function.notifyAll();
                }
            }
        };
        SetQueue.IMP.addTask(run);
        try {
            synchronized (function) {
                while (running.get()) {
                    function.wait(timeout);
                }
            }
        } catch (InterruptedException e) {
            MainUtil.handleError(e);
        }
        if (run.value != null && run.value instanceof RuntimeException) {
            throw (RuntimeException) run.value;
        }
        return (T) run.value;
    }
}
