#pragma once

#include <queue>
#include <mutex>
#include <condition_variable>
#include <Tracy.hpp> // NOLINT
#include <functional>

namespace data {
    /// Thread safe queue
    template <class T>
    class safe_queue {
    private:
        std::queue<T> queue;
        TracyLockableN(std::mutex, mutex, "Queue");
        std::condition_variable_any cv;

    public:
        std::pair<std::function<bool(T&)>, std::unique_lock<LockableBase(std::mutex)>> extractor() {
            return std::make_pair([&](T& out) {
                if (queue.empty())
                    return false;

                out = queue.front();
                queue.pop();
                return true;
            }, std::unique_lock<LockableBase(std::mutex)>(mutex));
        }

        void push(const T& in) {
            std::unique_lock<LockableBase(std::mutex)> lock(mutex);
            queue.push(in);
            cv.notify_one();
        }

        template <typename... _Args>
        void emplace(_Args &&... __args) {
            std::unique_lock<LockableBase(std::mutex)> lock(mutex);
            queue.emplace(std::forward<_Args>(__args)...);
            cv.notify_one();
        }

        bool pop(T& out) {
            std::unique_lock<LockableBase(std::mutex)> lock(mutex);
            if (queue.empty())
                return false;

            out = queue.front();
            queue.pop();
            return true;
        }

        bool empty() {
            std::unique_lock<LockableBase(std::mutex)> lock(mutex);
            return queue.empty();
        }

        size_t size() {
            std::unique_lock<LockableBase(std::mutex)> lock(mutex);
            return queue.size();
        }

        void notify_all() {
            cv.notify_all();
        }
        void notify_one() {
            cv.notify_one();
        }

        void wait() {
            std::unique_lock<LockableBase(std::mutex)> lock(mutex);
            if(queue.empty())
                cv.wait(lock);
        }

    };
}
