#include "node.h"
#include "object.h"

using namespace boa;
using namespace Napi;

Napi::FunctionReference PythonNode::constructor;

Napi::Object PythonNode::Init(Napi::Env env, Napi::Object exports) {
  Napi::HandleScope scope(env);
  Napi::Function func =
      DefineClass(env, "Python",
                  {
                      InstanceMethod("builtins", &PythonNode::Builtins),
                      InstanceMethod("eval", &PythonNode::Eval),
                      InstanceMethod("globals", &PythonNode::Globals),
                      InstanceMethod("import", &PythonNode::Import),
                      InstanceMethod("print", &PythonNode::Print),
                  });

  constructor = Napi::Persistent(func);
  constructor.SuppressDestruct();

  exports.Set("Python", func);
  return exports;
}

PythonNode::PythonNode(const CallbackInfo &info)
    : Napi::ObjectWrap<PythonNode>(info) {
  Napi::Env env = info.Env();
  Napi::HandleScope scope(env);
  {
    if (Py_IsInitialized() == 0) {
      pybind::initialize_interpreter();
      initialized = true;
    }
    // Set Python Arguments.
    if (info[0].IsArray()) {
      Napi::Array jargv = info[0].As<Napi::Array>();
      size_t argc = jargv.Length();
      wchar_t *argv[argc];

      for (size_t idx = 0; idx < argc; idx++) {
        Napi::Value v = jargv[idx];
        std::string s = std::string(v.As<String>());
        argv[idx] = Py_DecodeLocale(s.c_str(), nullptr);
      }
      PySys_SetArgvEx(argc, argv, 0);

      for (wchar_t *item : argv)
        PyMem_RawFree(item);
    }
  }
}

PythonNode::~PythonNode() {
  if (initialized && Py_IsInitialized() == true) {
    pybind::finalize_interpreter();
  }
}

void PythonNode::Finalize(Napi::Env env) {
  // fprintf(stderr, "boa(Python) is done\n");
}

Napi::Value PythonNode::Builtins(const CallbackInfo &info) {
  PyObject *builtins = PyEval_GetBuiltins();
  if (builtins == NULL) {
    Napi::TypeError::New(info.Env(), "Python builtins is null.")
        .ThrowAsJavaScriptException();
    return info.Env().Undefined();
  }
  return PythonObject::NewInstance(
      info.Env(), pybind::reinterpret_borrow<pybind::object>(builtins));
}

Napi::Value PythonNode::Eval(const CallbackInfo &info) {
  if (info.Length() < 1 || !info[0].IsString()) {
    Napi::TypeError::New(info.Env(), "String is excepted.")
        .ThrowAsJavaScriptException();
    return info.Env().Undefined();
  }
  std::string expr = std::string(info[0].As<String>());
  Object opts = info[1].As<Object>();
  auto globals = PythonObject::Unwrap(opts.Get("globals").As<Object>());
  auto locals = PythonObject::Unwrap(opts.Get("locals").As<Object>());
  auto result = pybind::eval(expr, globals->value(), locals->value());
  return PythonObject::NewInstance(info.Env(), result);
}

Napi::Value PythonNode::Globals(const CallbackInfo &info) {
  return PythonObject::NewInstance(info.Env(), pybind::globals());
}

Napi::Value PythonNode::Import(const CallbackInfo &info) {
  if (info.Length() < 1 || !info[0].IsString()) {
    Napi::TypeError::New(info.Env(), "String is excepted.")
        .ThrowAsJavaScriptException();
    return info.Env().Undefined();
  }
  std::string mModulePath = std::string(info[0].As<String>());
  try {
    pybind::object obj = pybind::module::import(mModulePath.c_str());
    return PythonObject::NewInstance(info.Env(), obj);
  } catch (pybind::error_already_set &e) {
    Napi::TypeError::New(info.Env(), e.what()).ThrowAsJavaScriptException();
    return info.Env().Undefined();
  }
}

Napi::Value PythonNode::Print(const CallbackInfo &info) {
  if (info.Length() < 1 || !info[0].IsString()) {
    Napi::TypeError::New(info.Env(), "String is expected.")
        .ThrowAsJavaScriptException();
    return info.Env().Undefined();
  }
  std::string msg = std::string(info[0].As<String>());
  pybind::print(msg);
  return info.Env().Undefined();
}
