#pragma once

#include "../ds/helpers.h"
#include "localalloc.h"

#if defined(SNMALLOC_EXTERNAL_THREAD_ALLOC)
#  define SNMALLOC_THREAD_TEARDOWN_DEFINED
#endif

#if defined(SNMALLOC_USE_THREAD_CLEANUP)
#  if defined(SNMALLOC_THREAD_TEARDOWN_DEFINED)
#    error At most one out of method of thread teardown can be specified.
#  else
#    define SNMALLOC_THREAD_TEARDOWN_DEFINED
#  endif
#endif

#if defined(SNMALLOC_USE_PTHREAD_DESTRUCTORS)
#  if defined(SNMALLOC_THREAD_TEARDOWN_DEFINED)
#    error At most one out of method of thread teardown can be specified.
#  else
#    include <pthread.h>
#    define SNMALLOC_THREAD_TEARDOWN_DEFINED
#  endif
#endif

#if !defined(SNMALLOC_THREAD_TEARDOWN_DEFINED)
#  define SNMALLOC_USE_CXX_THREAD_DESTRUCTORS
#endif
extern "C" void _malloc_thread_cleanup();

namespace snmalloc
{
#ifdef SNMALLOC_EXTERNAL_THREAD_ALLOC
  /**
   * Version of the `ThreadAlloc` interface that does no management of thread
   * local state.
   *
   * It assumes that Alloc has been defined, and `ThreadAllocExternal` class
   * has access to snmalloc_core.h.
   */
  class ThreadAlloc
  {
  protected:
    static void register_cleanup() {}

  public:
    static SNMALLOC_FAST_PATH Alloc& get()
    {
      return ThreadAllocExternal::get();
    }
  };

  /**
   * Function passed as a template parameter to `Allocator` to allow lazy
   * replacement.  There is nothing to initialise in this case, so we expect
   * this to never be called.
   */
#  ifdef _MSC_VER
// 32Bit Windows release MSVC is determining this as having unreachable code for
// f(nullptr), which is true.  But other platforms don't. Disabling the warning
// seems simplist.
#    pragma warning(push)
#    pragma warning(disable : 4702)
#  endif
  inline void register_clean_up()
  {
    error("Critical Error: This should never be called.");
  }
#  ifdef _MSC_VER
#    pragma warning(pop)
#  endif
#else

  /**
   * Holds the thread local state for the allocator.  The state is constant
   * initialised, and has no direct dectructor.  Instead snmalloc will call
   * `register_clean_up` on the slow path for bringing up thread local state.
   * This is responsible for calling `teardown`, which effectively destructs the
   * data structure, but in a way that allow it to still be used.
   */
  class ThreadAlloc
  {
  public:
    /**
     * Handle on thread local allocator
     *
     * This structure will self initialise if it has not been called yet.
     * It can be used during thread teardown, but its performance will be
     * less good.
     */
    static SNMALLOC_FAST_PATH Alloc& get()
    {
      SNMALLOC_REQUIRE_CONSTINIT static thread_local Alloc alloc;
      return alloc;
    }
  };

#  ifdef SNMALLOC_USE_PTHREAD_DESTRUCTORS
  /**
   * Used to give correct signature to teardown required by pthread_key.
   */
  inline void pthread_cleanup(void*)
  {
    ThreadAlloc::get().teardown();
  }

  /**
   * Used to give correct signature to teardown required by atexit.
   */
  inline void pthread_cleanup_main_thread()
  {
    ThreadAlloc::get().teardown();
  }

  /**
   * Used to give correct signature to the pthread call for the Singleton class.
   */
  inline void pthread_create(pthread_key_t* key) noexcept
  {
    pthread_key_create(key, &pthread_cleanup);
    // Main thread does not call pthread_cleanup if `main` returns or `exit` is
    // called, so use an atexit handler to guarantee that the cleanup is run at
    // least once.  If the main thread exits with `pthread_exit` then it will be
    // called twice but this case is already handled because other destructors
    // can cause the per-thread allocator to be recreated.
    atexit(&pthread_cleanup_main_thread);
  }

  /**
   * Performs thread local teardown for the allocator using the pthread library.
   *
   * This removes the dependence on the C++ runtime.
   */
  inline void register_clean_up()
  {
    Singleton<pthread_key_t, &pthread_create> p_key;
    // We need to set a non-null value, so that the destructor is called,
    // we never look at the value.
    pthread_setspecific(p_key.get(), reinterpret_cast<void*>(1));
#    ifdef SNMALLOC_TRACING
    std::cout << "Using pthread clean up" << std::endl;
#    endif
  }
#  elif defined(SNMALLOC_USE_CXX_THREAD_DESTRUCTORS)
  /**
   * This function is called by each thread once it starts using the
   * thread local allocator.
   *
   * This implementation depends on nothing outside of a working C++
   * environment and so should be the simplest for initial bringup on an
   * unsupported platform.
   */
  inline void register_clean_up()
  {
    static thread_local OnDestruct dummy(
      []() { ThreadAlloc::get().teardown(); });
    UNUSED(dummy);
#    ifdef SNMALLOC_TRACING
    std::cout << "Using C++ destructor clean up" << std::endl;
#    endif
  }
#  endif
#endif
} // namespace snmalloc

#ifdef SNMALLOC_USE_THREAD_CLEANUP
/**
 * Entry point that allows libc to call into the allocator for per-thread
 * cleanup.
 */
inline void _malloc_thread_cleanup()
{
  snmalloc::ThreadAlloc::get().teardown();
}

namespace snmalloc
{
  /**
   * No-op version of register_clean_up.  This is called unconditionally by
   * globalconfig but is not necessary when using a libc hook.
   */
  inline void register_clean_up() {}
}
#endif
