/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-â
âvi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8                                :viâ
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ¡
â Copyright 2022 Justine Alexandra Roberts Tunney                              â
â                                                                              â
â Permission to use, copy, modify, and/or distribute this software for         â
â any purpose with or without fee is hereby granted, provided that the         â
â above copyright notice and this permission notice appear in all copies.      â
â                                                                              â
â THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL                â
â WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED                â
â WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE             â
â AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL         â
â DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR        â
â PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER               â
â TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR             â
â PERFORMANCE OF THIS SOFTWARE.                                                â
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ*/
#include "libc/atomic.h"
#include "libc/calls/calls.h"
#include "libc/calls/struct/sched_param.h"
#include "libc/calls/struct/sigaction.h"
#include "libc/dce.h"
#include "libc/errno.h"
#include "libc/macros.internal.h"
#include "libc/mem/gc.h"
#include "libc/mem/mem.h"
#include "libc/nexgen32e/nexgen32e.h"
#include "libc/nexgen32e/vendor.internal.h"
#include "libc/runtime/runtime.h"
#include "libc/runtime/stack.h"
#include "libc/sysv/consts/prot.h"
#include "libc/sysv/consts/sa.h"
#include "libc/sysv/consts/sched.h"
#include "libc/sysv/consts/sig.h"
#include "libc/testlib/ezbench.h"
#include "libc/testlib/subprocess.h"
#include "libc/testlib/testlib.h"
#include "libc/thread/thread.h"
#include "libc/thread/thread2.h"

void OnUsr1(int sig, struct siginfo *si, void *vctx) {
  struct ucontext *ctx = vctx;
}

void SetUp(void) {
  struct sigaction sig = {.sa_sigaction = OnUsr1, .sa_flags = SA_SIGINFO};
  sigaction(SIGUSR1, &sig, 0);
}

void TriggerSignal(void) {
  sched_yield();
  raise(SIGUSR1);
  sched_yield();
}

static void *Increment(void *arg) {
  ASSERT_EQ(gettid(), pthread_getthreadid_np());
  TriggerSignal();
  return (void *)((uintptr_t)arg + 1);
}

TEST(pthread_create, testCreateReturnJoin) {
  void *rc;
  pthread_t id;
  ASSERT_EQ(0, pthread_create(&id, 0, Increment, (void *)1));
  ASSERT_EQ(0, pthread_join(id, &rc));
  ASSERT_EQ((void *)2, rc);
}

static void *IncExit(void *arg) {
  CheckStackIsAligned();
  TriggerSignal();
  pthread_exit((void *)((uintptr_t)arg + 1));
}

TEST(pthread_create, testCreateExitJoin) {
  void *rc;
  pthread_t id;
  ASSERT_EQ(0, pthread_create(&id, 0, IncExit, (void *)2));
  ASSERT_EQ(0, pthread_join(id, &rc));
  ASSERT_EQ((void *)3, rc);
}

static void *CheckSchedule(void *arg) {
  int rc, policy;
  struct sched_param prio;
  ASSERT_EQ(0, pthread_getschedparam(pthread_self(), &policy, &prio));
  ASSERT_EQ(SCHED_OTHER, policy);
  ASSERT_EQ(sched_get_priority_min(SCHED_OTHER), prio.sched_priority);
  if (IsWindows() || IsXnu() || IsOpenbsd()) {
    ASSERT_EQ(ENOSYS, pthread_setschedparam(pthread_self(), policy, &prio));
  } else {
    ASSERT_EQ(0, pthread_setschedparam(pthread_self(), policy, &prio));
  }
  return 0;
}

TEST(pthread_create, scheduling) {
  pthread_t id;
  pthread_attr_t attr;
  struct sched_param pri = {sched_get_priority_min(SCHED_OTHER)};
  ASSERT_EQ(0, pthread_attr_init(&attr));
  ASSERT_EQ(0, pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED));
  ASSERT_EQ(0, pthread_attr_setschedpolicy(&attr, SCHED_OTHER));
  ASSERT_EQ(0, pthread_attr_setschedparam(&attr, &pri));
  ASSERT_EQ(0, pthread_create(&id, &attr, CheckSchedule, 0));
  ASSERT_EQ(0, pthread_attr_destroy(&attr));
  ASSERT_EQ(0, pthread_join(id, 0));
}

static void *CheckStack(void *arg) {
  char buf[1024 * 1024];
  TriggerSignal();
  CheckLargeStackAllocation(buf, 1024 * 1024);
  return 0;
}

TEST(pthread_create, testBigStack) {
  pthread_t id;
  pthread_attr_t attr;
  ASSERT_EQ(0, pthread_attr_init(&attr));
  ASSERT_EQ(0, pthread_attr_setstacksize(&attr, 2 * 1000 * 1000));
  ASSERT_EQ(0, pthread_create(&id, &attr, CheckStack, 0));
  ASSERT_EQ(0, pthread_attr_destroy(&attr));
  ASSERT_EQ(0, pthread_join(id, 0));
}

static void *CheckStack2(void *arg) {
  char buf[57244];
  TriggerSignal();
  CheckLargeStackAllocation(buf, sizeof(buf));
  return 0;
}

TEST(pthread_create, testBiggerGuardSize) {
  pthread_t id;
  pthread_attr_t attr;
  ASSERT_EQ(0, pthread_attr_init(&attr));
  ASSERT_EQ(0, pthread_attr_setstacksize(&attr, 65536));
  ASSERT_EQ(0, pthread_attr_setguardsize(&attr, 8192));
  ASSERT_EQ(0, pthread_create(&id, &attr, CheckStack2, 0));
  ASSERT_EQ(0, pthread_attr_destroy(&attr));
  ASSERT_EQ(0, pthread_join(id, 0));
}

TEST(pthread_create, testCustomStack_withReallySmallSize) {
  char *stk;
  size_t siz;
  pthread_t id;
  pthread_attr_t attr;
  siz = PTHREAD_STACK_MIN;
  stk = malloc(siz);
  ASSERT_EQ(0, pthread_attr_init(&attr));
  ASSERT_EQ(0, pthread_attr_setstack(&attr, stk, siz));
  ASSERT_EQ(0, pthread_create(&id, &attr, Increment, 0));
  ASSERT_EQ(0, pthread_attr_destroy(&attr));
  ASSERT_EQ(0, pthread_join(id, 0));
  // we still own the stack memory
  ASSERT_EQ(0, pthread_attr_init(&attr));
  ASSERT_EQ(0, pthread_attr_setstack(&attr, stk, siz));
  ASSERT_EQ(0, pthread_create(&id, &attr, Increment, 0));
  ASSERT_EQ(0, pthread_attr_destroy(&attr));
  ASSERT_EQ(0, pthread_join(id, 0));
  free(stk);
}

void *JoinMainWorker(void *arg) {
  void *rc;
  pthread_t main_thread = (pthread_t)arg;
  _gc(malloc(32));
  _gc(malloc(32));
  ASSERT_EQ(0, pthread_join(main_thread, &rc));
  ASSERT_EQ(123, (intptr_t)rc);
  return 0;
}

TEST(pthread_join, mainThread) {
  pthread_t id;
  _gc(malloc(32));
  _gc(malloc(32));
  SPAWN(fork);
  ASSERT_EQ(0, pthread_create(&id, 0, JoinMainWorker, (void *)pthread_self()));
  pthread_exit((void *)123);
  EXITS(0);
}

TEST(pthread_join, mainThreadDelayed) {
  pthread_t id;
  _gc(malloc(32));
  _gc(malloc(32));
  SPAWN(fork);
  ASSERT_EQ(0, pthread_create(&id, 0, JoinMainWorker, (void *)pthread_self()));
  usleep(10000);
  pthread_exit((void *)123);
  EXITS(0);
}

TEST(pthread_exit, fromMainThread_whenNoThreadsWereCreated) {
  SPAWN(fork);
  pthread_exit((void *)123);
  EXITS(0);
}

atomic_int g_cleanup1;
atomic_int g_cleanup2;

void OnCleanup(void *arg) {
  *(atomic_int *)arg = true;
}

void *CleanupExit(void *arg) {
  pthread_cleanup_push(OnCleanup, &g_cleanup1);
  pthread_cleanup_push(OnCleanup, &g_cleanup2);
  pthread_cleanup_pop(false);
  pthread_exit(0);
  pthread_cleanup_pop(false);
  return 0;
}

TEST(pthread_cleanup, pthread_exit_alwaysCallsCallback) {
  pthread_t id;
  g_cleanup1 = false;
  g_cleanup2 = false;
  ASSERT_EQ(0, pthread_create(&id, 0, CleanupExit, 0));
  ASSERT_EQ(0, pthread_join(id, 0));
  ASSERT_TRUE(g_cleanup1);
  ASSERT_FALSE(g_cleanup2);
}

void *CleanupNormal(void *arg) {
  pthread_cleanup_push(OnCleanup, &g_cleanup1);
  pthread_cleanup_push(OnCleanup, &g_cleanup2);
  pthread_cleanup_pop(true);
  pthread_cleanup_pop(true);
  return 0;
}

TEST(pthread_cleanup, pthread_normal) {
  pthread_t id;
  g_cleanup1 = false;
  g_cleanup2 = false;
  ASSERT_EQ(0, pthread_create(&id, 0, CleanupNormal, 0));
  ASSERT_EQ(0, pthread_join(id, 0));
  ASSERT_TRUE(g_cleanup1);
  ASSERT_TRUE(g_cleanup2);
}

////////////////////////////////////////////////////////////////////////////////
// BENCHMARKS

static void CreateJoin(void) {
  pthread_t id;
  ASSERT_EQ(0, pthread_create(&id, 0, Increment, 0));
  ASSERT_EQ(0, pthread_join(id, 0));
}

// this is de facto the same as create+join
static void CreateDetach(void) {
  pthread_t id;
  ASSERT_EQ(0, pthread_create(&id, 0, Increment, 0));
  ASSERT_EQ(0, pthread_detach(id));
}

// this is really fast
static void CreateDetached(void) {
  pthread_t th;
  pthread_attr_t attr;
  ASSERT_EQ(0, pthread_attr_init(&attr));
  ASSERT_EQ(0, pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED));
  ASSERT_EQ(0, pthread_create(&th, &attr, Increment, 0));
  ASSERT_EQ(0, pthread_attr_destroy(&attr));
}

BENCH(pthread_create, bench) {
  EZBENCH2("CreateJoin", donothing, CreateJoin());
  EZBENCH2("CreateDetach", donothing, CreateDetach());
  EZBENCH2("CreateDetached", donothing, CreateDetached());
  while (!pthread_orphan_np()) {
    pthread_decimate_np();
  }
}
