#include "PluginAPI.hpp"
#include "../Compositor.hpp"
#include "../debug/HyprCtl.hpp"
#include "../plugins/PluginSystem.hpp"
#include "../managers/eventLoop/EventLoopManager.hpp"
#include "../config/legacy/ConfigManager.hpp"
#include "../config/lua/ConfigManager.hpp"
#include "../notification/NotificationOverlay.hpp"
#include "../layout/target/Target.hpp"
#include "../layout/supplementary/WorkspaceAlgoMatcher.hpp"
#include "event/EventBus.hpp"
#include <dlfcn.h>
#include <filesystem>

#if defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__)
#include <sys/sysctl.h>
#endif

#include <sstream>

APICALL const char* __hyprland_api_get_hash() {
    static auto stripPatch = [](const char* ver) -> std::string {
        std::string_view v = ver;
        if (!v.contains('.'))
            return std::string{v};

        return std::string{v.substr(0, v.find_last_of('.'))};
    };

    static const std::string ver = (std::string{GIT_COMMIT_HASH} + "_aq_" + stripPatch(AQUAMARINE_VERSION) + "_hu_" + stripPatch(HYPRUTILS_VERSION) + "_hg_" +
                                    stripPatch(HYPRGRAPHICS_VERSION) + "_hc_" + stripPatch(HYPRCURSOR_VERSION) + "_hlg_" + stripPatch(HYPRLANG_VERSION));

    return ver.c_str();
}

APICALL SP<HOOK_CALLBACK_FN> HyprlandAPI::registerCallbackDynamic(HANDLE handle, const std::string& event, HOOK_CALLBACK_FN fn) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return nullptr;

    //auto PFN = g_pHookSystem->hookDynamic(event, fn, handle);
    //PLUGIN->m_registeredCallbacks.emplace_back(event, WP<HOOK_CALLBACK_FN>(PFN));
    return nullptr;
}

APICALL bool HyprlandAPI::unregisterCallback(HANDLE handle, SP<HOOK_CALLBACK_FN> fn) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return false;

    //g_pHookSystem->unhook(fn);
    // std::erase_if(PLUGIN->m_registeredCallbacks, [&](const auto& other) { return other.second.lock() == fn; });

    return true;
}

APICALL std::string HyprlandAPI::invokeHyprctlCommand(const std::string& call, const std::string& args, const std::string& format) {
    if (args.empty())
        return g_pHyprCtl->makeDynamicCall(format + "/" + call);
    else
        return g_pHyprCtl->makeDynamicCall(format + "/" + call + " " + args);
}

APICALL bool HyprlandAPI::addLayout(HANDLE handle, const std::string& name, IHyprLayout* layout) {
    return false;
}

APICALL bool HyprlandAPI::removeLayout(HANDLE handle, IHyprLayout* layout) {
    return false;
}

APICALL bool HyprlandAPI::addTiledAlgo(HANDLE handle, const std::string& name, const std::type_info* typeInfo, std::function<UP<Layout::ITiledAlgorithm>()>&& factory) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return false;

    PLUGIN->m_registeredAlgos.emplace_back(name);

    return Layout::Supplementary::algoMatcher()->registerTiledAlgo(name, typeInfo, std::move(factory));
}

APICALL bool HyprlandAPI::addFloatingAlgo(HANDLE handle, const std::string& name, const std::type_info* typeInfo, std::function<UP<Layout::IFloatingAlgorithm>()>&& factory) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return false;

    PLUGIN->m_registeredAlgos.emplace_back(name);

    return Layout::Supplementary::algoMatcher()->registerFloatingAlgo(name, typeInfo, std::move(factory));
}

APICALL bool HyprlandAPI::removeAlgo(HANDLE handle, const std::string& name) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return false;

    std::erase(PLUGIN->m_registeredAlgos, name);

    return Layout::Supplementary::algoMatcher()->unregisterAlgo(name);
}

APICALL bool HyprlandAPI::reloadConfig() {
    g_pEventLoopManager->doLater([] { Config::mgr()->reload(); });
    return true;
}

APICALL bool HyprlandAPI::addNotification(HANDLE handle, const std::string& text, const CHyprColor& color, const float timeMs) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return false;

    Notification::overlay()->addNotification(text, color, timeMs);

    return true;
}

APICALL CFunctionHook* HyprlandAPI::createFunctionHook(HANDLE handle, const void* source, const void* destination) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return nullptr;

    return g_pFunctionHookSystem->initHook(handle, const_cast<void*>(source), const_cast<void*>(destination));
}

APICALL bool HyprlandAPI::removeFunctionHook(HANDLE handle, CFunctionHook* hook) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return false;

    return g_pFunctionHookSystem->removeHook(hook);
}

APICALL bool HyprlandAPI::addWindowDecoration(HANDLE handle, PHLWINDOW pWindow, UP<IHyprWindowDecoration> pDecoration) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return false;

    if (!validMapped(pWindow))
        return false;

    PLUGIN->m_registeredDecorations.push_back(pDecoration.get());

    pWindow->addWindowDeco(std::move(pDecoration));

    pWindow->layoutTarget()->recalc();

    return true;
}

APICALL bool HyprlandAPI::removeWindowDecoration(HANDLE handle, IHyprWindowDecoration* pDecoration) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return false;

    for (auto const& w : Desktop::windowState()->windows()) {
        for (auto const& d : w->m_windowDecorations) {
            if (d.get() == pDecoration) {
                w->removeWindowDeco(pDecoration);
                return true;
            }
        }
    }

    return false;
}

APICALL bool HyprlandAPI::addConfigValue(HANDLE handle, const std::string& name, const Hyprlang::CConfigValue& value) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (Config::mgr()->type() != Config::CONFIG_LEGACY)
        return false;

    if (!g_pPluginSystem->m_allowConfigVars)
        return false;

    if (!PLUGIN)
        return false;

    if (!name.starts_with("plugin:"))
        return false;

    Config::Legacy::mgr()->addPluginConfigVar(handle, name, value);
    return true;
}

APICALL bool HyprlandAPI::addConfigKeyword(HANDLE handle, const std::string& name, Hyprlang::PCONFIGHANDLERFUNC fn, Hyprlang::SHandlerOptions opts) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (Config::mgr()->type() != Config::CONFIG_LEGACY)
        return false;

    if (!g_pPluginSystem->m_allowConfigVars)
        return false;

    if (!PLUGIN)
        return false;

    Config::Legacy::mgr()->addPluginKeyword(handle, name, fn, opts);
    return true;
}

APICALL Hyprlang::CConfigValue* HyprlandAPI::getConfigValue(HANDLE handle, const std::string& name) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (Config::mgr()->type() != Config::CONFIG_LEGACY)
        return nullptr;

    if (!PLUGIN)
        return nullptr;

    if (name.starts_with("plugin:"))
        return Config::Legacy::mgr()->getHyprlangConfigValuePtr(name.substr(7), "plugin");

    return Config::Legacy::mgr()->getHyprlangConfigValuePtr(name);
}

APICALL void* HyprlandAPI::getFunctionAddressFromSignature(HANDLE handle, const std::string& sig) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return nullptr;

    return dlsym(nullptr, sig.c_str());
}

APICALL bool HyprlandAPI::addDispatcher(HANDLE handle, const std::string& name, std::function<void(std::string)> handler) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return false;

    PLUGIN->m_registeredDispatchers.push_back(name);

    g_pKeybindManager->m_dispatchers[name] = [handler](std::string arg1) -> SDispatchResult {
        handler(arg1);
        return {};
    };

    return true;
}

APICALL bool HyprlandAPI::addDispatcherV2(HANDLE handle, const std::string& name, std::function<SDispatchResult(std::string)> handler) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return false;

    PLUGIN->m_registeredDispatchers.push_back(name);

    g_pKeybindManager->m_dispatchers[name] = handler;

    return true;
}

APICALL bool HyprlandAPI::removeDispatcher(HANDLE handle, const std::string& name) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return false;

    std::erase_if(g_pKeybindManager->m_dispatchers, [&](const auto& other) { return other.first == name; });
    std::erase_if(PLUGIN->m_registeredDispatchers, [&](const auto& other) { return other == name; });

    return true;
}

APICALL bool addNotificationV2(HANDLE handle, const std::unordered_map<std::string, std::any>& data) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return false;

    try {
        auto iterator = data.find("text");
        if (iterator == data.end())
            return false;

        // mandatory
        std::string text;
        try {
            text = std::any_cast<std::string>(iterator->second);
        } catch (std::exception& e) {
            // attempt const char*
            text = std::any_cast<const char*>(iterator->second);
        }

        iterator = data.find("time");
        if (iterator == data.end())
            return false;

        const auto TIME = std::any_cast<uint64_t>(iterator->second);

        iterator = data.find("color");
        if (iterator == data.end())
            return false;

        const auto COLOR = std::any_cast<CHyprColor>(iterator->second);

        // optional
        eIcons icon = ICON_NONE;
        iterator    = data.find("icon");
        if (iterator != data.end())
            icon = std::any_cast<eIcons>(iterator->second);

        Notification::overlay()->addNotification(text, COLOR, TIME, icon);

    } catch (std::exception& e) {
        // bad any_cast most likely, plugin error
        return false;
    }

    return true;
}

APICALL std::vector<SFunctionMatch> HyprlandAPI::findFunctionsByName(HANDLE handle, const std::string& name) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return std::vector<SFunctionMatch>{};

#if defined(KERN_PROC_PATHNAME)
    int mib[] = {
        CTL_KERN,
#if defined(__NetBSD__)
        KERN_PROC_ARGS,
        -1,
        KERN_PROC_PATHNAME,
#else
        KERN_PROC,
        KERN_PROC_PATHNAME,
        -1,
#endif
    };
    u_int  miblen        = sizeof(mib) / sizeof(mib[0]);
    char   exe[PATH_MAX] = "/nonexistent";
    size_t sz            = sizeof(exe);
    sysctl(mib, miblen, &exe, &sz, NULL, 0);
    const auto FPATH = std::filesystem::canonical(exe);
#elif defined(__OpenBSD__)
    // Neither KERN_PROC_PATHNAME nor /proc are supported
    const auto FPATH = std::filesystem::canonical("/usr/local/bin/Hyprland");
#else
    const auto FPATH = std::filesystem::canonical("/proc/self/exe");
#endif

#ifdef __clang__
    static const auto SYMBOLS          = execAndGet(("llvm-nm -D -j \"" + FPATH.string() + "\"").c_str());
    static const auto SYMBOLSDEMANGLED = execAndGet(("llvm-nm -D -j --demangle \"" + FPATH.string() + "\"").c_str());
#else
    static const auto SYMBOLS          = execAndGet(("nm -D -j \"" + FPATH.string() + "\"").c_str());
    static const auto SYMBOLSDEMANGLED = execAndGet(("nm -D -j --demangle=auto \"" + FPATH.string() + "\"").c_str());
#endif

    auto demangledFromID = [&](size_t id) -> std::string {
        size_t pos   = 0;
        size_t count = 0;
        while (count < id) {
            pos++;
            pos = SYMBOLSDEMANGLED.find('\n', pos);
            if (pos == std::string::npos)
                return "";
            count++;
        }

        // Skip the newline char itself
        if (pos != 0)
            pos++;

        return SYMBOLSDEMANGLED.substr(pos, SYMBOLSDEMANGLED.find('\n', pos) - pos);
    };

    if (SYMBOLS.empty()) {
        Log::logger->log(Log::ERR, R"(Unable to search for function "{}": no symbols found in binary (is "{}" in path?))", name,
#ifdef __clang__
                         "llvm-nm"
#else
                         "nm"
#endif
        );
        return {};
    }

    std::vector<SFunctionMatch> matches;

    std::istringstream          inStream(SYMBOLS);
    std::string                 line;
    int                         lineNo = 0;
    while (std::getline(inStream, line)) {
        if (line.contains(name)) {
            void* address = dlsym(nullptr, line.c_str());

            if (!address)
                continue;

            matches.push_back({address, line, demangledFromID(lineNo)});
        }
        lineNo++;
    }

    return matches;
}

APICALL SVersionInfo HyprlandAPI::getHyprlandVersion(HANDLE handle) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return {};

    return {GIT_COMMIT_HASH, GIT_TAG, GIT_DIRTY != std::string(""), GIT_BRANCH, GIT_COMMIT_MESSAGE, GIT_COMMITS};
}

APICALL SP<SHyprCtlCommand> HyprlandAPI::registerHyprCtlCommand(HANDLE handle, SHyprCtlCommand cmd) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return nullptr;

    auto PTR = g_pHyprCtl->registerCommand(cmd);
    PLUGIN->m_registeredHyprctlCommands.push_back(PTR);
    return PTR;
}

APICALL bool HyprlandAPI::unregisterHyprCtlCommand(HANDLE handle, SP<SHyprCtlCommand> cmd) {

    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return false;

    std::erase_if(PLUGIN->m_registeredHyprctlCommands, [&](const auto& other) { return !other || other == cmd; });
    g_pHyprCtl->unregisterCommand(cmd);

    return true;
}

APICALL bool HyprlandAPI::addConfigValueV2(HANDLE handle, SP<Config::Values::IValue> value) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return false;

    PLUGIN->m_registeredApiValues.emplace_back(value);

    auto ret = Config::mgr()->registerPluginValue(handle, value);
    if (!ret) {
        Log::logger->log(Log::ERR, "failed to register plugin value \"{}\": {}", value->name(), ret.error());
        return false;
    }

    value->commence();

    return true;
}

APICALL bool HyprlandAPI::addLuaFunction(HANDLE handle, const std::string& namespace_, const std::string& name, PLUGIN_LUA_FN fn) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return false;

    if (!Config::mgr() || Config::mgr()->type() != Config::CONFIG_LUA)
        return false;

    auto ret = dynamicPointerCast<Config::Lua::CConfigManager>(WP<Config::IConfigManager>(Config::mgr()))->registerPluginLuaFunction(handle, namespace_, name, fn);
    if (!ret) {
        Log::logger->log(Log::ERR, "failed to register lua plugin function {}.{}: {}", namespace_, name, ret.error());
        return false;
    }

    return true;
}

APICALL bool HyprlandAPI::removeLuaFunction(HANDLE handle, const std::string& namespace_, const std::string& name) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return false;

    if (!Config::mgr() || Config::mgr()->type() != Config::CONFIG_LUA)
        return false;

    auto ret = dynamicPointerCast<Config::Lua::CConfigManager>(WP<Config::IConfigManager>(Config::mgr()))->unregisterPluginLuaFunction(handle, namespace_, name);
    if (!ret) {
        Log::logger->log(Log::ERR, "failed to unregister lua plugin function {}.{}: {}", namespace_, name, ret.error());
        return false;
    }

    return true;
}

APICALL bool HyprlandAPI::addEvent(HANDLE handle, SP<Event::CEventBus::CCustomEvent> event) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return false;

    const auto ret = Event::bus()->addPluginEvent(event);
    if (!ret) {
        Log::logger->log(Log::ERR, ret.error());
        return false;
    }

    return true;
}

APICALL bool HyprlandAPI::removeEvent(HANDLE handle, const std::string& name) {
    auto* const PLUGIN = g_pPluginSystem->getPluginByHandle(handle);

    if (!PLUGIN)
        return false;

    const auto ret = Event::bus()->removePluginEvent(name);
    if (!ret) {
        Log::logger->log(Log::ERR, ret.error());
        return false;
    }

    return true;
}
