#include <iostream>
#include <algorithm>
#include <stdexcept>
#include <memory>
#include <vector>
#include <sstream>
#include <dlfcn.h>
#include <xcb/xcb.h>
#include <user/senseApiDesc.h>
#include <user/deviceAttachmentSpec.h>
#include <spinscale/co/invokers.h>
#include <xcbXorg/xcbXorg.h>
#include "xcbWindow.h"

// Function pointers to API entry points exported by libxcbXorg.
struct XcbXorgDllState
{
    struct XcbXorgFunctions
    {
        get_or_create_connection_fn* getOrCreateConnection = nullptr;
        cleanup_connections_fn* cleanupConnections = nullptr;
        dereference_connection_fn* dereferenceConnection = nullptr;
        find_window_by_id_fn* findWindowById = nullptr;
        find_window_by_name_fn* findWindowByName = nullptr;
    } fns;
    void* dlopenHandle = nullptr;
};

static XcbXorgDllState xcbXorg;

// Salmanoff hooks, obtained from SMO_GET_STIM_BUFF_API_DESC_FN_NAME().
static const smo::stim_buff::SmoCallbacks* smoHooksPtr = nullptr;
static smo::stim_buff::SmoThreadingModelDesc smoThreadingModelDesc;

// Attached windows.
static std::vector<std::unique_ptr<xcb_window::AttachedWindow>>
    g_attachedWindows;

namespace xcb_window {

std::string WindowSelector::stringify() const
{
    std::ostringstream os;

    os << "Display: " << display
       << ", Screen: " << screen << ", Window: ";
    if (matchType == xcb_xorg::window_search::MatchType::ID) {
        os << windowId;
    } else {
        os << "\"" << windowName << "\"";
    }
    os << " (matchType="
        << (matchType == xcb_xorg::window_search::MatchType::EXACT ? "exact" :
            (matchType == xcb_xorg::window_search::MatchType::SUBSTRING) ? "substring" : "id")
       << ")";
    return os.str();
}

AttachedWindow::AttachedWindow(
    const std::shared_ptr<smo::device::DeviceAttachmentSpec>& spec
    )
    : deviceAttachmentSpec(spec)
{
    // Validate required function pointers are available
    if (!xcbXorg.fns.getOrCreateConnection ||
        !xcbXorg.fns.findWindowById || !xcbXorg.fns.findWindowByName
        || !xcbXorg.fns.dereferenceConnection)
    {
        throw std::runtime_error("xcbWindow:" + std::string(__func__) +
            ": Required xcbXorg function pointers not available");
    }

	windowSelector.display = smo::device::DeviceAttachmentSpec
		::parseRequiredParamAsInt(spec->providerParams, "display");
	windowSelector.screen = smo::device::DeviceAttachmentSpec
		::parseRequiredParamAsInt(spec->providerParams, "screen");

	parseWindowSelector(*spec);

    // Get connection from libxcbXorg
    std::shared_ptr<xcb_xorg::XcbConnection> conn =
        (*xcbXorg.fns.getOrCreateConnection)(
            windowSelector.display, windowSelector.screen);

    xcbConnectionShared = conn;

    // Find the target window
    xcb_window_t foundWindow = 0;
    const xcb_setup_t* setup = xcb_get_setup(conn->getConnection());
    const xcb_screen_t* screen = xcb_setup_roots_iterator(setup).data;

    if (windowSelector.matchType == xcb_xorg::window_search::MatchType::ID)
    {
        foundWindow = (*xcbXorg.fns.findWindowById)(
            conn.get(), screen->root,
            windowSelector.windowId);
    }
    else
    {
        foundWindow = (*xcbXorg.fns.findWindowByName)(
            conn.get(), screen->root,
            windowSelector.windowName, actualWindowName,
            windowSelector.matchType);
    }

    if (!foundWindow)
    {
        throw std::runtime_error("xcbWindow:" + std::string(__func__) +
            ": Window not found: " + windowSelector.stringify());
    }
}

void AttachedWindow::parseWindowSelector(
    const smo::device::DeviceAttachmentSpec& spec
    )
{
    // Default match type
    windowSelector.matchType = xcb_xorg::window_search::MatchType::SUBSTRING;

    // Check if 'dev-id', 'dev-string', or 'dev-substring' is specified
    for (const auto& param : spec.stimBuffApiParams)
    {
        if (param.first == "dev-id" || param.first == "devid")
        {
            windowSelector.matchType = xcb_xorg::window_search::MatchType::ID;
            break;
        }
        if (param.first == "dev-string" || param.first == "dev-str"
            || param.first == "devstr" || param.first == "devstring")
        {
            windowSelector.matchType = xcb_xorg::window_search::MatchType::EXACT;
        }
        if (param.first == "dev-substring" || param.first == "dev-substr"
            || param.first == "devsubstr" || param.first == "devsubstring")
        {
            windowSelector.matchType = xcb_xorg::window_search::MatchType::SUBSTRING;
        }
    }

    if (windowSelector.matchType == xcb_xorg::window_search::MatchType::ID)
    {
        try {
            windowSelector.windowId = std::stoul(
                spec.deviceSelector, nullptr, 0);
        } catch (const std::exception&) {
            throw std::runtime_error(
                "xcbWindow:" + std::string(__func__) + ": Window selector: "
                "'dev-id' present, but selector is not numeric");
        }
    }
    else {
        windowSelector.windowName = spec.deviceSelector;
    }
}

std::string AttachedWindow::stringify() const {
    std::ostringstream os;

    auto matchTypeStr = [](xcb_xorg::window_search::MatchType mt) -> const char* {
        switch (mt) {
            case xcb_xorg::window_search::MatchType::SUBSTRING:
                return "substring";
            case xcb_xorg::window_search::MatchType::EXACT:
                return "exact";
            case xcb_xorg::window_search::MatchType::ID:
                return "id";
            default:
                return "unknown";
        }
    };

    auto formatWindowId = [](uint32_t id) -> std::string {
        std::ostringstream oss;
        oss << "Window ID 0x" << std::hex << id << std::dec;
        return oss.str();
    };

    os << "Display: " << windowSelector.display
       << ", Screen: " << windowSelector.screen
       << ", MatchType: " << matchTypeStr(windowSelector.matchType)
       << ", Target: ";

    if (windowSelector.matchType == xcb_xorg::window_search::MatchType::ID) {
        os << formatWindowId(windowSelector.windowId);
    } else {
        os << '"' << windowSelector.windowName << '"';
    }

    os << ", Found: ";

    if (windowSelector.matchType == xcb_xorg::window_search::MatchType::ID) {
        os << formatWindowId(windowSelector.windowId);
    } else {
        os << '"' << actualWindowName << '"';
        if (windowSelector.matchType ==
                xcb_xorg::window_search::MatchType::SUBSTRING &&
            actualWindowName != windowSelector.windowName)
        {
            os << " (matched substring '"
               << windowSelector.windowName << "')";
        }
    }

    return os.str();
}

AttachedWindow::~AttachedWindow()
{
    if (xcbConnectionShared && xcbXorg.fns.dereferenceConnection) {
        (*xcbXorg.fns.dereferenceConnection)(xcbConnectionShared);
    }
}

} // namespace xcb_window

// SenseApi functions
static sscl::co::ViralNonPostingInvoker<int> xcbWindow_initializeCInd(void)
{
    if (!smoHooksPtr)
    {
        throw std::runtime_error(std::string(__func__) + ": SMO hooks "
            "pointers not filled in.");
    }

    // Try to load libxcbXorg using the search path hook
    auto libPath = smoHooksPtr->searchForLibInSmoSearchPaths("libxcbXorg.so.0");
    xcbXorg.dlopenHandle = dlopen(
        libPath.value_or("libxcbXorg.so.0").c_str(),
        RTLD_LAZY);

    if (!xcbXorg.dlopenHandle)
    {
        throw std::runtime_error("xcbWindow:" + std::string(__func__) +
            ": Failed to load libxcbXorg: " + std::string(dlerror()));
    }

    // Fill in function pointers from libxcbXorg.
    xcbXorg.fns.getOrCreateConnection =
        reinterpret_cast<get_or_create_connection_fn*>(
            dlsym(xcbXorg.dlopenHandle, "xcb_xorg_get_or_create_connection"));
    xcbXorg.fns.cleanupConnections = reinterpret_cast<cleanup_connections_fn*>(
        dlsym(xcbXorg.dlopenHandle, "xcb_xorg_cleanup_connections"));
    xcbXorg.fns.findWindowById = reinterpret_cast<find_window_by_id_fn*>(
        dlsym(xcbXorg.dlopenHandle, "xcb_xorg_find_window_by_id"));
    xcbXorg.fns.findWindowByName = reinterpret_cast<find_window_by_name_fn*>(
        dlsym(xcbXorg.dlopenHandle, "xcb_xorg_find_window_by_name"));
    xcbXorg.fns.dereferenceConnection =
        reinterpret_cast<dereference_connection_fn*>(
            dlsym(xcbXorg.dlopenHandle, "xcb_xorg_dereference_connection"));

    if (!xcbXorg.fns.getOrCreateConnection || !xcbXorg.fns.cleanupConnections
        || !xcbXorg.fns.findWindowById || !xcbXorg.fns.findWindowByName
        || !xcbXorg.fns.dereferenceConnection)
    {
        throw std::runtime_error(
            std::string("xcbWindow:") + __func__ +
            ": Failed to get required function pointers from libxcbXorg");
    }

    co_return 0;
}

static sscl::co::ViralNonPostingInvoker<int> xcbWindow_finalizeCInd(void)
{
    g_attachedWindows.clear();

    if (xcbXorg.dlopenHandle)
    {
        dlclose(xcbXorg.dlopenHandle);
        xcbXorg.dlopenHandle = nullptr;
        xcbXorg.fns = { nullptr, nullptr, nullptr, nullptr, nullptr };
    }

    co_return 0;
}

static sscl::co::DynamicViralPostingInvoker<smo::stim_buff::StimBuffDeviceOpResult>
xcbWindow_attachDeviceCReq(
	[[maybe_unused]] sscl::co::ExplicitPostTarget postTarget,
	const std::shared_ptr<smo::device::DeviceAttachmentSpec>& desc,
	const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
	(void)postTarget;
	// Not used yet, but may be used later.
	(void)componentThread;

	try {
		g_attachedWindows.emplace_back(
			std::make_unique<xcb_window::AttachedWindow>(desc));
	} catch (const std::exception& exc) {
		std::cerr << __func__ << ": Exception while attaching X11 window: "
			<< exc.what() << "\n";
		co_return smo::stim_buff::StimBuffDeviceOpResult{false, desc};
	}

    std::cout << __func__ << ": Attached X11 window:\n  "
        << g_attachedWindows.back()->stringify()
        << "\n";

    co_return smo::stim_buff::StimBuffDeviceOpResult{true, desc};
}

static sscl::co::DynamicViralPostingInvoker<smo::stim_buff::StimBuffDeviceOpResult>
xcbWindow_detachDeviceCReq(
	[[maybe_unused]] sscl::co::ExplicitPostTarget postTarget,
	const std::shared_ptr<smo::device::DeviceAttachmentSpec>& spec)
{
	(void)postTarget;
    auto it = std::find_if(g_attachedWindows.begin(), g_attachedWindows.end(),
        [&spec](const std::unique_ptr<xcb_window::AttachedWindow>& window) {
            return window->getDeviceAttachmentSpec() == spec;
        }
    );

    if (it == g_attachedWindows.end())
    {
		std::cerr << __func__ << ": Device not found for detachment:\n"
			<< spec->stringify() << "\n";

		co_return smo::stim_buff::StimBuffDeviceOpResult{false, spec};
    }

    g_attachedWindows.erase(it);
    std::cout << __func__ << ": Detached X11 window device:\n"
        << spec->stringify() << "\n";

	co_return smo::stim_buff::StimBuffDeviceOpResult{true, spec};
}

// SenseApi descriptor
static smo::stim_buff::StimBuffApiDesc xcbWindowApiDesc = {
    .name = "xcb",
    .exportedQualeIfaceApis = { { "visual-qualeiface" } },
    .sal_mgmt_libOps = {
        .initializeCInd = xcbWindow_initializeCInd,
        .finalizeCInd = xcbWindow_finalizeCInd,
        .attachDeviceCReq = xcbWindow_attachDeviceCReq,
        .detachDeviceCReq = xcbWindow_detachDeviceCReq
    }
};

// Exported function
extern "C" smo::stim_buff::SMO_GET_STIM_BUFF_API_DESC_FN_TYPEDEF
    SMO_GET_STIM_BUFF_API_DESC_FN_NAME;

const smo::stim_buff::StimBuffApiDesc& SMO_GET_STIM_BUFF_API_DESC_FN_NAME(
    const smo::stim_buff::SmoCallbacks& callbacks,
    const smo::stim_buff::SmoThreadingModelDesc& threadingModel)
{
    smoHooksPtr = &callbacks;
    smoThreadingModelDesc = threadingModel;
    return xcbWindowApiDesc;
}
