/*
 * Copyright 2010-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
 * that can be found in the LICENSE file.
 */

#include "ConditionVariable.hpp"

#include <condition_variable>
#include <mutex>
#include <vector>

#include "gmock/gmock.h"
#include "gtest/gtest.h"

#include "concurrent/ScopedThread.hpp"
#include "TestSupport.hpp"

using namespace kotlin;

template <typename T>
class ConditionVariableTest : public testing::Test {};

using CVTypes = testing::Types<
#ifndef KONAN_WINDOWS // winpthreads are acting strange in our mingw toolchain.
        std::condition_variable,
        std::condition_variable_any,
#endif
        ConditionVariableSpin>;
class CVNames {
public:
    template <typename T>
    static std::string GetName(int) {
        if constexpr (std::is_same_v<T, std::condition_variable>) {
            return "condition_variable";
        } else if constexpr (std::is_same_v<T, std::condition_variable_any>) {
            return "condition_variable_any";
        } else if constexpr (std::is_same_v<T, ConditionVariableSpin>) {
            return "ConditionVariableSpin";
        }
    }
};
TYPED_TEST_SUITE(ConditionVariableTest, CVTypes, CVNames);

TYPED_TEST(ConditionVariableTest, NotifyNobody) {
    using CVUnderTest = TypeParam;

    CVUnderTest cv;
    cv.notify_one();
    cv.notify_all();
}

TYPED_TEST(ConditionVariableTest, WaitOne) {
    using CVUnderTest = TypeParam;

    bool flag = false;
    std::mutex m;
    CVUnderTest cv;

    std::atomic<bool> waiting = false;
    ScopedThread thread([&] {
        std::unique_lock guard(m);
        EXPECT_FALSE(flag);
        waiting.store(true, std::memory_order_relaxed);
        while (!flag) {
            cv.wait(guard);
        }
    });

    while (!waiting.load(std::memory_order_relaxed)) {
        std::this_thread::yield();
    }
    {
        std::unique_lock guard(m);
        flag = true;
    }
    cv.notify_one();

    thread.join();
}

TYPED_TEST(ConditionVariableTest, WaitOneNotifyUnderLock) {
    using CVUnderTest = TypeParam;

    bool flag = false;
    std::mutex m;
    CVUnderTest cv;

    std::atomic<bool> waiting = false;
    ScopedThread thread([&] {
        std::unique_lock guard(m);
        EXPECT_FALSE(flag);
        waiting.store(true, std::memory_order_relaxed);
        while (!flag) {
            cv.wait(guard);
        }
    });

    while (!waiting.load(std::memory_order_relaxed)) {
        std::this_thread::yield();
    }
    {
        std::unique_lock guard(m);
        flag = true;
        cv.notify_one();
    }

    thread.join();
}

TYPED_TEST(ConditionVariableTest, WaitAll) {
    using CVUnderTest = TypeParam;

    bool flag = false;
    std::mutex m;
    CVUnderTest cv;

    std::atomic<size_t> waiting = 0;
    std::vector<ScopedThread> threads;
    for (int i = 0; i < kDefaultThreadCount; ++i) {
        threads.emplace_back([&] {
            std::unique_lock guard(m);
            EXPECT_FALSE(flag);
            waiting.fetch_add(1, std::memory_order_relaxed);
            while (!flag) {
                cv.wait(guard);
            }
        });
    }

    while (waiting.load(std::memory_order_relaxed) != threads.size()) {
        std::this_thread::yield();
    }
    {
        std::unique_lock guard(m);
        flag = true;
    }
    cv.notify_all();

    threads.clear();
}

TYPED_TEST(ConditionVariableTest, WaitAllNotifyUnderLock) {
    using CVUnderTest = TypeParam;

    bool flag = false;
    std::mutex m;
    CVUnderTest cv;

    std::atomic<size_t> waiting = 0;
    std::vector<ScopedThread> threads;
    for (int i = 0; i < kDefaultThreadCount; ++i) {
        threads.emplace_back([&] {
            std::unique_lock guard(m);
            EXPECT_FALSE(flag);
            waiting.fetch_add(1, std::memory_order_relaxed);
            while (!flag) {
                cv.wait(guard);
            }
        });
    }

    while (waiting.load(std::memory_order_relaxed) != threads.size()) {
        std::this_thread::yield();
    }
    {
        std::unique_lock guard(m);
        flag = true;
        cv.notify_all();
    }

    threads.clear();
}

TYPED_TEST(ConditionVariableTest, WaitPredicateOne) {
    using CVUnderTest = TypeParam;

    bool flag = false;
    std::mutex m;
    CVUnderTest cv;

    std::atomic<bool> waiting = false;
    ScopedThread thread([&] {
        std::unique_lock guard(m);
        EXPECT_FALSE(flag);
        waiting.store(true, std::memory_order_relaxed);
        cv.wait(guard, [&] { return flag; });
        EXPECT_TRUE(flag);
    });

    while (!waiting.load(std::memory_order_relaxed)) {
        std::this_thread::yield();
    }
    {
        std::unique_lock guard(m);
        flag = true;
    }
    cv.notify_all();

    thread.join();
}

TYPED_TEST(ConditionVariableTest, WaitPredicateOneNotifyUnderLock) {
    using CVUnderTest = TypeParam;

    bool flag = false;
    std::mutex m;
    CVUnderTest cv;

    std::atomic<bool> waiting = 0;
    ScopedThread thread([&] {
        std::unique_lock guard(m);
        EXPECT_FALSE(flag);
        waiting.store(true, std::memory_order_relaxed);
        cv.wait(guard, [&] { return flag; });
        EXPECT_TRUE(flag);
    });

    while (!waiting.load(std::memory_order_relaxed)) {
        std::this_thread::yield();
    }
    {
        std::unique_lock guard(m);
        flag = true;
        cv.notify_all();
    }

    thread.join();
}

TYPED_TEST(ConditionVariableTest, WaitPredicateAll) {
    using CVUnderTest = TypeParam;

    bool flag = false;
    std::mutex m;
    CVUnderTest cv;

    std::atomic<size_t> waiting = 0;
    std::vector<ScopedThread> threads;
    for (int i = 0; i < kDefaultThreadCount; ++i) {
        threads.emplace_back([&] {
            std::unique_lock guard(m);
            EXPECT_FALSE(flag);
            waiting.fetch_add(1, std::memory_order_relaxed);
            cv.wait(guard, [&] { return flag; });
            EXPECT_TRUE(flag);
        });
    }

    while (waiting.load(std::memory_order_relaxed) != threads.size()) {
        std::this_thread::yield();
    }
    {
        std::unique_lock guard(m);
        flag = true;
    }
    cv.notify_all();

    threads.clear();
}

TYPED_TEST(ConditionVariableTest, WaitPredicateAllNotifyUnderLock) {
    using CVUnderTest = TypeParam;

    bool flag = false;
    std::mutex m;
    CVUnderTest cv;

    std::atomic<size_t> waiting = 0;
    std::vector<ScopedThread> threads;
    for (int i = 0; i < kDefaultThreadCount; ++i) {
        threads.emplace_back([&] {
            std::unique_lock guard(m);
            EXPECT_FALSE(flag);
            waiting.fetch_add(1, std::memory_order_relaxed);
            cv.wait(guard, [&] { return flag; });
            EXPECT_TRUE(flag);
        });
    }

    while (waiting.load(std::memory_order_relaxed) != threads.size()) {
        std::this_thread::yield();
    }
    {
        std::unique_lock guard(m);
        flag = true;
        cv.notify_all();
    }

    threads.clear();
}

TYPED_TEST(ConditionVariableTest, Checkpoint) {
    constexpr uint64_t epochsCount = 1000;
    using CVUnderTest = TypeParam;

    uint64_t epochScheduled = 0;
    uint64_t epochStarted = 0;
    uint64_t epochFinished = 0;
    std::mutex m;
    CVUnderTest cv;

    auto schedule = [&] {
        std::unique_lock guard(m);
        if (epochScheduled > epochStarted) {
            return epochScheduled;
        }
        epochScheduled = epochStarted + 1;
        return epochScheduled;
    };

    std::vector<ScopedThread> threads;
    std::array<std::atomic<uint64_t>, kDefaultThreadCount> checkpoints = {0};
    for (int i = 0; i < kDefaultThreadCount; ++i) {
        threads.emplace_back([&, i] {
            while (true) {
                uint64_t epoch = schedule();
                if (epoch >= epochsCount) return;
                {
                    std::unique_lock guard(m);
                    if (epochFinished < epoch) {
                        checkpoints[i].store(2 * epoch, std::memory_order_relaxed);
                        cv.wait(guard, [&] { return epochFinished >= epoch; });
                        checkpoints[i].store(2 * epoch + 1, std::memory_order_relaxed);
                    }
                }
            }
        });
    }

    while (epochStarted <= epochsCount) {
        {
            std::unique_lock guard(m);
            ++epochStarted;
        }
        std::this_thread::yield();
        {
            std::unique_lock guard(m);
            epochFinished = epochStarted;
        }
        cv.notify_all();
        for (auto& checkpoint : checkpoints) {
            while (true) {
                auto value = checkpoint.load(std::memory_order_relaxed);
                auto epoch = value / 2;
                bool isWaiting = value % 2 == 0;
                if (epoch > epochFinished) break;
                if (!isWaiting) break;
                std::this_thread::yield();
            }
        }
    }
    threads.clear();
}
