#include "stdafx.h"

#include "xtr/core/PyCore.h"

#pragma warning( push )
#pragma warning( disable : 4100 )
#include "Python.h"
#pragma warning( pop )

namespace cxxpython {

Object Object::FromNew(_object* objectIn) noexcept {
  return Object{ objectIn };
}

Object Object::FromBorrowed(_object* objectIn) noexcept {
  Py_IncRef(objectIn);
  return Object{ objectIn };
}

Object::~Object() noexcept {
  DecreaseRefCount();
}

void Object::DecreaseRefCount() noexcept {
  if (object != nullptr) {
    try {
      Py_DecRef(object);
    } catch (...) {}
  }
}

Object::Object(const Object& obj2) noexcept {
  object = obj2.object;
  Py_IncRef(object);
}

Object& Object::operator=(const Object& obj2) noexcept {
  if (this != &obj2) {
    DecreaseRefCount();
    object = obj2.object;
    Py_IncRef(object);
  }
  return *this;
}

Object::Object(Object&& obj2) noexcept {
  object = obj2.object;
  obj2.object = nullptr;
}

Object& Object::operator=(Object&& obj2) noexcept {
  if (this != &obj2) {
    DecreaseRefCount();
    object = obj2.object;
    obj2.object = nullptr;
  }
  return *this;
}

Object Object::at(int index) const noexcept {
  return Object::FromBorrowed(PyTuple_GetItem(object, index));
}

std::string ToString(const Object& object) {
  Py_ssize_t size{};
  const auto* charsFromPython = PyUnicode_AsUTF8AndSize(object.get(), &size);
  if (charsFromPython == nullptr) {
    return {};
  } else {
    std::string result(charsFromPython, size);
    return result;
  }
}

long ToLong(const Object& object) noexcept {
  return PyLong_AsLong(object.get());
}

bool ToBool(const Object& object) noexcept {
  return object.get() == Py_True;
}

Callable::Callable(_object* object) noexcept
  : func{ Object::FromBorrowed(object) } {}

Object Callable::operator()() const noexcept {
  return Object::FromNew(PyObject_CallObject(func.get(), nullptr));
}

#pragma warning( push )
#pragma warning( disable : 26492 26477 26481 ) // ignore warnings for variadic function

Object Callable::operator()(const char* format, ...) const noexcept {
  va_list args; // NOLINT
  va_start(args, format); // NOLINT
  auto argsObject = Object::FromNew(Py_VaBuildValue(format, args));
  va_end(args);
  return Object::FromNew(PyObject_CallObject(func.get(), argsObject.get()));
}

#pragma warning( pop )

PyCore::~PyCore() {
  Stop();
}

PyCore& PyCore::Instance() {
  static PyCore instance{};
  return instance;
}

bool PyCore::IsRunning() const noexcept {
  return isRunning;
}

bool PyCore::Start(const wchar_t* pyPath) noexcept {
  if (IsRunning()) {
    return false;
  }

  PyConfig config;
  PyConfig_InitPythonConfig(&config);
  if (pyPath != nullptr) {
    PyStatus err = PyConfig_SetString(&config, &config.home, pyPath);
    if (PyStatus_Exception(err)) {
      PyConfig_Clear(&config);
      return false;
    }
  }

  const PyStatus status = Py_InitializeFromConfig(&config);
  if (PyStatus_Exception(status)) {
    /*if (PyStatus_IsExit(status)) {
    }
    Py_ExitStatusException(status);*/
    PyConfig_Clear(&config);
    return false;
  }

  PyConfig_Clear(&config);
  isRunning = true;
  return true;
}

void PyCore::Stop() noexcept {
  if (IsRunning()) {
    isRunning = false;
    modules.clear();
    Py_FinalizeEx();
  }
}

bool PyCore::LoadModule(const std::string& moduleName, const std::string& pyImport) {
  if (!IsRunning()) {
    return false;
  } else if (moduleName.empty()) {
    return false;
  } else if (modules.contains(moduleName)) {
    return true;
  } 

  auto nameObj = Object::FromNew(PyUnicode_FromString(pyImport.c_str()));
  if (nameObj == nullptr) {
    return false;
  }

  auto moduleObj = Object::FromNew(PyImport_Import(nameObj.get()));
  if (moduleObj == nullptr) {
    return false;
  }

  auto dictionaryObj = Object::FromBorrowed(PyModule_GetDict(moduleObj.get()));
  if (dictionaryObj == nullptr) {
    return false;
  }

  modules.insert(std::pair(
    moduleName,
    Module{ .pyModule = moduleObj, .pyDictionary = std::move(dictionaryObj) }
  ));
  return true;
}

Callable PyCore::PyFunction(const std::string& moduleName, const std::string& funcName) const {
  if (!IsRunning()) {
    return nullptr;
  } else if (const auto it = modules.find(moduleName); it == std::end(modules)) {
    return nullptr;
  } else if (const auto nameObj = Object::FromNew(PyUnicode_FromString(funcName.c_str()));
             nameObj == nullptr) {
    return nullptr;
  } else if (auto* pFunc = PyDict_GetItem(it->second.pyDictionary.get(), nameObj.get());
             PyCallable_Check(pFunc) == 0) {
    PyErr_Print();
    return nullptr;
  } else {
    return Callable(pFunc);
  }
}

} // namespace cxxpython