#pragma once
// IWYU pragma: private, include "rlbox.hpp"
// IWYU pragma: friend "rlbox_.*\.hpp"

#include <algorithm>
#include <atomic>
#ifdef RLBOX_MEASURE_TRANSITION_TIMES
#  include <chrono>
#endif
#include <cstdlib>
#include <limits>
#include <map>
#include <mutex>
#ifndef RLBOX_USE_CUSTOM_SHARED_LOCK
#  include <shared_mutex>
#endif
#ifdef RLBOX_MEASURE_TRANSITION_TIMES
#  include <sstream>
#  include <string>
#endif
#include <stdint.h>
#include <type_traits>
#include <utility>
#include <vector>

#include "rlbox_conversion.hpp"
#include "rlbox_helpers.hpp"
#include "rlbox_stdlib_polyfill.hpp"
#include "rlbox_struct_support.hpp"
#include "rlbox_type_traits.hpp"
#include "rlbox_wrapper_traits.hpp"

#ifdef RLBOX_MEASURE_TRANSITION_TIMES
using namespace std::chrono;
#endif

namespace rlbox {

namespace convert_fn_ptr_to_sandbox_equivalent_detail {
  template<typename T, typename T_Sbx>
  using conv = ::rlbox::detail::convert_to_sandbox_equivalent_t<T, T_Sbx>;

  template<typename T_Ret, typename... T_Args>
  using T_Func = T_Ret (*)(T_Args...);

  template<typename T_Sbx, typename T_Ret, typename... T_Args>
  T_Func<conv<T_Ret, T_Sbx>, conv<T_Args, T_Sbx>...> helper(
    T_Ret (*)(T_Args...));
}

#if defined(RLBOX_MEASURE_TRANSITION_TIMES) ||                                 \
  defined(RLBOX_TRANSITION_ACTION_OUT) || defined(RLBOX_TRANSITION_ACTION_IN)
enum class rlbox_transition
{
  INVOKE,
  CALLBACK
};
#endif
#ifdef RLBOX_MEASURE_TRANSITION_TIMES
struct rlbox_transition_timing
{
  rlbox_transition invoke;
  const char* name;
  void* ptr;
  int64_t time;

  std::string to_string()
  {
    std::ostringstream ret;
    if (invoke == rlbox_transition::INVOKE) {
      ret << name;
    } else {
      ret << "Callback " << ptr;
    }
    ret << " : " << time << "\n";

    return ret.str();
  }
};
#endif

#ifndef RLBOX_SINGLE_THREADED_INVOCATIONS
#  error                                                                       \
    "RLBox does not yet support threading. Please define RLBOX_SINGLE_THREADED_INVOCATIONS prior to including RLBox and ensure you are only using it from a single thread. If threading is required, please file a bug."
#endif

/**
 * @brief Encapsulation for sandboxes.
 *
 * @tparam T_Sbx Type of sandbox. For the null sandbox this is
 * `rlbox_noop_sandbox`
 */
template<typename T_Sbx>
class rlbox_sandbox : protected T_Sbx
{
  KEEP_CLASSES_FRIENDLY

private:
#ifdef RLBOX_MEASURE_TRANSITION_TIMES
  std::vector<rlbox_transition_timing> transition_times;
#endif

  static inline RLBOX_SHARED_LOCK(sandbox_list_lock);
  // The actual type of the vector is std::vector<rlbox_sandbox<T_Sbx>*>
  // However clang 5, 6 have bugs where compilation seg-faults on this type
  // So we just use this std::vector<void*>
  static inline std::vector<void*> sandbox_list;

  RLBOX_SHARED_LOCK(func_ptr_cache_lock);
  std::map<std::string, void*> func_ptr_map;

  app_pointer_map<typename T_Sbx::T_PointerType> app_ptr_map;

  // This variable tracks of the sandbox has already been created/destroyed.
  // APIs in this class should be called only when the sandbox is created.
  // However, it is expensive to check in APIs such as invoke or in the callback
  // interceptor. What's more, there could be time of check time of use issues
  // in the checks as well.
  // In general, we leave it up to the user to ensure these APIs are never
  // called prior to sandbox construction or after destruction. We perform some
  // conservative sanity checks, where they would not add too much overhead.
  enum class Sandbox_Status
  {
    NOT_CREATED,
    INITIALIZING,
    CREATED,
    CLEANING_UP
  };
  std::atomic<Sandbox_Status> sandbox_created = Sandbox_Status::NOT_CREATED;

  std::mutex callback_lock;
  std::vector<void*> callback_keys;

  void* transition_state = nullptr;

  template<typename T>
  using convert_fn_ptr_to_sandbox_equivalent_t = decltype(
    ::rlbox::convert_fn_ptr_to_sandbox_equivalent_detail::helper<T_Sbx>(
      std::declval<T>()));

  template<typename T>
  inline constexpr void check_invoke_param_type_is_ok()
  {
    using T_NoRef = std::remove_reference_t<T>;

    if_constexpr_named(cond1, detail::rlbox_is_wrapper_v<T_NoRef>)
    {
      if_constexpr_named(
        subcond1,
        !std::is_same_v<T_Sbx, detail::rlbox_get_wrapper_sandbox_t<T_NoRef>>)
      {
        rlbox_detail_static_fail_because(
          cond1 && subcond1,
          "Mixing tainted data from a different sandbox types. This could "
          "happen due to couple of different reasons.\n"
          "1. You are using 2 sandbox types for example'rlbox_noop_sandbox' "
          "and 'rlbox_lucet_sandbox', and are passing tainted data from one "
          "sandbox as parameters into a function call to the other sandbox. "
          "This is not allowed, unwrap the tainted data with copy_and_verify "
          "or other unwrapping APIs first.\n"
          "2. You have inadvertantly forgotten to set/remove "
          "RLBOX_USE_STATIC_CALLS depending on the sandbox type. Some sandbox "
          "types like rlbox_noop_sandbox require this to be set to a given "
          "value, while other types like rlbox_lucet_sandbox, require this not "
          "to be set.");
      }
    }
    else if_constexpr_named(cond2,
                            std::is_null_pointer_v<T_NoRef> ||
                              detail::is_fundamental_or_enum_v<T_NoRef>)
    {}
    else
    {
      constexpr auto unknownCase = !(cond1 || cond2);
      rlbox_detail_static_fail_because(
        unknownCase,
        "Arguments to a sandbox function call should be primitives or wrapped "
        "types like tainted, callbacks etc.");
    }
  }

  template<typename T>
  inline auto invoke_process_param(T&& param)
  {
    check_invoke_param_type_is_ok<T>();

    using T_NoRef = std::remove_reference_t<T>;

    if constexpr (detail::rlbox_is_tainted_opaque_v<T_NoRef>) {
      auto ret = from_opaque(param);
      return ret.UNSAFE_sandboxed(*this);
    } else if constexpr (detail::rlbox_is_wrapper_v<T_NoRef>) {
      return param.UNSAFE_sandboxed(*this);
    } else if constexpr (std::is_null_pointer_v<T_NoRef>) {
      tainted<void*, T_Sbx> ret = nullptr;
      return ret.UNSAFE_sandboxed(*this);
    } else if constexpr (detail::is_fundamental_or_enum_v<T_NoRef>) {
      // For unwrapped primitives, assign to a tainted var and then unwrap so
      // that we adjust for machine model
      tainted<T_NoRef, T_Sbx> ret = param;
      return ret.UNSAFE_sandboxed(*this);
    } else {
      rlbox_detail_static_fail_because(
        detail::true_v<T_NoRef>,
        "Only tainted types, callbacks or primitive values such as ints can be "
        "passed as parameters.\n"
        "To make a parameter tainted, try moving the allocation into the "
        "sandbox.\n"
        "If the parameter is a callback, try registering the callback via the "
        "register_callback API.");
    }
  }

  template<typename T, typename T_Arg>
  inline tainted<T, T_Sbx> sandbox_callback_intercept_convert_param(
    rlbox_sandbox<T_Sbx>& sandbox,
    const T_Arg& arg)
  {
    tainted<T, T_Sbx> ret;
    using namespace detail;
    convert_type<T_Sbx,
                 adjust_type_direction::TO_APPLICATION,
                 adjust_type_context::SANDBOX>(
      ret.get_raw_value_ref(),
      arg,
      nullptr /* example_unsandboxed_ptr */,
      &sandbox);
    return ret;
  }

  template<typename T_Ret, typename... T_Args>
  static detail::convert_to_sandbox_equivalent_t<T_Ret, T_Sbx>
  sandbox_callback_interceptor(
    detail::convert_to_sandbox_equivalent_t<T_Args, T_Sbx>... args)
  {
    std::pair<T_Sbx*, void*> context =
      T_Sbx::impl_get_executed_callback_sandbox_and_key();
    auto& sandbox = *(reinterpret_cast<rlbox_sandbox<T_Sbx>*>(context.first));
    auto key = context.second;

    using T_Func_Ret =
      std::conditional_t<std::is_void_v<T_Ret>, void, tainted<T_Ret, T_Sbx>>;
    using T_Func =
      T_Func_Ret (*)(rlbox_sandbox<T_Sbx>&, tainted<T_Args, T_Sbx>...);
    auto target_fn_ptr = reinterpret_cast<T_Func>(key);

#ifdef RLBOX_MEASURE_TRANSITION_TIMES
    high_resolution_clock::time_point enter_time = high_resolution_clock::now();
    auto on_exit = rlbox::detail::make_scope_exit([&] {
      auto exit_time = high_resolution_clock::now();
      int64_t ns = duration_cast<nanoseconds>(exit_time - enter_time).count();
      sandbox.transition_times.push_back(
        rlbox_transition_timing{ rlbox_transition::CALLBACK,
                                 nullptr /* func_name */,
                                 key /* func_ptr */,
                                 ns });
    });
#endif
#ifdef RLBOX_TRANSITION_ACTION_OUT
    RLBOX_TRANSITION_ACTION_OUT(rlbox_transition::CALLBACK,
                                nullptr /* func_name */,
                                key /* func_ptr */,
                                sandbox.transition_state);
#endif
#ifdef RLBOX_TRANSITION_ACTION_IN
    auto on_exit_transition = rlbox::detail::make_scope_exit([&] {
      RLBOX_TRANSITION_ACTION_IN(rlbox_transition::CALLBACK,
                                 nullptr /* func_name */,
                                 key /* func_ptr */,
                                 sandbox.transition_state);
    });
#endif
    if constexpr (std::is_void_v<T_Func_Ret>) {
      (*target_fn_ptr)(
        sandbox,
        sandbox.template sandbox_callback_intercept_convert_param<T_Args>(
          sandbox, args)...);
      return;
    } else {
      auto tainted_ret = (*target_fn_ptr)(
        sandbox,
        sandbox.template sandbox_callback_intercept_convert_param<T_Args>(
          sandbox, args)...);

      using namespace detail;
      convert_to_sandbox_equivalent_t<T_Ret, T_Sbx> ret;
      convert_type<T_Sbx,
                   adjust_type_direction::TO_SANDBOX,
                   adjust_type_context::SANDBOX>(
        ret,
        tainted_ret.get_raw_value_ref(),
        nullptr /* example_unsandboxed_ptr */,
        &sandbox);
      return ret;
    }
  }

  /**
   * @brief Unregister a callback function and disallow the sandbox from
   * calling this function henceforth.
   */
  template<typename T_Ret, typename... T_Args>
  inline void unregister_callback(void* key)
  {
    // Silently swallowing the failure is better here as RAII types may try to
    // cleanup callbacks after sandbox destruction
    if (sandbox_created.load() != Sandbox_Status::CREATED) {
      return;
    }

    this->template impl_unregister_callback<
      detail::convert_to_sandbox_equivalent_t<T_Ret, T_Sbx>,
      detail::convert_to_sandbox_equivalent_t<T_Args, T_Sbx>...>(key);

    std::lock_guard<std::mutex> lock(callback_lock);
    auto el_ref = std::find(callback_keys.begin(), callback_keys.end(), key);
    detail::dynamic_check(
      el_ref != callback_keys.end(),
      "Unexpected state. Unregistering a callback that was never registered.");
    callback_keys.erase(el_ref);
  }

  static T_Sbx* find_sandbox_from_example(const void* example_sandbox_ptr)
  {
    detail::dynamic_check(
      example_sandbox_ptr != nullptr,
      "Internal error: received a null example pointer. Please file a bug.");

    RLBOX_ACQUIRE_SHARED_GUARD(lock, sandbox_list_lock);
    for (auto sandbox_v : sandbox_list) {
      auto sandbox = reinterpret_cast<rlbox_sandbox<T_Sbx>*>(sandbox_v);
      if (sandbox->is_pointer_in_sandbox_memory(example_sandbox_ptr)) {
        return sandbox;
      }
    }

    return nullptr;
  }

  template<typename... T_Args>
  static auto impl_create_sandbox_helper(rlbox_sandbox<T_Sbx>* this_ptr,
                                         T_Args... args)
  {
    return this_ptr->impl_create_sandbox(std::forward<T_Args>(args)...);
  }

public:
  /**
   * @brief Unused member that allows the calling code to save data in a
   * "per-sandbox" storage. This can be useful to save context which is used
   * in callbacks.
   */
  void* sandbox_storage;

  /***** Function to adjust for custom machine models *****/

  template<typename T>
  using convert_to_sandbox_equivalent_nonclass_t =
    detail::convert_base_types_t<T,
                                 typename T_Sbx::T_ShortType,
                                 typename T_Sbx::T_IntType,
                                 typename T_Sbx::T_LongType,
                                 typename T_Sbx::T_LongLongType,
                                 typename T_Sbx::T_PointerType>;

  T_Sbx* get_sandbox_impl() { return this; }

  /**
   * @brief Create a new sandbox.
   *
   * @tparam T_Args Arguments passed to the underlying sandbox
   * implementation. For the null sandbox, no arguments are necessary.
   */
  template<typename... T_Args>
  inline bool create_sandbox(T_Args... args)
  {
#ifdef RLBOX_MEASURE_TRANSITION_TIMES
    // Warm up the timer. The first call is always slow (at least on the test
    // platform)
    for (int i = 0; i < 10; i++) {
      auto val = high_resolution_clock::now();
      RLBOX_UNUSED(val);
    }
#endif
    auto expected = Sandbox_Status::NOT_CREATED;
    bool success = sandbox_created.compare_exchange_strong(
      expected, Sandbox_Status::INITIALIZING /* desired */);
    detail::dynamic_check(
      success,
      "create_sandbox called when sandbox already created/is being "
      "created concurrently");

    using T_Result = rlbox::detail::polyfill::invoke_result_t<
      decltype(impl_create_sandbox_helper<T_Args...>),
      decltype(this),
      T_Args...>;

    bool created = true;
    if constexpr (std::is_same_v<T_Result, void>) {
      this->impl_create_sandbox(std::forward<T_Args>(args)...);
    } else if constexpr (std::is_same_v<T_Result, bool>) {
      created = this->impl_create_sandbox(std::forward<T_Args>(args)...);
    } else {
      rlbox_detail_static_fail_because(
        (!std::is_same_v<T_Result, void> && !std::is_same_v<T_Result, bool>),
        "Expected impl_create_sandbox to return void or a boolean");
    }

    if (created) {
      sandbox_created.store(Sandbox_Status::CREATED);
      RLBOX_ACQUIRE_UNIQUE_GUARD(lock, sandbox_list_lock);
      sandbox_list.push_back(this);
    }

    return created;
  }

  /**
   * @brief Destroy sandbox and reclaim any memory.
   */
  inline auto destroy_sandbox()
  {
    auto expected = Sandbox_Status::CREATED;
    bool success = sandbox_created.compare_exchange_strong(
      expected, Sandbox_Status::CLEANING_UP /* desired */);

    detail::dynamic_check(
      success,
      "destroy_sandbox called without sandbox creation/is being "
      "destroyed concurrently");

    {
      RLBOX_ACQUIRE_UNIQUE_GUARD(lock, sandbox_list_lock);
      auto el_ref = std::find(sandbox_list.begin(), sandbox_list.end(), this);
      detail::dynamic_check(
        el_ref != sandbox_list.end(),
        "Unexpected state. Destroying a sandbox that was never initialized.");
      sandbox_list.erase(el_ref);
    }

    sandbox_created.store(Sandbox_Status::NOT_CREATED);
    return this->impl_destroy_sandbox();
  }

  template<typename T>
  inline T get_unsandboxed_pointer(
    convert_to_sandbox_equivalent_nonclass_t<T> p) const
  {
    static_assert(std::is_pointer_v<T>);
    if (p == 0) {
      return nullptr;
    }
    auto ret = this->template impl_get_unsandboxed_pointer<T>(p);
    return reinterpret_cast<T>(ret);
  }

  template<typename T>
  inline convert_to_sandbox_equivalent_nonclass_t<T> get_sandboxed_pointer(
    const void* p) const
  {
    static_assert(std::is_pointer_v<T>);
    if (p == nullptr) {
      return 0;
    }
    return this->template impl_get_sandboxed_pointer<T>(p);
  }

  template<typename T>
  static inline T get_unsandboxed_pointer_no_ctx(
    convert_to_sandbox_equivalent_nonclass_t<T> p,
    const void* example_unsandboxed_ptr)
  {
    static_assert(std::is_pointer_v<T>);
    if (p == 0) {
      return nullptr;
    }
    auto ret = T_Sbx::template impl_get_unsandboxed_pointer_no_ctx<T>(
      p, example_unsandboxed_ptr, find_sandbox_from_example);
    return reinterpret_cast<T>(ret);
  }

  template<typename T>
  static inline convert_to_sandbox_equivalent_nonclass_t<T>
  get_sandboxed_pointer_no_ctx(const void* p,
                               const void* example_unsandboxed_ptr)
  {
    static_assert(std::is_pointer_v<T>);
    if (p == nullptr) {
      return 0;
    }
    return T_Sbx::template impl_get_sandboxed_pointer_no_ctx<T>(
      p, example_unsandboxed_ptr, find_sandbox_from_example);
  }

  /**
   * @brief Allocate a new pointer that is accessible to both the application
   * and sandbox. The pointer is allocated in sandbox memory.
   *
   * @tparam T The type of the pointer you want to create. If T=int, this
   * would return a pointer to an int.
   *
   * @return tainted<T*, T_Sbx> Tainted pointer accessible to the application
   * and sandbox.
   */
  template<typename T>
  inline tainted<T*, T_Sbx> malloc_in_sandbox()
  {
    const uint32_t defaultCount = 1;
    return malloc_in_sandbox<T>(defaultCount);
  }

  /**
   * @brief Allocate an array that is accessible to both the application
   * and sandbox. The pointer is allocated in sandbox memory.
   *
   * @tparam T The type of the array elements you want to create. If T=int, this
   * would return a pointer to an array of ints.
   *
   * @param count The number of array elements to allocate.
   *
   * @return tainted<T*, T_Sbx> Tainted pointer accessible to the application
   * and sandbox.
   */
  template<typename T>
  inline tainted<T*, T_Sbx> malloc_in_sandbox(uint32_t count)
  {
    // Silently swallowing the failure is better here as RAII types may try to
    // malloc after sandbox destruction
    if (sandbox_created.load() != Sandbox_Status::CREATED) {
      return tainted<T*, T_Sbx>::internal_factory(nullptr);
    }

    detail::dynamic_check(count != 0, "Malloc tried to allocate 0 bytes");
    if constexpr (sizeof(T) >= std::numeric_limits<uint32_t>::max()) {
      rlbox_detail_static_fail_because(sizeof(T) >=
                                         std::numeric_limits<uint32_t>::max(),
                                       "Tried to allocate an object over 4GB.");
    }
    auto total_size = static_cast<uint64_t>(sizeof(T)) * count;
    if constexpr (sizeof(size_t) == 4) {
      // On a 32-bit platform, we need to make sure that total_size is not >=4GB
      detail::dynamic_check(total_size < std::numeric_limits<uint32_t>::max(),
                            "Tried to allocate memory over 4GB");
    } else if constexpr (sizeof(size_t) != 8) {
      // Double check we are on a 64-bit platform
      // Note for static checks we need to have some dependence on T, so adding
      // a dummy
      constexpr bool dummy = sizeof(T) >= 0;
      rlbox_detail_static_fail_because(dummy && sizeof(size_t) != 8,
                                       "Expected 32 or 64 bit platform.");
    }
    auto ptr_in_sandbox = this->impl_malloc_in_sandbox(total_size);
    auto ptr = get_unsandboxed_pointer<T*>(ptr_in_sandbox);
    if (!ptr) {
      return tainted<T*, T_Sbx>(nullptr);
    }
    detail::dynamic_check(is_pointer_in_sandbox_memory(ptr),
                          "Malloc returned pointer outside the sandbox memory");
    auto ptr_end = reinterpret_cast<uintptr_t>(ptr + (count - 1));
    detail::dynamic_check(
      is_in_same_sandbox(ptr, reinterpret_cast<void*>(ptr_end)),
      "Malloc returned a pointer whose range goes beyond sandbox memory");
    auto cast_ptr = reinterpret_cast<T*>(ptr);
    return tainted<T*, T_Sbx>::internal_factory(cast_ptr);
  }

  /**
   * @brief Free the memory referenced by the tainted pointer.
   *
   * @param ptr Pointer to sandbox memory to free.
   */
  template<typename T>
  inline void free_in_sandbox(tainted<T*, T_Sbx> ptr)
  {
    // Silently swallowing the failure is better here as RAII types may try to
    // free after sandbox destruction
    if (sandbox_created.load() != Sandbox_Status::CREATED) {
      return;
    }

    this->impl_free_in_sandbox(ptr.get_raw_sandbox_value(*this));
  }

  /**
   * @brief Free the memory referenced by a tainted_volatile pointer ref.
   *
   * @param ptr_ref Pointer reference to sandbox memory to free.
   */
  template<typename T>
  inline void free_in_sandbox(tainted_volatile<T, T_Sbx>& ptr_ref)
  {
    tainted<T, T_Sbx> ptr = ptr_ref;
    free_in_sandbox(ptr);
  }

  /**
   * @brief Free the memory referenced by a tainted_opaque pointer.
   *
   * @param ptr_opaque Opaque pointer to sandbox memory to free.
   */
  template<typename T>
  inline void free_in_sandbox(tainted_opaque<T, T_Sbx> ptr_opaque)
  {
    tainted<T, T_Sbx> ptr = from_opaque(ptr_opaque);
    free_in_sandbox(ptr);
  }

  /**
   * @brief Check if two pointers are in the same sandbox.
   * For the null-sandbox, this always returns true.
   */
  static inline bool is_in_same_sandbox(const void* p1, const void* p2)
  {
    const size_t num_args =
      detail::func_arg_nums_v<decltype(T_Sbx::impl_is_in_same_sandbox)>;
    if constexpr (num_args == 2) {
      return T_Sbx::impl_is_in_same_sandbox(p1, p2);
    } else {
      return T_Sbx::impl_is_in_same_sandbox(p1, p2, find_sandbox_from_example);
    }
  }

  /**
   * @brief Check if the pointer points to this sandbox's memory.
   * For the null-sandbox, this always returns true.
   */
  inline bool is_pointer_in_sandbox_memory(const void* p)
  {
    return this->impl_is_pointer_in_sandbox_memory(p);
  }

  /**
   * @brief Check if the pointer points to application memory.
   * For the null-sandbox, this always returns true.
   */
  inline bool is_pointer_in_app_memory(const void* p)
  {
    return this->impl_is_pointer_in_app_memory(p);
  }

  inline size_t get_total_memory() { return this->impl_get_total_memory(); }

  inline void* get_memory_location()
  {
    return this->impl_get_memory_location();
  }

  void* get_transition_state() { return transition_state; }

  void set_transition_state(void* new_state) { transition_state = new_state; }

  /**
   * @brief For internal use only.
   * Grant access of the passed in buffer in to the sandbox instance. Called by
   * internal APIs only if the underlying sandbox supports
   * can_grant_deny_access by including the line
   * ```
   * using can_grant_deny_access = void;
   * ```
   */
  template<typename T>
  inline tainted<T*, T_Sbx> INTERNAL_grant_access(T* src,
                                                  size_t num,
                                                  bool& success)
  {
    auto ret = this->impl_grant_access(src, num, success);
    return tainted<T*, T_Sbx>::internal_factory(ret);
  }

  /**
   * @brief For internal use only.
   * Grant access of the passed in buffer in to the sandbox instance. Called by
   * internal APIs only if the underlying sandbox supports
   * can_grant_deny_access by including the line
   * ```
   * using can_grant_deny_access = void;
   * ```
   */
  template<typename T>
  inline T* INTERNAL_deny_access(tainted<T*, T_Sbx> src,
                                 size_t num,
                                 bool& success)
  {
    auto ret =
      this->impl_deny_access(src.INTERNAL_unverified_safe(), num, success);
    return ret;
  }

  void* lookup_symbol(const char* func_name)
  {
    {
      RLBOX_ACQUIRE_SHARED_GUARD(lock, func_ptr_cache_lock);

      auto func_ptr_ref = func_ptr_map.find(func_name);
      if (func_ptr_ref != func_ptr_map.end()) {
        return func_ptr_ref->second;
      }
    }

    void* func_ptr = this->impl_lookup_symbol(func_name);
    RLBOX_ACQUIRE_UNIQUE_GUARD(lock, func_ptr_cache_lock);
    func_ptr_map[func_name] = func_ptr;
    return func_ptr;
  }

  void* internal_lookup_symbol(const char* func_name)
  {
    {
      RLBOX_ACQUIRE_SHARED_GUARD(lock, func_ptr_cache_lock);

      auto func_ptr_ref = func_ptr_map.find(func_name);
      if (func_ptr_ref != func_ptr_map.end()) {
        return func_ptr_ref->second;
      }
    }

    void* func_ptr = 0;
    if constexpr (rlbox::detail::
                    has_member_using_needs_internal_lookup_symbol_v<T_Sbx>) {
      func_ptr = this->impl_internal_lookup_symbol(func_name);
    } else {
      func_ptr = this->impl_lookup_symbol(func_name);
    }
    RLBOX_ACQUIRE_UNIQUE_GUARD(lock, func_ptr_cache_lock);
    func_ptr_map[func_name] = func_ptr;
    return func_ptr;
  }

  // this is an internal function invoked from macros, so it has be public
  template<typename T, typename... T_Args>
  inline auto INTERNAL_invoke_with_func_name(const char* func_name,
                                             T_Args&&... params)
  {
    return INTERNAL_invoke_with_func_ptr<T, T_Args...>(
      func_name, lookup_symbol(func_name), std::forward<T_Args>(params)...);
  }

  // this is an internal function invoked from macros, so it has be public
  // Explicitly don't use inline on this, as this adds a lot of instructions
  // prior to function call. What's more, by not inlining, different function
  // calls with the same signature can share the same code segments for
  // sandboxed function execution in the binary
  template<typename T, typename... T_Args>
  auto INTERNAL_invoke_with_func_ptr(const char* func_name,
                                     void* func_ptr,
                                     T_Args&&... params)
  {
    // unused in some paths
    RLBOX_UNUSED(func_name);
#ifdef RLBOX_MEASURE_TRANSITION_TIMES
    auto enter_time = high_resolution_clock::now();
    auto on_exit = rlbox::detail::make_scope_exit([&] {
      auto exit_time = high_resolution_clock::now();
      int64_t ns = duration_cast<nanoseconds>(exit_time - enter_time).count();
      transition_times.push_back(rlbox_transition_timing{
        rlbox_transition::INVOKE, func_name, func_ptr, ns });
    });
#endif
#ifdef RLBOX_TRANSITION_ACTION_IN
    RLBOX_TRANSITION_ACTION_IN(
      rlbox_transition::INVOKE, func_name, func_ptr, transition_state);
#endif
#ifdef RLBOX_TRANSITION_ACTION_OUT
    auto on_exit_transition = rlbox::detail::make_scope_exit([&] {
      RLBOX_TRANSITION_ACTION_OUT(
        rlbox_transition::INVOKE, func_name, func_ptr, transition_state);
    });
#endif
    (check_invoke_param_type_is_ok<T_Args>(), ...);

    static_assert(
      rlbox::detail::polyfill::is_invocable_v<
        T,
        detail::rlbox_remove_wrapper_t<std::remove_reference_t<T_Args>>...>,
      "Mismatched arguments types for function");

    using T_Result = rlbox::detail::polyfill::invoke_result_t<
      T,
      detail::rlbox_remove_wrapper_t<std::remove_reference_t<T_Args>>...>;

    using T_Converted =
      std::remove_pointer_t<convert_fn_ptr_to_sandbox_equivalent_t<T*>>;

    if constexpr (std::is_void_v<T_Result>) {
      this->template impl_invoke_with_func_ptr<T>(
        reinterpret_cast<T_Converted*>(func_ptr),
        invoke_process_param(params)...);
      return;
    } else {
      auto raw_result = this->template impl_invoke_with_func_ptr<T>(
        reinterpret_cast<T_Converted*>(func_ptr),
        invoke_process_param(params)...);
      tainted<T_Result, T_Sbx> wrapped_result;
      using namespace detail;
      convert_type<T_Sbx,
                   adjust_type_direction::TO_APPLICATION,
                   adjust_type_context::SANDBOX>(
        wrapped_result.get_raw_value_ref(),
        raw_result,
        nullptr /* example_unsandboxed_ptr */,
        this /* sandbox_ptr */);
      return wrapped_result;
    }
  }

  // Useful in the porting stage to temporarily allow non tainted pointers to go
  // through. This will only ever work in the rlbox_noop_sandbox. Any sandbox
  // that actually enforces isolation will crash here.
  template<typename T2>
  tainted<T2, T_Sbx> UNSAFE_accept_pointer(T2 ptr)
  {
    static_assert(std::is_pointer_v<T2>,
                  "UNSAFE_accept_pointer expects a pointer param");
    tainted<T2, T_Sbx> ret;
    ret.assign_raw_pointer(*this, ptr);
    return ret;
  }

  template<typename T_Ret, typename... T_Args>
  using T_Cb_no_wrap = detail::rlbox_remove_wrapper_t<T_Ret>(
    detail::rlbox_remove_wrapper_t<T_Args>...);

  template<typename T_Ret>
  sandbox_callback<T_Cb_no_wrap<T_Ret>*, T_Sbx> register_callback(T_Ret (*)())
  {
    rlbox_detail_static_fail_because(
      detail::true_v<T_Ret>,
      "Modify the callback to change the first parameter to a sandbox. "
      "For instance if a callback has type\n\n"
      "int foo() {...}\n\n"
      "Change this to \n\n"
      "tainted<int, T_Sbx> foo(rlbox_sandbox<T_Sbx>& sandbox) {...}\n");

    // this is never executed, but we need it for the function to type-check
    std::abort();
  }

  /**
   * @brief Expose a callback function to the sandboxed code.
   *
   * @param func_ptr The callback to expose.
   *
   * @tparam T_RL   Sandbox reference type (first argument).
   * @tparam T_Ret  Return type of callback. Must be tainted or void.
   * @tparam T_Args Types of remaining callback arguments. Must be tainted.
   *
   * @return Wrapped callback function pointer that can be passed to the
   * sandbox.
   */
  template<typename T_RL, typename T_Ret, typename... T_Args>
  sandbox_callback<T_Cb_no_wrap<T_Ret, T_Args...>*, T_Sbx> register_callback(
    T_Ret (*func_ptr)(T_RL, T_Args...))
  {
    // Some branches don't use the param
    RLBOX_UNUSED(func_ptr);

    if_constexpr_named(cond1, !std::is_same_v<T_RL, rlbox_sandbox<T_Sbx>&>)
    {
      rlbox_detail_static_fail_because(
        cond1,
        "Modify the callback to change the first parameter to a sandbox. "
        "For instance if a callback has type\n\n"
        "int foo(int a, int b) {...}\n\n"
        "Change this to \n\n"
        "tainted<int, T_Sbx> foo(rlbox_sandbox<T_Sbx>& sandbox, "
        "tainted<int, T_Sbx> a, tainted<int, T_Sbx> b) {...}\n");
    }
    else if_constexpr_named(
      cond2, !(detail::rlbox_is_tainted_or_opaque_v<T_Args> && ...))
    {
      rlbox_detail_static_fail_because(
        cond2,
        "Change all arguments to the callback have to be tainted or "
        "tainted_opaque. "
        "For instance if a callback has type\n\n"
        "int foo(int a, int b) {...}\n\n"
        "Change this to \n\n"
        "tainted<int, T_Sbx> foo(rlbox_sandbox<T_Sbx>& sandbox, "
        "tainted<int, T_Sbx> a, tainted<int, T_Sbx> b) {...}\n");
    }
    else if_constexpr_named(
      cond3, (std::is_array_v<detail::rlbox_remove_wrapper_t<T_Args>> || ...))
    {
      rlbox_detail_static_fail_because(
        cond3,
        "Change all static array arguments to the callback to be pointers. "
        "For instance if a callback has type\n\n"
        "int foo(int a[4]) {...}\n\n"
        "Change this to \n\n"
        "tainted<int, T_Sbx> foo(rlbox_sandbox<T_Sbx>& sandbox, "
        "tainted<int*, T_Sbx> a) {...}\n");
    }
    else if_constexpr_named(
      cond4,
      !(std::is_void_v<T_Ret> || detail::rlbox_is_tainted_or_opaque_v<T_Ret>))
    {
      rlbox_detail_static_fail_because(
        cond4,
        "Change the callback return type to be tainted or tainted_opaque if it "
        "is not void. "
        "For instance if a callback has type\n\n"
        "int foo(int a, int b) {...}\n\n"
        "Change this to \n\n"
        "tainted<int, T_Sbx> foo(rlbox_sandbox<T_Sbx>& sandbox, "
        "tainted<int, T_Sbx> a, tainted<int, T_Sbx> b) {...}\n");
    }
    else
    {
      detail::dynamic_check(
        sandbox_created.load() == Sandbox_Status::CREATED,
        "register_callback called without sandbox creation");

      // Need unique key for each callback we register - just use the func addr
      void* unique_key = reinterpret_cast<void*>(func_ptr);

      // Make sure that the user hasn't previously registered this function...
      // If they have, we would returning 2 owning types (sandbox_callback) to
      // the same callback which would be bad
      {
        std::lock_guard<std::mutex> lock(callback_lock);
        bool exists =
          std::find(callback_keys.begin(), callback_keys.end(), unique_key) !=
          callback_keys.end();
        detail::dynamic_check(
          !exists, "You have previously already registered this callback.");
        callback_keys.push_back(unique_key);
      }

      auto callback_interceptor =
        sandbox_callback_interceptor<detail::rlbox_remove_wrapper_t<T_Ret>,
                                     detail::rlbox_remove_wrapper_t<T_Args>...>;

      auto callback_trampoline = this->template impl_register_callback<
        detail::convert_to_sandbox_equivalent_t<
          detail::rlbox_remove_wrapper_t<T_Ret>,
          T_Sbx>,
        detail::convert_to_sandbox_equivalent_t<
          detail::rlbox_remove_wrapper_t<T_Args>,
          T_Sbx>...>(unique_key, reinterpret_cast<void*>(callback_interceptor));

      auto tainted_func_ptr = reinterpret_cast<
        detail::rlbox_tainted_opaque_to_tainted_t<T_Ret, T_Sbx> (*)(
          T_RL, detail::rlbox_tainted_opaque_to_tainted_t<T_Args, T_Sbx>...)>(
        reinterpret_cast<void*>(func_ptr));

      auto ret = sandbox_callback<T_Cb_no_wrap<T_Ret, T_Args...>*, T_Sbx>(
        this,
        tainted_func_ptr,
        callback_interceptor,
        callback_trampoline,
        unique_key);
      return ret;
    }
  }

  // this is an internal function invoked from macros, so it has be public
  template<typename T>
  inline tainted<T*, T_Sbx> INTERNAL_get_sandbox_function_name(
    const char* func_name)
  {
    return INTERNAL_get_sandbox_function_ptr<T>(
      internal_lookup_symbol(func_name));
  }

  // this is an internal function invoked from macros, so it has be public
  template<typename T>
  inline tainted<T*, T_Sbx> INTERNAL_get_sandbox_function_ptr(void* func_ptr)
  {
    return tainted<T*, T_Sbx>::internal_factory(reinterpret_cast<T*>(func_ptr));
  }

  /**
   * @brief Create a "fake" pointer referring to a location in the application
   * memory
   *
   * @param ptr The pointer to refer to
   *
   * @return The app_pointer object that refers to this location.
   */
  template<typename T>
  app_pointer<T*, T_Sbx> get_app_pointer(T* ptr)
  {
    auto max_ptr = (typename T_Sbx::T_PointerType)(get_total_memory() - 1);
    auto idx = app_ptr_map.get_app_pointer_idx((void*)ptr, max_ptr);
    auto idx_as_ptr = this->template impl_get_unsandboxed_pointer<T>(idx);
    // Right now we simply assume that any integer can be converted to a valid
    // pointer in the sandbox This may not be true for some sandboxing mechanism
    // plugins in the future In this case, we will have to come up with
    // something more clever to construct indexes that look like valid pointers
    // Add a check for now to make sure things work fine
    detail::dynamic_check(is_pointer_in_sandbox_memory(idx_as_ptr),
                          "App pointers are not currently supported for this "
                          "rlbox sandbox plugin. Please file a bug.");
    auto ret = app_pointer<T*, T_Sbx>(
      &app_ptr_map, idx, reinterpret_cast<T*>(idx_as_ptr));
    return ret;
  }

  /**
   * @brief The mirror of get_app_pointer. Take a tainted pointer which is
   * actually an app_pointer, and get the application location being pointed to
   *
   * @param tainted_ptr The tainted pointer that is actually an app_pointer
   *
   * @return The original location being referred to by the app_ptr
   */
  template<typename T>
  T* lookup_app_ptr(tainted<T*, T_Sbx> tainted_ptr)
  {
    auto idx = tainted_ptr.get_raw_sandbox_value(*this);
    void* ret = app_ptr_map.lookup_index(idx);
    return reinterpret_cast<T*>(ret);
  }

#ifdef RLBOX_MEASURE_TRANSITION_TIMES
  inline std::vector<rlbox_transition_timing>&
  process_and_get_transition_times()
  {
    return transition_times;
  }
  inline int64_t get_total_ns_time_in_sandbox_and_transitions()
  {
    int64_t ret = 0;
    for (auto& transition_time : transition_times) {
      if (transition_time.invoke == rlbox_transition::INVOKE) {
        ret += transition_time.time;
      } else {
        ret -= transition_time.time;
      }
    }
    return ret;
  }
  inline void clear_transition_times() { transition_times.clear(); }
#endif
};

#if defined(__clang__)
#  pragma clang diagnostic push
#  pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments"
#elif defined(__GNUC__) || defined(__GNUG__)
// Can't turn off the variadic macro warning emitted from -pedantic so use a
// hack to stop GCC emitting warnings for the reminder of this file
#  pragma GCC system_header
#elif defined(_MSC_VER)
// Doesn't seem to emit the warning
#else
// Don't know the compiler... just let it go through
#endif

/**
 * @def  invoke_sandbox_function
 * @brief Call sandbox function.
 *
 * @param func_name The sandboxed library function to call.
 * @param ... Arguments to function should be simple or tainted values.
 * @return Tainted value or void.
 */
#ifdef RLBOX_USE_STATIC_CALLS

#  define sandbox_lookup_symbol_helper(prefix, func_name) prefix(func_name)

#  define invoke_sandbox_function(func_name, ...)                              \
    template INTERNAL_invoke_with_func_ptr<decltype(func_name)>(               \
      #func_name,                                                              \
      sandbox_lookup_symbol_helper(RLBOX_USE_STATIC_CALLS(), func_name),       \
      ##__VA_ARGS__)

#  define get_sandbox_function_address(func_name)                              \
    template INTERNAL_get_sandbox_function_ptr<decltype(func_name)>(           \
      sandbox_lookup_symbol_helper(RLBOX_USE_STATIC_CALLS(), func_name))

#else

#  define invoke_sandbox_function(func_name, ...)                              \
    template INTERNAL_invoke_with_func_name<decltype(func_name)>(              \
      #func_name, ##__VA_ARGS__)

#  define get_sandbox_function_address(func_name)                              \
    template INTERNAL_get_sandbox_function_name<decltype(func_name)>(#func_name)

#endif

#define sandbox_invoke(sandbox, func_name, ...)                                \
  (sandbox).invoke_sandbox_function(func_name, ##__VA_ARGS__)

#define sandbox_function_address(sandbox, func_name)                           \
  (sandbox).get_sandbox_function_address(func_name)

#if defined(__clang__)
#  pragma clang diagnostic pop
#else
#endif

}
