#include "XcbInterface.h"

#include "DesktopManager.h"

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

#include "PaintEvent.h"
#include "Screen.h"
#include "XcbScreen.h"
#include "UiEvent.h"
#include "VisualLayer.h"
#include "XcbKeyboard.h"
#include "XcbWindow.h"
#include "XcbLayerInterface.h"
#include "XcbEventInterface.h"
#include "XcbWindowInterface.h"
#include "GlxInterface.h"
#include "FileLogger.h"


XcbInterface::XcbInterface(DesktopManager* desktopManager)
      : mDesktopManager(desktopManager),
        mConnection(nullptr),
        mX11Display(),
        mGlxInterface(),
        mXcbEventInterface(XcbEventInterface::Create()),
        mXcbWindowInterface(XcbWindowInterface::Create())
{

}

XcbInterface::~XcbInterface()
{

}

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

    mDesktopManager->SetKeyboard(XcbKeyboard::Create());
}

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::initializeOpenGl()
{
    mGlxInterface = GlxInterface::Create();
    const auto default_screen = DefaultScreen(mX11Display);
    mGlxInterface->SetupContext(mX11Display, default_screen);
}

void XcbInterface::createOpenGlDrawable(mt::Window* window)
{
    if (!mUseHardwareRendering or !window or !window->GetPlatformWindow())
    {
        return;
    }

    auto xcb_window = dynamic_cast<XcbWindow*>(window->GetPlatformWindow());
    if (!xcb_window)
    {
        return;
    }

    if (!mGlxInterface->SetupWindow(mX11Display, xcb_window->GetHandle()))
    {
        MLOG_ERROR("Failed to set up OpenGL Drawable");
        xcb_destroy_window(mConnection, xcb_window->GetHandle());
    }
}

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;
    uint32_t values[2] = {XcbLayerInterface::GetColor(240, 240, 240), 0};
    xcb_create_gc(mConnection, gc, window, mask, values);
    xcb_screen->SetGraphicsContext(gc);
}

void XcbInterface::mapWindow(mt::Window* window)
{
    mXcbWindowInterface->Map(window, mConnection);
}

void XcbInterface::showWindow(mt::Window* window)
{
    mXcbWindowInterface->Show(window, mConnection);

    if (mUseHardwareRendering)
    {
        createOpenGlDrawable(window);
    }
}

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

void XcbInterface::addWindow(mt::Window* window)
{
    auto screen = mDesktopManager->GetDefaultScreen();
    mXcbWindowInterface->Add(window, mConnection, screen, getEventMask());
}

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

    auto mainWindow = mDesktopManager->GetWindowManager()->GetMainWindow();
    auto defaultScreen = mDesktopManager->GetDefaultScreen();
    auto xcb_screen = dynamic_cast<XcbScreen*>(defaultScreen->GetPlatformScreen());
    mXcbWindowInterface->Paint(mainWindow, mConnection,
            xcb_screen->GetNativeScreen(), xcb_screen->GetGraphicsContext());
}

void XcbInterface::onExposeEvent(xcb_expose_event_t* event)
{
    // Update the window
    if (mUseHardwareRendering)
    {
        mGlxInterface->Draw();
        mGlxInterface->SwapBuffers(mX11Display);
    }
    else
    {
        //auto window = mWindows[event->window];
        //window->SetSize(event->width, event->height);
        onPaint();
        xcb_flush(mConnection);
    }
}

void XcbInterface::loop()
{
    if (!mConnection)
    {
        return;
    }
    xcb_generic_event_t *event;
    while ((event = xcb_wait_for_event(mConnection)))
    {
        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 = mXcbEventInterface->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 = mXcbEventInterface->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 = mXcbEventInterface->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 = mXcbEventInterface->ConvertButtonRelease(release);
                mDesktopManager->OnUiEvent(std::move(ui_event));
                break;
            }
            default:
                /* Unknown event type, ignore it */
                break;
        }
        onEventsDispatched();
        free(event);
    }
    onLoopCompleted();
}

void XcbInterface::onLoopCompleted()
{
    if (mUseHardwareRendering)
    {
        mGlxInterface->DestroyWindow(mX11Display);
        mGlxInterface->DestroyContext(mX11Display);
    }
}

void XcbInterface::onEventsDispatched()
{
    if (mDesktopManager->IsModified())
    {
        mDesktopManager->SetIsModified(false);
        auto mainWindow = mDesktopManager->GetWindowManager()->GetMainWindow();
        mXcbWindowInterface->Clear(mainWindow, mConnection);
    }
}

void XcbInterface::shutDown()
{
    if (!mConnection)
    {
        return;
    }
    MLOG_INFO("starting shutdown");
    xcb_disconnect(mConnection);
    MLOG_INFO("Ending shutdown");
}
