#pragma once

#include <string>
#include <map>

// Note: forward declare basic object class for Python
struct _object; // NOLINT(bugprone-reserved-identifier, cert-dcl37-c, cert-dcl51-cpp)

namespace cxxpython {

//! PyObject wrapper
/*!
  Uses Python ref counting
*/
class Object {
  _object* object;

public:
  ~Object() noexcept;
  Object(const Object& obj2) noexcept;
  Object& operator=(const Object& obj2) noexcept;
  Object(Object&& obj2) noexcept;
  Object& operator=(Object&& obj2) noexcept;

private:
  explicit Object(_object* objectIn) noexcept
    : object{ objectIn } {}

public:
  explicit(false) Object(nullptr_t) noexcept // NOLINT(google-explicit-constructor, hicpp-explicit-conversions)
    : object{ nullptr } {}
  [[nodiscard]] static Object FromNew(_object* objectIn) noexcept;
  [[nodiscard]] static Object FromBorrowed(_object* objectIn) noexcept;

public:
  [[nodiscard]] Object at(int index) const noexcept;
  [[nodiscard]] constexpr bool operator!=(nullptr_t) const noexcept { return object != nullptr; }
  [[nodiscard]] constexpr bool operator==(nullptr_t) const noexcept { return object == nullptr; }

  // Note: use with Python/C API only!
  [[nodiscard]] constexpr _object* get() noexcept { return object; }
  [[nodiscard]] constexpr _object* get() const noexcept { return object; }

private:
  void DecreaseRefCount() noexcept;
};

//! Convert object to string
std::string ToString(const Object& object);

//! Convert object to long
long ToLong(const Object& object) noexcept;

//! Convert object to bool
bool ToBool(const Object& object) noexcept;

//! Python callable wrapper
class Callable {
  Object func;

public:
  explicit(false) Callable(nullptr_t) noexcept // NOLINT(google-explicit-constructor, hicpp-explicit-conversions)
    : func{ nullptr } {}
  explicit Callable(_object* object) noexcept;

public:
  constexpr bool operator!=(nullptr_t) const noexcept { return func != nullptr; }
  constexpr bool operator==(nullptr_t) const noexcept { return func == nullptr; }

  Object operator()() const noexcept;
  Object operator()(const char* format, ...) const noexcept;
};

//! Python interpreter wrapper
class PyCore {
  struct Module {
    Object pyModule{ nullptr };
    Object pyDictionary{ nullptr };
  };

  bool isRunning{ false };
  std::map<std::string, Module> modules{};

public:
  PyCore(const PyCore&) = delete;
  PyCore& operator=(const PyCore&) = delete;

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

public:
  static PyCore& Instance();

public:
  //! Start Python interpreter from local path.
  /*!
  Requires: The argument should point to a zero-terminated character string
  in static storage whose contents will not change
  for the duration of the program’s execution
  see https://docs.python.org/3/c-api/init.html#c.Py_SetPythonHome
  */
  bool Start(const wchar_t* pyPath = nullptr) noexcept;

  //! Stop Python interpreter.
  /*!
    Requires: Every Object should be destroyed before calling PyCore.Stop()
  */
  void Stop() noexcept;
  [[nodiscard]] bool IsRunning() const noexcept;

  bool LoadModule(const std::string& moduleName, const std::string& pyFileName);
  [[nodiscard]] Callable PyFunction(const std::string& moduleName, const std::string& funcName) const;
};

} // namespace cxxpython