#include <frostbite2D/script/quickjs_vm.h>

#include <SDL2/SDL.h>
#include <SDL2/SDL_gamecontroller.h>

#include <frostbite2D/2d/actor.h>
#include <frostbite2D/2d/sprite.h>
#include <frostbite2D/2d/text_sprite.h>
#include <frostbite2D/event/joystick_event.h>
#include <frostbite2D/event/key_event.h>
#include <frostbite2D/event/mouse_event.h>
#include <frostbite2D/event/window_event.h>
#include <frostbite2D/graphics/types.h>
#include <frostbite2D/resource/asset.h>
#include <frostbite2D/scene/scene.h>
#include <frostbite2D/scene/ui_scene.h>
#include <frostbite2D/script/quickjs_bridge_base.h>

#include <quickjspp.hpp>

#include <cctype>
#include <fstream>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string_view>
#include <filesystem>
#include <sstream>
#include <string>
#include <system_error>
#include <utility>

namespace frostbite2D {
namespace {

namespace fs = std::filesystem;

constexpr char kHotReloadTagPrefix[] = "#__fbhot=";

class JsScene : public JsActor {
public:
  JsScene() : JsScene(MakePtr<Scene>()) {}
  explicit JsScene(Ptr<Scene> scene) : JsActor(scene), scene_(std::move(scene)) {}

  void AddChild(const std::shared_ptr<JsActor>& child) {
    if (!scene_ || !child) {
      return;
    }
    scene_->AddChild(child->native());
  }

private:
  Ptr<Scene> scene_;
};

class JsUIScene : public JsScene {
public:
  JsUIScene() : JsUIScene(MakePtr<UIScene>()) {}
  explicit JsUIScene(Ptr<UIScene> scene)
      : JsScene(scene), uiScene_(std::move(scene)) {}

private:
  Ptr<UIScene> uiScene_;
};

BlendMode resolveBlendMode(int mode) {
  switch (mode) {
    case static_cast<int>(BlendMode::None):
      return BlendMode::None;
    case static_cast<int>(BlendMode::Normal):
      return BlendMode::Normal;
    case static_cast<int>(BlendMode::Additive):
      return BlendMode::Additive;
    case static_cast<int>(BlendMode::Screen):
      return BlendMode::Screen;
    case static_cast<int>(BlendMode::Premultiplied):
      return BlendMode::Premultiplied;
    case static_cast<int>(BlendMode::Subtractive):
      return BlendMode::Subtractive;
    case static_cast<int>(BlendMode::Multiply):
      return BlendMode::Multiply;
    default:
      return BlendMode::Normal;
  }
}

class JsSprite : public JsActor {
public:
  JsSprite() : JsSprite(MakePtr<Sprite>()) {}
  explicit JsSprite(Ptr<Sprite> sprite) : JsActor(sprite), sprite_(std::move(sprite)) {}

  static std::shared_ptr<JsSprite> CreateFromFile(const std::string& path) {
    Ptr<Sprite> sprite = Sprite::createFromFile(path);
    if (!sprite) {
      throw std::runtime_error("Failed to create sprite from file: " + path);
    }
    return std::make_shared<JsSprite>(sprite);
  }

  static std::shared_ptr<JsSprite> CreateFromNpk(const std::string& path,
                                                 uint32_t frameIndex) {
    Ptr<Sprite> sprite = Sprite::createFromNpk(path, frameIndex);
    if (!sprite) {
      throw std::runtime_error("Failed to create sprite from npk: " + path);
    }
    return std::make_shared<JsSprite>(sprite);
  }

  void SetSize(double width, double height) {
    if (sprite_) {
      sprite_->SetSize(static_cast<float>(width), static_cast<float>(height));
    }
  }

  void SetOffset(const std::shared_ptr<JsVec2>& offset) {
    if (sprite_ && offset) {
      sprite_->SetOffset(offset->toNative());
    }
  }

  void SetOffsetXY(double x, double y) {
    if (sprite_) {
      sprite_->SetOffset(static_cast<float>(x), static_cast<float>(y));
    }
  }

  void SetBlendMode(int mode) {
    if (sprite_) {
      sprite_->SetBlendMode(resolveBlendMode(mode));
    }
  }

private:
  Ptr<Sprite> sprite_;
};

class JsTextSprite : public JsSprite {
public:
  JsTextSprite() : JsTextSprite(TextSprite::create()) {}
  explicit JsTextSprite(Ptr<TextSprite> textSprite)
      : JsSprite(textSprite), textSprite_(std::move(textSprite)) {
    if (!textSprite_) {
      textSprite_ = TextSprite::create();
    }
  }

  static std::shared_ptr<JsTextSprite> Create() {
    Ptr<TextSprite> textSprite = TextSprite::create();
    if (!textSprite) {
      throw std::runtime_error("Failed to create text sprite");
    }
    return std::make_shared<JsTextSprite>(textSprite);
  }

  static std::shared_ptr<JsTextSprite> CreateWithFont(const std::string& text,
                                                      const std::string& font) {
    Ptr<TextSprite> textSprite = TextSprite::create(text, font);
    if (!textSprite) {
      throw std::runtime_error("Failed to create text sprite with font");
    }
    return std::make_shared<JsTextSprite>(textSprite);
  }

  void SetText(const std::string& text) {
    if (textSprite_) {
      textSprite_->SetText(text);
    }
  }

  std::string GetText() const {
    if (!textSprite_) {
      return "";
    }
    return textSprite_->GetText();
  }

  void SetFont(const std::string& fontName) {
    if (textSprite_) {
      textSprite_->SetFont(fontName);
    }
  }

  std::string GetFontName() const {
    if (!textSprite_) {
      return "";
    }
    return textSprite_->GetFontName();
  }

  void SetTextColor(double r, double g, double b, double a) {
    if (textSprite_) {
      textSprite_->SetTextColor(static_cast<float>(r), static_cast<float>(g),
                                static_cast<float>(b), static_cast<float>(a));
    }
  }

  std::shared_ptr<JsVec2> GetTextSize() const {
    if (!textSprite_) {
      return std::make_shared<JsVec2>(0.0, 0.0);
    }
    return std::make_shared<JsVec2>(textSprite_->GetTextSize());
  }

private:
  Ptr<TextSprite> textSprite_;
};

std::string joinLogArguments(const qjs::rest<qjs::Value>& args) {
  std::string message;
  for (size_t i = 0; i < args.size(); ++i) {
    if (i > 0) {
      message += " ";
    }
    message += args[i].as<std::string>();
  }
  return message;
}

qjs::Value wrapJsValue(JSContext* ctx, JSValue&& value) {
  qjs::Value wrapped(std::move(value));
  wrapped.ctx = ctx;
  return wrapped;
}

std::string keyCodeToString(KeyCode keyCode) {
  const char* keyName =
      SDL_GetKeyName(static_cast<SDL_Keycode>(static_cast<int32>(keyCode)));
  if (!keyName || keyName[0] == '\0') {
    return "Unknown";
  }
  return keyName;
}

std::string mouseButtonToString(MouseButton button) {
  switch (button) {
    case MouseButton::Left:
      return "Left";
    case MouseButton::Right:
      return "Right";
    case MouseButton::Middle:
      return "Middle";
    case MouseButton::X1:
      return "X1";
    case MouseButton::X2:
      return "X2";
    default:
      return "Unknown";
  }
}

std::string controllerButtonToString(uint8 button) {
  const char* name = SDL_GameControllerGetStringForButton(
      static_cast<SDL_GameControllerButton>(button));
  if (!name || name[0] == '\0') {
    return "unknown";
  }
  return name;
}

std::string controllerAxisToString(uint8 axis) {
  const char* name = SDL_GameControllerGetStringForAxis(
      static_cast<SDL_GameControllerAxis>(axis));
  if (!name || name[0] == '\0') {
    return "unknown";
  }
  return name;
}

std::string normalizeScriptPath(const std::string& scriptDirectory,
                                const std::string& relativePath) {
  if (relativePath.find("://") != std::string::npos) {
    return relativePath;
  }

  std::error_code ec;
  fs::path candidate = fs::u8path(relativePath);
  if (candidate.is_absolute()) {
    return relativePath;
  }

  std::string path = scriptDirectory.empty() ? relativePath : scriptDirectory;
  for (char& c : path) {
    if (c == '\\') {
      c = '/';
    }
  }

  if (!path.empty() && path.back() != '/') {
    path += '/';
  }
  path += relativePath;
  return path;
}

std::string escapeJsString(const std::string& value) {
  std::string escaped;
  escaped.reserve(value.size());
  for (char c : value) {
    switch (c) {
      case '\\':
        escaped += "\\\\";
        break;
      case '"':
        escaped += "\\\"";
        break;
      default:
        escaped += c;
        break;
    }
  }
  return escaped;
}

bool hasUriScheme(std::string_view value) {
  const size_t schemePos = value.find("://");
  const size_t slashPos = value.find('/');
  return schemePos != std::string_view::npos &&
         (slashPos == std::string_view::npos || schemePos < slashPos);
}

std::string stripHotReloadTag(std::string_view moduleName) {
  std::string path(moduleName);
  const size_t tagPos = path.find(kHotReloadTagPrefix);
  if (tagPos != std::string::npos) {
    path.erase(tagPos);
  }
  return path;
}

std::string moduleNameToFilePath(std::string_view moduleName) {
  std::string path = stripHotReloadTag(moduleName);
  if (path.rfind("file://", 0) == 0) {
    path.erase(0, 7);
#ifdef _WIN32
    if (path.size() > 2 && path[0] == '/' &&
        std::isalpha(static_cast<unsigned char>(path[1])) && path[2] == ':') {
      path.erase(0, 1);
    }
#endif
  }
  return path;
}

std::optional<std::string> readModuleSource(std::string_view moduleName) {
  const std::string filePath = moduleNameToFilePath(moduleName);
  if (filePath.empty()) {
    return std::nullopt;
  }
  if (hasUriScheme(filePath) && filePath.rfind("file://", 0) != 0) {
    return std::nullopt;
  }

  std::error_code ec;
  fs::path path = fs::u8path(filePath);
  if (!fs::exists(path, ec)) {
    return std::nullopt;
  }

  std::ifstream file(path, std::ios::in | std::ios::binary);
  if (!file.is_open()) {
    return std::nullopt;
  }

  std::stringstream buffer;
  buffer << file.rdbuf();
  return buffer.str();
}

std::string toModuleUrl(std::string_view moduleName) {
  const std::string filePath = moduleNameToFilePath(moduleName);
  if (filePath.empty()) {
    return std::string(moduleName);
  }
  if (hasUriScheme(filePath)) {
    return filePath;
  }

  fs::path path = fs::u8path(filePath);
  if (!path.is_absolute()) {
    path = "." / path;
  }
  std::error_code ec;
  path = fs::weakly_canonical(path, ec);
  if (ec) {
    path = fs::absolute(path, ec);
  }
  return "file://" + path.generic_string();
}

} // namespace

QuickJSVM& QuickJSVM::get() {
  static QuickJSVM instance;
  return instance;
}

QuickJSVM::~QuickJSVM() {
  shutdown();
}

void QuickJSVM::setScriptDirectory(const std::string& path) {
  scriptDirectory_ = path;
}

void QuickJSVM::setEntryScript(const std::string& path) {
  entryScript_ = path;
}

void QuickJSVM::setDebugMode(bool enable) {
  debugMode_ = enable;
}

qjs::Value QuickJSVM::newObject() {
  if (!context_) {
    throw std::runtime_error("QuickJSVM: context is not initialized");
  }
  return context_->newObject();
}

qjs::Value QuickJSVM::loadModule(const std::string& relativePath) {
  return loadModule(relativePath, "");
}

qjs::Value QuickJSVM::loadModule(const std::string& relativePath,
                                const std::string& cacheTag) {
  if (!context_) {
    throw std::runtime_error("QuickJSVM: context is not initialized");
  }

  std::string scriptPath = normalizeScriptPath(scriptDirectory_, relativePath);
  if (!cacheTag.empty()) {
    scriptPath += kHotReloadTagPrefix;
    scriptPath += cacheTag;
  }
  const std::string globalName =
      "__fbLoadedModule" + std::to_string(nextEvalId_++);
  const std::string loaderFilename =
      "<QuickJSVM:" + relativePath + ":" + std::to_string(nextEvalId_) + ">";
  const std::string loaderSource =
      "import * as " + globalName + " from \"" + escapeJsString(scriptPath) +
      "\";\n" + "globalThis." + globalName + " = " + globalName + ";\n";

  context_->eval(loaderSource, loaderFilename.c_str(), JS_EVAL_TYPE_MODULE);
  if (!runPendingJobs()) {
    throw std::runtime_error("QuickJSVM: module initialization failed");
  }

  return context_->global()[globalName.c_str()];
}

qjs::Value QuickJSVM::construct(const qjs::Value& constructor,
                                const qjs::Value& argument) {
  JSContext* ctx = constructor.ctx;
  if (!ctx) {
    throw std::runtime_error("QuickJSVM: constructor context is null");
  }

  JSValue argv[1] = {JS_DupValue(ctx, argument.v)};
  JSValue result = JS_CallConstructor(ctx, constructor.v, 1, argv);
  JS_FreeValue(ctx, argv[0]);
  if (JS_IsException(result)) {
    throw qjs::exception{ctx};
  }
  return wrapJsValue(ctx, std::move(result));
}

qjs::Value QuickJSVM::callMethod(const qjs::Value& object, const char* name) {
  JSContext* ctx = object.ctx;
  if (!ctx) {
    throw std::runtime_error("QuickJSVM: object context is null");
  }

  JSValue functionValue = JS_GetPropertyStr(ctx, object.v, name);
  if (JS_IsException(functionValue)) {
    throw qjs::exception{ctx};
  }
  if (JS_IsUndefined(functionValue) || JS_IsNull(functionValue)) {
    JS_FreeValue(ctx, functionValue);
    return wrapJsValue(ctx, JS_DupValue(ctx, JS_UNDEFINED));
  }

  JSValue result = JS_Call(ctx, functionValue, object.v, 0, nullptr);
  JS_FreeValue(ctx, functionValue);
  if (JS_IsException(result)) {
    throw qjs::exception{ctx};
  }
  return wrapJsValue(ctx, std::move(result));
}

qjs::Value QuickJSVM::callMethod(const qjs::Value& object, const char* name,
                                 const qjs::Value& argument) {
  JSContext* ctx = object.ctx;
  if (!ctx) {
    throw std::runtime_error("QuickJSVM: object context is null");
  }

  JSValue functionValue = JS_GetPropertyStr(ctx, object.v, name);
  if (JS_IsException(functionValue)) {
    throw qjs::exception{ctx};
  }
  if (JS_IsUndefined(functionValue) || JS_IsNull(functionValue)) {
    JS_FreeValue(ctx, functionValue);
    return wrapJsValue(ctx, JS_DupValue(ctx, JS_UNDEFINED));
  }

  JSValue argv[1] = {JS_DupValue(ctx, argument.v)};
  JSValue result = JS_Call(ctx, functionValue, object.v, 1, argv);
  JS_FreeValue(ctx, argv[0]);
  JS_FreeValue(ctx, functionValue);
  if (JS_IsException(result)) {
    throw qjs::exception{ctx};
  }
  return wrapJsValue(ctx, std::move(result));
}

qjs::Value QuickJSVM::callMethod(const qjs::Value& object, const char* name,
                                 double argument) {
  JSContext* ctx = object.ctx;
  if (!ctx) {
    throw std::runtime_error("QuickJSVM: object context is null");
  }

  JSValue functionValue = JS_GetPropertyStr(ctx, object.v, name);
  if (JS_IsException(functionValue)) {
    throw qjs::exception{ctx};
  }
  if (JS_IsUndefined(functionValue) || JS_IsNull(functionValue)) {
    JS_FreeValue(ctx, functionValue);
    return wrapJsValue(ctx, JS_DupValue(ctx, JS_UNDEFINED));
  }

  JSValue argv[1] = {JS_NewFloat64(ctx, argument)};
  JSValue result = JS_Call(ctx, functionValue, object.v, 1, argv);
  JS_FreeValue(ctx, argv[0]);
  JS_FreeValue(ctx, functionValue);
  if (JS_IsException(result)) {
    throw qjs::exception{ctx};
  }
  return wrapJsValue(ctx, std::move(result));
}

qjs::Value QuickJSVM::wrapActor(const Ptr<Actor>& actor) {
  if (!context_) {
    throw std::runtime_error("QuickJSVM: context is not initialized");
  }
  return context_->newValue(std::make_shared<JsActor>(actor));
}

qjs::Value QuickJSVM::wrapVec2(const Vec2& value) {
  if (!context_) {
    throw std::runtime_error("QuickJSVM: context is not initialized");
  }
  return context_->newValue(std::make_shared<JsVec2>(value));
}

qjs::Value QuickJSVM::wrapEvent(const Event& event) {
  if (!context_) {
    throw std::runtime_error("QuickJSVM: context is not initialized");
  }

  qjs::Value eventObject = newObject();

  switch (event.getType()) {
    case EventType::KeyDown:
    case EventType::KeyUp: {
      const auto& keyEvent = static_cast<const KeyEvent&>(event);
      eventObject["type"] = std::string(
          event.getType() == EventType::KeyDown ? "KeyDown" : "KeyUp");
      eventObject["key"] = keyCodeToString(keyEvent.getKeyCode());
      eventObject["repeat"] = keyEvent.isRepeat();
      break;
    }
    case EventType::JoystickButtonDown:
    case EventType::JoystickButtonUp: {
      const auto& buttonEvent =
          static_cast<const JoystickButtonEvent&>(event);
      eventObject["type"] = std::string(
          event.getType() == EventType::JoystickButtonDown
              ? "JoystickButtonDown"
              : "JoystickButtonUp");
      eventObject["button"] = static_cast<int>(buttonEvent.getButton());
      eventObject["buttonName"] =
          controllerButtonToString(buttonEvent.getButton());
      break;
    }
    case EventType::JoystickAxis: {
      const auto& axisEvent = static_cast<const JoystickAxisEvent&>(event);
      eventObject["type"] = std::string("JoystickAxis");
      eventObject["deviceId"] = axisEvent.getDeviceId();
      eventObject["axis"] = static_cast<int>(axisEvent.getAxis());
      eventObject["axisName"] = controllerAxisToString(axisEvent.getAxis());
      eventObject["value"] = static_cast<int>(axisEvent.getValue());
      eventObject["normalizedValue"] =
          static_cast<double>(axisEvent.getNormalizedValue());
      break;
    }
    case EventType::MouseButtonDown:
    case EventType::MouseButtonUp: {
      const auto& buttonEvent = static_cast<const MouseButtonEvent&>(event);
      eventObject["type"] = std::string(
          event.getType() == EventType::MouseButtonDown ? "MouseButtonDown"
                                                        : "MouseButtonUp");
      eventObject["button"] = static_cast<int>(buttonEvent.getButton());
      eventObject["buttonName"] =
          mouseButtonToString(buttonEvent.getButton());
      eventObject["x"] = buttonEvent.getX();
      eventObject["y"] = buttonEvent.getY();
      break;
    }
    case EventType::MouseMove: {
      const auto& moveEvent = static_cast<const MouseMoveEvent&>(event);
      eventObject["type"] = std::string("MouseMove");
      eventObject["x"] = moveEvent.getX();
      eventObject["y"] = moveEvent.getY();
      break;
    }
    case EventType::MouseWheel: {
      const auto& wheelEvent = static_cast<const MouseWheelEvent&>(event);
      eventObject["type"] = std::string("MouseWheel");
      eventObject["x"] = static_cast<int>(wheelEvent.getPosition().x);
      eventObject["y"] = static_cast<int>(wheelEvent.getPosition().y);
      eventObject["wheelX"] = wheelEvent.getX();
      eventObject["wheelY"] = wheelEvent.getY();
      break;
    }
    case EventType::WindowResize: {
      const auto& resizeEvent = static_cast<const WindowResizeEvent&>(event);
      eventObject["type"] = std::string("WindowResize");
      eventObject["width"] = resizeEvent.getWidth();
      eventObject["height"] = resizeEvent.getHeight();
      break;
    }
    default:
      return wrapJsValue(context_->ctx, JS_DupValue(context_->ctx, JS_UNDEFINED));
  }

  return eventObject;
}

std::optional<EventType> QuickJSVM::parseEventTypeName(
    std::string_view typeName) const {
  if (typeName == "KeyDown") {
    return EventType::KeyDown;
  }
  if (typeName == "KeyUp") {
    return EventType::KeyUp;
  }
  if (typeName == "JoystickButtonDown") {
    return EventType::JoystickButtonDown;
  }
  if (typeName == "JoystickButtonUp") {
    return EventType::JoystickButtonUp;
  }
  if (typeName == "JoystickAxis") {
    return EventType::JoystickAxis;
  }
  if (typeName == "MouseButtonDown") {
    return EventType::MouseButtonDown;
  }
  if (typeName == "MouseButtonUp") {
    return EventType::MouseButtonUp;
  }
  if (typeName == "MouseMove") {
    return EventType::MouseMove;
  }
  if (typeName == "MouseWheel") {
    return EventType::MouseWheel;
  }
  if (typeName == "WindowResize") {
    return EventType::WindowResize;
  }
  return std::nullopt;
}

void QuickJSVM::RegisterBindingInstaller(BindingInstaller installer) {
  if (!installer) {
    return;
  }
  if (initialized_ && context_ && !installer(*context_)) {
    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                 "[QuickJS] Failed to install external bindings");
    return;
  }
  bindingInstallers_.push_back(std::move(installer));
}

void QuickJSVM::RegisterBootstrapScript(const std::string& script) {
  if (script.empty()) {
    return;
  }
  if (initialized_ && context_) {
    try {
      context_->eval(script, "<quickjs-extension-bootstrap>", JS_EVAL_TYPE_MODULE);
      runPendingJobs();
    } catch (qjs::exception) {
      logScriptException("Failed to bootstrap external QuickJS globals");
      return;
    } catch (const std::exception& e) {
      SDL_LogError(SDL_LOG_CATEGORY_ERROR,
                   "[QuickJS] Failed to bootstrap external globals: %s",
                   e.what());
      return;
    }
  }
  bootstrapScripts_.push_back(script);
}

bool QuickJSVM::init() {
  if (initialized_) {
    return true;
  }

  try {
    runtime_ = std::make_unique<qjs::Runtime>();
    context_ = std::make_unique<qjs::Context>(*runtime_);
    context_->moduleLoader = [](std::string_view moduleName) {
      return qjs::Context::ModuleData{
          toModuleUrl(moduleName), readModuleSource(moduleName)};
    };
    if (!registerBindings()) {
      shutdown();
      return false;
    }
    if (!installGlobals()) {
      shutdown();
      return false;
    }
    initialized_ = true;
    SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[QuickJS] VM initialized");
    return true;
  } catch (const std::exception& e) {
    SDL_LogError(SDL_LOG_CATEGORY_ERROR, "[QuickJS] Failed to initialize VM: %s",
                 e.what());
    shutdown();
    return false;
  }
}

void QuickJSVM::shutdown() {
  if (!runtime_ && !context_) {
    initialized_ = false;
    return;
  }

  context_.reset();
  runtime_.reset();
  initialized_ = false;
  SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[QuickJS] VM shutdown");
}

bool QuickJSVM::run() {
  if (!initialized_ || !context_) {
    SDL_LogError(SDL_LOG_CATEGORY_ERROR,
                 "[QuickJS] VM is not initialized before run()");
    return false;
  }

  const std::string entryPath = buildEntryPath();
  auto scriptContent = Asset::get().readFileToString(entryPath);
  if (!scriptContent) {
    SDL_LogError(SDL_LOG_CATEGORY_ERROR,
                 "[QuickJS] Failed to read entry script: %s",
                 entryPath.c_str());
    return false;
  }

  try {
    context_->eval(*scriptContent, entryPath.c_str(), JS_EVAL_TYPE_MODULE);
    if (!runPendingJobs()) {
      return false;
    }
    if (debugMode_) {
      SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
                  "[QuickJS] Executed entry script: %s", entryPath.c_str());
    }
    return true;
  } catch (qjs::exception) {
    logScriptException("Failed to execute entry script");
  } catch (const std::exception& e) {
    SDL_LogError(SDL_LOG_CATEGORY_ERROR, "[QuickJS] Runtime error: %s", e.what());
  }
  return false;
}

bool QuickJSVM::registerBindings() {
  if (!context_) {
    return false;
  }

  auto& module = context_->addModule("frostbite");

  module.function("log", [](qjs::rest<qjs::Value> args) {
    std::string message = joinLogArguments(args);
    SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[QuickJS] %s", message.c_str());
  });

  module.class_<JsVec2>("Vec2")
      .constructor<double, double>()
      .fun<&JsVec2::x>("x")
      .fun<&JsVec2::y>("y");

  module.class_<JsActor>("Actor")
      .constructor<>()
      .fun<&JsActor::GetName>("GetName")
      .fun<&JsActor::SetName>("SetName")
      .fun<&JsActor::GetParent>("GetParent")
      .fun<&JsActor::GetPosition>("GetPosition")
      .fun<&JsActor::SetPosition>("SetPosition")
      .fun<&JsActor::SetPositionXY>("SetPositionXY")
      .fun<&JsActor::GetTopLeftPosition>("GetTopLeftPosition")
      .fun<&JsActor::SetTopLeftPosition>("SetTopLeftPosition")
      .fun<&JsActor::SetTopLeftPositionXY>("SetTopLeftPositionXY")
      .fun<&JsActor::GetRotation>("GetRotation")
      .fun<&JsActor::SetRotation>("SetRotation")
      .fun<&JsActor::GetScale>("GetScale")
      .fun<&JsActor::SetScale>("SetScale")
      .fun<&JsActor::SetScaleValue>("SetScaleValue")
      .fun<&JsActor::IsVisible>("IsVisible")
      .fun<&JsActor::SetVisible>("SetVisible")
      .fun<&JsActor::AddChild>("AddChild")
      .fun<&JsActor::RemoveAllChildren>("RemoveAllChildren")
      .fun<&JsActor::GetSize>("GetSize")
      .fun<&JsActor::SetSize>("SetSize")
      .fun<&JsActor::SetSizeWH>("SetSizeWH")
      .fun<&JsActor::GetAnchor>("GetAnchor")
      .fun<&JsActor::SetAnchor>("SetAnchor")
      .fun<&JsActor::SetAnchorXY>("SetAnchorXY")
      .fun<&JsActor::LocalToWorld>("LocalToWorld")
      .fun<&JsActor::WorldToLocal>("WorldToLocal")
      .fun<&JsActor::ContainsWorldPoint>("ContainsWorldPoint")
      .fun<&JsActor::IsEventReceiveEnabled>("IsEventReceiveEnabled")
      .fun<&JsActor::EnableEventReceive>("EnableEventReceive")
      .fun<&JsActor::DisableEventReceive>("DisableEventReceive")
      .fun<&JsActor::AddEventListener>("AddEventListener")
      .fun<&JsActor::RemoveEventListener>("RemoveEventListener")
      .fun<&JsActor::ClearEventListeners>("ClearEventListeners");

  module.class_<JsScene>("Scene")
      .constructor<>()
      .base<JsActor>()
      .fun<&JsScene::AddChild>("AddChild");

  module.class_<JsSprite>("Sprite")
      .constructor<>()
      .base<JsActor>()
      .fun<&JsSprite::SetSize>("SetSize")
      .fun<&JsSprite::SetOffset>("SetOffset")
      .fun<&JsSprite::SetOffsetXY>("SetOffsetXY")
      .fun<&JsSprite::SetBlendMode>("SetBlendMode");

  module.function("createSpriteFromFile",
                  [](const std::string& path) { return JsSprite::CreateFromFile(path); });
  module.function("createSpriteFromNpk", [](const std::string& path, uint32_t frameIndex) {
    return JsSprite::CreateFromNpk(path, frameIndex);
  });

  module.class_<JsTextSprite>("TextSprite")
      .constructor<>()
      .base<JsSprite>()
      .fun<&JsTextSprite::SetText>("SetText")
      .fun<&JsTextSprite::GetText>("GetText")
      .fun<&JsTextSprite::SetFont>("SetFont")
      .fun<&JsTextSprite::GetFontName>("GetFontName")
      .fun<&JsTextSprite::SetTextColor>("SetTextColor")
      .fun<&JsTextSprite::GetTextSize>("GetTextSize");

  module.function("createTextSprite", []() { return JsTextSprite::Create(); });
  module.function("createTextSpriteWithFont",
                  [](const std::string& text, const std::string& font) {
                    return JsTextSprite::CreateWithFont(text, font);
                  });

  module.class_<JsUIScene>("UIScene").constructor<>().base<JsScene>();

  module.add("BLEND_NONE", static_cast<int>(BlendMode::None));
  module.add("BLEND_NORMAL", static_cast<int>(BlendMode::Normal));
  module.add("BLEND_ADDITIVE", static_cast<int>(BlendMode::Additive));
  module.add("BLEND_SCREEN", static_cast<int>(BlendMode::Screen));
  module.add("BLEND_PREMULTIPLIED", static_cast<int>(BlendMode::Premultiplied));
  module.add("BLEND_SUBTRACTIVE", static_cast<int>(BlendMode::Subtractive));
  module.add("BLEND_MULTIPLY", static_cast<int>(BlendMode::Multiply));

  for (const auto& installer : bindingInstallers_) {
    if (!installer) {
      continue;
    }
    if (!installer(*context_)) {
      SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                   "[QuickJS] Failed to install external bindings");
      return false;
    }
  }

  return true;
}

bool QuickJSVM::installGlobals() {
  static constexpr char kBootstrapScript[] = R"(
import * as frostbite from "frostbite";
globalThis.frostbite = frostbite;
globalThis.log = frostbite.log;
globalThis.Vec2 = frostbite.Vec2;
globalThis.Actor = frostbite.Actor;
globalThis.Scene = frostbite.Scene;
globalThis.UIScene = frostbite.UIScene;
globalThis.Sprite = frostbite.Sprite;
globalThis.Sprite.createFromFile = frostbite.createSpriteFromFile;
globalThis.Sprite.createFromNpk = frostbite.createSpriteFromNpk;
globalThis.TextSprite = frostbite.TextSprite;
globalThis.TextSprite.create = frostbite.createTextSprite;
globalThis.TextSprite.createWithFont = frostbite.createTextSpriteWithFont;
globalThis.BlendMode = {
  None: frostbite.BLEND_NONE,
  Normal: frostbite.BLEND_NORMAL,
  Additive: frostbite.BLEND_ADDITIVE,
  Screen: frostbite.BLEND_SCREEN,
  Premultiplied: frostbite.BLEND_PREMULTIPLIED,
  Subtractive: frostbite.BLEND_SUBTRACTIVE,
  Multiply: frostbite.BLEND_MULTIPLY
};
)";

  try {
    std::string bootstrapScript = kBootstrapScript;
    for (const auto& script : bootstrapScripts_) {
      if (!script.empty()) {
        bootstrapScript += "\n";
        bootstrapScript += script;
        bootstrapScript += "\n";
      }
    }
    context_->eval(bootstrapScript, "<quickjs-bootstrap>", JS_EVAL_TYPE_MODULE);
    return runPendingJobs();
  } catch (qjs::exception) {
    logScriptException("Failed to bootstrap QuickJS globals");
    return false;
  } catch (const std::exception& e) {
    SDL_LogError(SDL_LOG_CATEGORY_ERROR,
                 "[QuickJS] Failed to bootstrap globals: %s", e.what());
    return false;
  }
}

bool QuickJSVM::runPendingJobs() {
  if (!runtime_) {
    return false;
  }

  try {
    while (runtime_->executePendingJob() != nullptr) {
    }
    return true;
  } catch (qjs::exception) {
    logScriptException("Unhandled exception in QuickJS pending job");
    return false;
  } catch (const std::exception& e) {
    SDL_LogError(SDL_LOG_CATEGORY_ERROR,
                 "[QuickJS] Pending job runtime error: %s", e.what());
    return false;
  }
}

void QuickJSVM::logException(const char* stage) {
  logScriptException(stage);
}

void QuickJSVM::logScriptException(const char* stage) {
  if (!context_) {
    SDL_LogError(SDL_LOG_CATEGORY_ERROR, "[QuickJS] %s", stage);
    return;
  }

  try {
    auto exception = context_->getException();
    const std::string message = exception.as<std::string>();
    SDL_LogError(SDL_LOG_CATEGORY_ERROR, "[QuickJS] %s: %s", stage,
                 message.c_str());
    if ((bool)exception["stack"]) {
      const std::string stack = exception["stack"].as<std::string>();
      SDL_LogError(SDL_LOG_CATEGORY_ERROR, "[QuickJS] stack: %s", stack.c_str());
    }
  } catch (const std::exception& e) {
    SDL_LogError(SDL_LOG_CATEGORY_ERROR,
                 "[QuickJS] %s (failed to decode exception: %s)", stage,
                 e.what());
  } catch (...) {
    SDL_LogError(SDL_LOG_CATEGORY_ERROR,
                 "[QuickJS] %s (failed to decode exception)", stage);
  }
}

std::string QuickJSVM::buildEntryPath() const {
  const std::string scriptDir =
      scriptDirectory_.empty() ? "assets/scripts" : scriptDirectory_;
  const std::string entry = entryScript_.empty() ? "main.js" : entryScript_;
  if (scriptDir.empty()) {
    return entry;
  }
  if (scriptDir.back() == '/' || scriptDir.back() == '\\') {
    return scriptDir + entry;
  }
  return scriptDir + "/" + entry;
}

} // namespace frostbite2D
