#pragma once

#include <frostbite2D/types/type_alias.h>

#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

#include <frostbite2D/event/event.h>

namespace qjs {
class Runtime;
class Context;
class Value;
} // namespace qjs

namespace frostbite2D {

class Actor;
struct Vec2;

class QuickJSVM {
public:
  using BindingInstaller = std::function<bool(qjs::Context&)>;

  static QuickJSVM& get();

  void setScriptDirectory(const std::string& path);
  void setEntryScript(const std::string& path);
  void setDebugMode(bool enable);

  bool init();
  void shutdown();
  bool run();

  bool isInitialized() const { return initialized_; }
  qjs::Context* getContext() const { return context_.get(); }
  const std::string& getScriptDirectory() const { return scriptDirectory_; }

  bool runPendingJobs();
  void logException(const char* stage);

  qjs::Value newObject();
  qjs::Value loadModule(const std::string& relativePath);
  qjs::Value loadModule(const std::string& relativePath,
                        const std::string& cacheTag);
  qjs::Value construct(const qjs::Value& constructor, const qjs::Value& argument);
  qjs::Value callMethod(const qjs::Value& object, const char* name);
  qjs::Value callMethod(const qjs::Value& object, const char* name,
                        const qjs::Value& argument);
  qjs::Value callMethod(const qjs::Value& object, const char* name,
                        double argument);
  qjs::Value wrapActor(const Ptr<Actor>& actor);
  qjs::Value wrapVec2(const Vec2& value);
  qjs::Value wrapEvent(const Event& event);
  std::optional<EventType> parseEventTypeName(std::string_view typeName) const;
  void RegisterBindingInstaller(BindingInstaller installer);
  void RegisterBootstrapScript(const std::string& script);

private:
  QuickJSVM() = default;
  ~QuickJSVM();

  QuickJSVM(const QuickJSVM&) = delete;
  QuickJSVM& operator=(const QuickJSVM&) = delete;

  bool registerBindings();
  bool installGlobals();
  void logScriptException(const char* stage);
  std::string buildEntryPath() const;

  bool initialized_ = false;
  bool debugMode_ = false;
  std::string scriptDirectory_ = "assets/scripts";
  std::string entryScript_ = "main.js";
  uint64 nextEvalId_ = 1;
  std::vector<BindingInstaller> bindingInstallers_;
  std::vector<std::string> bootstrapScripts_;

  std::unique_ptr<qjs::Runtime> runtime_;
  std::unique_ptr<qjs::Context> context_;
};

} // namespace frostbite2D
