#include "qnn_api.hpp"

#include <dlfcn.h>

#include <cstdarg>
#include <cstdio>

#include "QnnLog.h"

namespace {

// QNN log callback -> stderr (visible on the Nerves console / journald).
void qnn_log_callback(const char *fmt, QnnLog_Level_t level, uint64_t timestamp, va_list args) {
    (void)timestamp;
    static const char *level_names[] = {"?", "error", "warn", "info", "verbose", "debug"};
    unsigned idx = (level >= 1 && level <= 5) ? level : 0;
    std::fprintf(stderr, "[qnn:%s] ", level_names[idx]);
    std::vfprintf(stderr, fmt, args);
    std::fputc('\n', stderr);
}

typedef Qnn_ErrorHandle_t (*GetProvidersFn)(const QnnInterface_t ***, uint32_t *);
typedef Qnn_ErrorHandle_t (*GetSystemProvidersFn)(const QnnSystemInterface_t ***, uint32_t *);

} // namespace

std::string qnn_api_open(QnnApi &api,
                         const std::string &backend_lib,
                         const std::string &system_lib,
                         int log_level) {
    api.backend_lib_name = backend_lib;

    api.core_lib = dlopen(backend_lib.c_str(), RTLD_NOW | RTLD_GLOBAL);
    if (!api.core_lib) {
        return std::string("dlopen_failed: ") + dlerror();
    }

    auto get_providers =
        reinterpret_cast<GetProvidersFn>(dlsym(api.core_lib, "QnnInterface_getProviders"));
    if (!get_providers) {
        qnn_api_close(api);
        return "missing_symbol: QnnInterface_getProviders";
    }

    const QnnInterface_t **providers = nullptr;
    uint32_t num_providers = 0;
    if (get_providers(&providers, &num_providers) != QNN_SUCCESS || !providers ||
        num_providers == 0) {
        qnn_api_close(api);
        return "no_interface_providers";
    }

    // Pick the provider with a matching major version and the highest minor.
    // The calls used here have been stable across all 2.x minors.
    const QnnInterface_t *chosen = nullptr;
    for (uint32_t i = 0; i < num_providers; i++) {
        const Qnn_Version_t &v = providers[i]->apiVersion.coreApiVersion;
        if (v.major != QNN_API_VERSION_MAJOR) continue;
        if (!chosen || v.minor > chosen->apiVersion.coreApiVersion.minor) {
            chosen = providers[i];
        }
    }
    if (!chosen) {
        qnn_api_close(api);
        return "no_compatible_interface_provider";
    }
    api.fn = chosen->QNN_INTERFACE_VER_NAME;

    api.system_lib = dlopen(system_lib.c_str(), RTLD_NOW | RTLD_GLOBAL);
    if (!api.system_lib) {
        std::string err = std::string("dlopen_system_failed: ") + dlerror();
        qnn_api_close(api);
        return err;
    }

    auto get_sys_providers = reinterpret_cast<GetSystemProvidersFn>(
        dlsym(api.system_lib, "QnnSystemInterface_getProviders"));
    if (!get_sys_providers) {
        qnn_api_close(api);
        return "missing_symbol: QnnSystemInterface_getProviders";
    }

    const QnnSystemInterface_t **sys_providers = nullptr;
    uint32_t num_sys = 0;
    if (get_sys_providers(&sys_providers, &num_sys) != QNN_SUCCESS || !sys_providers ||
        num_sys == 0) {
        qnn_api_close(api);
        return "no_system_interface_providers";
    }

    const QnnSystemInterface_t *sys_chosen = nullptr;
    for (uint32_t i = 0; i < num_sys; i++) {
        const Qnn_Version_t &v = sys_providers[i]->systemApiVersion;
        if (v.major != QNN_SYSTEM_API_VERSION_MAJOR) continue;
        if (!sys_chosen || v.minor > sys_chosen->systemApiVersion.minor) {
            sys_chosen = sys_providers[i];
        }
    }
    if (!sys_chosen) {
        qnn_api_close(api);
        return "no_compatible_system_provider";
    }
    api.sys_fn = sys_chosen->QNN_SYSTEM_INTERFACE_VER_NAME;

    if (log_level > 0 && api.fn.logCreate) {
        // Log handle creation is best-effort; some backends run without it.
        api.fn.logCreate(qnn_log_callback, static_cast<QnnLog_Level_t>(log_level), &api.log);
    }

    if (!api.fn.backendCreate) {
        qnn_api_close(api);
        return "missing_fn: backendCreate";
    }
    Qnn_ErrorHandle_t err = api.fn.backendCreate(api.log, nullptr, &api.backend);
    if (err != QNN_SUCCESS) {
        qnn_api_close(api);
        return "backend_create_failed";
    }

    if (api.fn.backendGetBuildId) {
        const char *id = nullptr;
        if (api.fn.backendGetBuildId(&id) == QNN_SUCCESS && id) {
            api.build_id = id;
        }
    }

    // deviceCreate is optional: the CPU backend reports UNSUPPORTED_FEATURE,
    // and HTP auto-detects the local SoC with a null config.
    if (api.fn.deviceCreate) {
        Qnn_ErrorHandle_t dev_err = api.fn.deviceCreate(api.log, nullptr, &api.device);
        if (dev_err != QNN_SUCCESS) {
            api.device = nullptr;
        }
    }

    return "";
}

void qnn_api_close(QnnApi &api) {
    if (api.device && api.fn.deviceFree) {
        api.fn.deviceFree(api.device);
    }
    api.device = nullptr;

    if (api.backend && api.fn.backendFree) {
        api.fn.backendFree(api.backend);
    }
    api.backend = nullptr;

    if (api.log && api.fn.logFree) {
        api.fn.logFree(api.log);
    }
    api.log = nullptr;

    if (api.system_lib) {
        dlclose(api.system_lib);
        api.system_lib = nullptr;
    }
    if (api.core_lib) {
        dlclose(api.core_lib);
        api.core_lib = nullptr;
    }
}
