/*************************************************************************************
 *  Copyright (C) 2012 by Alejandro Fiestas Olivares <afiestas@kde.org>              *
 *  Copyright (C) 2012, 2013 by Daniel Vrátil <dvratil@redhat.com>                   *
 *                                                                                   *
 *  This library is free software; you can redistribute it and/or                    *
 *  modify it under the terms of the GNU Lesser General Public                       *
 *  License as published by the Free Software Foundation; either                     *
 *  version 2.1 of the License, or (at your option) any later version.               *
 *                                                                                   *
 *  This library 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                *
 *  Lesser General Public License for more details.                                  *
 *                                                                                   *
 *  You should have received a copy of the GNU Lesser General Public                 *
 *  License along with this library; if not, write to the Free Software              *
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA       *
 *************************************************************************************/

#include "xrandr.h"
#include "xrandrconfig.h"
#include "xrandrscreen.h"
#include "../xcbwrapper.h"
#include "../xcbeventlistener.h"

#include "config.h"
#include "output.h"
#include "edid.h"

#include <QFile>
#include <QRect>
#include <QAbstractEventDispatcher>
#include <QTimer>
#include <QTime>

#include <QX11Info>
#include <QGuiApplication>

xcb_screen_t* XRandR::s_screen = 0;
xcb_window_t XRandR::s_rootWindow = 0;
XRandRConfig* XRandR::s_internalConfig = 0;
int XRandR::s_randrBase = 0;
int XRandR::s_randrError = 0;
bool XRandR::s_monitorInitialized = false;
bool XRandR::s_has_1_3 = false;
bool XRandR::s_xorgCacheInitialized = false;

using namespace KScreen;

Q_LOGGING_CATEGORY(KSCREEN_XRANDR, "kscreen.xrandr")

XRandR::XRandR()
    : KScreen::AbstractBackend()
    , m_x11Helper(0)
    , m_isValid(false)
    , m_configChangeCompressor(0)
{
    qRegisterMetaType<xcb_randr_output_t>("xcb_randr_output_t");
    qRegisterMetaType<xcb_randr_crtc_t>("xcb_randr_crtc_t");
    qRegisterMetaType<xcb_randr_mode_t>("xcb_randr_mode_t");
    qRegisterMetaType<xcb_randr_connection_t>("xcb_randr_connection_t");
    qRegisterMetaType<xcb_randr_rotation_t>("xcb_randr_rotation_t");

    // Use our own connection to make sure that we won't mess up Qt's connection
    // if something goes wrong on our side.
    xcb_generic_error_t *error = 0;
    xcb_randr_query_version_reply_t* version;
    XCB::connection();
    version = xcb_randr_query_version_reply(XCB::connection(), xcb_randr_query_version(XCB::connection(), XCB_RANDR_MAJOR_VERSION, XCB_RANDR_MINOR_VERSION), &error);

    if (!version || error) {
        XCB::closeConnection();
        free(error);
        return;
    }

    if ((version->major_version > 1) || ((version->major_version == 1) && (version->minor_version >= 2))) {
        m_isValid = true;
    } else {
        XCB::closeConnection();
        qCWarning(KSCREEN_XRANDR) << "XRandR extension not available or unsupported version";
        return;
    }

    if (s_screen == 0) {
        s_screen = XCB::screenOfDisplay(XCB::connection(), QX11Info::appScreen());
        s_rootWindow = s_screen->root;

        xcb_prefetch_extension_data(XCB::connection(), &xcb_randr_id);
        auto  reply = xcb_get_extension_data(XCB::connection(), &xcb_randr_id);
        s_randrBase = reply->first_event;
        s_randrError = reply->first_error;
    }

    XRandR::s_has_1_3 = (version->major_version > 1 || (version->major_version == 1 && version->minor_version >= 3));

    if (s_internalConfig == 0) {
        s_internalConfig = new XRandRConfig();
    }

    if (!s_monitorInitialized) {
        m_x11Helper = new XCBEventListener();
        connect(m_x11Helper, &XCBEventListener::outputChanged,
                this, &XRandR::outputChanged,
                Qt::QueuedConnection);
        connect(m_x11Helper, &XCBEventListener::crtcChanged,
                this, &XRandR::crtcChanged,
                Qt::QueuedConnection);
        connect(m_x11Helper, &XCBEventListener::screenChanged,
                this, &XRandR::screenChanged,
                Qt::QueuedConnection);

        m_configChangeCompressor = new QTimer(this);
        m_configChangeCompressor->setSingleShot(true);
        m_configChangeCompressor->setInterval(500);
        connect(m_configChangeCompressor, &QTimer::timeout,
                [&]() {
                    qCDebug(KSCREEN_XRANDR) << "Emitting configChanged()";
                    Q_EMIT configChanged(config());
                });

        s_monitorInitialized = true;
    }
}

XRandR::~XRandR()
{
    delete m_x11Helper;
}

QString XRandR::name() const
{
    return QString("XRandR");
}

QString XRandR::serviceName() const
{
    return QLatin1Literal("org.kde.KScreen.Backend.XRandR");
}


void XRandR::outputChanged(xcb_randr_output_t output, xcb_randr_crtc_t crtc,
                           xcb_randr_mode_t mode, xcb_randr_connection_t connection)
{
    XRandROutput *xOutput = s_internalConfig->output(output);
    XCB::PrimaryOutput primary(XRandR::rootWindow());
    if (!xOutput) {
        s_internalConfig->addNewOutput(output);
    } else {
        switch (crtc == XCB_NONE && mode == XCB_NONE && connection == XCB_RANDR_CONNECTION_DISCONNECTED) {
        case true: {
            XCB::OutputInfo info(output, XCB_TIME_CURRENT_TIME);
            if (info.isNull()) {
                s_internalConfig->removeOutput(output);
                qCDebug(KSCREEN_XRANDR) << "Output" << output << " removed";
                break;
            }
            // info is valid: fall-through
            Q_FALLTHROUGH();
        }
        case false: {
            xOutput->update(crtc, mode, connection, (primary->output == output));
            qCDebug(KSCREEN_XRANDR) << "Output" << xOutput->id() << ": connected =" << xOutput->isConnected() << ", enabled =" << xOutput->isEnabled();
            break;
        }
        } // switch
    }

    m_configChangeCompressor->start();
}

void XRandR::crtcChanged(xcb_randr_crtc_t crtc, xcb_randr_mode_t mode,
                         xcb_randr_rotation_t rotation, const QRect& geom)
{
    XRandRCrtc *xCrtc = s_internalConfig->crtc(crtc);
    if (!xCrtc) {
        s_internalConfig->addNewCrtc(crtc);
    } else {
        xCrtc->update(mode, rotation, geom);
    }

    m_configChangeCompressor->start();
}

void XRandR::screenChanged(xcb_randr_rotation_t rotation,
                           const QSize &sizePx, const QSize &sizeMm)
{
    Q_UNUSED(sizeMm);

    QSize newSizePx = sizePx;
    if (rotation == XCB_RANDR_ROTATION_ROTATE_90 || rotation == XCB_RANDR_ROTATION_ROTATE_270) {
        newSizePx.transpose();
    }

    XRandRScreen *xScreen = s_internalConfig->screen();
    Q_ASSERT(xScreen);
    xScreen->update(newSizePx);

    m_configChangeCompressor->start();
}


ConfigPtr XRandR::config() const
{
    return s_internalConfig->toKScreenConfig();
}

void XRandR::setConfig(const ConfigPtr &config)
{
    if (!config) {
        return;
    }

    qCDebug(KSCREEN_XRANDR) << "XRandR::setConfig";
    s_internalConfig->applyKScreenConfig(config);
    qCDebug(KSCREEN_XRANDR) << "XRandR::setConfig done!";
}

QByteArray XRandR::edid(int outputId) const
{
    const XRandROutput *output = s_internalConfig->output(outputId);
    if (!output) {
        return QByteArray();
    }

    return output->edid();
}

bool XRandR::isValid() const
{
    return m_isValid;
}

quint8* XRandR::getXProperty(xcb_randr_output_t output, xcb_atom_t atom, size_t &len)
{
    quint8 *result;

    auto cookie = xcb_randr_get_output_property(XCB::connection(), output, atom,
                                                XCB_ATOM_ANY,
                                                0, 100, false, false);
    auto reply = xcb_randr_get_output_property_reply(XCB::connection(), cookie, NULL);
    if (reply->type == XCB_ATOM_INTEGER && reply->format == 8) {
        result = new quint8[reply->num_items];
        memcpy(result, xcb_randr_get_output_property_data(reply), reply->num_items);
        len = reply->num_items;
    } else {
        result = nullptr;
    }

    free(reply);
    return result;
}

quint8 *XRandR::outputEdid(xcb_randr_output_t outputId, size_t &len)
{
    quint8 *result;

    auto edid_atom = XCB::InternAtom(false, 4, "EDID")->atom;
    result = XRandR::getXProperty(outputId, edid_atom, len);
    if (result == NULL) {
        auto edid_atom = XCB::InternAtom(false, 9, "EDID_DATA")->atom;
        result = XRandR::getXProperty(outputId, edid_atom, len);
    }
    if (result == NULL) {
        auto edid_atom = XCB::InternAtom(false, 25, "XFree86_DDC_EDID1_RAWDATA")->atom;
        result = XRandR::getXProperty(outputId, edid_atom, len);
    }

    if (result) {
        if (len % 128 == 0) {
            return result;
        } else {
            len = 0;
            delete[] result;
        }
    }

    return 0;
}

xcb_randr_get_screen_resources_reply_t* XRandR::screenResources()
{
    if (XRandR::s_has_1_3) {
        if (XRandR::s_xorgCacheInitialized) {
            // HACK: This abuses the fact that xcb_randr_get_screen_resources_reply_t
            // and xcb_randr_get_screen_resources_current_reply_t are the same
            return reinterpret_cast<xcb_randr_get_screen_resources_reply_t*>(
                xcb_randr_get_screen_resources_current_reply(XCB::connection(),
                    xcb_randr_get_screen_resources_current(XCB::connection(), XRandR::rootWindow()),
                    NULL));
        } else {
            /* XRRGetScreenResourcesCurrent is faster then XRRGetScreenResources
             * because it returns cached values. However the cached values are not
             * available until someone calls XRRGetScreenResources first. In case
             * we happen to be the first ones, we need to fill the cache first. */
            XRandR::s_xorgCacheInitialized = true;
        }
    }

    return xcb_randr_get_screen_resources_reply(XCB::connection(),
        xcb_randr_get_screen_resources(XCB::connection(), XRandR::rootWindow()), NULL);
}

xcb_window_t XRandR::rootWindow()
{
    return s_rootWindow;
}

xcb_screen_t* XRandR::screen()
{
    return s_screen;
}
