#include <pybind11/critical_section.h>

#include "pybind11_tests.h"

#include <atomic>
#include <chrono>
#include <thread>
#include <utility>

#if defined(PYBIND11_HAS_STD_BARRIER)
#    include <barrier>
#endif

namespace test_scoped_critical_section_ns {

void test_one_nullptr() { py::scoped_critical_section lock{py::handle{}}; }

void test_two_nullptrs() { py::scoped_critical_section lock{py::handle{}, py::handle{}}; }

void test_first_nullptr() {
    py::dict d;
    py::scoped_critical_section lock{py::handle{}, d};
}

void test_second_nullptr() {
    py::dict d;
    py::scoped_critical_section lock{d, py::handle{}};
}

// Referenced test implementation: https://github.com/PyO3/pyo3/blob/v0.25.0/src/sync.rs#L874
class BoolWrapper {
public:
    explicit BoolWrapper(bool value) : value_{value} {}
    bool get() const { return value_.load(std::memory_order_acquire); }
    void set(bool value) { value_.store(value, std::memory_order_release); }

private:
    std::atomic_bool value_{false};
};

#if defined(PYBIND11_HAS_STD_BARRIER)

// Modifying the C/C++ members of a Python object from multiple threads requires a critical section
// to ensure thread safety and data integrity.
// These tests use a scoped critical section to ensure that the Python object is accessed in a
// thread-safe manner.

void test_scoped_critical_section(const py::handle &cls) {
    auto barrier = std::barrier(2);
    auto bool_wrapper = cls(false);
    bool output = false;

    {
        // Release the GIL to allow run threads in parallel.
        py::gil_scoped_release gil_release{};

        std::thread t1([&]() {
            // Use gil_scoped_acquire to ensure we have a valid Python thread state
            // before entering the critical section. Otherwise, the critical section
            // will cause a segmentation fault.
            py::gil_scoped_acquire ensure_tstate{};
            // Enter the critical section with the same object as the second thread.
            py::scoped_critical_section lock{bool_wrapper};
            // At this point, the object is locked by this thread via the scoped_critical_section.
            // This barrier will ensure that the second thread waits until this thread has released
            // the critical section before proceeding.
            barrier.arrive_and_wait();
            // Sleep for a short time to simulate some work in the critical section.
            // This sleep is necessary to test the locking mechanism properly.
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
            auto *bw = bool_wrapper.cast<BoolWrapper *>();
            bw->set(true);
        });

        std::thread t2([&]() {
            // This thread will wait until the first thread has entered the critical section due to
            // the barrier.
            barrier.arrive_and_wait();
            {
                // Use gil_scoped_acquire to ensure we have a valid Python thread state
                // before entering the critical section. Otherwise, the critical section
                // will cause a segmentation fault.
                py::gil_scoped_acquire ensure_tstate{};
                // Enter the critical section with the same object as the first thread.
                py::scoped_critical_section lock{bool_wrapper};
                // At this point, the critical section is released by the first thread, the value
                // is set to true.
                auto *bw = bool_wrapper.cast<BoolWrapper *>();
                output = bw->get();
            }
        });

        t1.join();
        t2.join();
    }

    if (!output) {
        throw std::runtime_error("Scoped critical section test failed: output is false");
    }
}

void test_scoped_critical_section2(const py::handle &cls) {
    auto barrier = std::barrier(3);
    auto bool_wrapper1 = cls(false);
    auto bool_wrapper2 = cls(false);
    std::pair<bool, bool> output{false, false};

    {
        // Release the GIL to allow run threads in parallel.
        py::gil_scoped_release gil_release{};

        std::thread t1([&]() {
            // Use gil_scoped_acquire to ensure we have a valid Python thread state
            // before entering the critical section. Otherwise, the critical section
            // will cause a segmentation fault.
            py::gil_scoped_acquire ensure_tstate{};
            // Enter the critical section with two different objects.
            // This will ensure that the critical section is locked for both objects.
            py::scoped_critical_section lock{bool_wrapper1, bool_wrapper2};
            // At this point, objects are locked by this thread via the scoped_critical_section.
            // This barrier will ensure that other threads wait until this thread has released
            // the critical section before proceeding.
            barrier.arrive_and_wait();
            // Sleep for a short time to simulate some work in the critical section.
            // This sleep is necessary to test the locking mechanism properly.
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
            auto *bw1 = bool_wrapper1.cast<BoolWrapper *>();
            auto *bw2 = bool_wrapper2.cast<BoolWrapper *>();
            bw1->set(true);
            bw2->set(true);
        });

        std::thread t2([&]() {
            // This thread will wait until the first thread has entered the critical section due to
            // the barrier.
            barrier.arrive_and_wait();
            {
                // Use gil_scoped_acquire to ensure we have a valid Python thread state
                // before entering the critical section. Otherwise, the critical section
                // will cause a segmentation fault.
                py::gil_scoped_acquire ensure_tstate{};
                // Enter the critical section with the same object as the first thread.
                py::scoped_critical_section lock{bool_wrapper1};
                // At this point, the critical section is released by the first thread, the value
                // is set to true.
                auto *bw1 = bool_wrapper1.cast<BoolWrapper *>();
                output.first = bw1->get();
            }
        });

        std::thread t3([&]() {
            // This thread will wait until the first thread has entered the critical section due to
            // the barrier.
            barrier.arrive_and_wait();
            {
                // Use gil_scoped_acquire to ensure we have a valid Python thread state
                // before entering the critical section. Otherwise, the critical section
                // will cause a segmentation fault.
                py::gil_scoped_acquire ensure_tstate{};
                // Enter the critical section with the same object as the first thread.
                py::scoped_critical_section lock{bool_wrapper2};
                // At this point, the critical section is released by the first thread, the value
                // is set to true.
                auto *bw2 = bool_wrapper2.cast<BoolWrapper *>();
                output.second = bw2->get();
            }
        });

        t1.join();
        t2.join();
        t3.join();
    }

    if (!output.first || !output.second) {
        throw std::runtime_error(
            "Scoped critical section test with two objects failed: output is false");
    }
}

void test_scoped_critical_section2_same_object_no_deadlock(const py::handle &cls) {
    auto barrier = std::barrier(2);
    auto bool_wrapper = cls(false);
    bool output = false;

    {
        // Release the GIL to allow run threads in parallel.
        py::gil_scoped_release gil_release{};

        std::thread t1([&]() {
            // Use gil_scoped_acquire to ensure we have a valid Python thread state
            // before entering the critical section. Otherwise, the critical section
            // will cause a segmentation fault.
            py::gil_scoped_acquire ensure_tstate{};
            // Enter the critical section with the same object as the second thread.
            py::scoped_critical_section lock{bool_wrapper, bool_wrapper}; // same object used here
            // At this point, the object is locked by this thread via the scoped_critical_section.
            // This barrier will ensure that the second thread waits until this thread has released
            // the critical section before proceeding.
            barrier.arrive_and_wait();
            // Sleep for a short time to simulate some work in the critical section.
            // This sleep is necessary to test the locking mechanism properly.
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
            auto *bw = bool_wrapper.cast<BoolWrapper *>();
            bw->set(true);
        });

        std::thread t2([&]() {
            // This thread will wait until the first thread has entered the critical section due to
            // the barrier.
            barrier.arrive_and_wait();
            {
                // Use gil_scoped_acquire to ensure we have a valid Python thread state
                // before entering the critical section. Otherwise, the critical section
                // will cause a segmentation fault.
                py::gil_scoped_acquire ensure_tstate{};
                // Enter the critical section with the same object as the first thread.
                py::scoped_critical_section lock{bool_wrapper};
                // At this point, the critical section is released by the first thread, the value
                // is set to true.
                auto *bw = bool_wrapper.cast<BoolWrapper *>();
                output = bw->get();
            }
        });

        t1.join();
        t2.join();
    }

    if (!output) {
        throw std::runtime_error(
            "Scoped critical section test with same object failed: output is false");
    }
}

#else

void test_scoped_critical_section(const py::handle &) {}
void test_scoped_critical_section2(const py::handle &) {}
void test_scoped_critical_section2_same_object_no_deadlock(const py::handle &) {}

#endif

} // namespace test_scoped_critical_section_ns

TEST_SUBMODULE(scoped_critical_section, m) {
    using namespace test_scoped_critical_section_ns;

    m.def("test_one_nullptr", test_one_nullptr);
    m.def("test_two_nullptrs", test_two_nullptrs);
    m.def("test_first_nullptr", test_first_nullptr);
    m.def("test_second_nullptr", test_second_nullptr);

    auto BoolWrapperClass = py::class_<BoolWrapper>(m, "BoolWrapper")
                                .def(py::init<bool>())
                                .def("get", &BoolWrapper::get)
                                .def("set", &BoolWrapper::set);
    auto BoolWrapperHandle = py::handle(BoolWrapperClass);
    (void) BoolWrapperHandle.ptr(); // suppress unused variable warning

    m.attr("has_barrier") =
#ifdef PYBIND11_HAS_STD_BARRIER
        true;
#else
        false;
#endif

    m.def("test_scoped_critical_section",
          [BoolWrapperHandle]() -> void { test_scoped_critical_section(BoolWrapperHandle); });
    m.def("test_scoped_critical_section2",
          [BoolWrapperHandle]() -> void { test_scoped_critical_section2(BoolWrapperHandle); });
    m.def("test_scoped_critical_section2_same_object_no_deadlock", [BoolWrapperHandle]() -> void {
        test_scoped_critical_section2_same_object_no_deadlock(BoolWrapperHandle);
    });
}
