// Copyright (C) 2024 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 "rootsurfacecontainer.h"

#include "output/output.h"
#include "seat/helper.h"
#include "surface/surfacewrapper.h"

#include <wcursor.h>
#include <woutput.h>
#include <woutputitem.h>
#include <woutputlayout.h>
#include <wxdgpopupsurface.h>
#include <wxdgtoplevelsurface.h>

#include <qwoutputlayout.h>

#include <QQuickWindow>

WAYLIB_SERVER_USE_NAMESPACE

OutputListModel::OutputListModel(QObject *parent)
    : ObjectListModel("output", parent)
{
}

RootSurfaceContainer::RootSurfaceContainer(QQuickItem *parent)
    : SurfaceContainer(parent)
    , m_outputModel(new OutputListModel(this))
    , m_cursor(new WCursor(this))
{
    m_cursor->setEventWindow(window());
}

void RootSurfaceContainer::init(WServer *server)
{
    m_outputLayout = new WOutputLayout(server);
    m_cursor->setLayout(m_outputLayout);
    connect(m_outputLayout, &WOutputLayout::implicitWidthChanged, this, [this] {
        const auto width = m_outputLayout->implicitWidth();
        window()->setWidth(width);
        setWidth(width);
    });

    connect(m_outputLayout, &WOutputLayout::implicitHeightChanged, this, [this] {
        const auto height = m_outputLayout->implicitHeight();
        window()->setHeight(height);
        setHeight(height);
    });

    m_outputLayout->safeConnect(&qw_output_layout::notify_change, this, [this] {
        for (auto output : std::as_const(outputs())) {
            output->updatePositionFromLayout();
        }
        ensureCursorVisible();

        // for (auto s : m_surfaceContainer->surfaces()) {
        //     ensureSurfaceNormalPositionValid(s);
        //     updateSurfaceOutputs(s);
        // }
    });

    m_dragSurfaceItem = new WSurfaceItem(window()->contentItem());
    m_dragSurfaceItem->setZ(
        static_cast<std::underlying_type_t<WOutputLayout::Layer>>(WOutputLayout::Layer::Cursor)
        - 1);
    m_dragSurfaceItem->setFlags(WSurfaceItem::DontCacheLastBuffer);

    m_cursor->safeConnect(&WCursor::positionChanged, this, [this] {
        m_dragSurfaceItem->setPosition(m_cursor->position());
    });

    m_cursor->safeConnect(&WCursor::requestedDragSurfaceChanged, this, [this] {
        m_dragSurfaceItem->setSurface(m_cursor->requestedDragSurface());
    });
}

SurfaceWrapper *RootSurfaceContainer::getSurface(WSurface *surface) const
{
    const auto surfaces = this->surfaces();
    for (const auto &wrapper : surfaces) {
        if (wrapper->surface() == surface)
            return wrapper;
    }
    return nullptr;
}

SurfaceWrapper *RootSurfaceContainer::getSurface(WToplevelSurface *surface) const
{
    const auto surfaces = this->surfaces();
    for (const auto &wrapper : surfaces) {
        if (wrapper->shellSurface() == surface)
            return wrapper;
    }
    return nullptr;
}

void RootSurfaceContainer::destroyForSurface(SurfaceWrapper *wrapper)
{
    if (wrapper == moveResizeState.surface)
        endMoveResize();

    wrapper->markWrapperToRemoved();
}

void RootSurfaceContainer::addOutput(Output *output)
{
    m_outputModel->addObject(output);
    m_outputLayout->autoAdd(output->output());
    if (!m_primaryOutput)
        setPrimaryOutput(output);

    // Register all outputs (including primary and proxy/copy) to ensure
    // LayerSurfaceContainer is initialized for every active display.
    SurfaceContainer::addOutput(output);
}

void RootSurfaceContainer::removeOutput(Output *output)
{
    m_outputModel->removeObject(output);
    SurfaceContainer::removeOutput(output);

    if (moveResizeState.surface && moveResizeState.surface->ownsOutput() == output) {
        endMoveResize();
    }

    m_outputLayout->remove(output->output());
    if (m_primaryOutput == output) {
        const auto outputs = m_outputLayout->outputs();
        if (!outputs.isEmpty()) {
            auto newPrimaryOutput = Helper::instance()->getOutput(outputs.first());
            setPrimaryOutput(newPrimaryOutput);
        }
    }

    // ensure cursor within output
    const auto outputPos = output->outputItem()->position();
    if (output->geometry().contains(m_cursor->position()) && m_primaryOutput) {
        const auto posInOutput = m_cursor->position() - outputPos;
        const auto newCursorPos = m_primaryOutput->outputItem()->position() + posInOutput;

        if (m_primaryOutput->geometry().contains(newCursorPos))
            Helper::instance()->setCursorPosition(newCursorPos);
        else
            Helper::instance()->setCursorPosition(m_primaryOutput->geometry().center());
    }
}

void RootSurfaceContainer::beginMoveResize(SurfaceWrapper *surface, Qt::Edges edges)
{
    if (surface->surfaceState() != SurfaceWrapper::State::Normal || surface->isAnimationRunning())
        return;

    Q_ASSERT(!moveResizeState.surface);
    moveResizeState.surface = surface;
    moveResizeState.startGeometry = surface->geometry();
    moveResizeState.resizeEdges = edges;
    surface->setXwaylandPositionFromSurface(false);
    surface->setPositionAutomatic(false);
}

void RootSurfaceContainer::doMoveResize(const QPointF &incrementPos)
{
    Q_ASSERT(moveResizeState.surface);

    if (moveResizeState.resizeEdges) {
        QRectF geo = moveResizeState.startGeometry;

        if (moveResizeState.resizeEdges & Qt::LeftEdge)
            geo.setLeft(geo.left() + incrementPos.x());
        if (moveResizeState.resizeEdges & Qt::TopEdge)
            geo.setTop(geo.top() + incrementPos.y());

        if (moveResizeState.resizeEdges & Qt::RightEdge)
            geo.setRight(geo.right() + incrementPos.x());
        if (moveResizeState.resizeEdges & Qt::BottomEdge)
            geo.setBottom(geo.bottom() + incrementPos.y());

        QRectF alignedGeometry = moveResizeState.surface->alignGeometryToPixelGrid(geo);
        moveResizeState.surface->resize(alignedGeometry.size());

        // TODO: Pixel alignment for window position and size during move/resize operations
        // Current approach: Align position and size in the manager layer
        // Better approach:
        //   1. Keep logical geometry (position + size) intact at manager layer (no alignment)
        //   2. Only apply pixel alignment at render time in the renderer
    } else {
        auto new_pos = moveResizeState.startGeometry.topLeft() + incrementPos;
        QPointF alignedPos = moveResizeState.surface->alignToPixelGrid(new_pos);
        moveResizeState.surface->setPosition(alignedPos);
    }
}

void RootSurfaceContainer::cancelMoveResize(SurfaceWrapper *surface)
{
    if (moveResizeState.surface != surface)
        return;
    endMoveResize();
}

void RootSurfaceContainer::endMoveResize()
{
    if (!moveResizeState.surface)
        return;

    if (moveResizeState.surface->shellSurface()->isInitialized()) {
        auto o = moveResizeState.surface->ownsOutput();
        moveResizeState.surface->shellSurface()->setResizeing(false);

        if (!o || !moveResizeState.surface->outputs().contains(o->output())) {
            o = cursorOutput();
            Q_ASSERT(o);
            moveResizeState.surface->setOwnsOutput(o);
        }

        ensureSurfaceNormalPositionValid(moveResizeState.surface);

        moveResizeState.surface->setXwaylandPositionFromSurface(true);
    }

    moveResizeState.surface = nullptr;
    Q_EMIT moveResizeFinised();
}

SurfaceWrapper *RootSurfaceContainer::moveResizeSurface() const
{
    return moveResizeState.surface;
}

void RootSurfaceContainer::startMove(SurfaceWrapper *surface)
{
    endMoveResize();
    beginMoveResize(surface, Qt::Edges{ 0 });

    Helper::instance()->activateSurface(surface);
}

void RootSurfaceContainer::startResize(SurfaceWrapper *surface, Qt::Edges edges)
{
    endMoveResize();
    Q_ASSERT(edges != 0);

    beginMoveResize(surface, edges);
    surface->shellSurface()->setResizeing(true);
    Helper::instance()->activateSurface(surface);
}

void RootSurfaceContainer::addSurface(SurfaceWrapper *)
{
    Q_UNREACHABLE_RETURN();
}

void RootSurfaceContainer::removeSurface(SurfaceWrapper *)
{
    Q_UNREACHABLE_RETURN();
}

void RootSurfaceContainer::addBySubContainer(SurfaceContainer *sub, SurfaceWrapper *surface)
{
    SurfaceContainer::addBySubContainer(sub, surface);

    if (surface->type() != SurfaceWrapper::Type::Layer) {
        // RootSurfaceContainer does not have control over layer surface's position and ownsOutput
        // All things are done in LayerSurfaceContainer
        connect(surface, &SurfaceWrapper::requestMove, this, [this] {
            auto surface = qobject_cast<SurfaceWrapper *>(sender());
            Q_ASSERT(surface);
            startMove(surface);
        });
        connect(surface, &SurfaceWrapper::requestResize, this, [this](Qt::Edges edges) {
            auto surface = qobject_cast<SurfaceWrapper *>(sender());
            Q_ASSERT(surface);
            startResize(surface, edges);
        });

        if (!surface->ownsOutput()) {
            auto parentSurface = surface->parentSurface();
            auto output = parentSurface ? parentSurface->ownsOutput() : primaryOutput();

            if (auto xdgPopupSurface = qobject_cast<WXdgPopupSurface *>(surface->shellSurface())) {
                if (parentSurface->type() != SurfaceWrapper::Type::Layer) {
                    // If parentSurface is Layer surface, follow parentSurface->ownsOutput
                    auto pos = parentSurface->position() + parentSurface->surfaceItem()->position()
                        + xdgPopupSurface->getPopupPosition();
                    if (auto op = m_outputLayout->handle()->output_at(pos.x(), pos.y()))
                        output =
                            Helper::instance()->getOutput(WOutput::fromHandle(qw_output::from(op)));
                }
            }
            surface->setOwnsOutput(output);
        }

        connect(surface, &SurfaceWrapper::geometryChanged, this, [this, surface] {
            updateSurfaceOutputs(surface);
        });

        updateSurfaceOutputs(surface);
    }
}

void RootSurfaceContainer::removeBySubContainer(SurfaceContainer *sub, SurfaceWrapper *surface)
{
    if (moveResizeState.surface == surface)
        endMoveResize();

    SurfaceContainer::removeBySubContainer(sub, surface);
}

bool RootSurfaceContainer::filterSurfaceGeometryChanged(SurfaceWrapper *surface,
                                                        QRectF &newGeometry,
                                                        const QRectF &oldGeometry)
{
    if (surface != moveResizeState.surface)
        return false;
    if (moveResizeState.setSurfacePositionForAnchorEdgets) {
        Q_ASSERT(newGeometry.size() == oldGeometry.size());
        return true;
    }

    if (moveResizeState.resizeEdges != 0) {
        QRectF geometry = newGeometry;
        if (moveResizeState.resizeEdges & Qt::RightEdge)
            geometry.moveLeft(oldGeometry.left());
        if (moveResizeState.resizeEdges & Qt::BottomEdge)
            geometry.moveTop(oldGeometry.top());
        if (moveResizeState.resizeEdges & Qt::LeftEdge)
            geometry.moveRight(oldGeometry.right());
        if (moveResizeState.resizeEdges & Qt::TopEdge)
            geometry.moveBottom(oldGeometry.bottom());

        if (geometry.topLeft() != newGeometry.topLeft()) {
            newGeometry = geometry;
            moveResizeState.setSurfacePositionForAnchorEdgets = true;
            QPointF alignedPos = moveResizeState.surface->alignToPixelGrid(geometry.topLeft());
            surface->setPosition(alignedPos);
            moveResizeState.setSurfacePositionForAnchorEdgets = false;
        }
    }

    return false;
}

bool RootSurfaceContainer::filterSurfaceStateChange(SurfaceWrapper *surface,
                                                    [[maybe_unused]] SurfaceWrapper::State newState,
                                                    [[maybe_unused]] SurfaceWrapper::State oldState)
{
    return surface == moveResizeState.surface;
}

WOutputLayout *RootSurfaceContainer::outputLayout() const
{
    return m_outputLayout;
}

WCursor *RootSurfaceContainer::cursor() const
{
    return m_cursor;
}

Output *RootSurfaceContainer::cursorOutput() const
{
    Q_ASSERT(m_cursor->layout() == m_outputLayout);
    const auto &pos = m_cursor->position();
    auto o = m_outputLayout->handle()->output_at(pos.x(), pos.y());
    if (!o)
        return nullptr;

    return Helper::instance()->getOutput(WOutput::fromHandle(qw_output::from(o)));
}

Output *RootSurfaceContainer::primaryOutput() const
{
    return m_primaryOutput;
}

void RootSurfaceContainer::setPrimaryOutput(Output *newPrimaryOutput)
{
    if (m_primaryOutput == newPrimaryOutput)
        return;
    m_primaryOutput = newPrimaryOutput;
    Q_EMIT primaryOutputChanged();
}

const QList<Output *> &RootSurfaceContainer::outputs() const
{
    return m_outputModel->objects();
}

void RootSurfaceContainer::ensureCursorVisible()
{
    const auto cursorPos = m_cursor->position();
    if (m_outputLayout->handle()->output_at(cursorPos.x(), cursorPos.y()))
        return;

    if (m_primaryOutput) {
        Helper::instance()->setCursorPosition(m_primaryOutput->geometry().center());
    }
}

void RootSurfaceContainer::updateSurfaceOutputs(SurfaceWrapper *surface)
{
    const QRectF geometry = surface->geometry();
    auto outputs = m_outputLayout->getIntersectedOutputs(geometry.toRect());
    surface->setOutputs(outputs);
    // TODO: Update ownsOutput during move/resize on multi-output systems
}

static qreal pointToRectMinDistance(const QPointF &pos, const QRectF &rect)
{
    if (rect.contains(pos))
        return 0;
    return std::min({ std::abs(rect.x() - pos.x()),
                      std::abs(rect.y() - pos.y()),
                      std::abs(rect.right() - pos.x()),
                      std::abs(rect.bottom() - pos.y()) });
}

static QRectF adjustRectToMakePointVisible(const QRectF &inputRect,
                                           const QPointF &absolutePoint,
                                           const QList<QRectF> &visibleAreas)
{
    Q_ASSERT(inputRect.contains(absolutePoint));
    QRectF adjustedRect = inputRect;

    QRectF targetRect;
    qreal distanceToTargetRect = std::numeric_limits<qreal>::max();
    for (const QRectF &area : visibleAreas) {
        Q_ASSERT(!area.isEmpty());
        if (area.contains(absolutePoint))
            return adjustedRect;
        const auto distance = pointToRectMinDistance(absolutePoint, area);
        if (distance < distanceToTargetRect) {
            distanceToTargetRect = distance;
            targetRect = area;
        }
    }
    Q_ASSERT(!targetRect.isEmpty());

    if (absolutePoint.x() < targetRect.x())
        adjustedRect.moveLeft(adjustedRect.x() + targetRect.x() - absolutePoint.x());
    else if (absolutePoint.x() > targetRect.right())
        adjustedRect.moveRight(adjustedRect.right() + targetRect.right() - absolutePoint.x());

    if (absolutePoint.y() < targetRect.y())
        adjustedRect.moveTop(adjustedRect.y() + targetRect.y() - absolutePoint.y());
    else if (absolutePoint.y() > targetRect.bottom())
        adjustedRect.moveBottom(adjustedRect.bottom() + targetRect.bottom() - absolutePoint.y());

    return adjustedRect;
}

void RootSurfaceContainer::ensureSurfaceNormalPositionValid(SurfaceWrapper *surface)
{
    if (surface->type() == SurfaceWrapper::Type::Layer)
        return;

    auto normalGeo = surface->normalGeometry();
    if (normalGeo.size().isEmpty())
        return;

    auto output = surface->ownsOutput();
    if (!output)
        return;

    QList<QRectF> outputRects;
    outputRects.reserve(outputs().size());
    for (auto o : outputs())
        outputRects << o->validGeometry();

    // Ensure window is not outside the screen
    const QPointF mustVisiblePosOfSurface(qMin(normalGeo.right(), normalGeo.x() + 20),
                                          qMin(normalGeo.bottom(), normalGeo.y() + 20));
    normalGeo = adjustRectToMakePointVisible(normalGeo, mustVisiblePosOfSurface, outputRects);

    // Ensure titlebar is not outside the screen
    const auto titlebarGeometry = surface->titlebarGeometry().translated(normalGeo.topLeft());
    if (titlebarGeometry.isValid()) {
        bool titlebarGeometryAdjusted = false;
        for (auto r : std::as_const(outputRects)) {
            if ((r & titlebarGeometry).isEmpty())
                continue;
            if (titlebarGeometry.top() < r.top()) {
                normalGeo.moveTop(normalGeo.top() + r.top() - titlebarGeometry.top());
                titlebarGeometryAdjusted = true;
            }
        }

        if (!titlebarGeometryAdjusted) {
            normalGeo =
                adjustRectToMakePointVisible(normalGeo, titlebarGeometry.topLeft(), outputRects);
        }
    }

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

OutputListModel *RootSurfaceContainer::outputModel() const
{
    return m_outputModel;
}

void RootSurfaceContainer::moveSurfacesToOutput(const QList<SurfaceWrapper *> &surfaces,
                                                Output *targetOutput,
                                                Output *sourceOutput)
{
    if (!surfaces.isEmpty() && targetOutput) {
        const QRectF targetGeometry = targetOutput->geometry();

        for (auto *surface : surfaces) {
            if (!surface)
                continue;

            const QSizeF size = surface->size();
            QPointF newPos;

            if (surface->ownsOutput() == targetOutput) {
                newPos = surface->position();
            } else {
                const QRectF sourceGeometry =
                    sourceOutput ? sourceOutput->geometry() : surface->ownsOutput()->geometry();
                const QPointF relativePos = surface->position() - sourceGeometry.center();
                newPos = targetGeometry.center() + relativePos;
                surface->setOwnsOutput(targetOutput);
            }
            newPos.setX(
                qBound(targetGeometry.left(), newPos.x(), targetGeometry.right() - size.width()));
            newPos.setY(
                qBound(targetGeometry.top(), newPos.y(), targetGeometry.bottom() - size.height()));
            surface->setPosition(newPos);
        }
    }
}
