/**
 *
 * @file
 * Copyright 2025 Digitalwerk GmbH.
 *
 *     This Source Code Form is subject to the terms of the Mozilla
 *     Public License, v. 2.0. If a copy of the MPL was not distributed
 *     with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
 * 
 * If it is not possible or desirable to put the notice in a particular file, then
 * You may include the notice in a location (such as a LICENSE file in a
 * relevant directory) where a recipient would be likely to look for such a notice.
 *
 * You may add additional accurate notices of copyright ownership.
 */

#include <adtfdat_processing/processor.h>
#include <pybind11/embed.h>
#include <pybind11/chrono.h>

#ifdef _WIN32
#include <Windows.h>
#else
#include <link.h>
#include <dlfcn.h>
#endif

#include <iostream>
namespace
{

#ifdef _WIN32
EXTERN_C IMAGE_DOS_HEADER __ImageBase;
#endif

std::string getOwnModuleFileName()
{
    std::string module_file_name;

#ifdef _WIN32
    LPTSTR strThis = new TCHAR[_MAX_PATH];
    ::GetModuleFileName((HINSTANCE)&__ImageBase, strThis, _MAX_PATH);
    module_file_name = strThis;
    delete [] strThis;
#else
    Dl_info dl_info;
    dladdr((void *)(unsigned long)&getOwnModuleFileName, &dl_info);
    module_file_name = dl_info.dli_fname;
#endif

    return module_file_name;
}

}

using namespace pybind11::literals;

// taken from https://stackoverflow.com/questions/6157409/stdvector-to-boostpythonlist
// Converts a C++ vector to a python list
template <class T>
pybind11::list toPythonList(const std::vector<T>& vector) {
    typename std::vector<T>::const_iterator iter;
    pybind11::list list;
    for (iter = vector.begin(); iter != vector.end(); ++iter) {
        list.append(*iter);
    }
    return list;
}

class PythonProcessor : public adtfdat_processing::Processor
{
public:
    PythonProcessor()
    {
        FILE_PLUGIN_META_INFORMATION(BUILD_VERSION, "Python Processor to process data using ADTF File Python Bindings.");

        setConfiguration(
            adtf_file::PropertyBuilder("script", "")
                .setDescription("The python script to process."));

        try
        {
            _interpreter_guard = std::make_unique<pybind11::scoped_interpreter>();
        }
        catch (...)
        {
            // if this instance is created from within a python call itself
            // or if another processor already created an interpreter,
            // we ignore this exception.
        }
    }

    ~PythonProcessor()
    {
    }

    std::string getProcessorIdentifier() const override
    {
        return "python";
    }

    bool isCompatible(const adtf_file::Stream&) const override
    {
        return true;
    }

    void open(const std::vector<adtf_file::Stream>& streams, const std::string& destination_url) override
    {
        const auto processor_script = getConfiguration().at("script").value;
        if (processor_script.empty())
        {
            throw std::runtime_error("no python processor script specified, please set the 'script' property accordingly.");
        }

        try
        {
            pybind11::exec(
                R"(import sys
import adtf_file
import importlib.util
spec = importlib.util.spec_from_file_location('processor', r')" +
                a_util::filesystem::Path(processor_script).toString() + R"(')
processor = importlib.util.module_from_spec(spec)
sys.modules['processor'] = processor
spec.loader.exec_module(processor)
)");
        }
        catch (const std::exception& ex)
        {
            // we need to catch a python exception immediatelly and it can not be rethrown,
            // because if the python intepreter is deinitialize the exception content may be invalid!
            throw std::runtime_error("Unable to load python processor script: " + std::string(ex.what()));
        }

        try
        {
            pybind11::exec("processor.open(streams, destination_url)",
                           pybind11::globals(),
                           pybind11::dict("streams"_a=toPythonList(streams), "destination_url"_a=destination_url));
        }
        catch (const std::exception& ex)
        {
            // we need to catch a python exception immediatelly and it can not be rethrown,
            // because if the python intepreter is deinitialize the exception content may be invalid!
            throw std::runtime_error("python open call error: " + std::string(ex.what()));
        }
    }

    void process(const adtf_file::FileItem& item) override
    {
        try
        {
            pybind11::exec("processor.process(item)", pybind11::globals(), pybind11::dict("item"_a=item));
        }
        catch (std::exception& ex)
        {
            // we need to catch a python exception immediatelly and it can not be rethrown,
            // because if the python intepreter is deinitialize the exception content may be invalid!
            throw std::runtime_error("python process error: " + std::string(ex.what()));
        }
    }

private:
    std::unique_ptr<pybind11::scoped_interpreter> _interpreter_guard;
};

#ifndef _WIN32
static int find_python_library(struct dl_phdr_info *info, size_t size, void *data)
{
    const std::string helper(info->dlpi_name);
    if (helper.find("libpython") != std::string::npos)
    {
        *static_cast<std::string*>(data) = helper;
        return 1;
    }

    return 0;
}
#endif

static adtf_file::PluginInitializer initializer([] {
#ifndef _WIN32
    // we need to make sure all symbols from python are globally available
    std::string python_library;
    dl_iterate_phdr(find_python_library, &python_library);
    if (!dlopen(python_library.c_str(), RTLD_GLOBAL|RTLD_NOW))
    {
        throw std::runtime_error(dlerror());
    }
#endif

    adtf_file::getObjects().push_back(
        std::make_shared<adtfdat_processing::ProcessorFactoryImplementation<PythonProcessor>>());
});
