// Copyright (C) 2024-2026 UnionTech Software Technology Co., Ltd.
// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include <wscopedvalue.h>
#include "output.h"

#include "cmdline.h"
#include "common/treelandlogging.h"
#include "core/rootsurfacecontainer.h"
#include "outputconfig.hpp"
#include "seat/helper.h"
#include "surface/surfacewrapper.h"
#include "treelandconfig.hpp"
#include "treelanduserconfig.hpp"
#include "workspace/workspace.h"
#include "wallpapermanager.h"

#include <wcursor.h>
#include <winputpopupsurface.h>
#include <winputpopupsurfaceitem.h>
#include <wlayersurface.h>
#include <woutputhelper.h>
#include <woutputitem.h>
#include <woutputlayout.h>
#include <woutputrenderwindow.h>
#include <wquicktextureproxy.h>
#include <wsurfaceitem.h>
#include <wxdgpopupsurface.h>
#include <wxdgpopupsurfaceitem.h>

#include <QQmlEngine>

#include <algorithm>
#include <optional>

#define SAME_APP_OFFSET_FACTOR 1.0
#define DIFF_APP_OFFSET_FACTOR 2.0
#define POPUP_EDGE_MARGIN 10

QString Output::getOutputId(wlr_output *output)
{
    const QString model = QString::fromUtf8(output->model);
    const QString serial = QString::fromUtf8(output->serial);
    if (!model.isEmpty() && !serial.isEmpty()) {
        return model + serial;
    }

    return QString::fromUtf8(output->name);
}

QString Output::getOutputId()
{
    return getOutputId(output()->handle());
}

Output *Output::create(WOutput *output, QQmlEngine *engine, QObject *parent)
{
    auto isSoftwareCursor = [](WOutput *output) -> bool {
        return wlr_output_is_x11(output->handle()) || Helper::instance()->globalConfig()->forceSoftwareCursor();
    };
    QQmlComponent delegate(engine, "Treeland", "PrimaryOutput");
    QObject *obj = delegate.beginCreate(engine->rootContext());
    delegate.setInitialProperties(obj, { { "forceSoftwareCursor", isSoftwareCursor(output) } });
    delegate.completeCreate();
    WOutputItem *outputItem = qobject_cast<WOutputItem *>(obj);
    Q_ASSERT(outputItem);
    QQmlEngine::setObjectOwnership(outputItem, QQmlEngine::CppOwnership);

    auto contentItem = Helper::instance()->window()->contentItem();
    outputItem->setParentItem(contentItem);
    outputItem->setOutput(output);

    connect(Helper::instance()->globalConfig(),
            &TreelandConfig::forceSoftwareCursorChanged,
            obj,
            [obj, output, isSoftwareCursor]() {
                auto forceSoftwareCursor = isSoftwareCursor(output);
                qCInfo(lcTlOutput) << "forceSoftwareCursor changed to" << forceSoftwareCursor;
                obj->setProperty("forceSoftwareCursor", forceSoftwareCursor);
            });

    auto o = new Output(outputItem, parent);
    o->m_type = Type::Primary;
    obj->setParent(o);

    o->minimizedSurfaces->setFilter([](SurfaceWrapper *s) {
        return s->isMinimized();
    });

    // Triggering layout updates using a queue helps reduce window jitter.
    // When the screen scaling factor changes, the scale of WOutput is updated first,
    // causing the size of WOutputItem to change. However, at this point, the
    // effectiveDevicePixelRatio of QWindow has not yet been updated.
    // This results in the size of maximized windows being updated prematurely.
    // Xwayland windows use the effectiveDevicePixelRatio to set the surfaceSizeRatio.
    // By updating within a queue, it ensures that the surfaceSizeRatio used when
    // resizing maximized Xwayland windows is accurate, avoiding multiple rapid
    // size changes to Xwayland windows in a short period.
    o->connect(outputItem,
               &WOutputItem::geometryChanged,
               o,
               &Output::arrangeAllSurfaces,
               Qt::QueuedConnection);


    // reset output color to config value
    o->applyOutputColorConfig();

    if (CmdLine::ref().enableDebugView()) {
        o->m_debugMenuBar = Helper::instance()->qmlEngine()->createMenuBar(outputItem, contentItem);
        o->m_debugMenuBar->setZ(RootSurfaceContainer::MenuBarZOrder);
#ifndef QT_DEBUG
        o->m_debugMenuBar->setVisible(false);
#else
        o->setExclusiveZone(Qt::TopEdge, o->m_debugMenuBar, o->m_debugMenuBar->height());
#endif

        connect(o->m_debugMenuBar, &QQuickItem::visibleChanged, o, [o]() {
            if (o->m_debugMenuBar->isVisible()) {
                o->setExclusiveZone(Qt::TopEdge, o->m_debugMenuBar, o->m_debugMenuBar->height());
            } else {
                o->removeExclusiveZone(o->m_debugMenuBar);
            }
        });
    }

    return o;
}

Output *Output::createCopy(WOutput *output, Output *proxy, QQmlEngine *engine, QObject *parent)
{
    QQmlComponent delegate(engine, "Treeland", "CopyOutput");
    QObject *obj = delegate.createWithInitialProperties(
        {
            { "targetOutputItem", QVariant::fromValue(proxy->outputItem()) },
        },
        engine->rootContext());
    WOutputItem *outputItem = qobject_cast<WOutputItem *>(obj);
    Q_ASSERT(outputItem);
    QQmlEngine::setObjectOwnership(outputItem, QQmlEngine::CppOwnership);

    auto contentItem = Helper::instance()->window()->contentItem();
    outputItem->setParentItem(contentItem);
    outputItem->setOutput(output);

    auto o = new Output(outputItem, parent);
    o->m_type = Type::Proxy;
    o->m_proxy = proxy;
    obj->setParent(o);

    o->updateOutputHardwareLayers();
    connect(proxy->screenViewport(),
            &WOutputViewport::hardwareLayersChanged,
            o,
            &Output::updateOutputHardwareLayers);

    return o;
}

Output::Output(WOutputItem *output, QObject *parent)
    : SurfaceListModel(parent)
    , m_item(output)
    , minimizedSurfaces(new SurfaceFilterModel(this))
    , m_backlight(Backlight::createForOutput(output->output()))
{
    m_outputViewport = output->property("screenViewport").value<WOutputViewport *>();

    QString outputName = Output::getOutputId(output->output()->handle());
    m_config = OutputConfig::createByName("org.deepin.dde.treeland.output",
                                    "org.deepin.dde.treeland",
                                    "/" + outputName, this);
}

Output::~Output()
{
    // Copy outputs reuse the primary output's hardware layers. Detach them
    // explicitly before destroying the viewport so that the output gets a new
    // frame and stale hardware-plane contents (notably the cursor) are cleared.
    if (m_type == Type::Proxy && m_outputViewport) {
        for (auto *layer : std::as_const(m_hardwareLayersOfPrimaryOutput)) {
            Helper::instance()->window()->detach(layer, m_outputViewport);
        }
        m_hardwareLayersOfPrimaryOutput.clear();
    }

    if (m_taskBar) {
        delete m_taskBar;
        m_taskBar = nullptr;
    }

    if (m_debugMenuBar) {
        delete m_debugMenuBar;
        m_debugMenuBar = nullptr;
    }

    if (m_item) {
        delete m_item;
        m_item = nullptr;
    }
}

bool Output::isPrimary() const
{
    return m_type == Type::Primary;
}

void Output::updatePositionFromLayout()
{
    WOutputLayout *layout = output()->layout();
    if (!layout) {
        return;
    }

    auto *layoutOutput = wlr_output_layout_get(layout->handle(), output()->handle());
    if (!layoutOutput) {
        return;
    }

    QPointF pos(layoutOutput->x, layoutOutput->y);
    m_item->setPosition(pos);
}

QQuickItem *Output::debugMenuBar() const
{
    return m_debugMenuBar;
}

std::pair<WOutputViewport *, QQuickItem *> Output::getOutputItemProperty()
{
    WOutputViewport *viewportCopy =
        outputItem()->findChild<WOutputViewport *>({}, Qt::FindDirectChildrenOnly);
    Q_ASSERT(viewportCopy);
    auto textureProxy = outputItem()->findChild<WQuickTextureProxy *>();
    Q_ASSERT(textureProxy);
    return std::make_pair(viewportCopy, textureProxy);
}

void Output::moveSurfaceWithTitlebarClamp(SurfaceWrapper *surface, const QPointF &pos)
{
    const auto validGeo = this->validGeometry();
    QRectF candidateGeo(pos, surface->normalGeometry().size());

    QRectF titlebarGeometry = surface->titlebarGeometry();
    if (!titlebarGeometry.isValid()) {
        qreal titleHeight = Helper::instance()->config()->windowTitlebarHeight();
        titlebarGeometry = QRectF(0, 0, candidateGeo.width(), titleHeight);
    }
    titlebarGeometry.translate(candidateGeo.topLeft());

    if ((titlebarGeometry & validGeo).isEmpty()) {
        if (titlebarGeometry.top() < validGeo.top()) {
            candidateGeo.moveTop(candidateGeo.top() + validGeo.top() - titlebarGeometry.top());
        } else if (titlebarGeometry.bottom() > validGeo.bottom()) {
            candidateGeo.moveBottom(candidateGeo.bottom()
                                    - (titlebarGeometry.bottom() - validGeo.bottom()));
        }
        // moveTop/moveBottom do not change left/right, so titlebarGeometry's
        // horizontal extent is still valid for the check below.
        if (titlebarGeometry.left() < validGeo.left()) {
            candidateGeo.moveLeft(candidateGeo.left() + validGeo.left() - titlebarGeometry.left());
        } else if (titlebarGeometry.right() > validGeo.right()) {
            candidateGeo.moveRight(candidateGeo.right()
                                   - (titlebarGeometry.right() - validGeo.right()));
        }
    }

    if (candidateGeo.topLeft() == surface->normalGeometry().topLeft()) {
        // Final clamped position is unchanged; skip no-op move.
        return;
    }

    surface->moveNormalGeometryInOutput(candidateGeo.topLeft());
}

void Output::placeUnderCursor(SurfaceWrapper *surface, quint32 yOffset)
{
    QSizeF cursorSize;
    QRectF normalGeo = surface->normalGeometry();
    WCursor *wCursor = Helper::instance()->seat()->cursor();
    if (!surface->ownsOutput()->outputItem()->cursorItems().isEmpty())
        cursorSize = surface->ownsOutput()->outputItem()->cursorItems()[0]->size();

    normalGeo.moveLeft(wCursor->position().x() + (cursorSize.width() - surface->width()) / 2);
    normalGeo.moveTop(wCursor->position().y() + cursorSize.height() + yOffset);
    moveSurfaceWithTitlebarClamp(surface, normalGeo.topLeft());
}

void Output::placeClientRequstPos(SurfaceWrapper *surface, QPoint clientRequstPos)
{
    QRectF normalGeo = surface->normalGeometry();
    normalGeo.moveLeft(clientRequstPos.x());
    normalGeo.moveTop(clientRequstPos.y());
    moveSurfaceWithTitlebarClamp(surface, normalGeo.topLeft());
}

void Output::placeCentered(SurfaceWrapper *surface)
{
    const auto validGeo = validGeometry();
    QRectF normalGeo = surface->normalGeometry();
    normalGeo.moveCenter(validGeo.center());
    normalGeo.moveTop(qMax(normalGeo.top(), validGeo.top()));
    normalGeo.moveLeft(qMax(normalGeo.left(), validGeo.left()));
    moveSurfaceWithTitlebarClamp(surface, normalGeo.topLeft());
}

void Output::placeSmartCascaded(SurfaceWrapper *surface)
{
    auto wpModel = Helper::instance()->workspace()->modelFromId(surface->workspaceId());
    Q_ASSERT(wpModel);
    /*
     The execution of Output::placeSmartCascaded and the surface obtaining focus occur almost
     simultaneously after the window is mapped for the first time; the order is not currently
     guaranteed.
    */
    auto latestActiveSurface = wpModel->latestActiveSurface();
    if (latestActiveSurface == surface)
        latestActiveSurface = wpModel->activePenultimateWindow();

    if (!latestActiveSurface) {
        placeCentered(surface);
        return;
    }

    const auto validGeo = validGeometry();
    QRectF normalGeo = surface->normalGeometry();
    QRectF latestActiveSurfaceGeo = latestActiveSurface->normalGeometry();

    qreal factor =
        (latestActiveSurface->appId() != surface->appId())
        ? DIFF_APP_OFFSET_FACTOR
        : SAME_APP_OFFSET_FACTOR;
    const QRectF titleBarGeometry = latestActiveSurface->titlebarGeometry();
    qreal offset = (titleBarGeometry.isNull() ? Helper::instance()->config()->windowTitlebarHeight()
                                              : titleBarGeometry.height())
        * factor;

    QPointF newPos;
    if (m_nextPlaceDirection == PlaceDirection::BottomRight) {
        newPos = calculateBottomRightPosition(latestActiveSurfaceGeo,
                                              normalGeo,
                                              validGeo,
                                              QSizeF(offset, offset));
    } else {
        newPos = calculateTopLeftPosition(latestActiveSurfaceGeo,
                                          normalGeo,
                                          validGeo,
                                          QSizeF(offset, offset));
    }

    newPos = constrainToValidArea(newPos, normalGeo.size(), validGeo);
    moveSurfaceWithTitlebarClamp(surface, newPos);
}

QPointF Output::calculateBottomRightPosition(const QRectF &activeGeo,
                                             const QRectF &normalGeo,
                                             const QRectF &validGeo,
                                             const QSizeF &offset)
{
    QPointF bottomRight(activeGeo.left() + offset.width(), activeGeo.top() + offset.height());

    if (bottomRight.x() + normalGeo.width() <= validGeo.right()
        && bottomRight.y() + normalGeo.height() <= validGeo.bottom()) {
        return bottomRight;
    }

    m_nextPlaceDirection = PlaceDirection::TopLeft;
    return QPointF(qMax(validGeo.left(), activeGeo.right() - normalGeo.width() - offset.width()),
                   qMax(validGeo.top(), activeGeo.bottom() - normalGeo.height() - offset.height()));
}

QPointF Output::calculateTopLeftPosition(const QRectF &activeGeo,
                                         const QRectF &normalGeo,
                                         const QRectF &validGeo,
                                         const QSizeF &offset)
{
    QPointF topLeft(activeGeo.right() - normalGeo.width() - offset.width(),
                    activeGeo.bottom() - normalGeo.height() - offset.height());

    if (topLeft.x() >= validGeo.left() && topLeft.y() >= validGeo.top()) {
        return topLeft;
    }

    m_nextPlaceDirection = PlaceDirection::BottomRight;
    return QPointF(qMin(validGeo.right() - normalGeo.width(), activeGeo.left() + offset.width()),
                   qMin(validGeo.bottom() - normalGeo.height(), activeGeo.top() + offset.height()));
}

QPointF Output::constrainToValidArea(const QPointF &pos,
                                     const QSizeF &windowSize,
                                     const QRectF &validGeo)
{
    QPointF newPos = pos;

    newPos.setX(qMax(newPos.x(), validGeo.left()));
    newPos.setX(qMin(newPos.x(), validGeo.right() - windowSize.width()));
    newPos.setY(qMax(newPos.y(), validGeo.top()));
    newPos.setY(qMin(newPos.y(), validGeo.bottom() - windowSize.height()));

    return newPos;
}

double Output::calcPreferredScale(double widthPx, double heightPx, double widthMm, double heightMm)
{
    if (widthPx <= 0 || heightPx <= 0 || widthMm <= 0 || heightMm <= 0) {
        return 1.0;
    }

    const double lenPx = std::hypot(widthPx, heightPx);
    const double lenMm = std::hypot(widthMm, heightMm);

    // A standard scale=1.0 display
    const double lenPxStd = std::hypot(1920.0, 1080.0);
    const double lenMmStd = std::hypot(477.0, 268.0);

    // This magic number is from startdde, I don't know why.
    const double a = 0.00158;
    // The scaling factor should be adjusted according to the size
    // of the standard screen. Even if the PPI is the same, the larger
    // the screen, the larger the scaling factor should be. Otherwise,
    // the scaling factor should be smaller, as this better aligns with
    // the visual needs of the human eye.
    const double fix = (lenMm - lenMmStd) * (lenPx / lenPxStd) * a;

    const double scaleFactor = (lenPx / lenMm) / (lenPxStd / lenMmStd) + fix;

    // Ensure step is within 0.25
    return std::round(scaleFactor * 4) / 4.0;
}

qreal Output::preferredScaleFactor(const QSize &pixelSize) const
{
    auto o = output()->handle();
    return calcPreferredScale(pixelSize.width(), pixelSize.height(), o->phys_width, o->phys_height);
}

void Output::enable()
{
    // Enable on default
    auto wlrOutput = output()->handle();
    WOutputStateGuard newState;
    // Don't care for WOutput::isEnabled, must do WOutput::commit here,
    // In order to ensure trigger wlr_output frame signal, WOutputRenderWindow
    // needs this signal to render next frame. Because the frame signal
    // may be emitted before WOutputRenderWindow::attach, if no commit here,
    // WOutputRenderWindow will ignore this output on render.
    const bool cachedEnabled = output()->property("_Enabled").toBool();
    const bool outputEnabled = wlrOutput->enabled;
    if (!cachedEnabled || !outputEnabled) {
        if (!wlrOutput->current_mode) {
            auto mode = wlr_output_preferred_mode(wlrOutput);
            if (mode) {
                wlr_output_state_set_mode(newState.get(), mode);

                // TODO: read user config
                wlr_output_state_set_scale(newState.get(), preferredScaleFactor({ mode->width, mode->height }));
            }
        } else {
            // TODO: read user config
            wlr_output_state_set_scale(newState.get(), preferredScaleFactor(output()->size()));
        }
        wlr_output_state_set_enabled(newState.get(), true);
        const bool commitOk = wlr_output_commit_state(wlrOutput, newState.get());
        if (!commitOk) {
            qCCritical(lcTlCore, "commit failed on output %s", wlrOutput->name);
            return;
        }
        output()->setProperty("_Enabled", true);
        qCInfo(lcTlOutput) << "Enabled output" << wlrOutput->name
                           << "cached:" << cachedEnabled
                           << "was enabled:" << outputEnabled;
    }
}

void Output::updateOutputHardwareLayers()
{
    WOutputViewport *viewportPrimary = m_proxy->screenViewport();
    std::pair<WOutputViewport *, QQuickItem *> copyOutput = getOutputItemProperty();
    const auto layers = viewportPrimary->hardwareLayers();
    for (auto layer : layers) {
        if (m_hardwareLayersOfPrimaryOutput.removeOne(layer))
            continue;
        Helper::instance()->window()->attach(layer,
                                             copyOutput.first,
                                             viewportPrimary,
                                             copyOutput.second);
    }
    for (auto oldLayer : std::as_const(m_hardwareLayersOfPrimaryOutput)) {
        Helper::instance()->window()->detach(oldLayer, copyOutput.first);
    }
    m_hardwareLayersOfPrimaryOutput = layers;
}

void Output::addSurface(SurfaceWrapper *surface)
{
    Q_ASSERT(!hasSurface(surface));
    SurfaceListModel::addSurface(surface);

    if (surface->type() == SurfaceWrapper::Type::Layer) {
        auto layer = qobject_cast<WLayerSurface *>(surface->shellSurface());
        QObject::connect(layer, &WLayerSurface::layerPropertiesChanged,
                           this,
                           &Output::arrangeLayerSurfaces);

        arrangeLayerSurfaces();
    } else {
        auto layoutSurface = [surface, this] {
            if (!surface->hasInitializeContainer())
                return;
            arrangeNonLayerSurface(surface, ArrangeReason::InitialPlacement);
        };

        connect(surface, &SurfaceWrapper::widthChanged, this, layoutSurface);
        connect(surface, &SurfaceWrapper::heightChanged, this, layoutSurface);
        connect(surface, &SurfaceWrapper::hasInitializeContainerChanged, this, layoutSurface);
        layoutSurface();

        auto setyOffset = [surface, this] {
            placeUnderCursor(surface, surface->autoPlaceYOffset());
        };
        connect(surface, &SurfaceWrapper::autoPlaceYOffsetChanged, this, setyOffset);
        if (surface->autoPlaceYOffset() != 0)
            setyOffset();

        if (surface->type() == SurfaceWrapper::Type::XdgPopup) {
            auto xdgPopupSurfaceItem = qobject_cast<WXdgPopupSurfaceItem *>(surface->surfaceItem());
            connect(xdgPopupSurfaceItem, &WXdgPopupSurfaceItem::implicitPositionChanged, this, [surface, this] {
                // Reposition should ignore positionAutomatic
                arrangePopupSurface(surface);
            });
        } else if (surface->type() == SurfaceWrapper::Type::InputPopup) {
            auto inputPopupSurfaceItem = qobject_cast<WInputPopupSurfaceItem *>(surface->surfaceItem());
            connect(inputPopupSurfaceItem, &WInputPopupSurfaceItem::referenceRectChanged, this, [surface, this] {
                // Reposition should ignore positionAutomatic
                arrangePopupSurface(surface);
            });
        }
    }
}

void Output::removeSurface(SurfaceWrapper *surface)
{
    clearPopupCache(surface);
    m_initialWindowPositionRatio.remove(surface);
    Q_ASSERT(hasSurface(surface));
    SurfaceListModel::removeSurface(surface);
    surface->disconnect(this);

    if (surface->type() == SurfaceWrapper::Type::Layer) {
        bool exclusiveZoneRemoved = false;
        if (auto ss = surface->shellSurface()) {
            ss->disconnect(this);
            exclusiveZoneRemoved = removeExclusiveZone(ss);
        }
        arrangeLayerSurfaces();
        if (exclusiveZoneRemoved) {
            arrangeNonLayerSurfaces(ArrangeReason::LayerSurfaceRemoved);
            Q_EMIT exclusiveZoneChanged();
        }
    }
}

WOutput *Output::output() const
{
    auto o = m_item->output();
    Q_ASSERT(o);
    return o;
}

WOutputItem *Output::outputItem() const
{
    return m_item;
}

void Output::setExclusiveZone(Qt::Edge edge, QObject *object, int value)
{
    Q_ASSERT(value > 0);
    removeExclusiveZone(object);
    switch (edge) {
    case Qt::TopEdge:
        m_topExclusiveZones.append(std::make_pair(object, value));
        m_exclusiveZone.setTop(m_exclusiveZone.top() + value);
        break;
    case Qt::BottomEdge:
        m_bottomExclusiveZones.append(std::make_pair(object, value));
        m_exclusiveZone.setBottom(m_exclusiveZone.bottom() + value);
        break;
    case Qt::LeftEdge:
        m_leftExclusiveZones.append(std::make_pair(object, value));
        m_exclusiveZone.setLeft(m_exclusiveZone.left() + value);
        break;
    case Qt::RightEdge:
        m_rightExclusiveZones.append(std::make_pair(object, value));
        m_exclusiveZone.setRight(m_exclusiveZone.right() + value);
        break;
    default:
        Q_UNREACHABLE_RETURN();
    }
}

bool Output::removeExclusiveZone(QObject *object)
{
    auto tmp = std::ranges::find(m_topExclusiveZones, object, &std::pair<QObject *, int>::first);
    if (tmp != m_topExclusiveZones.end()) {
        m_exclusiveZone.setTop(m_exclusiveZone.top() - tmp->second);
        m_topExclusiveZones.erase(tmp);
        Q_ASSERT(m_exclusiveZone.top() >= 0);
        return true;
    }

    tmp = std::ranges::find(m_bottomExclusiveZones, object, &std::pair<QObject *, int>::first);
    if (tmp != m_bottomExclusiveZones.end()) {
        m_exclusiveZone.setBottom(m_exclusiveZone.bottom() - tmp->second);
        m_bottomExclusiveZones.erase(tmp);
        Q_ASSERT(m_exclusiveZone.bottom() >= 0);
        return true;
    }

    tmp = std::ranges::find(m_leftExclusiveZones, object, &std::pair<QObject *, int>::first);
    if (tmp != m_leftExclusiveZones.end()) {
        m_exclusiveZone.setLeft(m_exclusiveZone.left() - tmp->second);
        m_leftExclusiveZones.erase(tmp);
        Q_ASSERT(m_exclusiveZone.left() >= 0);
        return true;
    }

    tmp = std::ranges::find(m_rightExclusiveZones, object, &std::pair<QObject *, int>::first);
    if (tmp != m_rightExclusiveZones.end()) {
        m_exclusiveZone.setRight(m_exclusiveZone.right() - tmp->second);
        m_rightExclusiveZones.erase(tmp);
        Q_ASSERT(m_exclusiveZone.right() >= 0);
        return true;
    }

    return false;
}

void Output::arrangeLayerSurface(SurfaceWrapper *surface)
{
    WLayerSurface *layer = qobject_cast<WLayerSurface *>(surface->shellSurface());
    Q_ASSERT(layer);
    if (!layer->handle()->initialized) {
        return;
    }

    auto validGeo = layer->exclusiveZone() == -1 ? geometry() : validGeometry();
    validGeo = validGeo.marginsRemoved(QMargins(layer->leftMargin(),
                                                layer->topMargin(),
                                                layer->rightMargin(),
                                                layer->bottomMargin()));
    auto anchor = layer->ancher();
    QRectF surfaceGeo(QPointF(0, 0), layer->desiredSize());

    if (anchor.testFlags(WLayerSurface::AnchorType::Left | WLayerSurface::AnchorType::Right)) {
        surfaceGeo.moveLeft(validGeo.left());
        surfaceGeo.setWidth(validGeo.width());
    } else if (anchor & WLayerSurface::AnchorType::Left) {
        surfaceGeo.moveLeft(validGeo.left());
    } else if (anchor & WLayerSurface::AnchorType::Right) {
        surfaceGeo.moveRight(validGeo.right());
    } else {
        surfaceGeo.moveLeft(validGeo.left() + (validGeo.width() - surfaceGeo.width()) / 2);
    }

    if (anchor.testFlags(WLayerSurface::AnchorType::Top | WLayerSurface::AnchorType::Bottom)) {
        surfaceGeo.moveTop(validGeo.top());
        surfaceGeo.setHeight(validGeo.height());
    } else if (anchor & WLayerSurface::AnchorType::Top) {
        surfaceGeo.moveTop(validGeo.top());
    } else if (anchor & WLayerSurface::AnchorType::Bottom) {
        surfaceGeo.moveBottom(validGeo.bottom());
    } else {
        surfaceGeo.moveTop(validGeo.top() + (validGeo.height() - surfaceGeo.height()) / 2);
    }

    if (layer->exclusiveZone() > 0) {
        // TODO: support set_exclusive_edge in layer-shell v5/wlroots 0.19
        switch (layer->getExclusiveZoneEdge()) {
            using enum WLayerSurface::AnchorType;
        case Top:
            setExclusiveZone(Qt::TopEdge, layer, layer->exclusiveZone());
            break;
        case Bottom:
            setExclusiveZone(Qt::BottomEdge, layer, layer->exclusiveZone());
            break;
        case Left:
            setExclusiveZone(Qt::LeftEdge, layer, layer->exclusiveZone());
            break;
        case Right:
            setExclusiveZone(Qt::RightEdge, layer, layer->exclusiveZone());
            break;
        default:
            qCWarning(lcTlOutput) << layer->appId()
                                 << " has set exclusive zone, but exclusive edge is invalid!";
            break;
        }
    }

    surface->setSize(surfaceGeo.size());
    surface->setPosition(surfaceGeo.topLeft());
}

void Output::arrangeLayerSurfaces()
{
    auto oldExclusiveZone = m_exclusiveZone;

    for (auto *s : std::as_const(surfaces())) {
        if (s->type() != SurfaceWrapper::Type::Layer)
            continue;
        removeExclusiveZone(s->shellSurface());
    }

    for (auto *s : std::as_const(surfaces())) {
        if (s->type() != SurfaceWrapper::Type::Layer)
            continue;

        auto layer = qobject_cast<WLayerSurface *>(s->shellSurface());
        Q_ASSERT(layer);
        if (layer->exclusiveZone() > 0)
            arrangeLayerSurface(s);
    }

    for (auto *s : std::as_const(surfaces())) {
        if (s->type() != SurfaceWrapper::Type::Layer)
            continue;

        auto layer = qobject_cast<WLayerSurface *>(s->shellSurface());
        Q_ASSERT(layer);
        if (layer->exclusiveZone() <= 0)
            arrangeLayerSurface(s);
    }

    if (oldExclusiveZone != m_exclusiveZone) {
        arrangeNonLayerSurfaces(ArrangeReason::ExclusiveZoneChanged);
        Q_EMIT exclusiveZoneChanged();
    }
}

void Output::arrangeNonLayerSurface(SurfaceWrapper *surface, ArrangeReason reason)
{
    Q_ASSERT(surface->type() != SurfaceWrapper::Type::Layer);
    if (surface->isIMCandidatePanel())
        return;

    surface->setFullscreenGeometry(geometry());
    const auto validGeo = this->validGeometry();
    surface->setMaximizedGeometry(validGeo);

    QRectF normalGeo = surface->normalGeometry();

    if (surface->positionAutomatic()) {
        if (normalGeo.isEmpty())
            return;

        // NOTE: Xwayland popups don't have parents
        SurfaceWrapper *parentSurfaceWrapper = surface->parentSurface();

        if (parentSurfaceWrapper) {
            if (surface->type() == SurfaceWrapper::Type::XdgPopup
                || surface->type() == SurfaceWrapper::Type::InputPopup) {
                arrangePopupSurface(surface);
                return;
            }
            QPointF dPos{ (parentSurfaceWrapper->width() - surface->width()) / 2,
                          (parentSurfaceWrapper->height() - surface->height()) / 2 };
            QPointF topLeft;
            topLeft.setX(parentSurfaceWrapper->x() + dPos.x());
            topLeft.setY(parentSurfaceWrapper->y() + dPos.y());
            if (parentSurfaceWrapper->ownsOutput() != this) {
                // Parent is on a different output: follow it without clamping
                // to this output's validGeo.
                surface->moveNormalGeometryInOutput(topLeft);
            } else {
                // Parent and child share this output: apply the titlebar clamp
                // so near-edge dialogs remain accessible.
                moveSurfaceWithTitlebarClamp(surface, topLeft);
            }
        } else {
            if (reason != ArrangeReason::InitialPlacement) {
                auto constrained =
                    constrainToValidArea(normalGeo.topLeft(), normalGeo.size(), validGeo);
                if (constrained != normalGeo.topLeft())
                    moveSurfaceWithTitlebarClamp(surface, constrained);
            } else {
                auto outputValidGeometry = surface->ownsOutput()->validGeometry();
                if (normalGeo.width() > outputValidGeometry.width()
                    || normalGeo.height() > outputValidGeometry.height())
                    surface->resize(outputValidGeometry.size());
                if (surface->type() == SurfaceWrapper::Type::XdgToplevel
                    || surface->type() == SurfaceWrapper::Type::SplashScreen) {
                    placeSmartCascaded(surface);
                } else {
                    placeCentered(surface);
                }
            }
        }
    } else if (reason == ArrangeReason::OutputGeometryChanged) {
        if (!m_initialWindowPositionRatio.contains(surface)) {
            qreal xRatio = 0.5, yRatio = 0.5; // Default center position
            if (validGeo.width() > normalGeo.width()) {
                xRatio = (normalGeo.x() - validGeo.x()) / (validGeo.width() - normalGeo.width());
                xRatio = qBound(0.0, xRatio, 1.0);
            }
            if (validGeo.height() > normalGeo.height()) {
                yRatio = (normalGeo.y() - validGeo.y()) / (validGeo.height() - normalGeo.height());
                yRatio = qBound(0.0, yRatio, 1.0);
            }
            m_initialWindowPositionRatio[surface] = QPointF(xRatio, yRatio);
        }

        QPointF ratio = m_initialWindowPositionRatio[surface];
        QPointF pos;
        pos.setX(validGeo.x() + ratio.x() * (validGeo.width() - normalGeo.width()));
        pos.setY(validGeo.y() + ratio.y() * (validGeo.height() - normalGeo.height()));

        // Boundary protection ensures that at least 30% of the window remains within the screen.
        const qreal minVisibleRatio = 0.3;
        const int minVisibleX = qMin(100, int(normalGeo.width() * minVisibleRatio));
        const int minVisibleY = qMin(100, int(normalGeo.height() * minVisibleRatio));
        pos.setX(qBound(validGeo.left() - normalGeo.width() + minVisibleX,
                        pos.x(),
                        validGeo.right() - minVisibleX));
        pos.setY(qBound(validGeo.top() - normalGeo.height() + minVisibleY,
                        pos.y(),
                        validGeo.bottom() - minVisibleY));
        moveSurfaceWithTitlebarClamp(surface, pos);
    } else {
        QPoint clientRequstPos = surface->clientRequstPos();
        if (!clientRequstPos.isNull()) {
            placeClientRequstPos(surface, clientRequstPos);
        } else if (reason == ArrangeReason::LayerSurfaceRemoved
                   || reason == ArrangeReason::ExclusiveZoneChanged) {
            // validGeo has changed (panel added/removed); re-run the titlebar
            // clamp so the titlebar remains visible in the updated work area.
            // The window position itself is preserved — only clamped if needed.
            moveSurfaceWithTitlebarClamp(surface, normalGeo.topLeft());
        }
        // For InitialPlacement with non-automatic positioning the compositor
        // deliberately does not move the window: client geometry is authoritative.
    }
}

namespace {
// Get the popup position (dPos) from the surface-local coordinate space,
// adjusted to the SurfaceWrapper coordinate space.
// - XDG popups: implicitPosition from wlr_xdg_popup_get_position already
//   subtracts parent_geom, so it's relative to content geometry (no adjustment).
// - Input popups: cursorRect is in raw surface coordinates; adjust by
//   content geometry offset (e.g., CSD title bar) to match the rendering
//   position of WSurfaceItem's contentContainer.
std::optional<QPointF> popupDPos(SurfaceWrapper *surface)
{
    using Type = SurfaceWrapper::Type;
    if (surface->type() == Type::XdgPopup) {
        auto *item = qobject_cast<WXdgPopupSurfaceItem *>(surface->surfaceItem());
        if (item)
            return item->implicitPosition();
    }

    if (surface->type() == Type::InputPopup) {
        auto *shell = qobject_cast<WInputPopupSurface *>(surface->shellSurface());
        if (shell) {
            QPointF dPos = shell->cursorRect().bottomLeft();
            auto parent = surface->parentSurface();
            if (parent && parent->shellSurface()) {
                const QPoint offset = parent->shellSurface()->getContentGeometry().topLeft();
                dPos -= QPointF(offset.x(), offset.y());
            }
            return dPos;
        }
    }

    qCWarning(lcTlOutput) << " Invalid popup surface type:" << surface->type();
    return std::nullopt;
}
} // namespace

QPointF Output::calculateBasePosition(SurfaceWrapper *surface, const QPointF &dPos) const
{
    auto parent = surface->parentSurface();
    if (!parent || !parent->surfaceItem()) {
        qCWarning(lcTlOutput) << " Invalid parent surface or surface item!";
        return QPointF();
    }

    const qreal titlebarOffset = parent->titlebarGeometry().isNull()
        ? 0.0
        : parent->titlebarGeometry().height();

    return QPointF(parent->x() + parent->surfaceItem()->x() + dPos.x(),
                   parent->y() + parent->surfaceItem()->y() + dPos.y() + titlebarOffset);
}

void Output::adjustToOutputBounds(QPointF &pos, const QRectF &normalGeo, const QRectF &outputRect) const
{
    if (pos.x() + normalGeo.width() > outputRect.right() - POPUP_EDGE_MARGIN) {
        pos.setX(outputRect.right() - normalGeo.width() - POPUP_EDGE_MARGIN);
    }
    if (pos.x() < outputRect.left() + POPUP_EDGE_MARGIN) {
        pos.setX(outputRect.left() + POPUP_EDGE_MARGIN);
    }

    if (pos.y() + normalGeo.height() > outputRect.bottom() - POPUP_EDGE_MARGIN) {
        pos.setY(outputRect.bottom() - normalGeo.height() - POPUP_EDGE_MARGIN);
    }
    if (pos.y() < outputRect.top() + POPUP_EDGE_MARGIN) {
        pos.setY(outputRect.top() + POPUP_EDGE_MARGIN);
    }
}

void Output::handleLayerShellPopup(SurfaceWrapper *surface, const QRectF &normalGeo)
{
    if (!surface->parentSurface() || !surface->parentSurface()->ownsOutput()) {
        qCWarning(lcTlOutput) << " Invalid LayerShell parent surface!";
        return;
    }

    auto parentOutput = surface->parentSurface()->ownsOutput()->outputItem();
    auto dPos = popupDPos(surface);
    if (!dPos.has_value())
        return;

    QPointF pos = calculateBasePosition(surface, dPos.value());
    if (pos.isNull()) {
        return;
    }

    QRectF outputRect(parentOutput->position(), parentOutput->size());
    adjustToOutputBounds(pos, normalGeo, outputRect);
    surface->moveNormalGeometryInOutput(pos);
}

void Output::handleRegularPopup(SurfaceWrapper *surface, const QRectF &normalGeo, WOutputItem *targetOutput)
{
    if (normalGeo.isEmpty()) {
        return;
    }

    auto dPos = popupDPos(surface);
    if (!dPos.has_value())
        return;

    QPointF pos = calculateBasePosition(surface, dPos.value());
    if (pos.isNull()) {
        return;
    }

    QRectF outputRect(targetOutput->position(), targetOutput->size());

    // TODO: Use xdg_positioner::constraint_adjustment to properly respect client's positioning constraints
    if (pos.x() < outputRect.left()) {
        pos.setX(outputRect.left());
    }
    if (pos.x() + normalGeo.width() > outputRect.right()) {
        pos.setX(outputRect.right() - normalGeo.width());
    }

    adjustToOutputBounds(pos, normalGeo, outputRect);

    QRectF newGeo = normalGeo;
    newGeo.moveTopLeft(pos);
    m_positionCache[surface] = qMakePair(pos, newGeo);
    surface->moveNormalGeometryInOutput(pos);
}

void Output::clearPopupCache(SurfaceWrapper *surface)
{
    if (!surface || m_positionCache.isEmpty()) {
        return;
    }

    if (surface->type() == SurfaceWrapper::Type::XdgPopup || surface->isInputPopupLike()) {
        m_positionCache.remove(surface);
    }
}

void Output::arrangePopupSurface(SurfaceWrapper *surface)
{
    SurfaceWrapper *parentSurfaceWrapper = surface->parentSurface();
    if (!parentSurfaceWrapper) {
        //  When an input popup is still alive while its parent text-input client is being torn down,
        //  arrangePopupSurface() can run in a transient state where parentSurface is temporarily unavailable.
        qCWarning(lcTlSurface) << "[popup] skip arrangePopupSurface: missing parent surface"
                                << "surface=" << surface;
        return;
    }

    QRectF normalGeo = surface->normalGeometry();
    if (normalGeo.isEmpty()) {
        return;
    }

    WOutputItem *targetOutput = nullptr;
    // TODO： Consider determining the targetOutput based on the upper left corner or overlapping area
    if (surface->type() == SurfaceWrapper::Type::XdgPopup) {
        auto *outputAtCursor = Helper::instance()->getOutputAtCursor();
        targetOutput = outputAtCursor ? outputAtCursor->outputItem() : nullptr;
    } else if (surface->isInputPopupLike()) {
        auto *parentOutput = parentSurfaceWrapper->ownsOutput();
        targetOutput = parentOutput ? parentOutput->outputItem() : nullptr;
    }

    if (!targetOutput) {
        qCInfo(lcTlSurface) << "[popup] skip arrangePopupSurface: missing target output"
                                << "surface=" << surface
                                << "parentSurface=" << parentSurfaceWrapper;
        return;
    }

    if (parentSurfaceWrapper->type() == SurfaceWrapper::Type::Layer) {
        handleLayerShellPopup(surface, normalGeo);
    } else {
        handleRegularPopup(surface, normalGeo, targetOutput);
    }
}

void Output::arrangeNonLayerSurfaces(ArrangeReason reason)
{
    for (SurfaceWrapper *surface : std::as_const(surfaces())) {
        if (surface->type() == SurfaceWrapper::Type::Layer
            || surface->type() == SurfaceWrapper::Type::LockScreen || surface->isInputPopupLike()
            || !surface->hasInitializeContainer())
            continue;
        arrangeNonLayerSurface(surface, reason);
    }
}

void Output::arrangeAllSurfaces()
{
    arrangeLayerSurfaces();
    arrangeNonLayerSurfaces(ArrangeReason::OutputGeometryChanged);
}

QMargins Output::exclusiveZone() const
{
    return m_exclusiveZone;
}

QRectF Output::rect() const
{
    return QRectF(QPointF(0, 0), m_item->size());
}

QRectF Output::geometry() const
{
    return QRectF(m_item->position(), m_item->size());
}

QRectF Output::validRect() const
{
    return rect().marginsRemoved(m_exclusiveZone);
}

WOutputViewport *Output::screenViewport() const
{
    return m_outputViewport;
}

QRectF Output::validGeometry() const
{
    return geometry().marginsRemoved(m_exclusiveZone);
}

// do not use config()->setBrightness or config()->setColorTemperature to set color temperature or brightness
// as doing so will have no effect.
// use Output::setOutputColor instead
OutputConfig *Output::config() const
{
    return m_config;
}

void Output::applyOutputColorConfig()
{
    auto *outputConfig = config();
    auto applyConfig = [this, outputConfig] {
        setOutputColor(outputConfig->brightness(),
                       static_cast<uint32_t>(outputConfig->colorTemperature()));
    };

    if (outputConfig->isInitializeSucceeded()) {
        applyConfig();
        return;
    }

    if (!outputConfig->isInitializeFailed()) {
        connect(outputConfig,
                &OutputConfig::configInitializeSucceed,
                this,
                applyConfig,
                Qt::SingleShotConnection);
    }
}

namespace {
static inline void kelvinToRGB(double kelvin, double &r, double &g, double &b)
{
    kelvin = std::clamp(kelvin, 1000.0, 20000.0) / 100.0;

    if (kelvin <= 66.0) r = 1.0;
    else r = std::clamp(329.698727446 * std::pow(kelvin - 60.0, -0.1332047592) / 255.0, 0.0, 1.0);

    if (kelvin <= 66.0)
        g = std::clamp((99.4708025861 * std::log(kelvin) - 161.1195681661) / 255.0, 0.0, 1.0);
    else
        g = std::clamp(288.1221695283 * std::pow(kelvin - 60.0, -0.0755148492) / 255.0, 0.0, 1.0);

    if (kelvin >= 66.0) b = 1.0;
    else if (kelvin <= 19.0) b = 0.0;
    else b = std::clamp((138.5177312231 * std::log(kelvin - 10.0) - 305.0447927307) / 255.0, 0.0, 1.0);
}

static inline void generateGammaLUT(uint32_t colorTemperature,
                                    qreal brightness,
                                    size_t gammaSize,
                                    QVector<uint16_t> &r,
                                    QVector<uint16_t> &g,
                                    QVector<uint16_t> &b)
{
    if (gammaSize == 0) {
        return;
    }

    double cr, cg, cb;
    kelvinToRGB(static_cast<double>(colorTemperature), cr, cg, cb);
    for (size_t i = 0; i < gammaSize; ++i) {
        double normalized = static_cast<double>(i) / static_cast<double>(gammaSize - 1);

        double rValue = std::clamp(normalized * cr * brightness, 0.0, 1.0);
        double gValue = std::clamp(normalized * cg * brightness, 0.0, 1.0);
        double bValue = std::clamp(normalized * cb * brightness, 0.0, 1.0);
        r[i] = static_cast<uint16_t>(std::round(rValue * 65535.0));
        g[i] = static_cast<uint16_t>(std::round(gValue * 65535.0));
        b[i] = static_cast<uint16_t>(std::round(bValue * 65535.0));
    }
}

}

// TODO: better Chromatic Adaptation algorithms can be implemented when the wlr_color_transform
// api is available. For now RGB scaling is used due to limitation of gamma LUT table.
// see: http://www.brucelindbloom.com/index.html?ChromAdaptEval.html
void Output::setOutputColor(qreal brightness,
                            uint32_t colorTemperature,
                            std::function<void(bool)> resultCallback)
{
    const bool brightnessRequested = brightness >= 0;
    const bool colorTemperatureRequested = colorTemperature != 0;

    if (!brightnessRequested)
        brightness = config()->brightness();
    if (!colorTemperatureRequested)
        colorTemperature = config()->colorTemperature();

    brightness = qBound(0.0, brightness, 1.0);
    colorTemperature = std::clamp(colorTemperature, 1000u, 20000u);

    qreal brightnessCorrection = 1.0;
    bool backlightApplied = false;

    if (m_backlight) {
        qreal backlightBrightness = m_backlight->setBrightness(brightness);
        backlightApplied = qFuzzyCompare(backlightBrightness, brightness);
        if (backlightBrightness != 0)
            brightnessCorrection = qBound(0.0, brightness / backlightBrightness, 1.0);
    } else {
        brightnessCorrection = brightness;
    }

    const size_t gammaSize = wlr_output_get_gamma_size(output()->handle());
    if (gammaSize == 0) {
        if (backlightApplied) {
            config()->setBrightness(brightness);
        }
        if (resultCallback)
            resultCallback(backlightApplied && !colorTemperatureRequested);
        qCWarning(lcTlOutput) << " Output " << output()->name()
                             << " does not support gamma LUT! Brightness and color temperature adjustments through gamma will have no effect.";
        return;
    }

    QVector<uint16_t> r(gammaSize);
    QVector<uint16_t> g(gammaSize);
    QVector<uint16_t> b(gammaSize);

    generateGammaLUT(colorTemperature,
                     brightnessCorrection,
                     gammaSize,
                     r,
                     g,
                     b);

    WOutputHelper::ExtraState newState;
    auto *viewport = screenViewport();
    auto *renderWindow = screenViewport()->outputRenderWindow();
    wlr_output_state_set_gamma_lut(newState.get(), gammaSize,
                                   r.constData(),
                                   g.constData(),
                                   b.constData());

    auto *outputHelper = renderWindow->getOutputHelper(viewport);
    outputHelper->setExtraState(newState);

    outputHelper->scheduleCommitJob([this, brightness, colorTemperature, newState, resultCallback](bool success, WOutputHelper::ExtraState state) {
        if (state == newState) {
            if (resultCallback)
                resultCallback(success);
            if (!success) {
                qCWarning(lcTlOutput) << "Failed to apply brightness and color temperature settings to output"
                                          << output()->name();
            } else {
                config()->setBrightness(brightness);
                config()->setColorTemperature(colorTemperature);
            }
        } else {
            qCWarning(lcTlOutput) << "Commit callback received unexpected state pointer!"
                                      << "Expected:" << newState.get()
                                      << "Got:" << state.get();
        }
    }, WOutputHelper::AfterCommitStage);
    renderWindow->update(viewport);
}
