#include "tile/base/deferred.h"
#include "tile/base/internal/logging.h"
#include "tile/base/internal/macro.h"
#include "tile/base/thread/scoped_lock.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <thread>

namespace tile {

int lock_count     = 0;
int try_lock_count = 0;
int unlock_count   = 0;

#define RESET_ENV()                                                                                                    \
    lock_count   = 0;                                                                                                  \
    unlock_count = 0;

struct MockMutex {
    void Lock() { ++lock_count; }

    void Unlock() { ++unlock_count; }
};

struct MockTryLockMutex {
    void Lock() { ++lock_count; }

    bool TryLock()
    {
        ++try_lock_count;
        return true;
    }

    void Unlock() { ++unlock_count; }
};

TEST(Mutex, MutexTraits)
{
    {
        struct Lockable1 {
            void Lock() {}

            void lock() {}
        };

        struct Lockable2 {
            bool Lock() { return false; }

            bool lock() { return false; }
        };

        struct Lockable3 {
            void Lock(int) {}

            void lock(int) {}
        };

        ASSERT_TRUE(tile::detail::thread::Lockable<Lockable1>::value);
        ASSERT_TRUE(tile::detail::thread::lockable<Lockable1>::value);
        ASSERT_FALSE(tile::detail::thread::Lockable<Lockable2>::value);
        ASSERT_FALSE(tile::detail::thread::lockable<Lockable2>::value);
        ASSERT_FALSE(tile::detail::thread::Lockable<Lockable3>::value);
        ASSERT_FALSE(tile::detail::thread::lockable<Lockable3>::value);
    }

    {
        struct TryLockable1 {
            bool TryLock() { return false; }

            bool try_lock() { return false; }
        };

        struct TryLockable2 {
            void TryLock() {}

            void try_lock() {}
        };

        struct TryLockable3 {
            bool TryLock(int) { return false; }

            bool try_lock(int) { return false; }
        };

        ASSERT_TRUE(tile::detail::thread::TryLockable<TryLockable1>::value);
        ASSERT_TRUE(tile::detail::thread::try_lockable<TryLockable1>::value);
        ASSERT_FALSE(tile::detail::thread::TryLockable<TryLockable2>::value);
        ASSERT_FALSE(tile::detail::thread::try_lockable<TryLockable2>::value);
        ASSERT_FALSE(tile::detail::thread::TryLockable<TryLockable3>::value);
        ASSERT_FALSE(tile::detail::thread::try_lockable<TryLockable3>::value);
    }
}

TEST(Mutex, LockableAndUnlockable)
{
    RESET_ENV();

    MockMutex m;
    tile::MutexTraits<MockMutex>::Lock(&m);
    ASSERT_EQ(1, lock_count);
    ASSERT_EQ(0, unlock_count);

    tile::MutexTraits<MockMutex>::Unlock(&m);
    ASSERT_EQ(1, lock_count);
    ASSERT_EQ(1, unlock_count);

    ASSERT_TRUE(tile::detail::thread::Lockable<MockMutex>::value);
    ASSERT_FALSE(tile::detail::thread::lockable<MockMutex>::value);
    ASSERT_TRUE(tile::detail::thread::Unlockable<MockMutex>::value);
    ASSERT_FALSE(tile::detail::thread::unlockable<MockMutex>::value);
    ASSERT_FALSE(tile::detail::thread::TryLockable<MockMutex>::value);
    ASSERT_FALSE(tile::detail::thread::try_lockable<MockMutex>::value);
}

TEST(Mutex, Lock_And_lock)
{
    RESET_ENV();
    // 1. Lock + lock, use Lock
    // 2. lock       , use lock
    // 3. Lock       , use Lock
    {
        struct M1 {
            void Lock() { lock_count++; }

            void lock() { lock_count--; }

            void unlock() { /* do nothing */ }
        } m1;

        ASSERT_TRUE(tile::detail::thread::Lockable<M1>::value);
        ASSERT_TRUE(tile::detail::thread::lockable<M1>::value);

        tile::MutexOperator<M1>::Lock(&m1);
        ASSERT_EQ(1, lock_count);

        struct M2 {
            void lock() { lock_count--; }

            void unlock() { /* do nothing */ }
        } m2;

        ASSERT_FALSE(tile::detail::thread::Lockable<M2>::value);
        ASSERT_TRUE(tile::detail::thread::lockable<M2>::value);

        tile::MutexOperator<M2>::Lock(&m2);
        ASSERT_EQ(0, lock_count);

        struct M3 {
            void Lock() { lock_count++; }

            void unlock() { /* do nothing */ }
        } m3;

        ASSERT_TRUE(tile::detail::thread::Lockable<M3>::value);
        ASSERT_FALSE(tile::detail::thread::lockable<M3>::value);

        tile::MutexOperator<M3>::Lock(&m3);
        ASSERT_EQ(1, lock_count);
    }
}

TEST(Mutex, Unlock_And_unlock)
{
    RESET_ENV();
    // 1. Unlock + unlock, use Unlock
    // 2. unlock       , use unlock
    // 3. Unlock       , use Unlock
    {
        struct M1 {
            void Unlock() { unlock_count++; }

            void unlock() { unlock_count--; }

            void lock() { /* do nothing */ }
        } m1;

        ASSERT_TRUE(tile::detail::thread::Unlockable<M1>::value);
        ASSERT_TRUE(tile::detail::thread::unlockable<M1>::value);

        tile::MutexOperator<M1>::Unlock(&m1);
        ASSERT_EQ(1, unlock_count);

        struct M2 {
            void unlock() { unlock_count--; }

            void lock() { /* do nothing */ }
        } m2;

        ASSERT_FALSE(tile::detail::thread::Unlockable<M2>::value);
        ASSERT_TRUE(tile::detail::thread::unlockable<M2>::value);

        tile::MutexOperator<M2>::Unlock(&m2);
        ASSERT_EQ(0, unlock_count);

        struct M3 {
            void Unlock() { unlock_count++; }

            void lock() { /* do nothing */ }
        } m3;

        ASSERT_TRUE(tile::detail::thread::Unlockable<M3>::value);
        ASSERT_FALSE(tile::detail::thread::unlockable<M3>::value);

        tile::MutexOperator<M3>::Unlock(&m3);
        ASSERT_EQ(1, unlock_count);
    }
}

TEST(Mutex, TryLock_And_try_lock)
{
    RESET_ENV();
    // 1. TryLock + try_lock, use TryLock
    // 2. try_lock       , use try_lock
    // 3. TryLock       , use TryLock
    {
        struct M1 {
            bool TryLock()
            {
                lock_count++;
                return true;
            }

            bool try_lock()
            {
                lock_count--;
                return true;
            }

            void lock() { /* do nothing */ }

            void unlock() { /* do nothing */ }
        } m1;

        ASSERT_TRUE(tile::detail::thread::TryLockable<M1>::value);
        ASSERT_TRUE(tile::detail::thread::try_lockable<M1>::value);
        ASSERT_TRUE(tile::MutexOperator<M1>::TryLock(&m1));
        ASSERT_EQ(1, lock_count);

        struct M2 {
            bool try_lock()
            {
                lock_count--;
                return true;
            }

            void lock() { /* do nothing */ }

            void unlock() { /* do nothing */ }
        } m2;

        ASSERT_FALSE(tile::detail::thread::TryLockable<M2>::value);
        ASSERT_TRUE(tile::detail::thread::try_lockable<M2>::value);
        ASSERT_TRUE(tile::MutexOperator<M2>::TryLock(&m2));
        ASSERT_EQ(0, lock_count);

        struct M3 {
            bool TryLock()
            {
                lock_count++;
                return true;
            }

            void lock() { /* do nothing */ }

            void unlock() { /* do nothing */ }
        } m3;

        ASSERT_TRUE(tile::detail::thread::TryLockable<M3>::value);
        ASSERT_FALSE(tile::detail::thread::try_lockable<M3>::value);
        ASSERT_TRUE(tile::MutexOperator<M3>::TryLock(&m3));
        ASSERT_EQ(1, lock_count);
    }
}

TEST(ScopedLock, SingleMutex)
{
    RESET_ENV();
    MockMutex m;
    {
        tile::ScopedLock lock(m);
        ASSERT_EQ(1, lock_count);
        ASSERT_EQ(0, unlock_count);
    }
    ASSERT_EQ(1, lock_count);
    ASSERT_EQ(1, unlock_count);
}

TEST(ScopedLock, TryLock)
{
    RESET_ENV();
    MockTryLockMutex m1;
    MockTryLockMutex m2;
    {
        tile::ScopedLock lock(m1, m2);
        ASSERT_EQ(0, lock_count);
        ASSERT_EQ(2, try_lock_count);
        ASSERT_EQ(0, unlock_count);
    }
    // tile::MutexOperator<MockTryLockMutex>::Unlock(&m);
    ASSERT_EQ(0, lock_count);
    ASSERT_EQ(2, try_lock_count);
    ASSERT_EQ(2, unlock_count);
}

TEST(ScopedLock, MultiThread)
{
    constexpr std::size_t kThreadCount = 5;
    constexpr std::size_t kLoopCount   = 10000;

    int x                              = 0;
    Mutex m;
    auto AddOne([&] {
        tile::ScopedLock lock(m);
        int old = x++;
        ASSERT_EQ(old + 1, x);
    });

    std::vector<std::thread> threads;
    threads.resize(kThreadCount);
    for (int i = 0; i < kThreadCount; i++) {
        threads[i] = std::thread([&] {
            for (int i = 0; i < kLoopCount; i++) { AddOne(); }
        });
    }
    while (x != kThreadCount * kLoopCount) { std::this_thread::yield(); }
    for (auto &&t : threads) { t.join(); }
}

TEST(ScopedLock, MultiThread_4Mutex)
{
    std::mutex m1;
    std::mutex m2;
    std::mutex m3;
    std::mutex m4;
    constexpr int kLockTestNum = 1000;
    constexpr int kThreadNum   = 24;
    std::atomic<int> test_success_count{0};

#define LOCK_ORDER_IMPL(prefix, id1, id2, id3, id4)                                                                    \
    std::thread TILE_INTERNAL_PP_CAT(thread_, __LINE__)([&] {                                                          \
        for (int i = 0; i < kLockTestNum; i++) {                                                                       \
            tile::ScopedLock lock(prefix##id1, prefix##id2, prefix##id3, prefix##id4);                                 \
            int progress = test_success_count.fetch_add(1);                                                            \
            TILE_LOG_INFO_IF_EVERY_SECOND(progress % (kThreadNum * kLockTestNum / 100) == 0, "progress: {}%",          \
                                          100.0 * progress / (kThreadNum * kLockTestNum));                             \
        }                                                                                                              \
    });                                                                                                                \
    tile::ScopedDeferred TILE_INTERNAL_PP_CAT(defer_, __LINE__)([&] {                                                  \
        TILE_INTERNAL_PP_CAT(thread_, __LINE__).join();                                                                \
    });

#define LOCK_ORDER(x, y, z, w) LOCK_ORDER_IMPL(m, x, y, z, w)

    {
        //
        LOCK_ORDER(1, 2, 3, 4);
        LOCK_ORDER(1, 2, 4, 3);
        LOCK_ORDER(1, 3, 2, 4);
        LOCK_ORDER(1, 3, 4, 2);
        LOCK_ORDER(1, 4, 2, 3);
        LOCK_ORDER(1, 4, 3, 2);
        //
        LOCK_ORDER(2, 1, 3, 4);
        LOCK_ORDER(2, 1, 4, 3);
        LOCK_ORDER(2, 3, 1, 4);
        LOCK_ORDER(2, 3, 4, 1);
        LOCK_ORDER(2, 4, 1, 3);
        LOCK_ORDER(2, 4, 3, 1);
        //
        LOCK_ORDER(3, 1, 2, 4);
        LOCK_ORDER(3, 1, 4, 2);
        LOCK_ORDER(3, 2, 1, 4);
        LOCK_ORDER(3, 2, 4, 1);
        LOCK_ORDER(3, 4, 1, 2);
        LOCK_ORDER(3, 4, 2, 1);
        //
        LOCK_ORDER(4, 1, 2, 3);
        LOCK_ORDER(4, 1, 3, 2);
        LOCK_ORDER(4, 2, 1, 3);
        LOCK_ORDER(4, 2, 3, 1);
        LOCK_ORDER(4, 3, 1, 2);
        LOCK_ORDER(4, 3, 2, 1);
    }
    TILE_LOG_INFO("progress {}%", 100.0 * test_success_count.load() / (kThreadNum * kLockTestNum));

#undef LOCK_ORDER_IMPL
#undef LOCK_ORDER
    ASSERT_EQ(kThreadNum * kLockTestNum, test_success_count.load());
}

TEST(ScopedLock, DuplicateLockCheck)
{
    TILE_LOG_INFO("log before InitGoogleLogging");
    ASSERT_DEATH(
        [] {
            std::mutex m;
            tile::ScopedLock lock(m, m);
        }(),
        ::testing::HasSubstr("You should not lock a lock twice"));
}

}// namespace tile
