// (C) Copyright 2006-7 Anthony Williams
//  Copyright (c) 2015-2023 Hartmut Kaiser
//
//  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)

#include <hpx/future.hpp>
#include <hpx/init.hpp>
#include <hpx/shared_mutex.hpp>
#include <hpx/thread.hpp>

#include <hpx/modules/testing.hpp>

#include <chrono>
#include <mutex>
#include <string>
#include <vector>

#include "shared_mutex_locking_thread.hpp"
#include "thread_group.hpp"

#define CHECK_LOCKED_VALUE_EQUAL(mutex_name, value, expected_value)            \
    {                                                                          \
        std::unique_lock<hpx::mutex> lock(mutex_name);                         \
        HPX_TEST_EQ(value, expected_value);                                    \
    }

void test_only_one_upgrade_lock_permitted()
{
    using shared_mutex_type = hpx::shared_mutex;
    using mutex_type = hpx::mutex;

    constexpr unsigned number_of_threads = 2;

    test::thread_group pool;

    unsigned unblocked_count = 0;
    unsigned max_simultaneous_running = 0;
    mutex_type unblocked_count_mutex;
    mutex_type finish_mutex;
    std::unique_lock<mutex_type> finish_lock(finish_mutex);

    try
    {
        shared_mutex_type rw_mutex;
        unsigned simultaneous_running_count = 0;
        hpx::condition_variable unblocked_condition;

        for (unsigned i = 0; i != number_of_threads; ++i)
        {
            pool.create_thread(
                test::locking_thread<hpx::upgrade_lock<shared_mutex_type>>(
                    rw_mutex, unblocked_count, unblocked_count_mutex,
                    unblocked_condition, finish_mutex,
                    simultaneous_running_count, max_simultaneous_running));
        }

        hpx::this_thread::sleep_for(std::chrono::seconds(1));

        CHECK_LOCKED_VALUE_EQUAL(unblocked_count_mutex, unblocked_count, 1u);

        finish_lock.unlock();
        pool.join_all();
    }
    catch (...)
    {
        pool.interrupt_all();
        pool.join_all();
        HPX_TEST(false);
    }

    CHECK_LOCKED_VALUE_EQUAL(
        unblocked_count_mutex, unblocked_count, number_of_threads);
    CHECK_LOCKED_VALUE_EQUAL(
        unblocked_count_mutex, max_simultaneous_running, 1u);
}

void test_can_lock_upgrade_if_currently_locked_shared()
{
    using shared_mutex_type = hpx::shared_mutex;
    using mutex_type = hpx::mutex;

    test::thread_group pool;

    unsigned unblocked_count = 0;
    unsigned max_simultaneous_running = 0;
    mutex_type unblocked_count_mutex;
    mutex_type finish_mutex;
    std::unique_lock<mutex_type> finish_lock(finish_mutex);

    constexpr unsigned reader_count = 10;

    try
    {
        shared_mutex_type rw_mutex;
        unsigned simultaneous_running_count = 0;
        hpx::condition_variable unblocked_condition;

        for (unsigned i = 0; i != reader_count; ++i)
        {
            pool.create_thread(
                test::locking_thread<std::shared_lock<shared_mutex_type>>(
                    rw_mutex, unblocked_count, unblocked_count_mutex,
                    unblocked_condition, finish_mutex,
                    simultaneous_running_count, max_simultaneous_running));
        }

        hpx::this_thread::sleep_for(std::chrono::seconds(1));

        pool.create_thread(
            test::locking_thread<hpx::upgrade_lock<shared_mutex_type>>(rw_mutex,
                unblocked_count, unblocked_count_mutex, unblocked_condition,
                finish_mutex, simultaneous_running_count,
                max_simultaneous_running));

        {
            std::unique_lock<mutex_type> lk(unblocked_count_mutex);
            // NOLINTNEXTLINE(bugprone-infinite-loop)
            while (unblocked_count < (reader_count + 1))
            {
                unblocked_condition.wait(lk);
            }
        }

        CHECK_LOCKED_VALUE_EQUAL(
            unblocked_count_mutex, unblocked_count, reader_count + 1);

        finish_lock.unlock();
        pool.join_all();
    }
    catch (...)
    {
        pool.interrupt_all();
        pool.join_all();
        HPX_TEST(false);
    }

    CHECK_LOCKED_VALUE_EQUAL(
        unblocked_count_mutex, unblocked_count, reader_count + 1);
    CHECK_LOCKED_VALUE_EQUAL(
        unblocked_count_mutex, max_simultaneous_running, reader_count + 1);
}

void test_can_lock_upgrade_to_unique_if_currently_locked_upgrade()
{
    using shared_mutex_type = hpx::shared_mutex;

    shared_mutex_type mtx;
    hpx::upgrade_lock<shared_mutex_type> l(mtx);
    hpx::upgrade_to_unique_lock<shared_mutex_type> const ul(l);
    HPX_TEST(ul.owns_lock());
}

void test_if_other_thread_has_write_lock_try_lock_shared_returns_false()
{
    using shared_mutex_type = hpx::shared_mutex;
    using mutex_type = hpx::mutex;

    shared_mutex_type rw_mutex;
    mutex_type finish_mutex;
    mutex_type unblocked_mutex;
    unsigned unblocked_count = 0;
    std::unique_lock<mutex_type> finish_lock(finish_mutex);
    hpx::thread writer(test::simple_writing_thread(
        rw_mutex, finish_mutex, unblocked_mutex, unblocked_count));

    hpx::this_thread::sleep_for(std::chrono::seconds(1));

    CHECK_LOCKED_VALUE_EQUAL(unblocked_mutex, unblocked_count, 1u);

    bool const try_succeeded = rw_mutex.try_lock_shared();
    HPX_TEST(!try_succeeded);
    if (try_succeeded)
    {
        rw_mutex.unlock_shared();
    }

    finish_lock.unlock();
    writer.join();
}

void test_if_other_thread_has_write_lock_try_lock_upgrade_returns_false()
{
    using shared_mutex_type = hpx::shared_mutex;
    using mutex_type = hpx::mutex;

    shared_mutex_type rw_mutex;
    mutex_type finish_mutex;
    mutex_type unblocked_mutex;
    unsigned unblocked_count = 0;
    std::unique_lock<mutex_type> finish_lock(finish_mutex);
    hpx::thread writer(test::simple_writing_thread(
        rw_mutex, finish_mutex, unblocked_mutex, unblocked_count));

    hpx::this_thread::sleep_for(std::chrono::seconds(1));

    CHECK_LOCKED_VALUE_EQUAL(unblocked_mutex, unblocked_count, 1u);

    bool const try_succeeded = rw_mutex.try_lock_upgrade();
    HPX_TEST(!try_succeeded);
    if (try_succeeded)
    {
        rw_mutex.unlock_upgrade();
    }

    finish_lock.unlock();
    writer.join();
}

void test_if_no_thread_has_lock_try_lock_shared_returns_true()
{
    using shared_mutex_type = hpx::shared_mutex;

    shared_mutex_type rw_mutex;
    bool const try_succeeded = rw_mutex.try_lock_shared();
    HPX_TEST(try_succeeded);
    if (try_succeeded)
    {
        rw_mutex.unlock_shared();
    }
}

void test_if_no_thread_has_lock_try_lock_upgrade_returns_true()
{
    using shared_mutex_type = hpx::shared_mutex;

    shared_mutex_type rw_mutex;
    bool const try_succeeded = rw_mutex.try_lock_upgrade();
    HPX_TEST(try_succeeded);
    if (try_succeeded)
    {
        rw_mutex.unlock_upgrade();
    }
}

void test_if_other_thread_has_shared_lock_try_lock_shared_returns_true()
{
    using shared_mutex_type = hpx::shared_mutex;
    using mutex_type = hpx::mutex;

    shared_mutex_type rw_mutex;
    mutex_type finish_mutex;
    mutex_type unblocked_mutex;
    unsigned unblocked_count = 0;
    std::unique_lock<mutex_type> finish_lock(finish_mutex);
    hpx::thread writer(test::simple_reading_thread(
        rw_mutex, finish_mutex, unblocked_mutex, unblocked_count));

    hpx::this_thread::sleep_for(std::chrono::seconds(1));

    CHECK_LOCKED_VALUE_EQUAL(unblocked_mutex, unblocked_count, 1u);

    bool const try_succeeded = rw_mutex.try_lock_shared();
    HPX_TEST(try_succeeded);
    if (try_succeeded)
    {
        rw_mutex.unlock_shared();
    }

    finish_lock.unlock();
    writer.join();
}

void test_if_other_thread_has_shared_lock_try_lock_upgrade_returns_true()
{
    using shared_mutex_type = hpx::shared_mutex;
    using mutex_type = hpx::mutex;

    shared_mutex_type rw_mutex;
    mutex_type finish_mutex;
    mutex_type unblocked_mutex;
    unsigned unblocked_count = 0;
    std::unique_lock<mutex_type> finish_lock(finish_mutex);
    hpx::thread writer(test::simple_reading_thread(
        rw_mutex, finish_mutex, unblocked_mutex, unblocked_count));

    hpx::this_thread::sleep_for(std::chrono::seconds(1));

    CHECK_LOCKED_VALUE_EQUAL(unblocked_mutex, unblocked_count, 1u);

    bool const try_succeeded = rw_mutex.try_lock_upgrade();
    HPX_TEST(try_succeeded);
    if (try_succeeded)
    {
        rw_mutex.unlock_upgrade();
    }

    finish_lock.unlock();
    writer.join();
}

///////////////////////////////////////////////////////////////////////////////
int hpx_main()
{
    test_only_one_upgrade_lock_permitted();
    test_can_lock_upgrade_if_currently_locked_shared();
    test_can_lock_upgrade_to_unique_if_currently_locked_upgrade();
    test_if_other_thread_has_write_lock_try_lock_shared_returns_false();
    test_if_other_thread_has_write_lock_try_lock_upgrade_returns_false();
    test_if_no_thread_has_lock_try_lock_shared_returns_true();
    test_if_no_thread_has_lock_try_lock_upgrade_returns_true();
    test_if_other_thread_has_shared_lock_try_lock_shared_returns_true();
    test_if_other_thread_has_shared_lock_try_lock_upgrade_returns_true();

    return hpx::local::finalize();
}

int main(int argc, char* argv[])
{
    // By default, this test should run on all available cores
    std::vector<std::string> const cfg = {"hpx.os_threads=all"};

    // Initialize and run HPX
    hpx::local::init_params init_args;
    init_args.cfg = cfg;
    HPX_TEST_EQ_MSG(hpx::local::init(hpx_main, argc, argv, init_args), 0,
        "HPX main exited with non-zero status");

    return hpx::util::report_errors();
}
