/*
 * This file is part of BlueMap, licensed under the MIT License (MIT).
 *
 * Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
 * Copyright (c) contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package de.bluecolored.bluemap.common.rendermanager;

import de.bluecolored.bluemap.api.debug.DebugDump;
import de.bluecolored.bluemap.core.logger.Logger;

import java.util.*;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;

public class RenderManager {
    private static final AtomicInteger nextRenderManagerIndex = new AtomicInteger(0);

    @DebugDump private final int id;
    @DebugDump private volatile boolean running;

    @DebugDump private long lastTimeBusy;

    private final AtomicInteger nextWorkerThreadIndex;
    @DebugDump private final Collection<WorkerThread> workerThreads;
    private final AtomicInteger busyCount;

    private ProgressTracker progressTracker;
    private volatile boolean newTask;

    @DebugDump private final LinkedList<RenderTask> renderTasks;

    public RenderManager() {
        this.id = nextRenderManagerIndex.getAndIncrement();
        this.nextWorkerThreadIndex = new AtomicInteger(0);

        this.running = false;
        this.workerThreads = new ConcurrentLinkedDeque<>();
        this.busyCount = new AtomicInteger(0);

        this.lastTimeBusy = -1;

        this.progressTracker = null;
        this.newTask = true;

        this.renderTasks = new LinkedList<>();
    }

    public void start(int threadCount) throws IllegalStateException {
        if (threadCount <= 0) throw new IllegalArgumentException("threadCount has to be 1 or more!");

        synchronized (this.workerThreads) {
            if (isRunning()) throw new IllegalStateException("RenderManager is already running!");
            this.workerThreads.clear();
            this.busyCount.set(0);

            if (progressTracker != null) progressTracker.cancel();
            progressTracker = new ProgressTracker(5000, 12); // 5-sec steps over one minute
            this.newTask = true;

            this.running = true;

            for (int i = 0; i < threadCount; i++) {
                WorkerThread worker = new WorkerThread();
                this.workerThreads.add(worker);
                worker.start();
            }
        }
    }

    public void stop() {
        synchronized (this.workerThreads) {
            this.running = false;
            for (WorkerThread worker : workerThreads) worker.interrupt();
            if (progressTracker != null) progressTracker.cancel();
        }
    }

    public boolean isRunning() {
        synchronized (this.workerThreads) {
            for (WorkerThread worker : workerThreads) {
                if (worker.isAlive()) return true;
            }

            return false;
        }
    }

    public void awaitIdle() throws InterruptedException {
        synchronized (this.renderTasks) {
            while (!this.renderTasks.isEmpty())
                this.renderTasks.wait(10000);
        }
    }

    public void awaitShutdown() throws InterruptedException {
        synchronized (this.workerThreads) {
            while (isRunning())
                this.workerThreads.wait(10000);
        }
    }

    public boolean scheduleRenderTask(RenderTask task) {
        synchronized (this.renderTasks) {
            if (containsRenderTask(task)) return false;

            removeTasksThatAreContainedIn(task);
            renderTasks.addLast(task);
            renderTasks.notifyAll();
            return true;
        }
    }

    public int scheduleRenderTasks(RenderTask... tasks) {
        return scheduleRenderTasks(Arrays.asList(tasks));
    }

    public int scheduleRenderTasks(Collection<RenderTask> tasks) {
        synchronized (this.renderTasks) {
            int count = 0;
            for (RenderTask task : tasks) {
                if (scheduleRenderTask(task)) count++;
            }
            return count;
        }
    }

    public boolean scheduleRenderTaskNext(RenderTask task) {
        synchronized (this.renderTasks) {
            if (renderTasks.size() <= 1) return scheduleRenderTask(task);
            if (containsRenderTask(task)) return false;

            removeTasksThatAreContainedIn(task);
            renderTasks.add(1, task);
            renderTasks.notifyAll();
            return true;
        }
    }

    public void reorderRenderTasks(Comparator<RenderTask> taskComparator) {
        synchronized (this.renderTasks) {
            if (renderTasks.size() <= 2) return;

            RenderTask currentTask = renderTasks.removeFirst();
            renderTasks.sort(taskComparator);
            renderTasks.addFirst(currentTask);
        }
    }

    public boolean removeRenderTask(RenderTask task) {
        synchronized (this.renderTasks) {
            if (this.renderTasks.isEmpty()) return false;

            // cancel the task if it is currently processed
            RenderTask first = renderTasks.getFirst();
            if (first.equals(task)) {
                first.cancel();
                return true;
            }

            // else remove it
            return renderTasks.remove(task);
        }
    }

    public void removeRenderTasksIf(Predicate<RenderTask> removeCondition) {
        synchronized (this.renderTasks) {
            if (this.renderTasks.isEmpty()) return;

            RenderTask first = renderTasks.removeFirst();
            if (removeCondition.test(first)) first.cancel();
            renderTasks.removeIf(removeCondition);
            renderTasks.addFirst(first);
        }
    }

    public void removeAllRenderTasks() {
        synchronized (this.renderTasks) {
            if (this.renderTasks.isEmpty()) return;

            RenderTask first = renderTasks.removeFirst();
            first.cancel();
            renderTasks.clear();
            renderTasks.addFirst(first);
        }
    }

    public long estimateCurrentRenderTaskTimeRemaining() {
        if (progressTracker == null) return 0;

        synchronized (this.renderTasks) {
            RenderTask task = getCurrentRenderTask();
            if (task == null) return 0;

            double progress = task.estimateProgress();
            long timePerProgress = progressTracker.getAverageTimePerProgress();
            return (long) ((1 - progress) * timePerProgress);
        }
    }

    public RenderTask getCurrentRenderTask() {
        synchronized (this.renderTasks) {
            if (this.renderTasks.isEmpty()) return null;
            return this.renderTasks.getFirst();
        }
    }

    public List<RenderTask> getScheduledRenderTasks() {
        synchronized (this.renderTasks) {
            return new ArrayList<>(this.renderTasks);
        }
    }

    public int getScheduledRenderTaskCount() {
        return this.renderTasks.size();
    }

    public boolean containsRenderTask(RenderTask task) {
        synchronized (this.renderTasks) {
            // checking all scheduled renderTasks except the first one, since that is already being processed
            Iterator<RenderTask> iterator = renderTasks.iterator();
            if (!iterator.hasNext()) return false;
            iterator.next(); // skip first

            while(iterator.hasNext()) {
                if (iterator.next().contains(task)) return true;
            }

            return false;
        }
    }

    public int getWorkerThreadCount() {
        return workerThreads.size();
    }

    public long getLastTimeBusy() {
        return lastTimeBusy;
    }

    private void removeTasksThatAreContainedIn(RenderTask containingTask) {
        synchronized (this.renderTasks) {
            if (renderTasks.size() < 2) return;
            RenderTask first = renderTasks.removeFirst();
            if (containingTask.contains(first)) first.cancel();
            renderTasks.removeIf(containingTask::contains);
            renderTasks.addFirst(first);
        }
    }

    private void doWork() throws Exception {
        RenderTask task;

        synchronized (this.renderTasks) {
            while (this.renderTasks.isEmpty())
                this.renderTasks.wait(10000);

            task = this.renderTasks.getFirst();
            if (this.newTask) {
                this.newTask = false;
                this.progressTracker.resetAndStart(task::estimateProgress);
            }

            // the following is making sure every render-thread is done working on this task (no thread is "busy")
            // before continuing working on the next RenderTask
            if (!task.hasMoreWork()) {
                if (busyCount.get() <= 0) {
                    this.renderTasks.removeFirst();
                    this.renderTasks.notifyAll();

                    this.newTask = true;

                    busyCount.set(0);
                } else {
                    this.renderTasks.wait(10000);
                }

                return;
            }

            this.busyCount.incrementAndGet();
            this.lastTimeBusy = System.currentTimeMillis();
        }

        try {
            task.doWork();
        } finally {
            synchronized (renderTasks) {
                int busyCount = this.busyCount.decrementAndGet();
                if (busyCount > 0) this.lastTimeBusy = System.currentTimeMillis();
                this.renderTasks.notifyAll();
            }
        }
    }

    public class WorkerThread extends Thread {

        private final int id;

        private WorkerThread() {
            this.id = RenderManager.this.nextWorkerThreadIndex.getAndIncrement();
            this.setName("RenderManager-" + RenderManager.this.id + "-" + this.id);
        }

        @Override
        @SuppressWarnings("BusyWait")
        public void run() {
            try {
                while (RenderManager.this.running) {
                    try {
                        RenderManager.this.doWork();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    } catch (Exception e) {
                        Logger.global.logError(
                                "RenderManager(" + RenderManager.this.id + "): WorkerThread(" + this.id +
                                "): Exception while doing some work!", e);

                        try {
                            // on error, wait a few seconds before resurrecting this render-thread
                            // if something goes wrong, this prevents running into the same error on all render-threads
                            // with full-speed over and over again :D
                            Thread.sleep(10000);
                        } catch (InterruptedException ie) {
                            Thread.currentThread().interrupt();
                        }
                    }
                }
            } finally {
                synchronized (RenderManager.this.workerThreads) {
                    RenderManager.this.workerThreads.remove(this);
                    RenderManager.this.workerThreads.notifyAll();
                }
            }
        }

    }

}
