//===-- Tests for standard condition variables ----------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "src/__support/CPP/atomic.h"
#include "src/threads/cnd_broadcast.h"
#include "src/threads/cnd_destroy.h"
#include "src/threads/cnd_init.h"
#include "src/threads/cnd_signal.h"
#include "src/threads/cnd_timedwait.h"
#include "src/threads/cnd_wait.h"
#include "src/threads/mtx_destroy.h"
#include "src/threads/mtx_init.h"
#include "src/threads/mtx_lock.h"
#include "src/threads/mtx_unlock.h"
#include "src/threads/thrd_create.h"
#include "src/threads/thrd_join.h"
#include "src/time/timespec_get.h"

#include "test/IntegrationTest/test.h"

#include <threads.h>
#include <time.h> // for TIME_UTC

static void add_ns(struct timespec &ts, long ns) {
  ts.tv_nsec += ns;
  if (ts.tv_nsec >= 1'000'000'000) {
    ++ts.tv_sec;
    ts.tv_nsec -= 1'000'000'000;
  }
}

namespace wait_notify_broadcast_test {

// The test in this namespace tests all condition variable operations. The
// main thread spawns THRD_COUNT threads, each of which wait on a condition
// variable |broadcast_cnd|. After spawing the threads, it waits on another
// condition variable |threads_ready_cnd| which will be signalled by the last
// thread before it starts waiting on |broadcast_cnd|. On signalled by the
// last thread, the main thread then wakes up to broadcast to all waiting
// threads to wake up. Each of the THRD_COUNT child threads increment
// |broadcast_count| by 1 before they start waiting on |broadcast_cnd|, and
// decrement it by 1 after getting signalled on |broadcast_cnd|.

constexpr unsigned int THRD_COUNT = 1000;

static LIBC_NAMESPACE::cpp::Atomic<unsigned int> broadcast_count(0);
static cnd_t broadcast_cnd, threads_ready_cnd;
static mtx_t broadcast_mtx, threads_ready_mtx;

int broadcast_thread_func(void *) {
  LIBC_NAMESPACE::mtx_lock(&broadcast_mtx);
  unsigned oldval = broadcast_count.fetch_add(1);
  if (oldval == THRD_COUNT - 1) {
    LIBC_NAMESPACE::mtx_lock(&threads_ready_mtx);
    LIBC_NAMESPACE::cnd_signal(&threads_ready_cnd);
    LIBC_NAMESPACE::mtx_unlock(&threads_ready_mtx);
  }

  LIBC_NAMESPACE::cnd_wait(&broadcast_cnd, &broadcast_mtx);
  LIBC_NAMESPACE::mtx_unlock(&broadcast_mtx);
  broadcast_count.fetch_sub(1);
  return 0;
}

void wait_notify_broadcast_test() {
  LIBC_NAMESPACE::cnd_init(&broadcast_cnd);
  LIBC_NAMESPACE::cnd_init(&threads_ready_cnd);
  LIBC_NAMESPACE::mtx_init(&broadcast_mtx, mtx_plain);
  LIBC_NAMESPACE::mtx_init(&threads_ready_mtx, mtx_plain);

  LIBC_NAMESPACE::mtx_lock(&threads_ready_mtx);
  thrd_t threads[THRD_COUNT];
  for (unsigned int i = 0; i < THRD_COUNT; ++i)
    LIBC_NAMESPACE::thrd_create(&threads[i], broadcast_thread_func, nullptr);

  LIBC_NAMESPACE::cnd_wait(&threads_ready_cnd, &threads_ready_mtx);
  LIBC_NAMESPACE::mtx_unlock(&threads_ready_mtx);

  LIBC_NAMESPACE::mtx_lock(&broadcast_mtx);
  ASSERT_EQ(broadcast_count.val, THRD_COUNT);
  LIBC_NAMESPACE::cnd_broadcast(&broadcast_cnd);
  LIBC_NAMESPACE::mtx_unlock(&broadcast_mtx);

  for (unsigned int i = 0; i < THRD_COUNT; ++i) {
    int retval = 0xBAD;
    LIBC_NAMESPACE::thrd_join(threads[i], &retval);
    ASSERT_EQ(retval, 0);
  }

  ASSERT_EQ(broadcast_count.val, 0U);

  LIBC_NAMESPACE::cnd_destroy(&broadcast_cnd);
  LIBC_NAMESPACE::cnd_destroy(&threads_ready_cnd);
  LIBC_NAMESPACE::mtx_destroy(&broadcast_mtx);
  LIBC_NAMESPACE::mtx_destroy(&threads_ready_mtx);
}

} // namespace wait_notify_broadcast_test

namespace single_waiter_test {

// In this namespace we set up test with two threads, one the main thread
// and the other a waiter thread. They wait on each other using condition
// variables and mutexes before proceeding to completion.

mtx_t waiter_mtx, main_thread_mtx;
cnd_t waiter_cnd, main_thread_cnd;

enum class WaitMode {
  Default,
  Timed,
};

int waiter_thread_func(void *arg) {
  auto *waiter_unblocked =
      static_cast<LIBC_NAMESPACE::cpp::Atomic<bool> *>(arg);
  LIBC_NAMESPACE::mtx_lock(&waiter_mtx);

  LIBC_NAMESPACE::mtx_lock(&main_thread_mtx);
  LIBC_NAMESPACE::cnd_signal(&main_thread_cnd);
  LIBC_NAMESPACE::mtx_unlock(&main_thread_mtx);

  LIBC_NAMESPACE::cnd_wait(&waiter_cnd, &waiter_mtx);
  waiter_unblocked->store(true);
  LIBC_NAMESPACE::mtx_unlock(&waiter_mtx);

  return 0x600D;
}

void single_waiter_test(WaitMode wait_mode) {
  ASSERT_EQ(LIBC_NAMESPACE::mtx_init(&waiter_mtx, mtx_plain),
            int(thrd_success));
  ASSERT_EQ(LIBC_NAMESPACE::mtx_init(&main_thread_mtx, mtx_plain),
            int(thrd_success));
  ASSERT_EQ(LIBC_NAMESPACE::cnd_init(&waiter_cnd), int(thrd_success));
  ASSERT_EQ(LIBC_NAMESPACE::cnd_init(&main_thread_cnd), int(thrd_success));

  ASSERT_EQ(LIBC_NAMESPACE::mtx_lock(&main_thread_mtx), int(thrd_success));

  thrd_t waiter_thread;
  LIBC_NAMESPACE::cpp::Atomic<bool> waiter_unblocked(false);
  LIBC_NAMESPACE::thrd_create(&waiter_thread, waiter_thread_func,
                              &waiter_unblocked);

  if (wait_mode == WaitMode::Default) {
    ASSERT_EQ(LIBC_NAMESPACE::cnd_wait(&main_thread_cnd, &main_thread_mtx),
              int(thrd_success));
  } else {
    ASSERT_EQ(wait_mode, WaitMode::Timed);
    struct timespec ts;
    ASSERT_EQ(LIBC_NAMESPACE::timespec_get(&ts, TIME_UTC), TIME_UTC);
    add_ns(ts, 50'000);
    int result =
        LIBC_NAMESPACE::cnd_timedwait(&main_thread_cnd, &main_thread_mtx, &ts);
    ASSERT_TRUE(result == int(thrd_success) || result == int(thrd_timedout));
  }
  ASSERT_EQ(LIBC_NAMESPACE::mtx_unlock(&main_thread_mtx), int(thrd_success));

  while (!waiter_unblocked.load()) {
    ASSERT_EQ(LIBC_NAMESPACE::mtx_lock(&waiter_mtx), int(thrd_success));
    ASSERT_EQ(LIBC_NAMESPACE::cnd_signal(&waiter_cnd), int(thrd_success));
    ASSERT_EQ(LIBC_NAMESPACE::mtx_unlock(&waiter_mtx), int(thrd_success));
  }

  int retval;
  LIBC_NAMESPACE::thrd_join(waiter_thread, &retval);
  ASSERT_EQ(retval, 0x600D);

  LIBC_NAMESPACE::mtx_destroy(&waiter_mtx);
  LIBC_NAMESPACE::mtx_destroy(&main_thread_mtx);
  LIBC_NAMESPACE::cnd_destroy(&waiter_cnd);
  LIBC_NAMESPACE::cnd_destroy(&main_thread_cnd);
}

} // namespace single_waiter_test

namespace timed_wait_test {

void timeout_test() {
  cnd_t cnd;
  mtx_t mtx;
  ASSERT_EQ(LIBC_NAMESPACE::cnd_init(&cnd), int(thrd_success));
  ASSERT_EQ(LIBC_NAMESPACE::mtx_init(&mtx, mtx_plain), int(thrd_success));

  ASSERT_EQ(LIBC_NAMESPACE::mtx_lock(&mtx), int(thrd_success));

  struct timespec ts;
  ts.tv_sec = 0;
  ts.tv_nsec = 0;
  ASSERT_EQ(LIBC_NAMESPACE::cnd_timedwait(&cnd, &mtx, &ts), int(thrd_timedout));

  ASSERT_EQ(LIBC_NAMESPACE::mtx_unlock(&mtx), int(thrd_success));

  LIBC_NAMESPACE::cnd_destroy(&cnd);
  LIBC_NAMESPACE::mtx_destroy(&mtx);
}

void future_timeout_test() {
  cnd_t cnd;
  mtx_t mtx;
  ASSERT_EQ(LIBC_NAMESPACE::cnd_init(&cnd), int(thrd_success));
  ASSERT_EQ(LIBC_NAMESPACE::mtx_init(&mtx, mtx_plain), int(thrd_success));

  ASSERT_EQ(LIBC_NAMESPACE::mtx_lock(&mtx), int(thrd_success));

  struct timespec ts;
  ASSERT_EQ(LIBC_NAMESPACE::timespec_get(&ts, TIME_UTC), TIME_UTC);
  add_ns(ts, 50'000);
  ASSERT_EQ(LIBC_NAMESPACE::cnd_timedwait(&cnd, &mtx, &ts), int(thrd_timedout));

  ASSERT_EQ(LIBC_NAMESPACE::mtx_unlock(&mtx), int(thrd_success));

  LIBC_NAMESPACE::cnd_destroy(&cnd);
  LIBC_NAMESPACE::mtx_destroy(&mtx);
}

} // namespace timed_wait_test

TEST_MAIN() {
  wait_notify_broadcast_test::wait_notify_broadcast_test();
  single_waiter_test::single_waiter_test(single_waiter_test::WaitMode::Default);
  single_waiter_test::single_waiter_test(single_waiter_test::WaitMode::Timed);
  timed_wait_test::timeout_test();
  timed_wait_test::future_timeout_test();
  return 0;
}
