// Cyphesis Online RPG Server and AI Engine
// Copyright (C) 2000-2005 Alistair Riddoch
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software Foundation,
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA


#include "Python.h"

#include "Python_API.h"
#include "Python_Script_Utils.h"

#include "rules/ai/BaseMind.h"

#include "common/globals.h"
#include "common/const.h"
#include "common/debug.h"
#include "common/AssetsManager.h"
#include "PythonMalloc.h"
#include "pythonbase/WrapperBase.h"
#include "Remotery.h"

#include <Atlas/Objects/Operation.h>
#include <Atlas/Objects/Anonymous.h>

#include <boost/algorithm/string.hpp>
#include <boost/asio/steady_timer.hpp>
#include <filesystem>

#include <basedir.h>

using Atlas::Message::Element;
using Atlas::Objects::Root;
using Atlas::Objects::Operation::RootOperation;
using Atlas::Objects::Entity::Anonymous;

static constexpr auto debug_flag = false;

/// \defgroup PythonWrappers Python Wrapper Types
///
/// Structure types based on the PyObject header used to wrap C++ objects
/// in Python.


//////////////////////////////////////////////////////////////////////////
// Logger replaces sys.stdout and sys.stderr so the nothing goes to output
//////////////////////////////////////////////////////////////////////////


/// \brief Python type to handle output from python scripts
///
/// An instance of this type is used to replace sys.stdout and sys.stderr
/// in the Python interpreter so that all script output goes to the cyphesis
/// log subsystem
struct LogWriter : public WrapperBase<spdlog::level::level_enum, LogWriter>
{
    std::string m_message;

    LogWriter(Py::PythonClassInstance* self, Py::Tuple& args, Py::Dict& kwds)
            : WrapperBase(self, args, kwds)
    {

    }

    LogWriter(Py::PythonClassInstance* self, spdlog::level::level_enum value)
            : WrapperBase(self, value)
    {

    }

    void python_log(spdlog::level::level_enum lvl, const std::string& msg)
    {

        m_message += msg;
        std::string::size_type n = 0;
        std::string::size_type p;
        for (p = m_message.find_first_of('\n');
             p != std::string::npos;
             p = m_message.find_first_of('\n', n)) {
			spdlog::log(lvl, m_message.substr(n, p - n));
            n = p + 1;
        }
        if (m_message.size() > n) {
            m_message = m_message.substr(n, m_message.size() - n);
        } else {
            m_message.clear();
        }

    }

    Py::Object write(const Py::Tuple& args)
    {
        if (args.length() > 0) {
            python_log(m_value, verifyString(args.front()));
        }

        return Py::None();
    }

    Py::Object flush()
    {
        return Py::None();
    }

    static void init_type()
    {
        behaviors().name("LogWriter");
        behaviors().doc("");

		register_method<&LogWriter::flush>("flush", "");
		register_method<&LogWriter::write>("write", "");

        behaviors().readyType();
    }

};

/// \brief Find a class in a Python module
///
/// @param module the imported Python module
/// @param package the name of the module for error reporting
/// @param type the name of the class or type
/// @return new reference
Py::Object Get_PyClass(const Py::Module& module,
                       const std::string& package,
                       const std::string& type)
{
    auto py_class = module.getAttr(type);
    if (py_class.isNull()) {
        spdlog::error("Could not find python class \"{}.{}\"",
                                   package, type);
        PyErr_Print();
        return Py::Null();
    }
    if (!py_class.isCallable()) {
        spdlog::error("Could not instance python class \"{}.{}\"",
                                   package, type);
        return Py::Null();
    }
    return py_class;
}

/// \brief Import a Python module
///
/// @param package the name of the module
/// @return new reference
Py::Module Get_PyModule(const std::string& package)
{
    Py::String package_name(package);
    PyObject* module = PyImport_Import(package_name.ptr());
    if (module == nullptr) {
        spdlog::error("Missing python module \"{}\"", package);
        PyErr_Print();
        return Py::Module(nullptr);
    }
    return Py::Module(module);
}

Py::Object Create_PyScript(const Py::Object& wrapper, const Py::Callable& py_class)
{
    try {
        return py_class.apply(Py::TupleN(wrapper));
    } catch (...) {
        if (PyErr_Occurred() == nullptr) {
            spdlog::error("Could not create python instance");
        } else {
            spdlog::error("Reporting python error");
            PyErr_Print();
        }
        return Py::None();
    }
}

sigc::signal<void()> python_reload_scripts;

std::vector<std::string> python_directories;

std::map<std::filesystem::path, std::string> changedPaths;

namespace {
    void reloadChangedPaths()
    {
        if (!changedPaths.empty()) {

            for (auto& entry: changedPaths) {
                auto& package = entry.second;
                auto& path = entry.first;
                auto module = Get_PyModule(package);
                if (!module.isNull()) {
                    spdlog::info("Reloading module \"{}\" from file {}.", package, path.string());
                    auto result = PyImport_ReloadModule(module.ptr());
                    if (result != module.ptr()) {
                        spdlog::warn("New pointer returned when reloading module \"{}\".", package);
                    }

                }

            }
            python_reload_scripts();
            changedPaths.clear();
        }
    }
}

void observe_python_directories(boost::asio::io_context& io_context, AssetsManager& assetsManager)
{

    for (auto& directory: python_directories) {
        assetsManager.observeDirectory(directory, [=, &io_context](const std::filesystem::path& path) {
            //Trim the ".py" extension
            if (boost::ends_with(path.string(), ".py")) {
                auto relative = path.string().substr(directory.length() + 1);
                relative = relative.substr(0, relative.size() - 3);
                static char separator[] = {std::filesystem::path::preferred_separator, 0};
                auto package = boost::replace_all_copy(relative, separator, ".");
                changedPaths[path] = package;

                auto timer = std::make_shared<boost::asio::steady_timer>(io_context);
#if BOOST_VERSION >= 106600
                timer->expires_after(std::chrono::milliseconds(20));
#else
                timer->expires_from_now(std::chrono::milliseconds(20));
#endif
                timer->async_wait([&, timer](const boost::system::error_code& ec) {
                    if (!ec) {
                        rmt_ScopedCPUSample(PythonReload, 0)
                        reloadChangedPaths();
                    }
                });
            }

        });
    }
}

void init_python_api(std::vector<std::function<std::string()>> initFunctions, std::vector<std::string> scriptDirectories, bool log_stdout)
{
    //If we're using the system Python installation then everything should be setup for Python to use.
    //But if we're instead using something like a Conan version where Python is installed in a different place
    //we need to tell Python where to find its stuff. This is done through the PYTHONHOME environment variable.
    //Our build system is setup so that it will inject the preprocessor variable by that same name in that case.
#if defined(PYTHONHOME)
    if (getenv("SNAP") != nullptr) {
        spdlog::info("Detected running as Snap so won't alter the Python home directory.");
    } else {
        spdlog::info("Setting Python home directory to " PYTHONHOME);
        setenv("PYTHONHOME", PYTHONHOME, 1);
    }
#endif

    python_directories = std::move(scriptDirectories);

    std::vector<std::string> modules;
    for (auto& function: initFunctions) {
        modules.emplace_back(function());
    }

    char* usemalloc = getenv("PYTHONMALLOC");

    if (usemalloc) {
        setupPythonMalloc();
        spdlog::info("Python is using malloc for memory allocation.");
    }

    Py_InitializeEx(0);

    //Make sure that all modules are imported, since this is needed to initialize all types.
    //Otherwise we risk that we try to invoke the types from the C++ code before they have
    //been populated.
    for (auto& moduleName: modules) {
        PyImport_ImportModule(moduleName.c_str());
    }

    PyImport_ImportModule("sys");
    PyImport_ImportModule("traceback");

    Py::Module sys_module(PyImport_Import(Py::String("sys").ptr()));

    if (sys_module.isNull()) {
        spdlog::critical("Python could not import sys module");
        return;
    }

    if (log_stdout) {
        LogWriter::init_type();
        sys_module.setAttr("stdout", LogWriter::wrap(spdlog::level::info));
        sys_module.setAttr("stderr", LogWriter::wrap(spdlog::level::err));
    }

    auto sys_path = sys_module.getAttr("path");
    if (!sys_path.isNull()) {
        if (sys_path.isList()) {
            Py::List paths(sys_path);

            for (auto& path: python_directories) {
                paths.append(Py::String(path));
            }

        } else {
            spdlog::critical("Python sys.path is not a list");
        }
    } else {
        spdlog::critical("Python could not import sys.path");
    }
}

void shutdown_python_api()
{

    Py_Finalize();
}

void run_user_scripts(const std::string& prefix)
{
    xdgHandle baseDirHandle{};
    std::filesystem::path path;
    if (xdgInitHandle(&baseDirHandle)) {
        auto dataHome = xdgDataHome(&baseDirHandle);
        if (dataHome) {
            path = dataHome;
        }
        xdgWipeHandle(&baseDirHandle);
    }

    if (!path.empty()) {
        path /= "cyphesis";
        path /= (prefix + ".d");
        spdlog::info("Looking for extra python scripts in {}.", path.string());
        if (std::filesystem::is_directory(path)) {
            std::filesystem::recursive_directory_iterator dir(path), end{};

            while (dir != end) {
                if (std::filesystem::is_regular_file(dir->status()) && dir->path().extension() == ".py") {
                    auto fileHandle = fopen(dir->path().c_str(), "r");
                    if (fileHandle) {
                        spdlog::info("Running script '{}'.", dir->path().c_str());
                        PyRun_SimpleFileEx(fileHandle, dir->path().c_str(), true);
                        if (PyErr_Occurred()) {
                            PyErr_Print();
                        }
                    }
                }
                ++dir;
            }
        }

    }
}
