/****************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtCore module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qxcbconnection.h"
#include "qxcbscreen.h"
#include "qxcbintegration.h"

#include <QtGui/private/qhighdpiscaling_p.h>
#include <QtCore/QString>
#include <QtCore/QList>

#include <qpa/qwindowsysteminterface.h>

#include <xcb/xinerama.h>

void QXcbConnection::xrandrSelectEvents()
{
    xcb_screen_iterator_t rootIter = xcb_setup_roots_iterator(setup());
    for (; rootIter.rem; xcb_screen_next(&rootIter)) {
        xcb_randr_select_input(xcb_connection(),
            rootIter.data->root,
            XCB_RANDR_NOTIFY_MASK_SCREEN_CHANGE |
            XCB_RANDR_NOTIFY_MASK_OUTPUT_CHANGE |
            XCB_RANDR_NOTIFY_MASK_CRTC_CHANGE |
            XCB_RANDR_NOTIFY_MASK_OUTPUT_PROPERTY
        );
    }
}

QXcbScreen* QXcbConnection::findScreenForCrtc(xcb_window_t rootWindow, xcb_randr_crtc_t crtc) const
{
    for (QXcbScreen *screen : m_screens) {
        if (screen->root() == rootWindow && screen->crtc() == crtc)
            return screen;
    }

    return nullptr;
}

QXcbScreen* QXcbConnection::findScreenForOutput(xcb_window_t rootWindow, xcb_randr_output_t output) const
{
    for (QXcbScreen *screen : m_screens) {
        if (screen->root() == rootWindow && screen->output() == output)
            return screen;
    }

    return nullptr;
}

QXcbVirtualDesktop* QXcbConnection::virtualDesktopForRootWindow(xcb_window_t rootWindow) const
{
    for (QXcbVirtualDesktop *virtualDesktop : m_virtualDesktops) {
        if (virtualDesktop->screen()->root == rootWindow)
            return virtualDesktop;
    }

    return nullptr;
}

/*!
    \brief Synchronizes the screen list, adds new screens, removes deleted ones
*/
void QXcbConnection::updateScreens(const xcb_randr_notify_event_t *event)
{
    if (event->subCode == XCB_RANDR_NOTIFY_CRTC_CHANGE) {
        xcb_randr_crtc_change_t crtc = event->u.cc;
        QXcbVirtualDesktop *virtualDesktop = virtualDesktopForRootWindow(crtc.window);
        if (!virtualDesktop)
            // Not for us
            return;

        QXcbScreen *screen = findScreenForCrtc(crtc.window, crtc.crtc);
        qCDebug(lcQpaScreen) << "QXcbConnection: XCB_RANDR_NOTIFY_CRTC_CHANGE:" << crtc.crtc
                             << "mode" << crtc.mode << "relevant screen" << screen;
        // Only update geometry when there's a valid mode on the CRTC
        // CRTC with node mode could mean that output has been disabled, and we'll
        // get RRNotifyOutputChange notification for that.
        if (screen && crtc.mode) {
            if (crtc.rotation == XCB_RANDR_ROTATION_ROTATE_90 ||
                crtc.rotation == XCB_RANDR_ROTATION_ROTATE_270)
                std::swap(crtc.width, crtc.height);
            screen->updateGeometry(QRect(crtc.x, crtc.y, crtc.width, crtc.height), crtc.rotation);
            if (screen->mode() != crtc.mode)
                screen->updateRefreshRate(crtc.mode);
        }

    } else if (event->subCode == XCB_RANDR_NOTIFY_OUTPUT_CHANGE) {
        xcb_randr_output_change_t output = event->u.oc;
        QXcbVirtualDesktop *virtualDesktop = virtualDesktopForRootWindow(output.window);
        if (!virtualDesktop)
            // Not for us
            return;

        QXcbScreen *screen = findScreenForOutput(output.window, output.output);
        qCDebug(lcQpaScreen) << "QXcbConnection: XCB_RANDR_NOTIFY_OUTPUT_CHANGE:" << output.output;

        if (screen && output.connection == XCB_RANDR_CONNECTION_DISCONNECTED) {
            qCDebug(lcQpaScreen) << "screen" << screen->name() << "has been disconnected";
            destroyScreen(screen);
        } else if (!screen && output.connection == XCB_RANDR_CONNECTION_CONNECTED) {
            // New XRandR output is available and it's enabled
            if (output.crtc != XCB_NONE && output.mode != XCB_NONE) {
                auto outputInfo = Q_XCB_REPLY(xcb_randr_get_output_info, xcb_connection(),
                                              output.output, output.config_timestamp);
                // Find a fake screen
                const auto scrs = virtualDesktop->screens();
                for (QPlatformScreen *scr : scrs) {
                    QXcbScreen *xcbScreen = static_cast<QXcbScreen *>(scr);
                    if (xcbScreen->output() == XCB_NONE) {
                        screen = xcbScreen;
                        break;
                    }
                }

                if (screen) {
                    QString nameWas = screen->name();
                    // Transform the fake screen into a physical screen
                    screen->setOutput(output.output, outputInfo.get());
                    updateScreen(screen, output);
                    qCDebug(lcQpaScreen) << "output" << screen->name()
                                         << "is connected and enabled; was fake:" << nameWas;
                } else {
                    screen = createScreen(virtualDesktop, output, outputInfo.get());
                    qCDebug(lcQpaScreen) << "output" << screen->name() << "is connected and enabled";
                }
                QHighDpiScaling::updateHighDpiScaling();
            }
        } else if (screen) {
            if (output.crtc == XCB_NONE && output.mode == XCB_NONE) {
                // Screen has been disabled
                auto outputInfo = Q_XCB_REPLY(xcb_randr_get_output_info, xcb_connection(),
                                              output.output, output.config_timestamp);
                if (!outputInfo || outputInfo->crtc == XCB_NONE) {
                    qCDebug(lcQpaScreen) << "output" << screen->name() << "has been disabled";
                    destroyScreen(screen);
                } else {
                    qCDebug(lcQpaScreen) << "output" << screen->name() << "has been temporarily disabled for the mode switch";
                    // Reset crtc to skip RRCrtcChangeNotify events,
                    // because they may be invalid in the middle of the mode switch
                    screen->setCrtc(XCB_NONE);
                }
            } else {
                updateScreen(screen, output);
                qCDebug(lcQpaScreen) << "output has changed" << screen;
            }
        }

        qCDebug(lcQpaScreen) << "primary output is" << qAsConst(m_screens).first()->name();
    }
}

bool QXcbConnection::checkOutputIsPrimary(xcb_window_t rootWindow, xcb_randr_output_t output)
{
    auto primary = Q_XCB_REPLY(xcb_randr_get_output_primary, xcb_connection(), rootWindow);
    if (!primary)
        qWarning("failed to get the primary output of the screen");

    const bool isPrimary = primary ? (primary->output == output) : false;

    return isPrimary;
}

void QXcbConnection::updateScreen(QXcbScreen *screen, const xcb_randr_output_change_t &outputChange)
{
    screen->setCrtc(outputChange.crtc); // Set the new crtc, because it can be invalid
    screen->updateGeometry(outputChange.config_timestamp);
    if (screen->mode() != outputChange.mode)
        screen->updateRefreshRate(outputChange.mode);
    // Only screen which belongs to the primary virtual desktop can be a primary screen
    if (screen->screenNumber() == primaryScreenNumber()) {
        if (!screen->isPrimary() && checkOutputIsPrimary(outputChange.window, outputChange.output)) {
            screen->setPrimary(true);

            // If the screen became primary, reshuffle the order in QGuiApplicationPrivate
            const int idx = m_screens.indexOf(screen);
            if (idx > 0) {
                qAsConst(m_screens).first()->setPrimary(false);
                m_screens.swapItemsAt(0, idx);
            }
            screen->virtualDesktop()->setPrimaryScreen(screen);
            QWindowSystemInterface::handlePrimaryScreenChanged(screen);
        }
    }
}

QXcbScreen *QXcbConnection::createScreen(QXcbVirtualDesktop *virtualDesktop,
                                         const xcb_randr_output_change_t &outputChange,
                                         xcb_randr_get_output_info_reply_t *outputInfo)
{
    QXcbScreen *screen = new QXcbScreen(this, virtualDesktop, outputChange.output, outputInfo);
    // Only screen which belongs to the primary virtual desktop can be a primary screen
    if (screen->screenNumber() == primaryScreenNumber())
        screen->setPrimary(checkOutputIsPrimary(outputChange.window, outputChange.output));

    if (screen->isPrimary()) {
        if (!m_screens.isEmpty())
            qAsConst(m_screens).first()->setPrimary(false);

        m_screens.prepend(screen);
    } else {
        m_screens.append(screen);
    }
    virtualDesktop->addScreen(screen);
    QWindowSystemInterface::handleScreenAdded(screen, screen->isPrimary());

    return screen;
}

void QXcbConnection::destroyScreen(QXcbScreen *screen)
{
    QXcbVirtualDesktop *virtualDesktop = screen->virtualDesktop();
    if (virtualDesktop->screens().count() == 1) {
        // If there are no other screens on the same virtual desktop,
        // then transform the physical screen into a fake screen.
        const QString nameWas = screen->name();
        screen->setOutput(XCB_NONE, nullptr);
        qCDebug(lcQpaScreen) << "transformed" << nameWas << "to fake" << screen;
    } else {
        // There is more than one screen on the same virtual desktop, remove the screen
        m_screens.removeOne(screen);
        virtualDesktop->removeScreen(screen);

        // When primary screen is removed, set the new primary screen
        // which belongs to the primary virtual desktop.
        if (screen->isPrimary()) {
            QXcbScreen *newPrimary = static_cast<QXcbScreen *>(virtualDesktop->screens().at(0));
            newPrimary->setPrimary(true);
            const int idx = m_screens.indexOf(newPrimary);
            if (idx > 0)
                m_screens.swapItemsAt(0, idx);
            QWindowSystemInterface::handlePrimaryScreenChanged(newPrimary);
        }

        QWindowSystemInterface::handleScreenRemoved(screen);
    }
}

void QXcbConnection::initializeScreens()
{
    xcb_screen_iterator_t it = xcb_setup_roots_iterator(setup());
    int xcbScreenNumber = 0;    // screen number in the xcb sense
    QXcbScreen *primaryScreen = nullptr;
    while (it.rem) {
        // Each "screen" in xcb terminology is a virtual desktop,
        // potentially a collection of separate juxtaposed monitors.
        // But we want a separate QScreen for each output (e.g. DVI-I-1, VGA-1, etc.)
        // which will become virtual siblings.
        xcb_screen_t *xcbScreen = it.data;
        QXcbVirtualDesktop *virtualDesktop = new QXcbVirtualDesktop(this, xcbScreen, xcbScreenNumber);
        m_virtualDesktops.append(virtualDesktop);
        QList<QPlatformScreen *> siblings;
        if (hasXRandr()) {
            // RRGetScreenResourcesCurrent is fast but it may return nothing if the
            // configuration is not initialized wrt to the hardware. We should call
            // RRGetScreenResources in this case.
            auto resources_current = Q_XCB_REPLY(xcb_randr_get_screen_resources_current,
                                                 xcb_connection(), xcbScreen->root);
            decltype(Q_XCB_REPLY(xcb_randr_get_screen_resources,
                                 xcb_connection(), xcbScreen->root)) resources;
            if (!resources_current) {
                qWarning("failed to get the current screen resources");
            } else {
                xcb_timestamp_t timestamp = 0;
                xcb_randr_output_t *outputs = nullptr;
                int outputCount = xcb_randr_get_screen_resources_current_outputs_length(resources_current.get());
                if (outputCount) {
                    timestamp = resources_current->config_timestamp;
                    outputs = xcb_randr_get_screen_resources_current_outputs(resources_current.get());
                } else {
                    resources = Q_XCB_REPLY(xcb_randr_get_screen_resources,
                                            xcb_connection(), xcbScreen->root);
                    if (!resources) {
                        qWarning("failed to get the screen resources");
                    } else {
                        timestamp = resources->config_timestamp;
                        outputCount = xcb_randr_get_screen_resources_outputs_length(resources.get());
                        outputs = xcb_randr_get_screen_resources_outputs(resources.get());
                    }
                }

                if (outputCount) {
                    auto primary = Q_XCB_REPLY(xcb_randr_get_output_primary, xcb_connection(), xcbScreen->root);
                    if (!primary) {
                        qWarning("failed to get the primary output of the screen");
                    } else {
                        for (int i = 0; i < outputCount; i++) {
                            auto output = Q_XCB_REPLY_UNCHECKED(xcb_randr_get_output_info,
                                                                xcb_connection(), outputs[i], timestamp);
                            // Invalid, disconnected or disabled output
                            if (!output)
                                continue;

                            if (output->connection != XCB_RANDR_CONNECTION_CONNECTED) {
                                qCDebug(lcQpaScreen, "Output %s is not connected", qPrintable(
                                            QString::fromUtf8((const char*)xcb_randr_get_output_info_name(output.get()),
                                                              xcb_randr_get_output_info_name_length(output.get()))));
                                continue;
                            }

                            if (output->crtc == XCB_NONE) {
                                qCDebug(lcQpaScreen, "Output %s is not enabled", qPrintable(
                                            QString::fromUtf8((const char*)xcb_randr_get_output_info_name(output.get()),
                                                              xcb_randr_get_output_info_name_length(output.get()))));
                                continue;
                            }

                            QXcbScreen *screen = new QXcbScreen(this, virtualDesktop, outputs[i], output.get());
                            siblings << screen;
                            m_screens << screen;

                            // There can be multiple outputs per screen, use either
                            // the first or an exact match.  An exact match isn't
                            // always available if primary->output is XCB_NONE
                            // or currently disconnected output.
                            if (primaryScreenNumber() == xcbScreenNumber) {
                                if (!primaryScreen || (primary && outputs[i] == primary->output)) {
                                    if (primaryScreen)
                                        primaryScreen->setPrimary(false);
                                    primaryScreen = screen;
                                    primaryScreen->setPrimary(true);
                                    siblings.prepend(siblings.takeLast());
                                }
                            }
                        }
                    }
                }
            }
        } else if (hasXinerama()) {
            // Xinerama is available
            auto screens = Q_XCB_REPLY(xcb_xinerama_query_screens, xcb_connection());
            if (screens) {
                xcb_xinerama_screen_info_iterator_t it = xcb_xinerama_query_screens_screen_info_iterator(screens.get());
                while (it.rem) {
                    xcb_xinerama_screen_info_t *screen_info = it.data;
                    QXcbScreen *screen = new QXcbScreen(this, virtualDesktop,
                                                        XCB_NONE, nullptr,
                                                        screen_info, it.index);
                    siblings << screen;
                    m_screens << screen;
                    xcb_xinerama_screen_info_next(&it);
                }
            }
        }
        if (siblings.isEmpty()) {
            // If there are no XRandR outputs or XRandR extension is missing,
            // then create a fake/legacy screen.
            QXcbScreen *screen = new QXcbScreen(this, virtualDesktop, XCB_NONE, nullptr);
            qCDebug(lcQpaScreen) << "created fake screen" << screen;
            m_screens << screen;
            if (primaryScreenNumber() == xcbScreenNumber) {
                primaryScreen = screen;
                primaryScreen->setPrimary(true);
            }
            siblings << screen;
        }
        virtualDesktop->setScreens(std::move(siblings));
        xcb_screen_next(&it);
        ++xcbScreenNumber;
    } // for each xcb screen

    for (QXcbVirtualDesktop *virtualDesktop : qAsConst(m_virtualDesktops))
        virtualDesktop->subscribeToXFixesSelectionNotify();

    if (m_virtualDesktops.isEmpty()) {
        qFatal("QXcbConnection: no screens available");
    } else {
        // Ensure the primary screen is first on the list
        if (primaryScreen) {
            if (qAsConst(m_screens).first() != primaryScreen) {
                m_screens.removeOne(primaryScreen);
                m_screens.prepend(primaryScreen);
            }
        }

        // Push the screens to QGuiApplication
        for (QXcbScreen *screen : qAsConst(m_screens)) {
            qCDebug(lcQpaScreen) << "adding" << screen << "(Primary:" << screen->isPrimary() << ")";
            QWindowSystemInterface::handleScreenAdded(screen, screen->isPrimary());
        }

        qCDebug(lcQpaScreen) << "primary output is" << qAsConst(m_screens).first()->name();
    }
}
