//  Copyright (c) 2007-2013 Hartmut Kaiser
//  Copyright (c) 2013 Agustin Berge
//
//  SPDX-License-Identifier: BSL-1.0
//  Distributed under the Boost Software License, Version 1.0. (See accompanying
//  file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

#pragma once

#include <pika/config.hpp>
#include <pika/coroutines/thread_enums.hpp>
#include <pika/lock_registration/detail/register_locks.hpp>
#include <pika/modules/errors.hpp>
#include <pika/modules/memory.hpp>
#include <pika/synchronization/detail/condition_variable.hpp>
#include <pika/synchronization/mutex.hpp>
#include <pika/synchronization/stop_token.hpp>
#include <pika/thread_support/assert_owns_lock.hpp>
#include <pika/thread_support/unlock_guard.hpp>
#include <pika/timing/steady_clock.hpp>
#include <pika/type_support/unused.hpp>

#include <mutex>
#include <utility>

///////////////////////////////////////////////////////////////////////////////
namespace pika {
    enum class cv_status
    {
        no_timeout,
        timeout,
        error
    };

    class condition_variable
    {
    private:
        using mutex_type = detail::condition_variable_data::mutex_type;
        using data_type = pika::memory::intrusive_ptr<detail::condition_variable_data>;

    public:
        condition_variable()
          : data_(data_type(new detail::condition_variable_data, false))
        {
        }

        // Preconditions: There is no thread blocked on *this. [Note: That is,
        //      all threads have been notified; they could subsequently block
        //      on the lock specified in the wait.This relaxes the usual rules,
        //      which would have required all wait calls to happen before
        //      destruction.Only the notification to unblock the wait needs to
        //      happen before destruction.The user should take care to ensure
        //      that no threads wait on *this once the destructor has been
        //      started, especially when the waiting threads are calling the
        //      wait functions in a loop or using the overloads of wait,
        //      wait_for, or wait_until that take a predicate. end note]
        //
        // IOW, ~condition_variable() can execute before a signaled thread
        // returns from a wait. If this happens with condition_variable, that
        // waiting thread will attempt to lock the destructed mutex.
        // To fix this, there must be shared ownership of the data members
        // between the condition_variable_any object and the member functions
        // wait (wait_for, etc.).
        ~condition_variable() = default;

        void notify_one(error_code& ec = throws)
        {
            std::unique_lock<mutex_type> l(data_->mtx_);
            data_->cond_.notify_one(PIKA_MOVE(l), ec);
        }

        void notify_all(error_code& ec = throws)
        {
            std::unique_lock<mutex_type> l(data_->mtx_);
            data_->cond_.notify_all(PIKA_MOVE(l), ec);
        }

        template <typename Mutex>
        void wait(std::unique_lock<Mutex>& lock, error_code& ec = throws)
        {
            PIKA_ASSERT_OWNS_LOCK(lock);

            auto data = data_;    // keep data alive

            [[maybe_unused]] util::ignore_all_while_checking ignore_lock;

            std::unique_lock<mutex_type> l(data->mtx_);
            ::pika::detail::unlock_guard<std::unique_lock<Mutex>> unlock(lock);

            // The following ensures that the inner lock will be unlocked
            // before the outer to avoid deadlock (fixes issue #3608)
            std::lock_guard<std::unique_lock<mutex_type>> unlock_next(l, std::adopt_lock);

            data->cond_.wait(l, ec);
        }

        template <typename Mutex, typename Predicate>
        void wait(std::unique_lock<Mutex>& lock, Predicate pred, error_code& /*ec*/ = throws)
        {
            PIKA_ASSERT_OWNS_LOCK(lock);

            while (!pred()) { wait(lock); }
        }

        template <typename Mutex>
        cv_status wait_until(std::unique_lock<Mutex>& lock,
            pika::chrono::steady_time_point const& abs_time, error_code& ec = throws)
        {
            PIKA_ASSERT_OWNS_LOCK(lock);

            auto data = data_;    // keep data alive

            [[maybe_unused]] util::ignore_all_while_checking ignore_lock;

            std::unique_lock<mutex_type> l(data->mtx_);
            ::pika::detail::unlock_guard<std::unique_lock<Mutex>> unlock(lock);

            // The following ensures that the inner lock will be unlocked
            // before the outer to avoid deadlock (fixes issue #3608)
            std::lock_guard<std::unique_lock<mutex_type>> unlock_next(l, std::adopt_lock);

            threads::detail::thread_restart_state const reason =
                data->cond_.wait_until(l, abs_time, ec);

            if (ec) return cv_status::error;

            // if the timer has hit, the waiting period timed out
            return (reason == threads::detail::thread_restart_state::timeout) ?    //-V110
                cv_status::timeout :
                cv_status::no_timeout;
        }

        template <typename Mutex, typename Predicate>
        bool
        wait_until(std::unique_lock<Mutex>& lock, pika::chrono::steady_time_point const& abs_time,
            Predicate pred, error_code& ec = throws)
        {
            PIKA_ASSERT_OWNS_LOCK(lock);

            while (!pred())
            {
                if (wait_until(lock, abs_time, ec) == cv_status::timeout) return pred();
            }
            return true;
        }

        template <typename Mutex>
        cv_status wait_for(std::unique_lock<Mutex>& lock,
            pika::chrono::steady_duration const& rel_time, error_code& ec = throws)
        {
            return wait_until(lock, rel_time.from_now(), ec);
        }

        template <typename Mutex, typename Predicate>
        bool wait_for(std::unique_lock<Mutex>& lock, pika::chrono::steady_duration const& rel_time,
            Predicate pred, error_code& ec = throws)
        {
            return wait_until(lock, rel_time.from_now(), PIKA_MOVE(pred), ec);
        }

    private:
        pika::concurrency::detail::cache_aligned_data_derived<data_type> data_;
    };

    ///////////////////////////////////////////////////////////////////////////
    class condition_variable_any
    {
    private:
        using mutex_type = detail::condition_variable_data::mutex_type;
        using data_type = pika::memory::intrusive_ptr<detail::condition_variable_data>;

    public:
        condition_variable_any()
          : data_(data_type(new detail::condition_variable_data, false))
        {
        }

        // Preconditions: There is no thread blocked on *this. [Note: That is,
        //      all threads have been notified; they could subsequently block
        //      on the lock specified in the wait.This relaxes the usual rules,
        //      which would have required all wait calls to happen before
        //      destruction.Only the notification to unblock the wait needs to
        //      happen before destruction.The user should take care to ensure
        //      that no threads wait on *this once the destructor has been
        //      started, especially when the waiting threads are calling the
        //      wait functions in a loop or using the overloads of wait,
        //      wait_for, or wait_until that take a predicate. end note]
        //
        // IOW, ~condition_variable_any() can execute before a signaled thread
        // returns from a wait. If this happens with condition_variable, that
        // waiting thread will attempt to lock the destructed mutex.
        // To fix this, there must be shared ownership of the data members
        // between the condition_variable_any object and the member functions
        // wait (wait_for, etc.).
        ~condition_variable_any() = default;

        void notify_one(error_code& ec = throws)
        {
            std::unique_lock<mutex_type> l(data_->mtx_);
            data_->cond_.notify_one(PIKA_MOVE(l), ec);
        }

        void notify_all(error_code& ec = throws)
        {
            std::unique_lock<mutex_type> l(data_->mtx_);
            data_->cond_.notify_all(PIKA_MOVE(l), ec);
        }

        template <typename Lock>
        void wait(Lock& lock, error_code& ec = throws)
        {
            PIKA_ASSERT_OWNS_LOCK(lock);

            auto data = data_;    // keep data alive

            [[maybe_unused]] util::ignore_all_while_checking ignore_lock;

            std::unique_lock<mutex_type> l(data->mtx_);
            ::pika::detail::unlock_guard<Lock> unlock(lock);

            // The following ensures that the inner lock will be unlocked
            // before the outer to avoid deadlock (fixes issue #3608)
            std::lock_guard<std::unique_lock<mutex_type>> unlock_next(l, std::adopt_lock);

            data->cond_.wait(l, ec);
        }

        template <typename Lock, typename Predicate>
        void wait(Lock& lock, Predicate pred, error_code& /* ec */ = throws)
        {
            PIKA_ASSERT_OWNS_LOCK(lock);

            while (!pred()) { wait(lock); }
        }

        template <typename Lock>
        cv_status wait_until(
            Lock& lock, pika::chrono::steady_time_point const& abs_time, error_code& ec = throws)
        {
            PIKA_ASSERT_OWNS_LOCK(lock);

            auto data = data_;    // keep data alive

            [[maybe_unused]] util::ignore_all_while_checking ignore_lock;

            std::unique_lock<mutex_type> l(data->mtx_);
            ::pika::detail::unlock_guard<Lock> unlock(lock);

            // The following ensures that the inner lock will be unlocked
            // before the outer to avoid deadlock (fixes issue #3608)
            std::lock_guard<std::unique_lock<mutex_type>> unlock_next(l, std::adopt_lock);

            threads::detail::thread_restart_state const reason =
                data->cond_.wait_until(l, abs_time, ec);

            if (ec) return cv_status::error;

            // if the timer has hit, the waiting period timed out
            return (reason == threads::detail::thread_restart_state::timeout) ?    //-V110
                cv_status::timeout :
                cv_status::no_timeout;
        }

        template <typename Lock, typename Predicate>
        bool wait_until(Lock& lock, pika::chrono::steady_time_point const& abs_time, Predicate pred,
            error_code& ec = throws)
        {
            PIKA_ASSERT_OWNS_LOCK(lock);

            while (!pred())
            {
                if (wait_until(lock, abs_time, ec) == cv_status::timeout) return pred();
            }
            return true;
        }

        template <typename Lock>
        cv_status
        wait_for(Lock& lock, pika::chrono::steady_duration const& rel_time, error_code& ec = throws)
        {
            return wait_until(lock, rel_time.from_now(), ec);
        }

        template <typename Lock, typename Predicate>
        bool wait_for(Lock& lock, pika::chrono::steady_duration const& rel_time, Predicate pred,
            error_code& ec = throws)
        {
            return wait_until(lock, rel_time.from_now(), PIKA_MOVE(pred), ec);
        }

        // 32.6.4.2, interruptible waits
        template <typename Lock, typename Predicate>
        bool wait(Lock& lock, stop_token stoken, Predicate pred, error_code& ec = throws)
        {
            if (stoken.stop_requested()) { return pred(); }

            auto data = data_;    // keep data alive

            auto f = [&data, &ec] {
                std::unique_lock<mutex_type> l(data->mtx_);
                data->cond_.notify_all(PIKA_MOVE(l), ec);
            };
            stop_callback<decltype(f)> cb(stoken, PIKA_MOVE(f));

            while (!pred())
            {
                [[maybe_unused]] util::ignore_all_while_checking ignore_lock;

                std::unique_lock<mutex_type> l(data->mtx_);
                if (stoken.stop_requested())
                {
                    // pred() has already evaluated to false since we last
                    // a acquired lock
                    return false;
                }

                ::pika::detail::unlock_guard<Lock> unlock(lock);

                // The following ensures that the inner lock will be unlocked
                // before the outer to avoid deadlock (fixes issue #3608)
                std::lock_guard<std::unique_lock<mutex_type>> unlock_next(l, std::adopt_lock);

                data->cond_.wait(l, ec);
            }

            return true;
        }

        template <typename Lock, typename Predicate>
        bool
        wait_until(Lock& lock, stop_token stoken, pika::chrono::steady_time_point const& abs_time,
            Predicate pred, error_code& ec = throws)
        {
            if (stoken.stop_requested()) { return pred(); }

            auto data = data_;    // keep data alive

            auto f = [&data, &ec] {
                std::unique_lock<mutex_type> l(data->mtx_);
                data->cond_.notify_all(PIKA_MOVE(l), ec);
            };
            stop_callback<decltype(f)> cb(stoken, PIKA_MOVE(f));

            while (!pred())
            {
                bool should_stop;
                {
                    [[maybe_unused]] util::ignore_all_while_checking ignore_lock;

                    std::unique_lock<mutex_type> l(data->mtx_);
                    if (stoken.stop_requested())
                    {
                        // pred() has already evaluated to false since we last
                        // acquired lock.
                        return false;
                    }

                    ::pika::detail::unlock_guard<Lock> unlock(lock);

                    // The following ensures that the inner lock will be unlocked
                    // before the outer to avoid deadlock (fixes issue #3608)
                    std::lock_guard<std::unique_lock<mutex_type>> unlock_next(l, std::adopt_lock);

                    threads::detail::thread_restart_state const reason =
                        data->cond_.wait_until(l, abs_time, ec);

                    if (ec) return false;

                    should_stop = (reason == threads::detail::thread_restart_state::timeout) ||
                        stoken.stop_requested();
                }

                if (should_stop) { return pred(); }
            }
            return true;
        }

        template <typename Lock, typename Predicate>
        bool wait_for(Lock& lock, stop_token stoken, pika::chrono::steady_duration const& rel_time,
            Predicate pred, error_code& ec = throws)
        {
            return wait_until(lock, stoken, rel_time.from_now(), PIKA_MOVE(pred), ec);
        }

    private:
        pika::concurrency::detail::cache_aligned_data_derived<data_type> data_;
    };
}    // namespace pika
