/*
    SPDX-FileCopyrightText: 2021-2022 Xaver Hugl <xaver.hugl@gmail.com>

    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "drmlease_v1.h"
#include "display.h"
#include "drmlease_v1_p.h"
#include "utils/common.h"
#include "utils/resource.h"

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

namespace KWin
{

static const quint32 s_version = 1;

DrmLeaseManagerV1::DrmLeaseManagerV1(DrmBackend *backend, Display *display, QObject *parent)
    : QObject(parent)
    , m_backend(backend)
    , m_display(display)
{
    const auto &gpus = m_backend->gpus();
    for (const auto &gpu : gpus) {
        addGpu(gpu.get());
    }
    connect(m_backend, &DrmBackend::gpuAdded, this, &DrmLeaseManagerV1::addGpu);
    connect(m_backend, &DrmBackend::gpuRemoved, this, &DrmLeaseManagerV1::removeGpu);
    connect(m_backend, &DrmBackend::outputsQueried, this, &DrmLeaseManagerV1::handleOutputsQueried);
}

DrmLeaseManagerV1::~DrmLeaseManagerV1()
{
    for (const auto device : m_leaseDevices) {
        device->remove();
    }
}

DrmLeaseDeviceV1Interface *DrmLeaseManagerV1::leaseDeviceForGpu(DrmGpu *gpu) const
{
    return m_leaseDevices.value(gpu);
}

void DrmLeaseManagerV1::addGpu(DrmGpu *gpu)
{
    m_leaseDevices[gpu] = new DrmLeaseDeviceV1Interface(m_display, gpu);
}

void DrmLeaseManagerV1::removeGpu(DrmGpu *gpu)
{
    if (auto device = m_leaseDevices.take(gpu)) {
        device->remove();
    }
}

void DrmLeaseManagerV1::handleOutputsQueried()
{
    for (const auto device : m_leaseDevices) {
        if (device->hasDrmMaster()) {
            device->offerAvailableConnectors();
            device->done();
        }
    }
}

DrmLeaseDeviceV1Interface::DrmLeaseDeviceV1Interface(Display *display, DrmGpu *gpu)
    : QtWaylandServer::wp_drm_lease_device_v1(*display, s_version)
    , m_gpu(gpu)
{
    const auto outputs = gpu->drmOutputs();
    for (const auto output : outputs) {
        addOutput(output);
    }
    connect(gpu, &DrmGpu::outputAdded, this, &DrmLeaseDeviceV1Interface::addOutput);
    connect(gpu, &DrmGpu::outputRemoved, this, &DrmLeaseDeviceV1Interface::removeOutput);
    connect(gpu, &DrmGpu::activeChanged, this, &DrmLeaseDeviceV1Interface::onGpuActiveChanged);
}

DrmLeaseDeviceV1Interface::~DrmLeaseDeviceV1Interface()
{
    while (!m_connectors.empty()) {
        removeOutput(m_connectors.begin()->first);
    }
}

void DrmLeaseDeviceV1Interface::addOutput(DrmAbstractOutput *output)
{
    DrmOutput *drmOutput = qobject_cast<DrmOutput *>(output);
    if (!drmOutput || !drmOutput->isNonDesktop()) {
        return;
    }
    m_connectors[drmOutput] = std::make_unique<DrmLeaseConnectorV1Interface>(this, drmOutput);

    if (m_hasDrmMaster) {
        offerConnector(m_connectors[drmOutput].get());
    }
}

void DrmLeaseDeviceV1Interface::removeOutput(DrmAbstractOutput *output)
{
    const auto it = m_connectors.find(output);
    if (it != m_connectors.end()) {
        DrmLeaseConnectorV1Interface *connector = it->second.get();
        connector->withdraw();
        for (DrmLeaseV1Interface *lease : std::as_const(m_leases)) {
            lease->removeConnector(connector);
        }
        for (DrmLeaseRequestV1Interface *leaseRequest : std::as_const(m_leaseRequests)) {
            if (leaseRequest->connectors().contains(connector)) {
                leaseRequest->invalidate();
            }
        }
        m_connectors.erase(it);
    }
}

void DrmLeaseDeviceV1Interface::onGpuActiveChanged(bool active)
{
    if (!active) {
        // Withdraw all connectors immediately. Re-offering happens later
        // in handleOutputsQueried() after updateOutputs() refreshes
        // connector state on the way back from VT switch.
        bool changed = false;
        for (const auto &[output, connector] : m_connectors) {
            if (!connector->withdrawn()) {
                connector->withdraw();
                changed = true;
            }
        }
        if (changed) {
            done();
        }
    }
    setDrmMaster(active);
}

void DrmLeaseDeviceV1Interface::setDrmMaster(bool hasDrmMaster)
{
    if (hasDrmMaster == m_hasDrmMaster) {
        return;
    }
    m_hasDrmMaster = hasDrmMaster;
    if (hasDrmMaster) {
        while (!m_pendingFds.isEmpty()) {
            FileDescriptor fd = m_gpu->createNonMasterFd();
            send_drm_fd(m_pendingFds.dequeue(), fd.get());
        }
    } else {
        for (DrmLeaseV1Interface *lease : std::as_const(m_leases)) {
            lease->revoke();
        }
    }
}

void DrmLeaseDeviceV1Interface::done()
{
    const auto resources = resourceMap();
    for (const auto resource : resources) {
        send_done(resource->handle);
    }
}

void DrmLeaseDeviceV1Interface::remove()
{
    for (DrmLeaseV1Interface *lease : std::as_const(m_leases)) {
        lease->deny();
    }
    for (const auto &[output, connector] : m_connectors) {
        connector->withdraw();
    }
    for (DrmLeaseRequestV1Interface *request : std::as_const(m_leaseRequests)) {
        request->invalidate();
    }
    done();
    globalRemove();
}

void DrmLeaseDeviceV1Interface::addLeaseRequest(DrmLeaseRequestV1Interface *leaseRequest)
{
    m_leaseRequests.push_back(leaseRequest);
}

void DrmLeaseDeviceV1Interface::removeLeaseRequest(DrmLeaseRequestV1Interface *leaseRequest)
{
    m_leaseRequests.removeOne(leaseRequest);
}

void DrmLeaseDeviceV1Interface::addLease(DrmLeaseV1Interface *lease)
{
    m_leases.push_back(lease);
}

void DrmLeaseDeviceV1Interface::removeLease(DrmLeaseV1Interface *lease)
{
    m_leases.removeOne(lease);
}

bool DrmLeaseDeviceV1Interface::hasDrmMaster() const
{
    return m_hasDrmMaster;
}

DrmGpu *DrmLeaseDeviceV1Interface::gpu() const
{
    return m_gpu;
}

void DrmLeaseDeviceV1Interface::offerConnector(DrmLeaseConnectorV1Interface *connector)
{
    // Mark the connector as available even if there are no currently bound
    // wp_drm_lease_device_v1 resources. Otherwise no send() happens, withdrawn
    // stays true, and future binds won't get this connector advertised.
    connector->setAvailable();
    for (const Resource *resource : resourceMap()) {
        auto connectorResource = connector->add(resource->client(), 0, resource->version());
        send_connector(resource->handle, connectorResource->handle);
        connector->send(connectorResource->handle);
    }
}

DrmLeaseConnectorV1Interface *DrmLeaseDeviceV1Interface::connectorForOutput(DrmAbstractOutput *output) const
{
    const auto it = m_connectors.find(output);
    return it != m_connectors.end() ? it->second.get() : nullptr;
}

bool DrmLeaseDeviceV1Interface::isLeased(DrmLeaseConnectorV1Interface *connector) const
{
    return std::ranges::any_of(m_leases, [connector](const DrmLeaseV1Interface *lease) {
        return lease->isGranted() && lease->hasConnector(connector);
    });
}

void DrmLeaseDeviceV1Interface::offerAvailableConnectors()
{
    for (const auto &[output, connector] : m_connectors) {
        if (connector->withdrawn()
            && connector->output()->connector()->isConnected()
            && !isLeased(connector.get())) {
            offerConnector(connector.get());
        }
    }
}

void DrmLeaseDeviceV1Interface::wp_drm_lease_device_v1_create_lease_request(Resource *resource, uint32_t id)
{
    wl_resource *requestResource = wl_resource_create(resource->client(), &wp_drm_lease_request_v1_interface,
                                                      resource->version(), id);
    if (!requestResource) {
        wl_resource_post_no_memory(resource->handle);
        return;
    }
    m_leaseRequests << new DrmLeaseRequestV1Interface(this, requestResource);
}

void DrmLeaseDeviceV1Interface::wp_drm_lease_device_v1_release(Resource *resource)
{
    send_released(resource->handle);
    wl_resource_destroy(resource->handle);
}

void DrmLeaseDeviceV1Interface::wp_drm_lease_device_v1_bind_resource(Resource *resource)
{
    if (isGlobalRemoved()) {
        return;
    }
    if (!m_hasDrmMaster) {
        m_pendingFds << resource->handle;
        return;
    }
    FileDescriptor fd = m_gpu->createNonMasterFd();
    send_drm_fd(resource->handle, fd.get());
    for (const auto &[output, connector] : m_connectors) {
        if (!connector->withdrawn()) {
            auto connectorResource = connector->add(resource->client(), 0, s_version);
            send_connector(resource->handle, connectorResource->handle);
            connector->send(connectorResource->handle);
        }
    }
    send_done(resource->handle);
}

void DrmLeaseDeviceV1Interface::wp_drm_lease_device_v1_destroy_resource(Resource *resource)
{
    m_pendingFds.removeOne(resource->handle);
}

void DrmLeaseDeviceV1Interface::wp_drm_lease_device_v1_destroy_global()
{
    delete this;
}

DrmLeaseConnectorV1Interface::DrmLeaseConnectorV1Interface(DrmLeaseDeviceV1Interface *leaseDevice, DrmOutput *output)
    : wp_drm_lease_connector_v1()
    , m_device(leaseDevice)
    , m_output(output)
{
}

uint32_t DrmLeaseConnectorV1Interface::id() const
{
    return m_output->connector()->id();
}

DrmLeaseDeviceV1Interface *DrmLeaseConnectorV1Interface::device() const
{
    return m_device;
}

DrmOutput *DrmLeaseConnectorV1Interface::output() const
{
    return m_output;
}

bool DrmLeaseConnectorV1Interface::withdrawn() const
{
    return m_withdrawn;
}

void DrmLeaseConnectorV1Interface::send(wl_resource *resource)
{
    m_withdrawn = false;
    send_connector_id(resource, m_output->connector()->id());
    send_name(resource, m_output->name());
    send_description(resource, m_output->description());
    send_done(resource);
}

void DrmLeaseConnectorV1Interface::withdraw()
{
    if (!m_withdrawn) {
        m_withdrawn = true;
        for (const Resource *resource : resourceMap()) {
            send_withdrawn(resource->handle);
        }
    }
}

void DrmLeaseConnectorV1Interface::setAvailable()
{
    m_withdrawn = false;
}

void DrmLeaseConnectorV1Interface::wp_drm_lease_connector_v1_destroy(Resource *resource)
{
    wl_resource_destroy(resource->handle);
}

DrmLeaseRequestV1Interface::DrmLeaseRequestV1Interface(DrmLeaseDeviceV1Interface *device, wl_resource *resource)
    : wp_drm_lease_request_v1(resource)
    , m_device(device)
{
}

DrmLeaseRequestV1Interface::~DrmLeaseRequestV1Interface()
{
    m_device->removeLeaseRequest(this);
}

QList<DrmLeaseConnectorV1Interface *> DrmLeaseRequestV1Interface::connectors() const
{
    return m_connectors;
}

void DrmLeaseRequestV1Interface::invalidate()
{
    m_connectors.clear();
    m_invalid = true;
}

void DrmLeaseRequestV1Interface::wp_drm_lease_request_v1_request_connector(Resource *resource, struct ::wl_resource *connector_handle)
{
    if (auto connector = resource_cast<DrmLeaseConnectorV1Interface *>(connector_handle)) {
        if (connector->device() != m_device) {
            wl_resource_post_error(resource->handle, WP_DRM_LEASE_REQUEST_V1_ERROR_WRONG_DEVICE, "Requested connector from invalid lease device");
        } else if (connector->withdrawn()) {
            qCWarning(KWIN_CORE) << "DrmLease: withdrawn connector requested";
            invalidate();
        } else if (m_connectors.contains(connector)) {
            wl_resource_post_error(resource->handle, WP_DRM_LEASE_REQUEST_V1_ERROR_DUPLICATE_CONNECTOR, "Requested connector twice");
        } else if (!m_invalid) {
            m_connectors << connector;
        }
    } else {
        qCWarning(KWIN_CORE, "DrmLease: Invalid connector requested");
        invalidate();
    }
}

void DrmLeaseRequestV1Interface::wp_drm_lease_request_v1_submit(Resource *resource, uint32_t id)
{
    wl_resource *leaseResource = wl_resource_create(resource->client(), &wp_drm_lease_v1_interface, s_version, id);
    if (!leaseResource) {
        wl_resource_post_no_memory(resource->handle);
        return;
    }
    DrmLeaseV1Interface *lease = new DrmLeaseV1Interface(m_device, m_connectors, leaseResource);
    m_device->addLease(lease);
    if (!m_device->hasDrmMaster()) {
        qCWarning(KWIN_CORE) << "DrmLease: rejecting lease request without drm master";
        lease->deny();
    } else if (m_invalid) {
        qCWarning(KWIN_CORE) << "DrmLease: rejecting lease request with a withdrawn connector";
        lease->deny();
    } else if (m_connectors.isEmpty()) {
        wl_resource_post_error(resource->handle, WP_DRM_LEASE_REQUEST_V1_ERROR_EMPTY_LEASE, "Requested lease without connectors");
    } else {
        QList<DrmOutput *> outputs;
        for (const DrmLeaseConnectorV1Interface *connector : m_connectors) {
            outputs.push_back(connector->output());
        }
        auto drmLease = m_device->gpu()->leaseOutputs(outputs);
        if (drmLease) {
            lease->grant(std::move(drmLease));
        } else {
            lease->deny();
        }
    }
    wl_resource_destroy(resource->handle);
}

void DrmLeaseRequestV1Interface::wp_drm_lease_request_v1_destroy_resource(Resource *resource)
{
    delete this;
}

DrmLeaseV1Interface::DrmLeaseV1Interface(DrmLeaseDeviceV1Interface *device, const QList<DrmLeaseConnectorV1Interface *> &connectors, wl_resource *resource)
    : wp_drm_lease_v1(resource)
    , m_device(device)
    , m_connectors(connectors)
{
}

DrmLeaseV1Interface::~DrmLeaseV1Interface()
{
    if (m_lease) {
        revoke();
    } else {
        deny();
    }
    m_device->removeLease(this);
}

void DrmLeaseV1Interface::grant(std::unique_ptr<DrmLease> &&lease)
{
    FileDescriptor tmp = std::move(lease->fd());
    send_lease_fd(tmp.get());
    m_lease = std::move(lease);
    connect(m_lease.get(), &DrmLease::revokeRequested, this, &DrmLeaseV1Interface::revoke);
    for (DrmLeaseConnectorV1Interface *connector : std::as_const(m_connectors)) {
        connector->withdraw();
    }
    m_device->done();
}

void DrmLeaseV1Interface::deny()
{
    Q_ASSERT(!m_lease);
    if (!m_finished) {
        m_finished = true;
        send_finished();
    }
}

void DrmLeaseV1Interface::revoke()
{
    Q_ASSERT(m_lease);
    if (!m_finished) {
        m_finished = true;
        send_finished();
    }
    m_lease.reset();
    // Connectors stay withdrawn; offerAvailableConnectors() will re-offer
    // them at the next outputsQueried if they are still connected.
}

bool DrmLeaseV1Interface::isGranted() const
{
    return m_lease != nullptr;
}

void DrmLeaseV1Interface::wp_drm_lease_v1_destroy(Resource *resource)
{
    wl_resource_destroy(resource->handle);
}

void DrmLeaseV1Interface::wp_drm_lease_v1_destroy_resource(Resource *resource)
{
    delete this;
}

void DrmLeaseV1Interface::removeConnector(DrmLeaseConnectorV1Interface *connector)
{
    if (m_connectors.removeOne(connector) && isGranted()) {
        revoke();
    }
}

bool DrmLeaseV1Interface::hasConnector(DrmLeaseConnectorV1Interface *connector) const
{
    return m_connectors.contains(connector);
}

}

#include "moc_drmlease_v1.cpp"
#include "moc_drmlease_v1_p.cpp"
