/*****************************************************************************
 * Copyright (C) 2020 VLC authors and VideoLAN
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/
#include <QScreen>

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include <vlc_window.h>
#include <vlc_cxx_helpers.hpp>

#include "compositor_x11.hpp"
#include "compositor_x11_renderwindow.hpp"
#include "compositor_x11_uisurface.hpp"
#include "mainctx.hpp"
#include "interface_window_handler.hpp"
#include "video_window_handler.hpp"
#include "mainui.hpp"
#include "compositor_accessibility.hpp"

#ifdef QT_DECLARATIVE_PRIVATE
#include <private/qquickitem_p.h>
#include <private/qquickwindow_p.h>
#endif

#ifndef X_DISPLAY_MISSING
#include <X11/Xlib.h>
#endif

using namespace vlc;

int CompositorX11::windowEnable(const vlc_window_cfg_t *)
{
    commonWindowEnable();
    m_renderWindow->enableVideoWindow();
    return VLC_SUCCESS;
}

void CompositorX11::windowDisable()
{
    m_renderWindow->disableVideoWindow();
    commonWindowDisable();
}

CompositorX11::CompositorX11(qt_intf_t *p_intf, QObject *parent)
    : CompositorVideo(p_intf, parent)
{
}

CompositorX11::~CompositorX11()
{
    if (m_conn)
        xcb_disconnect(m_conn);
}

static bool checkExtensionPresent(qt_intf_t* intf, xcb_connection_t *conn, const char* extension)
{
    bool ret = queryExtension(conn, extension, nullptr, nullptr);
    if (! ret)
        msg_Warn(intf, "X11 extension %s is missing", extension);
    return ret;
}

#define REGISTER_XCB_EXTENSION(c, extension, minor, major) \
    do { \
        xcb_ ## extension  ##_query_version_cookie_t cookie = xcb_## extension  ##_query_version(c, minor, major); \
        xcb_generic_error_t* error = nullptr; \
        auto reply = wrap_cptr(xcb_## extension  ##_query_version_reply(c, cookie, &error)); \
        if (error) { \
            msg_Warn(m_intf, "X11 extension %s is too old", #extension); \
            free(error); \
            return false; \
        } \
    } while(0)


bool CompositorX11::init()
{
    if (!qGuiApp->nativeInterface<QNativeInterface::QX11Application>())
    {
        msg_Info(m_intf, "not running an X11 platform");
        return false;
    }

    const QString& sceneGraphBackend = qEnvironmentVariable("QT_QUICK_BACKEND");
    if (!sceneGraphBackend.isEmpty() /* if empty, RHI is used */ &&
        sceneGraphBackend != QLatin1String("rhi") &&
        sceneGraphBackend != QLatin1String("software"))
    {
        // No RHI means no OpenGL, the graphics API check below is
        // only relevant when RHI is in use. If QT_QUICK_BACKEND is
        // set to software or openvg, then `QQuickWindow::graphicsApi()`
        // might still report OpenGL until the scene graph is initialized.
        // Unlike `QQuickWindow::graphicsApi()`, `sceneGraphBackend()`
        // is only valid after the window is constructed, so instead
        // of using `QQuickWindow::sceneGraphBackend()`, simply probe
        // the environment variable.
        return false;
    }

    const auto graphicsApi = QQuickWindow::graphicsApi();
    if (graphicsApi != QSGRendererInterface::OpenGL &&
        graphicsApi != QSGRendererInterface::Software)
    {
        msg_Warn(m_intf, "CompositorX11: Only software mode and rhi opengl are supported!");
        return false;
    }

    //open a separated X11 connection from Qt to be able to receive
    //damage events independently from Qt (even when Qt's main loop is stalled)
    m_conn = xcb_connect(nullptr, nullptr);
    if (xcb_connection_has_error(m_conn) != 0)
    {
        msg_Warn(m_intf, "can't open X11 connection");
        return false;
    }

    //check X11 extensions
    if (!checkExtensionPresent(m_intf, m_conn, "DAMAGE"))
        return false;
    REGISTER_XCB_EXTENSION(m_conn, damage, XCB_DAMAGE_MAJOR_VERSION, XCB_DAMAGE_MINOR_VERSION);

    if (!checkExtensionPresent(m_intf, m_conn, "RENDER"))
        return false;
    //0.11 required for Blend mode operators
    static_assert (XCB_RENDER_MAJOR_VERSION == 0 && XCB_RENDER_MINOR_VERSION >= 11,
            "X11 Render version is too old, 0.11+ is required");
    REGISTER_XCB_EXTENSION(m_conn, render, XCB_RENDER_MAJOR_VERSION, XCB_RENDER_MINOR_VERSION);

    if (!checkExtensionPresent(m_intf, m_conn, "Composite"))
        return false;
    //0.2 is required for NameWindowPixmap
    static_assert (XCB_COMPOSITE_MAJOR_VERSION == 0 && XCB_COMPOSITE_MINOR_VERSION >= 2,
            "X11 Composite version is too old, 0.2+ is required");
    REGISTER_XCB_EXTENSION(m_conn, composite, XCB_COMPOSITE_MAJOR_VERSION, XCB_COMPOSITE_MINOR_VERSION);

    if (!checkExtensionPresent(m_intf, m_conn, "XFIXES"))
        return false;
    //2.x is required for SetWindowShapeRegion
    static_assert (XCB_XFIXES_MAJOR_VERSION >= 2,
            "X11 Fixes version is too old, 2.0+ is required");
    REGISTER_XCB_EXTENSION(m_conn, xfixes, XCB_XFIXES_MAJOR_VERSION, XCB_XFIXES_MINOR_VERSION);

    // check whether we're running under "XWAYLAND"
    auto screens = qApp->screens();
    bool isXWayland = std::any_of(screens.begin(), screens.end(), [](QScreen* screen){
        return screen->name().startsWith("XWAYLAND");
    });

    if (isXWayland)
    {
        // if the X11 server is XWayland, test if it is a broken version
        const xcb_setup_t* setup = xcb_get_setup(m_conn);

        if (setup == nullptr)
        {
            msg_Info(m_intf, "error checking for XWayland version");
            return false;
        }

        if (setup->release_number < 12100000u)
        {
            msg_Info(m_intf, "running a broken XWayland version, disabling X11 composition");
            return false;
        }
    }

#if !defined(QT_NO_ACCESSIBILITY) && defined(QT_DECLARATIVE_PRIVATE)
   QAccessible::installFactory(&compositionAccessibleFactory);
#endif

    return true;
}

bool CompositorX11::makeMainInterface(MainCtx* mainCtx, std::function<void (QQuickWindow *)> aboutToShowQuickWindowCallback)
{
    m_mainCtx = mainCtx;

    m_renderWindow = std::make_unique<vlc::CompositorX11RenderWindow>(m_intf, m_conn);
    if (!m_renderWindow->init())
        return false;

    m_videoWidget = std::make_unique<DummyNativeWidget>();
    // widget would normally require WindowTransparentForInput, without this
    // we end up with an invisible area within our window that grabs our mouse events.
    // But using this this causes rendering issues with some VoutDisplay
    // (xcb_render for instance) instead, we manually, set a null input region afterwards
    // see setTransparentForMouseEvent
    m_videoWidget->winId();
    m_videoWidget->windowHandle()->setParent(m_renderWindow.get());

    //update manually EventMask as we don't use WindowTransparentForInput
    const uint32_t mask = XCB_CW_EVENT_MASK;
    const uint32_t values = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_STRUCTURE_NOTIFY
        | XCB_EVENT_MASK_PROPERTY_CHANGE | XCB_EVENT_MASK_FOCUS_CHANGE;

    const auto connection =  qGuiApp->nativeInterface<QNativeInterface::QX11Application>()->connection();

    xcb_change_window_attributes(connection, m_videoWidget->winId(), mask, &values);
    setTransparentForMouseEvent(connection, m_videoWidget->winId());
    m_videoWidget->setVisible(true);

    connect(m_mainCtx, &MainCtx::hasAcrylicSurfaceChanged, m_renderWindow.get(), &CompositorX11RenderWindow::setAcrylic);

    m_qmlView = std::make_unique<CompositorX11UISurface>(m_renderWindow.get());

    m_qmlView->setFlag(Qt::WindowType::WindowTransparentForInput);
    m_qmlView->setParent(m_renderWindow.get());
    m_qmlView->create();
    CompositorVideo::Flags flags = CompositorVideo::CAN_SHOW_PIP | HAS_ACRYLIC;
    if (m_renderWindow->supportExtendedFrame())
        flags |= CompositorVideo::HAS_EXTENDED_FRAME;
    assert(m_qmlView->getOffscreenWindow());
    m_qmlView->getOffscreenWindow()->create();
    if (!commonGUICreate(m_renderWindow.get(), m_qmlView.get(), flags))
        return false;

    if (aboutToShowQuickWindowCallback)
        aboutToShowQuickWindowCallback(m_qmlView->getOffscreenWindow());

    m_qmlView->setVisible(true);

    if (m_mainCtx->hasAcrylicSurface())
        m_renderWindow->m_hasAcrylic = true;

    m_renderWindow->setInterfaceWindow(m_qmlView.get());
    m_renderWindow->setVideoWindow(m_videoWidget->windowHandle());

    m_renderWindow->startRendering();

    return true;
}

void CompositorX11::destroyMainInterface()
{
    commonIntfDestroy();
    m_videoWidget.reset();
    m_renderWindow.reset();
}

void CompositorX11::unloadGUI()
{
    m_renderWindow->stopRendering();
    m_qmlView.reset();
    commonGUIDestroy();
}

void CompositorX11::onSurfacePositionChanged(const QPointF& position)
{
    m_pendingPosition = QPoint{static_cast<int>(position.x()), static_cast<int>(position.y())};
}

void CompositorX11::onSurfaceSizeChanged(const QSizeF& size)
{
    const QSizeF area = (size / m_videoWidget->window()->devicePixelRatioF());
    m_pendingSize = QSize{static_cast<int>(std::ceil(area.width())), static_cast<int>(std::ceil(area.height()))};
}

bool CompositorX11::setupVoutWindow(vlc_window_t* p_wnd, VoutDestroyCb destroyCb)
{
    if (m_wnd)
        return false;

    p_wnd->type = VLC_WINDOW_TYPE_XID;
    p_wnd->handle.xid = m_videoWidget->winId();
#ifndef X_DISPLAY_MISSING
    assert(qGuiApp);
    const auto x11App = qGuiApp->nativeInterface<QNativeInterface::QX11Application>();
    assert(x11App); // already checked in `init()`
    assert(x11App->display());
    p_wnd->display.x11 = XDisplayString(x11App->display());
#endif

    commonSetupVoutWindow(p_wnd, destroyCb);
    return true;
}

QWindow* CompositorX11::interfaceMainWindow() const { return m_renderWindow.get(); }

QQuickWindow *CompositorX11::quickWindow() const
{
    assert(m_qmlView);
    return m_qmlView->getOffscreenWindow();
}

QQuickItem * CompositorX11::activeFocusItem() const /* override */
{
    return m_qmlView->activeFocusItem();
}

void CompositorX11::commitSurface()
{
    if (m_renderWindow)
        m_renderWindow->setVideoGeometry(m_pendingPosition, m_pendingSize);
}

///////// DummyNativeWidget

DummyNativeWidget::DummyNativeWidget(QWidget* parent, Qt::WindowFlags f)
    : QWidget(parent, f)
{
    setAttribute(Qt::WA_NativeWindow, true);
    setAttribute(Qt::WA_OpaquePaintEvent, true);
    setAttribute(Qt::WA_PaintOnScreen, true);
    setAttribute(Qt::WA_MouseTracking, true);
    setAttribute(Qt::WA_TranslucentBackground, false);
    QWindow* w =  window()->windowHandle();
    assert(w);
    /*
     * force the window not to have an alpha channel, the  parent window
     * may have an alpha channel and child widget would inhertit the format
     * even if we set Qt::WA_TranslucentBackground to false. having an alpha
     * in this surface would lead to the video begin semi-tranparent.
     */
    QSurfaceFormat format = w->format();
    format.setAlphaBufferSize(0);
    w->setFormat(format);
}

DummyNativeWidget::~DummyNativeWidget()
{

}

QPaintEngine* DummyNativeWidget::paintEngine() const
{
    return nullptr;
}
