/*
    SPDX-FileCopyrightText: 2020 Vlad Zahorodnii <vlad.zahorodnii@kde.org>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "layershellv1window.h"
#include "core/output.h"
#include "core/pixelgrid.h"
#include "layershellv1integration.h"
#include "screenedge.h"
#include "wayland/layershell_v1.h"
#include "wayland/output.h"
#include "wayland/screenedge_v1.h"
#include "wayland/surface.h"
#include "wayland_server.h"
#include "workspace.h"

namespace KWin
{

static WindowType scopeToType(const QString &scope)
{
    static const QHash<QString, WindowType> scopeToType{
        {QStringLiteral("desktop"), WindowType::Desktop},
        {QStringLiteral("dock"), WindowType::Dock},
        {QStringLiteral("crititical-notification"), WindowType::CriticalNotification},
        {QStringLiteral("notification"), WindowType::Notification},
        {QStringLiteral("tooltip"), WindowType::Tooltip},
        {QStringLiteral("on-screen-display"), WindowType::OnScreenDisplay},
        {QStringLiteral("dialog"), WindowType::Dialog},
        {QStringLiteral("splash"), WindowType::Splash},
        {QStringLiteral("utility"), WindowType::Utility},
    };
    return scopeToType.value(scope.toLower(), WindowType::Normal);
}

LayerShellV1Window::LayerShellV1Window(LayerSurfaceV1Interface *shellSurface,
                                       LogicalOutput *output,
                                       LayerShellV1Integration *integration)
    : WaylandWindow(shellSurface->surface())
    , m_desiredOutput(output)
    , m_integration(integration)
    , m_shellSurface(shellSurface)
    , m_windowType(scopeToType(shellSurface->scope()))
{
    setOutput(output);
    setMoveResizeOutput(output);
    setSkipSwitcher(!isDesktop());
    setSkipPager(true);
    setSkipTaskbar(true);

    connect(shellSurface, &LayerSurfaceV1Interface::aboutToBeDestroyed,
            this, &LayerShellV1Window::destroyWindow);
    connect(shellSurface->surface(), &SurfaceInterface::aboutToBeDestroyed,
            this, &LayerShellV1Window::destroyWindow);

    connect(workspace(), &Workspace::outputRemoved,
            this, &LayerShellV1Window::handleOutputRemoved);

    connect(shellSurface->surface(), &SurfaceInterface::sizeChanged,
            this, &LayerShellV1Window::handleSizeChanged);
    connect(shellSurface->surface(), &SurfaceInterface::unmapped,
            this, &LayerShellV1Window::handleUnmapped);
    connect(shellSurface->surface(), &SurfaceInterface::committed,
            this, &LayerShellV1Window::handleCommitted);

    connect(shellSurface, &LayerSurfaceV1Interface::desiredSizeChanged,
            this, &LayerShellV1Window::scheduleRearrange);
    connect(shellSurface, &LayerSurfaceV1Interface::layerChanged,
            this, &LayerShellV1Window::scheduleRearrange);
    connect(shellSurface, &LayerSurfaceV1Interface::marginsChanged,
            this, &LayerShellV1Window::scheduleRearrange);
    connect(shellSurface, &LayerSurfaceV1Interface::anchorChanged,
            this, &LayerShellV1Window::scheduleRearrange);
    connect(shellSurface, &LayerSurfaceV1Interface::exclusiveZoneChanged,
            this, &LayerShellV1Window::scheduleRearrange);
    connect(shellSurface, &LayerSurfaceV1Interface::acceptsFocusChanged,
            this, &LayerShellV1Window::handleAcceptsFocusChanged);
    connect(shellSurface, &LayerSurfaceV1Interface::configureAcknowledged,
            this, &LayerShellV1Window::handleConfigureAcknowledged);

    m_rescalingTimer.setSingleShot(true);
    m_rescalingTimer.setInterval(0);
    connect(&m_rescalingTimer, &QTimer::timeout, this, &LayerShellV1Window::handleTargetScaleChange);
}

LayerSurfaceV1Interface *LayerShellV1Window::shellSurface() const
{
    return m_shellSurface;
}

LogicalOutput *LayerShellV1Window::desiredOutput() const
{
    return m_desiredOutput;
}

void LayerShellV1Window::scheduleRearrange()
{
    workspace()->scheduleRearrange();
}

WindowType LayerShellV1Window::windowType() const
{
    return m_windowType;
}

bool LayerShellV1Window::isPlaceable() const
{
    return false;
}

bool LayerShellV1Window::isCloseable() const
{
    return true;
}

bool LayerShellV1Window::isMovable() const
{
    return false;
}

bool LayerShellV1Window::isMovableAcrossScreens() const
{
    return false;
}

bool LayerShellV1Window::isResizable() const
{
    return false;
}

bool LayerShellV1Window::wantsInput() const
{
    return acceptsFocus() && readyForPainting();
}

StrutRect LayerShellV1Window::strutRect(StrutArea area) const
{
    switch (area) {
    case StrutAreaLeft:
        if (m_shellSurface->exclusiveEdge() == Qt::LeftEdge) {
            return StrutRect(QPointF(m_moveResizeGeometry.left(),
                                     m_moveResizeGeometry.top()),
                             QPointF(m_moveResizeGeometry.left() + m_shellSurface->exclusiveZone(),
                                     m_moveResizeGeometry.bottom()),
                             StrutAreaLeft);
        }
        return StrutRect();
    case StrutAreaRight:
        if (m_shellSurface->exclusiveEdge() == Qt::RightEdge) {
            return StrutRect(QPointF(m_moveResizeGeometry.right() - m_shellSurface->exclusiveZone(),
                                     m_moveResizeGeometry.top()),
                             QPointF(m_moveResizeGeometry.right(),
                                     m_moveResizeGeometry.bottom()),
                             StrutAreaRight);
        }
        return StrutRect();
    case StrutAreaTop:
        if (m_shellSurface->exclusiveEdge() == Qt::TopEdge) {
            return StrutRect(QPointF(m_moveResizeGeometry.left(),
                                     m_moveResizeGeometry.top()),
                             QPointF(m_moveResizeGeometry.right(),
                                     m_moveResizeGeometry.top() + m_shellSurface->exclusiveZone()),
                             StrutAreaTop);
        }
        return StrutRect();
    case StrutAreaBottom:
        if (m_shellSurface->exclusiveEdge() == Qt::BottomEdge) {
            return StrutRect(QPointF(m_moveResizeGeometry.left(),
                                     m_moveResizeGeometry.bottom() - m_shellSurface->exclusiveZone()),
                             QPointF(m_moveResizeGeometry.right(),
                                     m_moveResizeGeometry.bottom()),
                             StrutAreaBottom);
        }
        return StrutRect();
    default:
        return StrutRect();
    }
}

bool LayerShellV1Window::hasStrut() const
{
    return m_shellSurface->exclusiveZone() > 0;
}

void LayerShellV1Window::destroyWindow()
{
    if (m_screenEdge) {
        m_screenEdge->disconnect(this);
    }
    m_shellSurface->disconnect(this);
    m_shellSurface->surface()->disconnect(this);

    disconnect(workspace(), &Workspace::outputRemoved,
               this, &LayerShellV1Window::handleOutputRemoved);

    markAsDeleted();
    Q_EMIT closed();

    m_rescalingTimer.stop();
    cleanTabBox();
    StackingUpdatesBlocker blocker(workspace());
    cleanGrouping();
    waylandServer()->removeWindow(this);
    scheduleRearrange();
    unref();
}

void LayerShellV1Window::closeWindow()
{
    if (!isDeleted()) {
        m_shellSurface->sendClosed();
    }
}

Layer LayerShellV1Window::belongsToLayer() const
{
    switch (m_shellSurface->layer()) {
    case LayerSurfaceV1Interface::BackgroundLayer:
        return DesktopLayer;
    case LayerSurfaceV1Interface::BottomLayer:
        return BelowLayer;
    case LayerSurfaceV1Interface::TopLayer:
        return AboveLayer;
    case LayerSurfaceV1Interface::OverlayLayer:
        return OverlayLayer;
    default:
        Q_UNREACHABLE();
    }
}

bool LayerShellV1Window::acceptsFocus() const
{
    return !isDeleted() && m_shellSurface->acceptsFocus();
}

void LayerShellV1Window::moveResizeInternal(const RectF &rect, MoveResizeMode mode)
{
    const QSizeF requestedClientSize = surface()->snappedSize(nextFrameSizeToClientSize(rect.size()));

    if (!m_configureEvents.isEmpty()) {
        const LayerShellV1ConfigureEvent &lastLayerShellV1ConfigureEvent = m_configureEvents.constLast();
        if (lastLayerShellV1ConfigureEvent.size != requestedClientSize) {
            const quint32 serial = m_shellSurface->sendConfigure(requestedClientSize);
            m_configureEvents.append({serial, requestedClientSize});
        }
    } else if (requestedClientSize != clientSize()) {
        const quint32 serial = m_shellSurface->sendConfigure(requestedClientSize);
        m_configureEvents.append({serial, requestedClientSize});
    } else {
        updateGeometry(rect);
        return;
    }

    // The surface position is updated synchronously.
    RectF updateRect = m_frameGeometry;
    updateRect.moveTopLeft(rect.topLeft());
    updateGeometry(updateRect);
}

void LayerShellV1Window::doSetNextTargetScale()
{
    if (isDeleted()) {
        return;
    }
    surface()->setPreferredBufferScale(nextTargetScale());
    setTargetScale(nextTargetScale());
    m_rescalingTimer.start();
}

void LayerShellV1Window::doSetPreferredBufferTransform()
{
    if (isDeleted()) {
        return;
    }
    surface()->setPreferredBufferTransform(preferredBufferTransform());
}

void LayerShellV1Window::doSetPreferredColorDescription()
{
    if (isDeleted()) {
        return;
    }
    surface()->setPreferredColorDescription(preferredColorDescription());
}

void LayerShellV1Window::handleConfigureAcknowledged(quint32 serial)
{
    while (!m_configureEvents.isEmpty()) {
        const LayerShellV1ConfigureEvent head = m_configureEvents.takeFirst();
        if (head.serial == serial) {
            break;
        }
    }
}

void LayerShellV1Window::handleSizeChanged()
{
    updateGeometry(RectF(pos(), clientSizeToFrameSize(surface()->size())));
}

void LayerShellV1Window::handleUnmapped()
{
    m_integration->recreateWindow(shellSurface());
}

void LayerShellV1Window::handleCommitted()
{
    if (surface()->buffer()) {
        markAsMapped();
    }
}

void LayerShellV1Window::handleAcceptsFocusChanged()
{
    switch (m_shellSurface->layer()) {
    case LayerSurfaceV1Interface::TopLayer:
    case LayerSurfaceV1Interface::OverlayLayer:
        if (wantsInput()) {
            workspace()->activateWindow(this);
        }
        break;
    case LayerSurfaceV1Interface::BackgroundLayer:
    case LayerSurfaceV1Interface::BottomLayer:
        break;
    }
}

void LayerShellV1Window::handleOutputRemoved(LogicalOutput *output)
{
    if (output == m_desiredOutput) {
        closeWindow();
        destroyWindow();
    }
}

void LayerShellV1Window::setVirtualKeyboardGeometry(const RectF &geo)
{
    if (m_virtualKeyboardGeometry == geo) {
        return;
    }

    m_virtualKeyboardGeometry = geo;
    scheduleRearrange();
}

void LayerShellV1Window::showOnScreenEdge()
{
    // ShowOnScreenEdge can be called by an Edge, and setHidden could destroy the Edge
    // Use the singleshot to avoid use-after-free
    QTimer::singleShot(0, this, &LayerShellV1Window::deactivateScreenEdge);
}

void LayerShellV1Window::installAutoHideScreenEdgeV1(AutoHideScreenEdgeV1Interface *edge)
{
    m_screenEdge = edge;

    connect(edge, &AutoHideScreenEdgeV1Interface::destroyed,
            this, &LayerShellV1Window::deactivateScreenEdge);
    connect(edge, &AutoHideScreenEdgeV1Interface::activateRequested,
            this, &LayerShellV1Window::activateScreenEdge);
    connect(edge, &AutoHideScreenEdgeV1Interface::deactivateRequested,
            this, &LayerShellV1Window::deactivateScreenEdge);

    connect(this, &LayerShellV1Window::frameGeometryChanged, edge, [this]() {
        if (m_screenEdgeActive) {
            reserveScreenEdge();
        }
    });
}

void LayerShellV1Window::reserveScreenEdge()
{
    if (workspace()->screenEdges()->reserve(this, m_screenEdge->border())) {
        setHidden(true);
    } else {
        setHidden(false);
    }
}

void LayerShellV1Window::unreserveScreenEdge()
{
    setHidden(false);
    workspace()->screenEdges()->reserve(this, ElectricNone);
}

void LayerShellV1Window::activateScreenEdge()
{
    m_screenEdgeActive = true;
    reserveScreenEdge();
}

void LayerShellV1Window::deactivateScreenEdge()
{
    m_screenEdgeActive = false;
    unreserveScreenEdge();
}

void LayerShellV1Window::handleTargetScaleChange()
{
    moveResize(snapToPixels(m_moveResizeGeometry, m_targetScale));
}

} // namespace KWin

#include "moc_layershellv1window.cpp"
