/*
    KWin - the KDE window manager
    This file is part of the KDE project.

    SPDX-FileCopyrightText: 2018 Roman Gilg <subdiff@gmail.com>
    SPDX-FileCopyrightText: 2025 Vlad Zahorodnii <vlad.zahorodnii@kde.org>

    SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "transfer.h"
#include "atoms.h"
#include "selection.h"
#include "xwayland.h"

#include <xcb/xcb_event.h>
#include <xcb/xfixes.h>

#include <algorithm>
#include <unistd.h>

#include <xwayland_logging.h>

namespace KWin
{
namespace Xwl
{

// in Bytes: equals 64KB
static const uint32_t s_incrChunkSize = 63 * 1024;

Transfer::Transfer(xcb_atom_t selection, FileDescriptor fd, xcb_timestamp_t timestamp, QObject *parent)
    : QObject(parent)
    , m_atom(selection)
    , m_timestamp(timestamp)
    , m_fd(std::move(fd))
{
}

void Transfer::createSocketNotifier(QSocketNotifier::Type type)
{
    m_notifier = std::make_unique<QSocketNotifier>(m_fd.get(), type);
}

void Transfer::clearSocketNotifier()
{
    m_notifier.reset();
}

void Transfer::timeout()
{
    if (m_timeout) {
        endTransfer();
    }
    m_timeout = true;
}

void Transfer::endTransfer()
{
    clearSocketNotifier();
    m_fd.reset();
    Q_EMIT finished();
}

TransferWltoX::TransferWltoX(const xcb_selection_request_event_t &request, FileDescriptor fd, QObject *parent)
    : Transfer(request.selection, std::move(fd), 0, parent)
    , m_request(request)
{
}

void TransferWltoX::startTransferFromSource()
{
    createSocketNotifier(QSocketNotifier::Read);
    connect(socketNotifier(), &QSocketNotifier::activated, this, [this](int socket) {
        readWlSource();
    });
}

int TransferWltoX::flushSourceData()
{
    Q_ASSERT(!m_chunks.isEmpty());
    xcb_connection_t *xcbConn = kwinApp()->x11Connection();

    xcb_change_property(xcbConn,
                        XCB_PROP_MODE_REPLACE,
                        m_request.requestor,
                        m_request.property,
                        m_request.target,
                        8,
                        m_chunks.first().first.size(),
                        m_chunks.first().first.data());

    m_propertyIsSet = true;
    resetTimeout();

    const auto rm = m_chunks.takeFirst();
    return rm.first.size();
}

void TransferWltoX::startIncr()
{
    Q_ASSERT(m_chunks.size() == 1);

    xcb_connection_t *xcbConn = kwinApp()->x11Connection();

    // Note that m_request.requestor can be one of the managed windows. In which case, the event mask
    // should not be overwritten with only the PropertyChange flag, otherwise we can stop receiving
    // the WM-relevant events, for example FocusIn events.
    xcb_get_window_attributes_cookie_t cookie = xcb_get_window_attributes(xcbConn, m_request.requestor);
    if (xcb_get_window_attributes_reply_t *reply = xcb_get_window_attributes_reply(xcbConn, cookie, nullptr)) {
        if (!(reply->your_event_mask & XCB_EVENT_MASK_PROPERTY_CHANGE)) {
            uint32_t mask = reply->your_event_mask | XCB_EVENT_MASK_PROPERTY_CHANGE;
            xcb_change_window_attributes(xcbConn, m_request.requestor, XCB_CW_EVENT_MASK, &mask);
        }

        free(reply);
    }

    // spec says to make the available space larger
    const uint32_t chunkSpace = 1024 + s_incrChunkSize;
    xcb_change_property(xcbConn,
                        XCB_PROP_MODE_REPLACE,
                        m_request.requestor,
                        m_request.property,
                        atoms->incr,
                        32, 1, &chunkSpace);

    setIncr(true);
    // first data will be flushed after the property has been deleted
    // again by the requestor
    m_flushPropertyOnDelete = true;
    m_propertyIsSet = true;
    Selection::sendSelectionNotify(&m_request, true);
}

void TransferWltoX::readWlSource()
{
    if (m_chunks.size() == 0 || m_chunks.last().second == s_incrChunkSize) {
        // append new chunk
        auto next = QPair<QByteArray, int>();
        next.first.resize(s_incrChunkSize);
        next.second = 0;
        m_chunks.append(next);
    }

    const auto oldLen = m_chunks.last().second;
    const auto avail = s_incrChunkSize - m_chunks.last().second;
    Q_ASSERT(avail > 0);

    ssize_t readLen = read(fd(), m_chunks.last().first.data() + oldLen, avail);
    if (readLen == -1) {
        qCWarning(KWIN_XWL) << "Error reading in Wl data.";

        // TODO: cleanup X side?
        endTransfer();
        return;
    }
    m_chunks.last().second = oldLen + readLen;

    if (readLen == 0) {
        // at the fd end - complete transfer now
        m_chunks.last().first.resize(m_chunks.last().second);

        if (incr()) {
            // incremental transfer is to be completed now
            m_flushPropertyOnDelete = true;
            if (!m_propertyIsSet) {
                // flush if target's property is not set at the moment
                flushSourceData();
            }
            clearSocketNotifier();
        } else {
            // non incremental transfer is to be completed now,
            // data can be transferred to X client via a single property set
            flushSourceData();
            Selection::sendSelectionNotify(&m_request, true);
            endTransfer();
        }
    } else if (m_chunks.last().second == s_incrChunkSize) {
        // first chunk full, but not yet at fd end -> go incremental
        if (incr()) {
            m_flushPropertyOnDelete = true;
            if (!m_propertyIsSet) {
                // flush if target's property is not set at the moment
                flushSourceData();
            }
        } else {
            // starting incremental transfer
            startIncr();
        }
    }
    resetTimeout();
}

bool TransferWltoX::handlePropertyNotify(xcb_property_notify_event_t *event)
{
    if (event->window == m_request.requestor) {
        if (event->state == XCB_PROPERTY_DELETE && event->atom == m_request.property) {
            handlePropertyDelete();
        }
        return true;
    }
    return false;
}

void TransferWltoX::handlePropertyDelete()
{
    if (!incr()) {
        // non-incremental transfer: nothing to do
        return;
    }
    m_propertyIsSet = false;

    if (m_flushPropertyOnDelete) {
        if (!socketNotifier() && m_chunks.isEmpty()) {
            xcb_change_property(kwinApp()->x11Connection(),
                                XCB_PROP_MODE_REPLACE,
                                m_request.requestor,
                                m_request.property,
                                m_request.target,
                                8, 0, nullptr);
            m_flushPropertyOnDelete = false;
            endTransfer();
        } else if (!m_chunks.isEmpty()) {
            flushSourceData();
        }
    }
}

TransferXtoWl::TransferXtoWl(xcb_atom_t selection, xcb_atom_t target, FileDescriptor fd, xcb_timestamp_t timestamp, xcb_window_t parentWindow, QObject *parent)
    : Transfer(selection, std::move(fd), timestamp, parent)
{
    // create transfer window
    xcb_connection_t *xcbConn = kwinApp()->x11Connection();
    m_window = xcb_generate_id(xcbConn);
    const uint32_t values[] = {XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | XCB_EVENT_MASK_PROPERTY_CHANGE};
    xcb_create_window(xcbConn,
                      XCB_COPY_FROM_PARENT,
                      m_window,
                      parentWindow,
                      0, 0,
                      10, 10,
                      0,
                      XCB_WINDOW_CLASS_INPUT_OUTPUT,
                      XCB_COPY_FROM_PARENT,
                      XCB_CW_EVENT_MASK,
                      values);
    // convert selection
    xcb_convert_selection(xcbConn,
                          m_window,
                          selection,
                          target,
                          atoms->wl_selection,
                          timestamp);
}

TransferXtoWl::~TransferXtoWl()
{
    xcb_connection_t *xcbConn = kwinApp()->x11Connection();
    xcb_destroy_window(xcbConn, m_window);

    delete m_receiver;
    m_receiver = nullptr;
}

bool TransferXtoWl::handlePropertyNotify(xcb_property_notify_event_t *event)
{
    if (event->window == m_window) {
        if (event->state == XCB_PROPERTY_NEW_VALUE && event->atom == atoms->wl_selection) {
            getIncrChunk();
        }
        return true;
    }
    return false;
}

bool TransferXtoWl::handleSelectionNotify(xcb_selection_notify_event_t *event)
{
    if (event->requestor != m_window) {
        return false;
    }
    if (event->property == XCB_ATOM_NONE) {
        qCWarning(KWIN_XWL) << "Incoming X selection conversion failed";
        endTransfer();
        return true;
    }
    if (event->target == atoms->targets) {
        qCWarning(KWIN_XWL) << "Received targets too late";
        endTransfer();
        return true;
    }
    if (m_receiver) {
        // second selection notify element - misbehaving source
        endTransfer();
        return true;
    }

    m_receiver = new DataReceiver;

    startTransfer();
    return true;
}

void TransferXtoWl::startTransfer()
{
    xcb_connection_t *xcbConn = kwinApp()->x11Connection();
    auto cookie = xcb_get_property(xcbConn,
                                   1,
                                   m_window,
                                   atoms->wl_selection,
                                   XCB_GET_PROPERTY_TYPE_ANY,
                                   0,
                                   0x1fffffff);

    auto *reply = xcb_get_property_reply(xcbConn, cookie, nullptr);
    if (reply == nullptr) {
        qCWarning(KWIN_XWL) << "Can't get selection property.";
        endTransfer();
        return;
    }

    if (reply->type == atoms->incr) {
        setIncr(true);
        free(reply);
    } else {
        setIncr(false);
        // reply's ownership is transferred
        m_receiver->transferFromProperty(reply);
        dataSourceWrite();
    }
}

void TransferXtoWl::getIncrChunk()
{
    if (!incr()) {
        // source tries to sent incrementally, but did not announce it before
        return;
    }
    if (!m_receiver) {
        // receive mechanism has not yet been setup
        return;
    }
    xcb_connection_t *xcbConn = kwinApp()->x11Connection();

    auto cookie = xcb_get_property(xcbConn,
                                   0,
                                   m_window,
                                   atoms->wl_selection,
                                   XCB_GET_PROPERTY_TYPE_ANY,
                                   0,
                                   0x1fffffff);

    auto *reply = xcb_get_property_reply(xcbConn, cookie, nullptr);
    if (!reply) {
        qCWarning(KWIN_XWL) << "Can't get selection property.";
        endTransfer();
        return;
    }

    if (xcb_get_property_value_length(reply) > 0) {
        // reply's ownership is transferred
        m_receiver->transferFromProperty(reply);
        dataSourceWrite();
    } else {
        // Transfer complete
        free(reply);
        endTransfer();
    }
}

DataReceiver::~DataReceiver()
{
    if (m_propertyReply) {
        free(m_propertyReply);
        m_propertyReply = nullptr;
    }
}

void DataReceiver::transferFromProperty(xcb_get_property_reply_t *reply)
{
    m_propertyStart = 0;
    m_propertyReply = reply;

    setData(static_cast<char *>(xcb_get_property_value(reply)),
            xcb_get_property_value_length(reply));
}

void DataReceiver::setData(const char *value, int length)
{
    // simply set data without copy
    m_data = QByteArray::fromRawData(value, length);
}

QByteArray DataReceiver::data() const
{
    return QByteArray::fromRawData(m_data.data() + m_propertyStart,
                                   m_data.size() - m_propertyStart);
}

void DataReceiver::partRead(int length)
{
    m_propertyStart += length;
    if (m_propertyStart == m_data.size()) {
        Q_ASSERT(m_propertyReply);
        free(m_propertyReply);
        m_propertyReply = nullptr;
    }
}

void TransferXtoWl::dataSourceWrite()
{
    QByteArray property = m_receiver->data();

    ssize_t len = write(fd(), property.constData(), property.size());
    if (len == -1) {
        qCWarning(KWIN_XWL) << "X11 to Wayland write error on fd:" << fd();
        endTransfer();
        return;
    }

    m_receiver->partRead(len);
    if (len == property.size()) {
        // property completely transferred
        if (incr()) {
            clearSocketNotifier();
            xcb_connection_t *xcbConn = kwinApp()->x11Connection();
            xcb_delete_property(xcbConn,
                                m_window,
                                atoms->wl_selection);
        } else {
            // transfer complete
            endTransfer();
        }
    } else {
        if (!socketNotifier()) {
            createSocketNotifier(QSocketNotifier::Write);
            connect(socketNotifier(), &QSocketNotifier::activated, this, [this](int socket) {
                dataSourceWrite();
            });
        }
    }
    resetTimeout();
}

} // namespace Xwl
} // namespace KWin

#include "moc_transfer.cpp"
