#include "xcb.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

#include <xcb/xcb.h>
#include <xcb/randr.h>
#include <xcb/render.h>

#if defined(HAVE_XCB_ERRORS)
 #include <xcb/xcb_errors.h>
#endif

#define LOG_MODULE "xcb"
#define LOG_ENABLE_DBG 0
#include "log.h"

xcb_atom_t UTF8_STRING;
xcb_atom_t _NET_WM_PID;
xcb_atom_t _NET_WM_WINDOW_TYPE;
xcb_atom_t _NET_WM_WINDOW_TYPE_DOCK;
xcb_atom_t _NET_WM_STATE;
xcb_atom_t _NET_WM_STATE_ABOVE;
xcb_atom_t _NET_WM_STATE_STICKY;
xcb_atom_t _NET_WM_DESKTOP;
xcb_atom_t _NET_WM_STRUT;
xcb_atom_t _NET_WM_STRUT_PARTIAL;
xcb_atom_t _NET_ACTIVE_WINDOW;
xcb_atom_t _NET_CURRENT_DESKTOP;
xcb_atom_t _NET_WM_VISIBLE_NAME;
xcb_atom_t _NET_WM_NAME;

#if defined(HAVE_XCB_ERRORS)
static xcb_errors_context_t *err_context;
#endif

static void __attribute__((destructor))
fini(void)
{
#if defined(HAVE_XCB_ERRORS)
    xcb_errors_context_free(err_context);
#endif
}

bool
xcb_init(void)
{
    xcb_connection_t *conn = xcb_connect(NULL, NULL);
    if (xcb_connection_has_error(conn) > 0) {
        LOG_ERR("failed to connect to X");
        xcb_disconnect(conn);
        return false;
    }

#if defined(HAVE_XCB_ERRORS)
    xcb_errors_context_new(conn, &err_context);
#endif

#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG
    const xcb_setup_t *setup = xcb_get_setup(conn);

    /* Vendor release number */
    unsigned release = setup->release_number;
    unsigned major = release / 10000000; release %= 10000000;
    unsigned minor = release / 100000; release %= 100000;
    unsigned patch = release / 1000;
#endif

    LOG_DBG("%.*s %u.%u.%u (protocol: %u.%u)",
            xcb_setup_vendor_length(setup), xcb_setup_vendor(setup),
            major, minor, patch,
            setup->protocol_major_version,
            setup->protocol_minor_version);

    const xcb_query_extension_reply_t *randr =
        xcb_get_extension_data(conn, &xcb_randr_id);

    if (randr == NULL || !randr->present) {
        LOG_ERR("RANDR extension not present");
        xcb_disconnect(conn);
        return false;
    }

    const xcb_query_extension_reply_t *render =
        xcb_get_extension_data(conn, &xcb_render_id);

    if (render == NULL || !render->present) {
        LOG_ERR("RENDER extension not present");
        xcb_disconnect(conn);
        return false;
    }

    xcb_randr_query_version_cookie_t randr_cookie =
        xcb_randr_query_version(conn, XCB_RANDR_MAJOR_VERSION,
                                XCB_RANDR_MINOR_VERSION);
    xcb_render_query_version_cookie_t render_cookie =
        xcb_render_query_version(conn, XCB_RENDER_MAJOR_VERSION,
                                 XCB_RENDER_MINOR_VERSION);

    xcb_flush(conn);

    xcb_generic_error_t *e;
    xcb_randr_query_version_reply_t *randr_version =
        xcb_randr_query_version_reply(conn, randr_cookie, &e);
    if (e != NULL) {
        LOG_ERR("failed to query RANDR version: %s", xcb_error(e));
        free(e);
        xcb_disconnect(conn);
        return false;
    }

    xcb_render_query_version_reply_t *render_version =
        xcb_render_query_version_reply(conn, render_cookie, &e);
    if (e != NULL) {
        LOG_ERR("failed to query RENDER version: %s", xcb_error(e));
        free(e);
        xcb_disconnect(conn);
        return false;
    }

    LOG_DBG("RANDR: %u.%u",
            randr_version->major_version, randr_version->minor_version);
    LOG_DBG("RENDER: %u.%u",
            render_version->major_version, render_version->minor_version);

    free(randr_version);
    free(render_version);

    /* Cache atoms */
    UTF8_STRING = get_atom(conn, "UTF8_STRING");
    _NET_WM_PID = get_atom(conn, "_NET_WM_PID");
    _NET_WM_WINDOW_TYPE = get_atom(conn,  "_NET_WM_WINDOW_TYPE");
    _NET_WM_WINDOW_TYPE_DOCK = get_atom(conn, "_NET_WM_WINDOW_TYPE_DOCK");
    _NET_WM_STATE = get_atom(conn, "_NET_WM_STATE");
    _NET_WM_STATE_ABOVE = get_atom(conn, "_NET_WM_STATE_ABOVE");
    _NET_WM_STATE_STICKY = get_atom(conn, "_NET_WM_STATE_STICKY");
    _NET_WM_DESKTOP = get_atom(conn, "_NET_WM_DESKTOP");
    _NET_WM_STRUT = get_atom(conn, "_NET_WM_STRUT");
    _NET_WM_STRUT_PARTIAL = get_atom(conn, "_NET_WM_STRUT_PARTIAL");
    _NET_ACTIVE_WINDOW = get_atom(conn, "_NET_ACTIVE_WINDOW");
    _NET_CURRENT_DESKTOP = get_atom(conn, "_NET_CURRENT_DESKTOP");
    _NET_WM_VISIBLE_NAME = get_atom(conn, "_NET_WM_VISIBLE_NAME");
    _NET_WM_NAME = get_atom(conn, "_NET_WM_NAME");
    _NET_WM_PID = get_atom(conn, "_NET_WM_PID");

    xcb_disconnect(conn);
    return true;
}

xcb_atom_t
get_atom(xcb_connection_t *conn, const char *name)
{
    xcb_generic_error_t *e;
    xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(
        conn,
        xcb_intern_atom(conn, 0, strlen(name), name),
        &e);

    if (e != NULL) {
        LOG_ERR("%s: failed to get atom for %s", name, xcb_error(e));
        free(e);
        free(reply);

        return (xcb_atom_t){0};
    }

    xcb_atom_t ret = reply->atom;
    LOG_DBG("atom %s = 0x%08x", name, ret);

    if (ret == XCB_ATOM_NONE)
        LOG_ERR("%s: no such atom", name);

    assert(ret != XCB_ATOM_NONE);

    free(reply);
    return ret;
}

char *
get_atom_name(xcb_connection_t *conn, xcb_atom_t atom)
{
    xcb_generic_error_t *e;
    xcb_get_atom_name_reply_t *reply = xcb_get_atom_name_reply(
        conn, xcb_get_atom_name(conn, atom), &e);

    if (e != NULL) {
        LOG_ERR("failed to get atom name: %s", xcb_error(e));
        free(e);
        free(reply);
        return NULL;
    }

    char *name = strndup(
        xcb_get_atom_name_name(reply), xcb_get_atom_name_name_length(reply));

    LOG_DBG("atom name: %s", name);

    free(reply);
    return name;
}

const char *
xcb_error(const xcb_generic_error_t *error)
{
    static char msg[1024];

#if defined(HAVE_XCB_ERRORS)
    const char *major = xcb_errors_get_name_for_major_code(
        err_context, error->major_code);
    const char *minor = xcb_errors_get_name_for_minor_code(
        err_context, error->major_code, error->minor_code);

    const char *extension;
    const char *name = xcb_errors_get_name_for_error(
        err_context, error->error_code, &extension);

    snprintf(msg, sizeof(msg),
             "major=%s, minor=%s), code=%s, extension=%s, sequence=%u",
             major, minor, name, extension, error->sequence);
#else
    snprintf(msg, sizeof(msg), "op %hhu:%hu, code %hhu, sequence %hu",
             error->major_code, error->minor_code, error->error_code,
             error->sequence);
#endif

    return msg;
}
