/*
 * Copyright (c) 2014, 2023, Oracle and/or its affiliates.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2.0,
 * as published by the Free Software Foundation.
 *
 * This program is also distributed with certain software (including
 * but not limited to OpenSSL) that is licensed under separate terms, as
 * designated in a particular file or component or in included license
 * documentation.  The authors of MySQL hereby grant you an additional
 * permission to link the program and your derivative works with the
 * separately licensed software that they have included with MySQL.
 * This program is distributed in the hope that it will be useful,  but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
 * the GNU General Public License, version 2.0, for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include "scripting/jscript_context.h"

#include "my_config.h"

#if defined __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
#endif

#include <libplatform/libplatform.h>

#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif

#include <list>
#include <stack>

#include "mysqlshdk/include/shellcore/console.h"
#include "scripting/module_registry.h"
#include "scripting/object_factory.h"
#include "scripting/object_registry.h"

#include "scripting/jscript_array_wrapper.h"
#include "scripting/jscript_function_wrapper.h"
#include "scripting/jscript_map_wrapper.h"
#include "scripting/jscript_object_wrapper.h"
#include "scripting/types_jscript.h"
#include "utils/utils_general.h"
#include "utils/utils_path.h"
#include "utils/utils_string.h"

#include "scripting/jscript_core_definitions.h"
#include "scripting/jscript_type_conversion.h"

#include "mysqlshdk/include/shellcore/base_shell.h"  // FIXME

#include <cerrno>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#ifdef WIN32
#include <windows.h>
#endif
#include "utils/utils_file.h"

namespace v8 {
namespace debug {

enum class EvaluateGlobalMode {
  kDefault,
  kDisableBreaks,
  kDisableBreaksAndThrowOnSideEffect
};

// this symbol is exported from V8, but including the header file requires a
// bunch of other files from the V8's source tree, declare it here instead
v8::MaybeLocal<v8::Value> EvaluateGlobal(v8::Isolate *isolate,
                                         v8::Local<v8::String> source,
                                         EvaluateGlobalMode mode,
                                         bool repl_mode);

}  // namespace debug
}  // namespace v8

namespace shcore {

namespace {

using V8_args = v8::FunctionCallbackInfo<v8::Value>;

std::unique_ptr<v8::Platform> g_platform;

const std::string k_origin_shell = "(shell)";

#ifdef ENABLE_V8_TRACING

std::ofstream g_trace_file;

std::unique_ptr<v8::platform::tracing::TracingController> initialize_tracing() {
  g_trace_file.open(
      shcore::path::join_path(shcore::get_user_config_path(), "v8-trace.json"));

  auto controller =
      std::make_unique<v8::platform::tracing::TracingController>();
  controller->Initialize(
      v8::platform::tracing::TraceBuffer::CreateTraceBufferRingBuffer(
          v8::platform::tracing::TraceBuffer::kRingBufferChunks,
          v8::platform::tracing::TraceWriter::CreateJSONTraceWriter(
              g_trace_file)));

  auto tracing_config =
      v8::platform::tracing::TraceConfig::CreateDefaultTraceConfig();
  tracing_config->AddIncludedCategory("disabled-by-default-v8.runtime_stats");
  tracing_config->AddIncludedCategory("disabled-by-default-v8.gc_stats");
  tracing_config->SetTraceRecordMode(
      v8::platform::tracing::TraceRecordMode::RECORD_CONTINUOUSLY);
  controller->StartTracing(tracing_config);

  return controller;
}

void finalize_tracing() { g_trace_file.close(); }

#else  // !ENABLE_V8_TRACING

std::unique_ptr<v8::platform::tracing::TracingController> initialize_tracing() {
  return {};
}

void finalize_tracing() {}

#endif  // !ENABLE_V8_TRACING

void print_exception(const std::string &text) {
  mysqlsh::current_console()->print_diag(text);
}

class Current_script {
 public:
  Current_script() : m_root(path::getcwd()) {
    // fake top-level script
    push("mysqlsh");
  }

  void push(const std::string &script) {
    m_scripts.push(path::is_absolute(script)
                       ? script
                       : path::normalize(path::join_path(m_root, script)));
  }

  void pop() { m_scripts.pop(); }

  std::string current_script() const { return m_scripts.top(); }

  std::string current_folder() const { return path::dirname(current_script()); }

 private:
  std::string m_root;
  std::stack<std::string> m_scripts;
};

v8::MaybeLocal<v8::Value> evaluate_repl(v8::Isolate *isolate,
                                        v8::Local<v8::String> source) {
  return v8::debug::EvaluateGlobal(
      isolate, source, v8::debug::EvaluateGlobalMode::kDefault, true);
}

}  // namespace

/** Initializer for JS stuff
 *
 * Must be called once when the program is started.
 */
void SHCORE_PUBLIC JScript_context_init() {
  if (!g_platform) {
    g_platform = v8::platform::NewDefaultPlatform(
        1,  // number of worker threads to allocate for background jobs
        v8::platform::IdleTaskSupport::kDisabled,
        v8::platform::InProcessStackDumping::kDisabled, initialize_tracing());
    v8::V8::InitializePlatform(g_platform.get());
    v8::V8::Initialize();
  }
}

void SHCORE_PUBLIC JScript_context_fini() {
  if (g_platform) {
    v8::V8::Dispose();
    v8::V8::DisposePlatform();
    g_platform.reset(nullptr);

    finalize_tracing();
  }
}

class JScript_context::Impl {
 public:
  class Script_scope {
   public:
    Script_scope(Impl *owner, const std::string &s) : m_owner(owner) {
      m_owner->m_current_script.push(s);
    }

    Script_scope(const Script_scope &) = delete;
    Script_scope(Script_scope &&) = default;

    Script_scope &operator=(const Script_scope &) = delete;
    Script_scope &operator=(Script_scope &&) = default;

    ~Script_scope() {
      if (m_owner) {
        m_owner->m_current_script.pop();
      }
    }

   private:
    Impl *m_owner = nullptr;
  };

  explicit Impl(JScript_context *owner);
  ~Impl();

  static v8::Local<v8::String> v8_string(v8::Isolate *isolate,
                                         const char *data);
  static v8::Local<v8::String> v8_string(v8::Isolate *isolate,
                                         const std::string &data);
  static std::string to_string(v8::Isolate *isolate, v8::Local<v8::Value> obj);

  v8::Local<v8::String> v8_string(const char *data) const;
  v8::Local<v8::String> v8_string(const std::string &data) const;
  std::string to_string(v8::Local<v8::Value> obj) const;

  v8::Local<v8::ArrayBuffer> v8_array_buffer(const std::string &data);
  v8::Local<v8::ArrayBuffer> v8_array_buffer(const char *data, size_t length);

  v8::Isolate *isolate() const;
  v8::Local<v8::Context> context() const;

  void set_global_item(const std::string &global_name,
                       const std::string &item_name,
                       const v8::Local<v8::Value> &value) const;
  void set_global(const std::string &name,
                  const v8::Local<v8::Value> &value) const;
  v8::Local<v8::Value> get_global(const std::string &name) const;

  void terminate();
  bool is_terminating() const;
  void clear_is_terminating();

  std::weak_ptr<JScript_function_storage> store(
      v8::Local<v8::Function> function);
  void erase(const std::shared_ptr<JScript_function_storage> &function);

  Script_scope enter_script(const std::string &s);

 private:
  // global functions exposed to JS
  static void f_repr(const V8_args &args);
  static void f_unrepr(const V8_args &args);
  static void f_type(const V8_args &args);
  static void f_print(const V8_args &args, bool new_line);
  static void f_source(const V8_args &args);
  // private global functions
  static void f_list_native_modules(const V8_args &args);
  static void f_load_native_module(const V8_args &args);
  static void f_load_module(const V8_args &args);
  static void f_current_module_folder(const V8_args &args);

  /*
   * load_core_module loads the content of the given module file
   * and inserts the definitions on the JS globals.
   */
  void load_core_module() const;
  void load_module(const std::string &path, v8::Local<v8::Value> module,
                   bool *js_exception = nullptr);

  v8::Local<v8::FunctionTemplate> wrap_callback(
      v8::FunctionCallback callback) const;

  v8::Local<v8::Context> copy_global_context() const;
  void delete_context(v8::Local<v8::Context> context) const;
  void store_context(v8::Local<v8::Context> context);

  Value convert(const v8::Local<v8::Value> &value) const;
  v8::Local<v8::Value> convert(const Value &value) const;
  v8::Local<v8::String> type_info(v8::Local<v8::Value> value) const;

  JScript_context *m_owner = nullptr;
  v8::Isolate *m_isolate = nullptr;
  v8::Persistent<v8::Context> m_context;
  std::unique_ptr<v8::ArrayBuffer::Allocator> m_allocator;
  bool m_terminating = false;
  std::vector<v8::Global<v8::Context>> m_stored_contexts;
  std::list<std::shared_ptr<JScript_function_storage>> m_stored_functions;
  Current_script m_current_script;
};

JScript_context::Impl::Impl(JScript_context *owner)
    : m_owner(owner),
      m_allocator(v8::ArrayBuffer::Allocator::NewDefaultAllocator()) {
  JScript_context_init();

  v8::Isolate::CreateParams params;
  params.array_buffer_allocator = m_allocator.get();

  m_isolate = v8::Isolate::New(params);
  m_isolate->SetData(0, this);

  m_isolate->SetCaptureStackTraceForUncaughtExceptions(true);

  v8::Isolate::Scope isolate_scope(m_isolate);
  v8::HandleScope handle_scope(m_isolate);

  v8::Local<v8::ObjectTemplate> globals = v8::ObjectTemplate::New(m_isolate);

  // register symbols to be exported to JS in global namespace

  // repr(object) -> string
  globals->Set(v8_string("repr"), wrap_callback(&Impl::f_repr));

  // unrepr(string) -> object
  globals->Set(v8_string("unrepr"), wrap_callback(&Impl::f_unrepr));

  // type(object) -> string
  globals->Set(v8_string("type"), wrap_callback(&Impl::f_type));

  // print('hello')
  globals->Set(v8_string("print"), wrap_callback([](const V8_args &args) {
                 f_print(args, false);
               }));

  // println('hello')
  globals->Set(v8_string("println"),
               wrap_callback([](const V8_args &args) { f_print(args, true); }));

  // source('module')
  globals->Set(v8_string("source"), wrap_callback(&Impl::f_source));

  // private functions
  globals->Set(v8_string("__list_native_modules"),
               wrap_callback(&Impl::f_list_native_modules));

  globals->Set(v8_string("__load_native_module"),
               wrap_callback(&Impl::f_load_native_module));

  globals->Set(v8_string("__load_module"), wrap_callback(&Impl::f_load_module));

  globals->Set(v8_string("__current_module_folder"),
               wrap_callback(&Impl::f_current_module_folder));

  // create the global context
  v8::Local<v8::Context> lcontext =
      v8::Context::New(m_isolate, nullptr, globals);
  m_context.Reset(m_isolate, lcontext);

  // Loads the core module
  load_core_module();
}

JScript_context::Impl::~Impl() {
  {
    v8::HandleScope handle_scope(m_isolate);

    for (const auto &c : m_stored_contexts) {
      delete_context(c.Get(m_isolate));
    }

    m_stored_contexts.clear();
  }

  m_stored_functions.clear();

  // release the context
  m_context.Reset();
  // notify the isolate
  m_isolate->ContextDisposedNotification();
  // force GC
  m_isolate->LowMemoryNotification();
  // dispose isolate
  m_isolate->Dispose();
}

void JScript_context::Impl::load_core_module() const {
  v8::Isolate::Scope isolate_scope(m_isolate);
  v8::HandleScope handle_scope(m_isolate);
  v8::TryCatch try_catch{m_isolate};
  const auto lcontext = context();
  v8::Context::Scope context_scope(lcontext);

  shcore::Scoped_naming_style style(NamingStyle::LowerCamelCase);

  v8::ScriptOrigin script_origin{m_isolate, v8_string("core.js")};
  auto script = v8::Script::Compile(
      lcontext, v8_string("(function (){" + shcore::js_core_module + "})();"),
      &script_origin);

  v8::MaybeLocal<v8::Value> result;
  if (!script.IsEmpty()) {
    result = script.ToLocalChecked()->Run(lcontext);
  }

  if (result.IsEmpty()) {
    throw shcore::Exception::runtime_error("Unable to load the core module!!");
  }
}

void JScript_context::Impl::load_module(const std::string &path,
                                        v8::Local<v8::Value> module,
                                        bool *js_exception) {
  if (js_exception) {
    *js_exception = false;
  }

  const auto script_scope = enter_script(path);
  shcore::Scoped_naming_style style(NamingStyle::LowerCamelCase);

  // load the file
  std::string source;

  if (!load_text_file(path, source)) {
    throw std::runtime_error(
        shcore::str_format("Failed to open '%s'", path.c_str()));
  }

  // compile the source code
  v8::HandleScope handle_scope(m_isolate);

  const auto new_context = copy_global_context();
  v8::Context::Scope context_scope(new_context);

  v8::ScriptOrigin script_origin{m_isolate, v8_string(path)};
  // immediately invoked function expression inside of another IIFE, this saves
  // some C++ code to extract the data from the module object
  v8::MaybeLocal<v8::Script> script = v8::Script::Compile(
      new_context,
      v8_string("(function(m){(function(exports,module,__filename,__dirname){" +
                source + "})(m.exports,m,m.__filename,m.__dirname)});"),
      &script_origin);

  if (script.IsEmpty()) {
    delete_context(new_context);

    throw std::runtime_error(
        shcore::str_format("Failed to compile '%s'.", path.c_str()));
  }

  // run the code, creating the function
  auto maybe_result = script.ToLocalChecked()->Run(new_context);

  const auto handle_empty_result = [&path, &js_exception]() {
    if (js_exception) {
      // if result is empty, there was a JS exception
      *js_exception = true;
    }

    throw std::runtime_error(
        shcore::str_format("Failed to execute '%s'.", path.c_str()));
  };

  if (maybe_result.IsEmpty()) {
    // nothing was executed yet, it's save to delete the context
    delete_context(new_context);
    handle_empty_result();
  }

  auto result = maybe_result.ToLocalChecked();

  if (!result->IsFunction()) {
    // this shouldn't happen, as it's our code which creates this function
    throw std::runtime_error(
        shcore::str_format("Failed to execute '%s'.", path.c_str()));
  }

  // execute the function (and user code), passing the module object
  auto function = v8::Local<v8::Function>::Cast(result);
  v8::Local<v8::Value> args[] = {module};

  maybe_result = function->Call(new_context, new_context->Global(),
                                shcore::array_size(args), args);

  // store the context even if an exception was thrown, we don't know when
  // exception was generated, script could have done something meaningful
  // before that happened
  store_context(new_context);

  if (maybe_result.IsEmpty()) {
    handle_empty_result();
  }
}

void JScript_context::Impl::f_repr(const V8_args &args) {
  const auto isolate = args.GetIsolate();
  const auto self = static_cast<Impl *>(isolate->GetData(0));

  v8::HandleScope handle_scope(isolate);

  if (args.Length() != 1) {
    isolate->ThrowException(v8_string(isolate, "repr() takes 1 argument"));
  } else {
    try {
      args.GetReturnValue().Set(
          v8_string(isolate, self->convert(args[0]).repr()));
    } catch (const std::exception &e) {
      isolate->ThrowException(v8_string(isolate, e.what()));
    }
  }
}

void JScript_context::Impl::f_unrepr(const V8_args &args) {
  const auto isolate = args.GetIsolate();
  const auto self = static_cast<Impl *>(isolate->GetData(0));

  v8::HandleScope handle_scope(isolate);

  if (args.Length() != 1) {
    isolate->ThrowException(v8_string(isolate, "unrepr() takes 1 argument"));
  } else {
    try {
      args.GetReturnValue().Set(
          self->convert(Value::parse(to_string(isolate, args[0]))));
    } catch (const std::exception &e) {
      isolate->ThrowException(v8_string(isolate, e.what()));
    }
  }
}

void JScript_context::Impl::f_type(const V8_args &args) {
  const auto isolate = args.GetIsolate();
  const auto self = static_cast<Impl *>(isolate->GetData(0));

  v8::HandleScope handle_scope(isolate);

  if (args.Length() != 1) {
    isolate->ThrowException(v8_string(isolate, "type() takes 1 argument"));
  } else {
    try {
      args.GetReturnValue().Set(self->type_info(args[0]));
    } catch (const std::exception &e) {
      isolate->ThrowException(v8_string(isolate, e.what()));
    }
  }
}

void JScript_context::Impl::f_print(const V8_args &args, bool new_line) {
  const auto isolate = args.GetIsolate();
  const auto self = static_cast<Impl *>(isolate->GetData(0));

  v8::HandleScope outer_handle_scope(isolate);

  std::string text;

  for (int i = 0; i < args.Length(); i++) {
    v8::HandleScope handle_scope(isolate);
    if (i > 0) text.push_back(' ');

    try {
      text += self->convert(args[i]).descr(true);
    } catch (const std::exception &e) {
      isolate->ThrowException(v8_string(isolate, e.what()));
      break;
    }
  }
  if (new_line) text.append("\n");

  mysqlsh::current_console()->print(text);
}

void JScript_context::Impl::f_source(const V8_args &args) {
  const auto isolate = args.GetIsolate();
  const auto self = static_cast<Impl *>(isolate->GetData(0));

  v8::HandleScope outer_handle_scope(isolate);

  if (args.Length() != 1) {
    isolate->ThrowException(v8_string(isolate, "Invalid number of parameters"));
    return;
  }

  shcore::Scoped_naming_style style(NamingStyle::LowerCamelCase);

  v8::HandleScope handle_scope(isolate);
  const auto str = to_string(isolate, args[0]);

  // Loads the source content
  std::string source;
  if (load_text_file(str, source)) {
    self->m_owner->execute(source, str);
  } else {
    isolate->ThrowException(v8_string(isolate, "Error loading script"));
  }
}

void JScript_context::Impl::f_list_native_modules(const V8_args &args) {
  const auto isolate = args.GetIsolate();

  v8::HandleScope handle_scope(isolate);

  if (args.Length() != 0) {
    isolate->ThrowException(v8_string(isolate, "Invalid number of parameters"));
  } else {
    const auto core_modules = Object_factory::package_contents("__modules__");
    const auto size = static_cast<int>(core_modules.size());
    const auto array = v8::Array::New(isolate, size);

    for (int i = 0; i < size; ++i) {
      array
          ->Set(isolate->GetCurrentContext(), i,
                v8_string(isolate, core_modules[i]))
          .FromJust();
    }

    args.GetReturnValue().Set(array);
  }
}

void JScript_context::Impl::f_load_native_module(const V8_args &args) {
  const auto isolate = args.GetIsolate();
  const auto self = static_cast<Impl *>(isolate->GetData(0));

  v8::HandleScope handle_scope(isolate);

  if (args.Length() != 1) {
    isolate->ThrowException(v8_string(isolate, "Invalid number of parameters"));
  } else {
    try {
      const auto s = to_string(isolate, args[0]);
      shcore::Scoped_naming_style style(NamingStyle::LowerCamelCase);

      const auto core_modules = Object_factory::package_contents("__modules__");
      if (std::find(core_modules.begin(), core_modules.end(), s) !=
          core_modules.end()) {
        const auto module = Object_factory::call_constructor(
            "__modules__", s, shcore::Argument_list());
        args.GetReturnValue().Set(self->convert(shcore::Value(module)));
      }
    } catch (const std::exception &e) {
      isolate->ThrowException(v8_string(isolate, e.what()));
    }
  }
}

void JScript_context::Impl::f_load_module(const V8_args &args) {
  const auto isolate = args.GetIsolate();
  const auto self = static_cast<Impl *>(isolate->GetData(0));

  v8::HandleScope handle_scope(isolate);

  if (args.Length() != 2) {
    isolate->ThrowException(v8_string(isolate, "Invalid number of parameters"));
  } else {
    bool js_exception = false;

    try {
      self->load_module(to_string(isolate, args[0]), args[1], &js_exception);
    } catch (const std::runtime_error &ex) {
      // throw new JS exception only if there isn't one already
      if (!js_exception) {
        // needed to create the v8 exception
        v8::Isolate::Scope isolate_scope(isolate);
        isolate->ThrowException(
            v8::Exception::Error(v8_string(isolate, ex.what())));
      }
    }
  }
}

void JScript_context::Impl::f_current_module_folder(const V8_args &args) {
  const auto isolate = args.GetIsolate();
  const auto self = static_cast<Impl *>(isolate->GetData(0));

  v8::HandleScope handle_scope(isolate);

  if (args.Length() != 0) {
    isolate->ThrowException(v8_string(isolate, "Invalid number of parameters"));
  } else {
    args.GetReturnValue().Set(
        v8_string(isolate, self->m_current_script.current_folder()));
  }
}

void JScript_context::Impl::set_global_item(
    const std::string &global_name, const std::string &item_name,
    const v8::Local<v8::Value> &value) const {
  v8::HandleScope handle_scope(m_isolate);
  v8::Local<v8::Value> global = get_global(global_name);
  v8::Local<v8::Object> object = v8::Local<v8::Object>::Cast(global);
  const auto lcontext = context();

  object->Set(lcontext, v8_string(item_name), value).FromJust();
}

void JScript_context::Impl::set_global(
    const std::string &name, const v8::Local<v8::Value> &value) const {
  v8::HandleScope handle_scope(m_isolate);
  const auto ctx = context();

  if (value.IsEmpty() || !*value)
    ctx->Global()->Set(ctx, v8_string(name), v8::Null(m_isolate)).FromJust();
  else
    ctx->Global()->Set(ctx, v8_string(name), value).FromJust();
}

v8::Local<v8::Value> JScript_context::Impl::get_global(
    const std::string &name) const {
  const auto ctx = context();
  return ctx->Global()->Get(ctx, v8_string(name)).ToLocalChecked();
}

v8::Local<v8::String> JScript_context::Impl::v8_string(v8::Isolate *isolate,
                                                       const char *data) {
  return shcore::v8_string(isolate, data);
}

v8::Local<v8::String> JScript_context::Impl::v8_string(
    v8::Isolate *isolate, const std::string &data) {
  return v8_string(isolate, data.c_str());
}

v8::Local<v8::String> JScript_context::Impl::v8_string(const char *data) const {
  return v8_string(m_isolate, data);
}

v8::Local<v8::String> JScript_context::Impl::v8_string(
    const std::string &data) const {
  return v8_string(data.c_str());
}

v8::Local<v8::ArrayBuffer> JScript_context::Impl::v8_array_buffer(
    const std::string &data) {
  return v8_array_buffer(data.c_str(), data.size());
}

v8::Local<v8::ArrayBuffer> JScript_context::Impl::v8_array_buffer(
    const char *data, size_t length) {
  return shcore::v8_array_buffer(m_isolate, data, length);
}

std::string JScript_context::Impl::to_string(v8::Isolate *isolate,
                                             v8::Local<v8::Value> obj) {
  return shcore::to_string(isolate, obj);
}

std::string JScript_context::Impl::to_string(v8::Local<v8::Value> obj) const {
  return to_string(m_isolate, obj);
}

void JScript_context::Impl::terminate() {
  // TerminateExecution() generates an exception which cannot be caught in JS.
  // It can be called from any thread, which means that it does not
  // immediately raise an exception, instead it sets a flag which will be
  // checked by V8 engine at some in the future by
  // v8::internal::StackGuard::HandleInterrupts() and then generate an
  // exception. For some reason (optimization?), if JS code has several
  // consecutive native calls, the execution is not terminated until all of
  // them are called, i.e.:
  //   os.sleep(10); println('failed');
  // pressing CTRL-C during sleep (which calls terminate()) will still print
  // 'failed', while:
  //   function fail() { println('failed'); } os.sleep(10); fail();
  // in the same scenario will work fine. For this reason we need to manually
  // check the `m_terminating` flag before executing native code.
  m_terminating = true;
  m_isolate->TerminateExecution();
}

bool JScript_context::Impl::is_terminating() const { return m_terminating; }

void JScript_context::Impl::clear_is_terminating() { m_terminating = false; }

v8::Local<v8::FunctionTemplate> JScript_context::Impl::wrap_callback(
    v8::FunctionCallback callback) const {
  const auto data =
      v8::External::New(m_isolate, reinterpret_cast<void *>(callback));

  return v8::FunctionTemplate::New(
      m_isolate,
      [](const V8_args &args) {
        const auto isolate = args.GetIsolate();
        const auto self = static_cast<Impl *>(isolate->GetData(0));

        if (self->is_terminating()) return;

        const auto func = reinterpret_cast<v8::FunctionCallback>(
            v8::External::Cast(*args.Data())->Value());
        func(args);
      },
      data);
}

/**
 * Copies the global context.
 *
 * Context needs to be either deleted immediately (i.e. in case of an error),
 * or stored till JS context exists (in order to preserve execution
 * environment).
 */
v8::Local<v8::Context> JScript_context::Impl::copy_global_context() const {
  const auto new_context = v8::Context::New(m_isolate);

  // copy globals into the new context
  const auto origin = m_context.Get(m_isolate);

  const auto globals = origin->Global();
  const auto names = globals->GetPropertyNames(origin).ToLocalChecked();

  const auto new_globals = new_context->Global();

  for (uint32_t c = names->Length(), i = 0; i < c; ++i) {
    const auto name = names->Get(origin, i).ToLocalChecked();
    new_globals
        ->Set(new_context, name, globals->Get(origin, name).ToLocalChecked())
        .FromJust();
  }

  return new_context;
}

void JScript_context::Impl::delete_context(
    v8::Local<v8::Context> context) const {
  const auto globals = context->Global();
  const auto names = globals->GetPropertyNames(context).ToLocalChecked();

  for (uint32_t c = names->Length(), i = 0; i < c; ++i) {
    const auto name = names->Get(context, i).ToLocalChecked();

    if (globals->Delete(context, name).IsNothing()) {
      log_error("Failed to delete global JS: %s", to_string(name).c_str());
    }
  }
}

void JScript_context::Impl::store_context(v8::Local<v8::Context> context) {
  m_stored_contexts.emplace_back(m_isolate, context);
}

std::weak_ptr<JScript_function_storage> JScript_context::Impl::store(
    v8::Local<v8::Function> function) {
  m_stored_functions.emplace_back(
      std::make_shared<JScript_function_storage>(m_isolate, function));
  return m_stored_functions.back();
}

void JScript_context::Impl::erase(
    const std::shared_ptr<JScript_function_storage> &function) {
  m_stored_functions.remove(function);
}

v8::Isolate *JScript_context::Impl::isolate() const { return m_isolate; }

v8::Local<v8::Context> JScript_context::Impl::context() const {
  return v8::Local<v8::Context>::New(isolate(), m_context);
}

Value JScript_context::Impl::convert(const v8::Local<v8::Value> &value) const {
  return m_owner->convert(value);
}

v8::Local<v8::Value> JScript_context::Impl::convert(const Value &value) const {
  return m_owner->convert(value);
}

v8::Local<v8::String> JScript_context::Impl::type_info(
    v8::Local<v8::Value> value) const {
  return m_owner->type_info(value);
}

JScript_context::Impl::Script_scope JScript_context::Impl::enter_script(
    const std::string &s) {
  return Script_scope(this, s);
}

JScript_context::JScript_context(Object_registry *registry)
    : m_impl(std::make_unique<Impl>(this)),
      m_types(std::make_unique<JScript_type_bridger>(this)) {
  // initialize type conversion
  m_types->init();

  set_global("globals", Value(registry->_registry));
}

JScript_context::~JScript_context() { m_types->dispose(); }

void JScript_context::set_global_item(const std::string &global_name,
                                      const std::string &item_name,
                                      const Value &value) {
  // makes isolate the default isolate for this context
  v8::Isolate::Scope isolate_scope(isolate());
  // creates a pool for all the handles that are created in this scope
  // (except for persistent ones), which will be freed when the scope exits
  v8::HandleScope handle_scope(isolate());
  // catch everything that happens in this scope
  v8::TryCatch try_catch{isolate()};
  // set context to be the default context for everything in this scope
  v8::Context::Scope context_scope(context());

  m_impl->set_global_item(global_name, item_name, convert(value));
}

void JScript_context::set_global(const std::string &name, const Value &value) {
  // makes isolate the default isolate for this context
  v8::Isolate::Scope isolate_scope(isolate());
  // creates a pool for all the handles that are created in this scope
  // (except for persistent ones), which will be freed when the scope exits
  v8::HandleScope handle_scope(isolate());
  // catch everything that happens in this scope
  v8::TryCatch try_catch{isolate()};
  // set context to be the default context for everything in this scope
  v8::Context::Scope context_scope(context());

  m_impl->set_global(name, convert(value));
}

Value JScript_context::get_global(const std::string &name) {
  // makes isolate the default isolate for this context
  v8::Isolate::Scope isolate_scope(isolate());
  // creates a pool for all the handles that are created in this scope
  // (except for persistent ones), which will be freed when the scope exits
  v8::HandleScope handle_scope(isolate());
  // catch everything that happens in this scope
  v8::TryCatch try_catch{isolate()};
  // set context to be the default context for everything in this scope
  v8::Context::Scope context_scope(context());

  return convert(m_impl->get_global(name));
}

std::tuple<JSObject, std::string> JScript_context::get_global_js(
    const std::string &name) {
  // makes isolate the default isolate for this context
  v8::Isolate::Scope isolate_scope(isolate());
  // creates a pool for all the handles that are created in this scope
  // (except for persistent ones), which will be freed when the scope exits
  v8::HandleScope handle_scope(isolate());
  // catch everything that happens in this scope
  v8::TryCatch try_catch{isolate()};
  // set context to be the default context for everything in this scope
  const auto lcontext = context();
  v8::Context::Scope context_scope(lcontext);
  v8::Local<v8::Value> global(m_impl->get_global(name));

  if (global->IsObject()) {
    auto obj_type = m_impl->to_string(type_info(global));

    if (shcore::str_beginswith(obj_type, "m.")) {
      obj_type = obj_type.substr(2);
    }

    return std::make_tuple(
        JSObject(isolate(), global->ToObject(lcontext).ToLocalChecked()),
        obj_type);
  } else {
    return std::make_tuple(JSObject(), "");
  }
}

std::vector<std::pair<bool, std::string>> JScript_context::list_globals() {
  Value globals(
      execute(
          "Object.keys(this).map(function(x) {"
          " if (typeof this[x] == 'function') return '('+x; else return '.'+x;"
          "})",
          {})
          .first);
  assert(globals.type == shcore::Array);

  std::vector<std::pair<bool, std::string>> ret;
  for (shcore::Value g : *globals.as_array()) {
    std::string n = g.get_string();
    ret.push_back({n[0] == '(', n.substr(1)});
  }
  return ret;
}

std::tuple<bool, JSObject, std::string> JScript_context::get_member_of(
    const JSObject *obj, const std::string &name) {
  // makes isolate the default isolate for this context
  v8::Isolate::Scope isolate_scope(isolate());
  // creates a pool for all the handles that are created in this scope
  // (except for persistent ones), which will be freed when the scope exits
  v8::HandleScope handle_scope(isolate());
  // catch everything that happens in this scope
  v8::TryCatch try_catch{isolate()};
  // set context to be the default context for everything in this scope
  const auto lcontext = context();
  v8::Context::Scope context_scope(lcontext);

  shcore::Scoped_naming_style style(NamingStyle::LowerCamelCase);

  v8::Local<v8::Object> lobj(v8::Local<v8::Object>::New(isolate(), *obj));

  auto maybe_value = lobj->Get(lcontext, v8_string(name));
  if (!maybe_value.IsEmpty()) {
    const auto value = maybe_value.ToLocalChecked();

    auto type = m_impl->to_string(type_info(value));
    if (shcore::str_beginswith(type, "m.")) {
      type = type.substr(2);
    }

    if (value->IsFunction()) {
      return std::make_tuple(true, JSObject(), type);
    } else if (value->IsObject()) {
      const auto object = value->ToObject(lcontext).ToLocalChecked();
      return std::make_tuple(object->IsCallable(), JSObject(isolate(), object),
                             type);
    }
  }

  return std::make_tuple(false, JSObject(), "");
}

std::vector<std::pair<bool, std::string>> JScript_context::get_members_of(
    const JSObject *obj) {
  // makes isolate the default isolate for this context
  v8::Isolate::Scope isolate_scope(isolate());
  // creates a pool for all the handles that are created in this scope
  // (except for persistent ones), which will be freed when the scope exits
  v8::HandleScope handle_scope(isolate());
  // catch everything that happens in this scope
  v8::TryCatch try_catch{isolate()};
  // set context to be the default context for everything in this scope
  const auto lcontext = context();
  v8::Context::Scope context_scope(lcontext);

  shcore::Scoped_naming_style style(NamingStyle::LowerCamelCase);

  v8::Local<v8::Object> lobj(v8::Local<v8::Object>::New(isolate(), *obj));

  std::vector<std::pair<bool, std::string>> keys;
  if (*lobj) {
    v8::Local<v8::Array> props(
        lobj->GetPropertyNames(lcontext).ToLocalChecked());
    for (size_t i = 0; i < props->Length(); i++) {
      auto maybe_value =
          lobj->Get(lcontext, props->Get(lcontext, i).ToLocalChecked());
      bool is_func;
      if (!maybe_value.IsEmpty()) {
        const auto value = maybe_value.ToLocalChecked();
        if (value->IsFunction()) {
          is_func = true;
        } else if (value->IsObject() &&
                   value->ToObject(lcontext).ToLocalChecked()->IsCallable()) {
          is_func = true;
        } else {
          is_func = false;
        }
        keys.push_back(
            {is_func,
             m_impl->to_string(props->Get(lcontext, i).ToLocalChecked())});
      }
    }
  }
  return keys;
}

v8::Isolate *JScript_context::isolate() const { return m_impl->isolate(); }

v8::Local<v8::Context> JScript_context::context() const {
  return m_impl->context();
}

Value JScript_context::convert(const v8::Local<v8::Value> &value) const {
  return m_types->v8_value_to_shcore_value(value);
}

v8::Local<v8::Value> JScript_context::convert(const Value &value) const {
  return m_types->shcore_value_to_v8_value(value);
}

Argument_list JScript_context::convert_args(const V8_args &args) {
  Argument_list r;

  for (int c = args.Length(), i = 0; i < c; i++) {
    r.push_back(convert(args[i]));
  }

  return r;
}

v8::Local<v8::String> JScript_context::type_info(
    v8::Local<v8::Value> value) const {
  return m_types->type_info(value);
}

void JScript_context::set_argv(const std::vector<std::string> &argv) {
  shcore::Value args_value = get_global("sys").as_object()->get_member("argv");
  auto args = args_value.as_array();
  args->clear();
  for (auto &arg : argv) {
    args->push_back(Value(arg));
  }
}

std::pair<Value, bool> JScript_context::execute(const std::string &code_str,
                                                const std::string &source) {
  // makes isolate the default isolate for this context
  v8::Isolate::Scope isolate_scope(isolate());
  // creates a pool for all the handles that are created in this scope
  // (except for persistent ones), which will be freed when the scope exits
  v8::HandleScope handle_scope(isolate());
  // catch everything that happens in this scope
  v8::TryCatch try_catch{isolate()};
  // set context to be the default context for everything in this scope
  v8::Local<v8::Context> lcontext = context();
  v8::Context::Scope context_scope(lcontext);
  v8::ScriptOrigin origin(isolate(), v8_string(source));
  v8::Local<v8::String> code = v8_string(code_str);
  v8::MaybeLocal<v8::Script> script =
      v8::Script::Compile(lcontext, code, &origin);

  const auto script_scope = m_impl->enter_script(source);

  // Since ret_val can't be used to check whether all was ok or not
  // Will use a boolean flag

  shcore::Scoped_naming_style style(NamingStyle::LowerCamelCase);

  m_impl->clear_is_terminating();

  if (!script.IsEmpty()) {
    v8::MaybeLocal<v8::Value> result = script.ToLocalChecked()->Run(lcontext);
    if (is_terminating() || try_catch.HasTerminated()) {
      isolate()->CancelTerminateExecution();
      mysqlsh::current_console()->print_diag(
          "Script execution interrupted by user.");

      return {Value(), true};
    } else if (!try_catch.HasCaught()) {
      return {convert(result.ToLocalChecked()), false};
    } else {
      Value e = get_v8_exception_data(try_catch, false);

      throw Exception::scripting_error(format_exception(e));
    }
  } else {
    if (try_catch.HasCaught()) {
      Value e = get_v8_exception_data(try_catch, false);

      throw Exception::scripting_error(format_exception(e));
    } else {
      throw shcore::Exception::logic_error(
          "Unexpected error compiling script, no exception caught!");
    }
  }
}

std::pair<Value, bool> JScript_context::execute_interactive(
    const std::string &code_str, Input_state *r_state, bool flush) noexcept {
  // makes isolate the default isolate for this context
  v8::Isolate::Scope isolate_scope(isolate());
  // creates a pool for all the handles that are created in this scope
  // (except for persistent ones), which will be freed when the scope exits
  v8::HandleScope handle_scope(isolate());
  // catch everything that happens in this scope
  v8::TryCatch try_catch{isolate()};
  // set context to be the default context for everything in this scope
  v8::Local<v8::Context> lcontext = context();
  v8::Context::Scope context_scope(lcontext);
  v8::Local<v8::String> code = v8_string(code_str);

  shcore::Scoped_naming_style style(NamingStyle::LowerCamelCase);

  m_impl->clear_is_terminating();

  *r_state = Input_state::Ok;

  const auto console = mysqlsh::current_console();

  v8::MaybeLocal<v8::Value> result = evaluate_repl(isolate(), code);

  if (is_terminating() || try_catch.HasTerminated()) {
    isolate()->CancelTerminateExecution();
    console->print_diag("Script execution interrupted by user.");
  } else if (try_catch.HasCaught()) {
    // If there's no more input then the interpreter should not continue in
    // continued mode so any error should bubble up
    if (!flush) {
      // check if this was an error of type
      // - SyntaxError: Unexpected end of input
      // - SyntaxError: Unterminated template literal
      // which we treat as a multiline mode trigger or
      // - SyntaxError: Invalid or unexpected token
      // which may be a sign of unfinished C style comment
      const std::string unexpected_end_exc =
          "SyntaxError: Unexpected end of input";
      const std::string unterminated_template_exc =
          "SyntaxError: Unterminated template literal";
      const std::string unexpected_tok_exc =
          "SyntaxError: Invalid or unexpected token";

      auto message = m_impl->to_string(try_catch.Exception());

      if (message == unexpected_end_exc ||
          message == unterminated_template_exc) {
        *r_state = Input_state::ContinuedBlock;
      } else if (message == unexpected_tok_exc) {
        v8::ScriptOrigin origin(isolate(), v8_string(k_origin_shell));

        auto comment_pos = code_str.rfind("/*");

        while (comment_pos != std::string::npos &&
               code_str.find("*/", comment_pos + 2) == std::string::npos) {
          try_catch.Reset();

          if (!v8::Script::Compile(lcontext,
                                   v8_string(code_str.substr(0, comment_pos)),
                                   &origin)
                   .IsEmpty()) {
            *r_state = Input_state::ContinuedSingle;
          } else {
            message = m_impl->to_string(try_catch.Exception());

            if (message == unexpected_end_exc) {
              *r_state = Input_state::ContinuedBlock;
            } else if (message == unexpected_tok_exc && comment_pos > 0) {
              comment_pos = code_str.rfind("/*", comment_pos - 1);
              continue;
            }
          }
          break;
        }
      }
    }

    if (*r_state == Input_state::Ok) {
      Value exc(get_v8_exception_data(try_catch, true));

      if (exc) {
        print_exception(format_exception(exc));
      } else {
        console->print_diag("Error executing script");
      }
    }
  } else {
    try {
      auto checked_result = result.ToLocalChecked();

      assert(checked_result->IsPromise());

      v8::Local<v8::Promise> promise = checked_result.As<v8::Promise>();

      // wait till result promise is fulfilled
      while (v8::Promise::kPending == promise->State()) {
        isolate()->PerformMicrotaskCheckpoint();
      }

      // force GC on each execution in order to wipe the native objects and
      // release the resources (i.e. open connections)
      isolate()->LowMemoryNotification();

      const auto state = promise->State();

      if (v8::Promise::kFulfilled == state) {
        return {convert(promise->Result()).as_map()->at(".repl_result"), false};
      } else if (v8::Promise::kRejected == state) {
        if (!try_catch.HasCaught()) {
          isolate()->ThrowException(promise->Result());
        }

        print_exception(
            format_exception(get_v8_exception_data(try_catch, true)));
      } else {
        assert(false);
      }
    } catch (const std::exception &exc) {
      // we used to let the exception bubble up, but somehow, exceptions
      // thrown from v8_value_to_shcore_value() aren't being caught from
      // main.cc, leading to a crash due to unhandled exception.. so we catch
      // and print it here
      console->print_diag(std::string("INTERNAL ERROR: ").append(exc.what()));
    }
  }

  return {Value(), true};
}

std::string JScript_context::format_exception(const shcore::Value &exc) {
  std::string error_message;

  shcore::Scoped_naming_style style(NamingStyle::LowerCamelCase);

  if (exc.type == Map) {
    std::string type = exc.as_map()->get_string("type", "");
    std::string message = exc.as_map()->get_string("message", "");
    int64_t code = exc.as_map()->get_int("code", -1);
    std::string location = exc.as_map()->get_string("location", "");

    if (!message.empty()) {
      error_message += message;
      if (!type.empty() && code != -1)
        error_message += " (" + type + " " + std::to_string(code) + ")\n";
      else if (!type.empty())
        error_message += " (" + type + ")\n";
      else if (code != -1)
        error_message += " (" + std::to_string(code) + ") ";
      if (!location.empty()) error_message += " at " + location;
    }
  } else {
    error_message = "Unexpected format of exception object.";
  }

  if (error_message.length() &&
      error_message[error_message.length() - 1] != '\n')
    error_message += "\n";

  return error_message;
}

Value JScript_context::get_v8_exception_data(const v8::TryCatch &exc,
                                             bool interactive) {
  return get_v8_exception_data(exc.Exception(), exc.Message(), interactive);
}

Value JScript_context::get_v8_exception_data(v8::Local<v8::Value> exc,
                                             v8::Local<v8::Message> message,
                                             bool interactive) {
  Dictionary_t data;

  if (exc.IsEmpty() || exc->IsUndefined()) return Value();

  shcore::Scoped_naming_style style(NamingStyle::LowerCamelCase);

  if (exc->IsObject() &&
      JScript_map_wrapper::is_map(exc->ToObject(context()).ToLocalChecked())) {
    data = convert(exc).as_map();
  } else {
    data = make_dict();

    const auto excstr = m_impl->to_string(exc);

    if (!excstr.empty()) {
      // JS errors produced by V8 most likely will fall on this branch
      (*data)["message"] = Value(excstr);
    } else {
      (*data)["message"] = Value("Exception");
    }
  }

  bool include_location = !interactive;

  if (!message.IsEmpty()) {
    const auto lcontext = context();
    v8::Context::Scope context_scope(lcontext);

    // location
    const auto filename =
        m_impl->to_string(message->GetScriptOrigin().ResourceName());
    std::string text = (filename.empty() ? k_origin_shell : filename) + ":";

    {
      const auto line_number = message->GetLineNumber(lcontext);
      text += (line_number.IsJust() ? std::to_string(line_number.FromJust())
                                    : "?") +
              std::string{":"};
    }

    {
      // start column is zero-based
      const auto start_column = message->GetStartColumn(lcontext);
      text +=
          (start_column.IsJust() ? std::to_string(start_column.FromJust() + 1)
                                 : "?") +
          std::string{"\n"};
    }

    text.append("in ");

    {
      auto source_line = message->GetSourceLine(lcontext);

      if (!source_line.IsEmpty()) {
        text += m_impl->to_string(source_line.ToLocalChecked());
      }

      text += "\n";
    }

    {
      const auto start_column = message->GetStartColumn(lcontext);
      const auto end_column = message->GetEndColumn(lcontext);

      if (start_column.IsJust() && end_column.IsJust()) {
        const auto start = start_column.FromJust();
        const auto end = end_column.FromJust();
        // underline
        text.append(3 + start, ' ');
        text.append(end - start, '^');
        text.append("\n");
      }
    }

    {
      auto stack_trace = v8::TryCatch::StackTrace(lcontext, exc);

      if (!stack_trace.IsEmpty()) {
        const auto stack = m_impl->to_string(stack_trace.ToLocalChecked());

        if (!stack.empty()) {
          auto new_lines = std::count(stack.begin(), stack.end(), '\n');
          if (new_lines > 1) {
            text.append(stack).append("\n");
            include_location = true;
          }
        }
      } else {
        const auto stack = message->GetStackTrace();

        if (!stack.IsEmpty() && stack->GetFrameCount() > 1) {
          include_location = true;

          text.append((*data)["message"].as_string().append("\n"));

          const auto lisolate = isolate();

          for (int idx = 0; idx < stack->GetFrameCount(); ++idx) {
            const auto frame = stack->GetFrame(lisolate, idx);

            text.append("    at ");

            const auto func = frame->GetFunctionName();

            if (!func.IsEmpty()) {
              text.append(to_string(func)).append(" (");
            }

            {
              const auto script = frame->GetScriptName();
              std::string script_str;

              if (!script.IsEmpty()) {
                script_str = to_string(script);
              }

              text.append(script_str.empty() ? k_origin_shell : script_str);
            }

            text.append(":")
                .append(std::to_string(frame->GetLineNumber()))
                .append(":")
                .append(std::to_string(frame->GetColumn()));

            if (!func.IsEmpty()) {
              text.append(")");
            }

            text.append("\n");
          }
        }
      }
    }

    if (include_location) (*data)["location"] = Value(text);
  }

  return Value(data);
}

bool JScript_context::is_terminating() const {
  return m_impl->is_terminating();
}

void JScript_context::terminate() { m_impl->terminate(); }

v8::Local<v8::String> JScript_context::v8_string(const char *data) {
  return m_impl->v8_string(data);
}

v8::Local<v8::String> JScript_context::v8_string(const std::string &data) {
  return v8_string(data.c_str());
}

std::string JScript_context::to_string(v8::Local<v8::Value> obj) {
  return m_impl->to_string(obj);
}

v8::Local<v8::ArrayBuffer> JScript_context::v8_array_buffer(
    const std::string &data) {
  return m_impl->v8_array_buffer(data);
}

v8::Local<v8::ArrayBuffer> JScript_context::v8_array_buffer(const char *data,
                                                            size_t length) {
  return m_impl->v8_array_buffer(data, length);
}

std::string JScript_context::translate_exception(const v8::TryCatch &exc,
                                                 bool interactive) {
  return format_exception(get_v8_exception_data(exc, interactive));
}

bool JScript_context::load_plugin(const Plugin_definition &plugin) {
  bool ret_val = true;
  const auto &file_name = plugin.file;

  const auto iso = isolate();
  v8::HandleScope handle_scope(iso);
  v8::TryCatch try_catch{iso};
  const auto lcontext = context();
  v8::Context::Scope context_scope(lcontext);

  v8::ScriptOrigin script_origin{iso, v8_string("(load_plugin)")};
  v8::MaybeLocal<v8::Script> script =
      v8::Script::Compile(lcontext,
                          v8_string("require.__mh.__load_module(" +
                                    shcore::quote_string(file_name, '"') + ")"),
                          &script_origin);

  if (script.IsEmpty()) {
    ret_val = false;
    log_error("Error loading JavaScript file '%s':\n\t%s", file_name.c_str(),
              "Failed to compile.");
  } else {
    const auto result = script.ToLocalChecked()->Run(lcontext);

    if (result.IsEmpty()) {
      ret_val = false;
      if (try_catch.HasCaught()) {
        log_error("Error loading JavaScript file '%s':\n\tExecution failed: %s",
                  file_name.c_str(),
                  translate_exception(try_catch, false).c_str());
      } else {
        log_error("Error loading JavaScript file '%s':\n\tUnknown error",
                  file_name.c_str());
      }
    }
  }

  return ret_val;
}

std::weak_ptr<JScript_function_storage> JScript_context::store(
    v8::Local<v8::Function> function) {
  return m_impl->store(function);
}

void JScript_context::erase(
    const std::shared_ptr<JScript_function_storage> &function) {
  m_impl->erase(function);
}

v8::Local<v8::String> v8_string(v8::Isolate *isolate, const char *data) {
  return v8::String::NewFromUtf8(isolate, data, v8::NewStringType::kNormal)
      .ToLocalChecked();
}

v8::Local<v8::String> v8_string(v8::Isolate *isolate, const std::string &data) {
  return v8_string(isolate, data.c_str());
}

v8::Local<v8::ArrayBuffer> v8_array_buffer(v8::Isolate *isolate,
                                           const char *data, size_t length) {
  auto buffer = v8::ArrayBuffer::New(isolate, length);

  memcpy(buffer->GetBackingStore()->Data(), data, length);

  return buffer;
}

std::string to_string(v8::Isolate *isolate, v8::Local<v8::Value> obj) {
  const v8::String::Utf8Value utf8{
      isolate,
      obj->IsSymbol() ? obj.As<v8::Symbol>()->Description(isolate) : obj};
  const auto ptr = *utf8;
  return nullptr == ptr ? "" : std::string(ptr, utf8.length());
}

}  // namespace shcore
