#pragma once

// Adapted from https://github.com/highpower/xiva/blob/master/include/details/threaded_queue.hpp
#include <maps/libs/common/include/exception.h>

#include <condition_variable>
#include <list>
#include <thread>
#include <mutex>
#include <limits>

namespace maps::wiki {

template<typename Item>
class ThreadedQueue {
public:
    explicit ThreadedQueue(size_t threshold = std::numeric_limits<size_t>::max());
    virtual ~ThreadedQueue() = default;

    ThreadedQueue(const ThreadedQueue&) = delete;
    ThreadedQueue& operator=(const ThreadedQueue&) = delete;


    bool pop(Item& item);
    bool push(Item item);

    template<typename... Args>
    bool emplace(Args&&... args);

    void finish();

    bool finished() const;

    using RawContainer = std::list<Item>;

    void popAll(RawContainer& data);
    bool pushAll(RawContainer data);

    size_t pushedItemsCount() const;
    size_t poppedItemsCount() const;
    size_t pendingItemsCount() const;

private:
    bool finished_{false};
    mutable std::mutex mutex_;
    std::condition_variable emptyCondition_;
    std::condition_variable overflowCondition_;
    RawContainer items_;
    size_t threshold_;
    size_t pushedItemsCount_{0};
    size_t poppedItemsCount_{0};
};

template<typename Item>
inline ThreadedQueue<Item>::ThreadedQueue(size_t threshold)
    : threshold_(threshold)
{ }

template<typename Item>
inline bool ThreadedQueue<Item>::pop(Item& item)
{
    std::unique_lock<std::mutex> lock(mutex_);
    while (!finished_ && items_.empty()) {
        emptyCondition_.wait(lock);
    }
    if (items_.empty()) {
        return false;
    }

    std::swap(items_.front(), item);
    items_.pop_front();
    ++poppedItemsCount_;
    overflowCondition_.notify_one();
    return true;
}

template<typename Item>
inline void ThreadedQueue<Item>::popAll(ThreadedQueue<Item>::RawContainer& data)
{
    data.clear();
    std::unique_lock<std::mutex> lock(mutex_);
    while (!finished_ && items_.empty()) {
        emptyCondition_.wait(lock);
    }

    items_.swap(data);
    poppedItemsCount_ = pushedItemsCount_;
    overflowCondition_.notify_all();
}

template<typename Item>
inline bool ThreadedQueue<Item>::push(Item item)
{
    RawContainer buf;
    buf.push_back(std::move(item));
    return pushAll(std::move(buf));
}

template<typename Item>
template<typename... Args>
bool ThreadedQueue<Item>::emplace(Args&&... args)
{
    RawContainer buf;
    buf.emplace_back(std::forward<Args>(args)...);
    return pushAll(std::move(buf));
}

template<typename Item>
inline bool ThreadedQueue<Item>::pushAll(RawContainer data)
{
    if (data.empty()) {
        return true;
    }

    std::unique_lock<std::mutex> lock(mutex_);
    while(!finished_ && (pushedItemsCount_ - poppedItemsCount_) >= threshold_) {
        overflowCondition_.wait(lock);
    }
    if (finished_) {
        return false;
    }
    pushedItemsCount_ += data.size();
    items_.splice(std::end(items_), data);
    emptyCondition_.notify_all();

    return true;
}

template<typename Item>
inline void ThreadedQueue<Item>::finish()
{
    std::lock_guard<std::mutex> lock(mutex_);
    finished_ = true;
    emptyCondition_.notify_all();
    overflowCondition_.notify_all();
}

template<typename Item>
inline bool ThreadedQueue<Item>::finished() const
{
    std::lock_guard<std::mutex> lock(mutex_);
    return finished_;
}

template<typename Item>
inline size_t ThreadedQueue<Item>::pushedItemsCount() const
{
    std::lock_guard<std::mutex> lock(mutex_);
    return pushedItemsCount_;
}

template<typename Item>
inline size_t ThreadedQueue<Item>::poppedItemsCount() const
{
    std::lock_guard<std::mutex> lock(mutex_);
    return poppedItemsCount_;
}

template<typename Item>
inline size_t ThreadedQueue<Item>::pendingItemsCount() const
{
    std::lock_guard<std::mutex> lock(mutex_);
    return pushedItemsCount_ - poppedItemsCount_;
}

} // namespace maps::wiki
