#include "XcbInterface.h"

#include "DesktopManager.h"

#include <X11/Xlib.h>
#include <X11/Xlib-xcb.h> /* for XGetXCBConnection, link with libX11-xcb */
#include <xkbcommon/xkbcommon-x11.h>
#include <xcb/xcb.h>

// Header is c-like, uses explicit as a var
#define explicit explicit_
#include <xcb/xkb.h>
#undef explicit

#include "PaintEvent.h"
#include "ResizeEvent.h"
#include "Screen.h"
#include "XcbScreen.h"
#include "Color.h"
#include "Widget.h"
#include "UiEvent.h"
#include "XcbKeyboard.h"
#include "XcbWindow.h"

#include "XcbEventInterface.h"
#include "XcbGlInterface.h"
#include "XcbExtensionInterface.h"

#include "FileLogger.h"
#include "FontsManager.h"

#include <iostream>

namespace {
    typedef union {
        struct {
            uint8_t response_type;
            uint8_t xkbType;
            uint16_t sequence;
            xcb_timestamp_t time;
            uint8_t deviceID;
        } any;
        xcb_xkb_new_keyboard_notify_event_t new_keyboard_notify;
        xcb_xkb_map_notify_event_t map_notify;
        xcb_xkb_state_notify_event_t state_notify;
    } _xkb_event;
}

XcbInterface::XcbInterface(DesktopManager* desktopManager, std::unique_ptr<FontsManager> fontsManager, bool useHardware)
      : AbstractUIInterface(desktopManager, std::move(fontsManager), useHardware),
        mEventInterface(XcbEventInterface::Create()),
        mExtensionInterface(std::make_unique<XcbExtensionInterface>())
{

}

XcbInterface::~XcbInterface()
{
    shutDown();
}

void XcbInterface::initialize()
{
    connect();
    updateScreen();
    createGraphicsContext();
    if (mUseHardwareRendering)
    {
        initializeHardwareRendering();
    }

    mExtensionInterface->initialize(mConnection);
    mDesktopManager->setKeyboard(XcbKeyboard::Create(mConnection));

    const auto num_windows = mDesktopManager->getWindowManager()->getNumWindows();
    for(std::size_t idx=0; idx< num_windows; idx++)
    {
        addWindow(mDesktopManager->getWindowManager()->getWindow(idx));
    }

    mDesktopManager->getWindowManager()->getMainWindow()->show();
}

void XcbInterface::connect()
{
    if (mUseHardwareRendering)
    {
        mX11Display = XOpenDisplay(0);
        if (!mX11Display)
        {
            MLOG_ERROR("Can't open X11 display");
            return;
        }
        mConnection = XGetXCBConnection(mX11Display);
        if (!mConnection)
        {
            XCloseDisplay(mX11Display);
            MLOG_ERROR("Can't get xcb connection from display");
            return;
        }
        XSetEventQueueOwner(mX11Display, XCBOwnsEventQueue);
    }
    else
    {
        mConnection = xcb_connect(nullptr, nullptr);
    }
}

void XcbInterface::updateScreen()
{
    if (!mConnection)
    {
        return;
    }
    auto screen_iter = xcb_setup_roots_iterator(xcb_get_setup(mConnection));
    if (mUseHardwareRendering)
    {
        if (!mX11Display)
        {
            return;
        }
        const auto default_screen = DefaultScreen(mX11Display);
        for(int screen_num = default_screen; screen_iter.rem && screen_num > 0;
            --screen_num, xcb_screen_next(&screen_iter));
    }
    auto xcb_screen = XcbScreen::Create(screen_iter.data);
    auto screen = mt::Screen::Create();
    screen->SetPlatformScreen(std::move(xcb_screen));
    mDesktopManager->addScreen(std::move(screen));
}

void XcbInterface::initializeHardwareRendering()
{
    const auto default_screen = DefaultScreen(mX11Display);
    mGlInterface = XcbGlInterface::Create(mX11Display, default_screen);
}

void XcbInterface::createGraphicsContext()
{
    if (!mConnection || mDesktopManager->getDefaultScreen())
    {
        return;
    }
    auto xcb_screen = dynamic_cast<XcbScreen*>(mDesktopManager->getDefaultScreen()->GetPlatformScreen());
    if (!xcb_screen)
    {
        return;
    }

    auto gc = xcb_generate_id(mConnection);
    xcb_drawable_t window = xcb_screen->GetNativeScreen()->root;
    uint32_t mask = XCB_GC_FOREGROUND | XCB_GC_GRAPHICS_EXPOSURES;
    auto color = Color(240, 240, 240);
    uint32_t values[2] = {color.getAsUInt32(), 0};
    xcb_create_gc(mConnection, gc, window, mask, values);
    xcb_screen->SetGraphicsContext(gc);
}

void XcbInterface::mapWindow(mt::Window* window)
{
    window->map();
}

void XcbInterface::showWindow(mt::Window* window)
{
    window->show();
}

uint32_t XcbInterface::getEventMask()
{
    return XCB_EVENT_MASK_KEY_RELEASE |
            XCB_EVENT_MASK_BUTTON_PRESS |
            XCB_EVENT_MASK_BUTTON_RELEASE |
            XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_STRUCTURE_NOTIFY;
}

void XcbInterface::addWindow(mt::Window* window)
{
    auto screen = mDesktopManager->getDefaultScreen();
    XcbWindow::add(window, mConnection, screen, getEventMask(), mFontsManager.get(), mGlInterface.get());
}

void XcbInterface::onPaint()
{
    mDesktopManager->onUiEvent(PaintEvent::Create());

    auto mainWindow = mDesktopManager->getWindowManager()->getMainWindow();
    auto defaultScreen = mDesktopManager->getDefaultScreen();

    mainWindow->doPaint(defaultScreen);
}

void XcbInterface::onExposeEvent(xcb_expose_event_t* event)
{
    //auto window = mWindows[event->window];
    //window->SetSize(event->width, event->height);

    onPaint();
}

void XcbInterface::loop()
{
    initialize();

    xcb_generic_event_t *event;
    while ((event = xcb_wait_for_event(mConnection)))
    {
        auto handled = true;
        switch (event->response_type & ~0x80)
        {
            case XCB_EXPOSE:{
                auto expose_event = reinterpret_cast<xcb_expose_event_t*>(event);
                onExposeEvent(expose_event);
                break;
            }
            case XCB_KEY_PRESS: {
                auto kp = reinterpret_cast<xcb_key_press_event_t*>(event);
                auto ui_event = mEventInterface->ConvertKeyPress(kp, mDesktopManager->getKeyboard());
                mDesktopManager->onUiEvent(std::move(ui_event));
                break;
            }
            case XCB_KEY_RELEASE: {
                auto kr = reinterpret_cast<xcb_key_release_event_t*>(event);
                auto ui_event = mEventInterface->ConvertKeyRelease(kr, mDesktopManager->getKeyboard());
                mDesktopManager->onUiEvent(std::move(ui_event));
                break;
            }
            case XCB_BUTTON_PRESS: {
                auto press = reinterpret_cast<xcb_button_press_event_t*>(event);
                auto ui_event = mEventInterface->ConvertButtonPress(press);
                mDesktopManager->onUiEvent(std::move(ui_event));
                break;
            }
            case XCB_BUTTON_RELEASE: {
                auto release = reinterpret_cast<xcb_button_release_event_t*>(event);
                auto ui_event = mEventInterface->ConvertButtonRelease(release);
                mDesktopManager->onUiEvent(std::move(ui_event));
                break;
            }
            case XCB_CONFIGURE_NOTIFY:
            {
                auto config_event = (xcb_configure_notify_event_t*) event;
                auto mainWindow = mDesktopManager->getWindowManager()->getMainWindow();
                auto width = mainWindow->getWidth();
                auto height = mainWindow->getHeight();

                bool changed = false;
                if (config_event->width != width)
                {
                    width = config_event->width;
                    changed = true;
                }
                if (config_event->height != height)
                {
                    height = config_event->height;
                    changed = true;
                }
                if (changed)
                {
                    auto ui_event = std::make_unique<ResizeEvent>(width, height);
                    mDesktopManager->onUiEvent(std::move(ui_event));
                }
                break;
            }
            default:
                handled = false;
                break;
        }

        if (!handled)
        {
            if (mExtensionInterface->isXkBEvent(event->response_type))
            {
                auto xkb_event = reinterpret_cast<_xkb_event *>(event);
                if (xkb_event->any.xkbType == XCB_XKB_STATE_NOTIFY)
                {
                    dynamic_cast<XcbKeyboard*>(mDesktopManager->getKeyboard())->updateState(&xkb_event->state_notify);
                }
            }
        }


        free(event);
        mDesktopManager->onLoopIteration();
    }
}

void XcbInterface::shutDown()
{
    mDesktopManager->getWindowManager()->clearPlatformWindows();

    if (!mConnection)
    {
        return;
    }
    MLOG_INFO("starting shutdown");
    xcb_disconnect(mConnection);
    MLOG_INFO("Ending shutdown");
}
