#ifndef TILE_BASE_THREAD_COND_VAR_H
#define TILE_BASE_THREAD_COND_VAR_H

#pragma once
#include "tile/base/internal/logging.h"
#include "tile/base/internal/meta.h"

#include <chrono>
#include <memory>

namespace tile {
class Mutex;
class ScopedLock;
template<typename M, typename... Args>
class UniqueLock;

namespace detail {
namespace cond_var {

class CondVarProxy {
public:
    CondVarProxy();
    ~CondVarProxy();
    void Wait(Mutex *mutex);
    bool WaitFor(Mutex *mutex, std::size_t nanoseconds);
    void NotifyAll();
    void NotifyOne();

private:
    class Impl;
    std::unique_ptr<Impl> impl_;
};

class CondVar {
public:
    CondVar()  = default;
    ~CondVar() = default;

    template<typename LockType>
    void Wait(LockType &lock)
    {
        proxy_.Wait(GetMutex(&lock));
    }

    template<typename LockType, typename Predicate>
    void Wait(LockType &lock, Predicate pred)
    {
        while (!pred()) { Wait(lock); }
    }

    template<typename LockType, typename Rep, typename Period>
    bool WaitFor(LockType &lock, const std::chrono::duration<Rep, Period> &timeout_duration)
    {
        return proxy_.WaitFor(GetMutex(&lock),
                              std::chrono::duration_cast<std::chrono::nanoseconds>(timeout_duration).count());
    }

    template<typename LockType, typename Rep, typename Period, typename Predicate>
    bool WaitFor(LockType &lock, const std::chrono::duration<Rep, Period> &timeout_duration, Predicate pred)
    {
        if (!pred()) { WaitFor(lock, timeout_duration); }
        return pred();
    }

    template<typename LockType, typename Clock, typename Duration>
    bool WaitUntil(LockType &lock, const std::chrono::time_point<Clock, Duration> &abs_time)
    {
        // TODO: use Clock
        auto duration = abs_time - Clock::now();
        if (duration.count() <= 0) {
            // timeout
            return false;
        }

        return WaitFor(lock, duration);
    }

    template<typename LockType, typename Clock, typename Duration, typename Predicate>
    bool WaitUntil(LockType &lock, const std::chrono::time_point<Clock, Duration> &abs_time, Predicate pred)
    {
        if (!pred()) { WaitUntil(lock, abs_time); }
        return pred();
    }

    void NotifyOne() { proxy_.NotifyOne(); }

    void NotifyAll() { proxy_.NotifyAll(); }

private:
    template<typename C>
    struct has_get_mutex_fn {
        template<typename U = C,
                 typename   = enable_if_t<std::is_same<decltype(std::declval<U>().GetMutex()), Mutex *>::value>>
        static int Test(int);

        template<typename>
        static char Test(...);

        static constexpr bool value = std::is_same<decltype(Test<C>(0)), int>::value;
    };

    template<typename C>
    enable_if_t<has_get_mutex_fn<C>::value, Mutex *> GetMutex(C *c)
    {
        return c->GetMutex();
    }

    template<typename C>
    enable_if_t<!has_get_mutex_fn<C>::value, Mutex *> GetMutex(C *c)
    {
        static_assert(sizeof(C) == 0, "C does not have `Mutex* GetMutex()` method");
        return nullptr;
    }

private:
    friend class ScopedLock;
    CondVarProxy proxy_;
};
}// namespace cond_var
}// namespace detail

using CondVar = ::tile::detail::cond_var::CondVar;
}// namespace tile

#endif// TILE_BASE_THREAD_COND_VAR_H
