#include "threadpool.h"

/*
 * This is the idle loop each thread will be running until a job is ready
 * for execution
 */
void
global::ThreadPool::ThreadLoop()
{
        while (true)
        {
                std::function<void()> job;
                {
                        std::unique_lock<std::mutex> lock(this->queue_mutex);

                        /*
                         * This is equivalent to :
                         *
                         *		while (!this->jobs.empty() || should_terminate)
                         *			mutex_condition.wait(lock);
                         *
                         * we are essentially waiting for a job to be queued up or the terminate
                         *flag to be set. Another piece of useful information is that the predicate
                         *is checked under the lock as the precondition for .wait() is that the
                         *calling thread owns the lock.
                         *
                         * Now, when .wait() is run, the lock is unlocked the the executing thread
                         *is blocked and is added to a list of threads current waiting on the
                         *predicate. In our case whether there are new jobs available for the
                         *terminate flag is set. Once the condition variables are true i.e there are
                         *new jobs or we are terminating, the lock is reacquired by the thread and
                         *the thread is unblocked.
                         */
                        mutex_condition.wait(
                            lock, [this] { return !this->jobs.empty() || this->should_terminate; });

                        if (this->should_terminate)
                                return;

                        /* get the first job in the queue*/
                        job = jobs.front();
                        jobs.pop();
                }
                /* run the job */
                job();
        }
}

global::ThreadPool::ThreadPool(int ThreadCount)
{
        this->thread_count     = ThreadCount;
        this->should_terminate = false;

        /* Initiate our threads and store them in our threads vector */
        for (int i = 0; i < this->thread_count; i++)
        {
                this->threads.emplace_back(std::thread(&ThreadPool::ThreadLoop, this));
        }
}

void
global::ThreadPool::QueueJob(const std::function<void()>& job)
{
        /* push a job into our job queue safely by holding our queue lock */
        std::unique_lock<std::mutex> lock(this->queue_mutex);

        this->jobs.push(job);
        lock.unlock();

        mutex_condition.notify_one();
}

void
global::ThreadPool::Stop()
{
        /* safely set our termination flag to true */
        std::unique_lock<std::mutex> lock(this->queue_mutex);
        should_terminate = true;
        lock.unlock();

        /* unlock all threads waiting on our condition */
        mutex_condition.notify_all();

        /* join the threads and clear our threads vector */
        for (std::thread& thread : threads)
        {
                thread.join();
        }
        threads.clear();
}

bool
global::ThreadPool::Busy()
{
        /* allows us to wait for when the job queue is empty allowing us to safely call the
         * destructor */
        std::unique_lock<std::mutex> lock(this->queue_mutex);

        bool pool_busy = !jobs.empty();
        this->queue_mutex.unlock();

        return pool_busy;
}