#include "ModalLayer.h"

#include <cstddef>

#include <QApplication>
#include <QEventLoop>
#include <QFrame>
#include <QMetaObject>
#include <QMouseEvent>
#include <QPainter>
#include <QPoint>
#include <QScrollArea>
#include <QSize>

#include "ModalDialog.h"

ModalLayer* ModalLayer::findFor(const QWidget& widget)
{
    for (QWidget* candidate = widget.parentWidget(); candidate != nullptr;
         candidate = candidate->parentWidget())
    {
        ModalLayer* layer = qobject_cast<ModalLayer*>(candidate);
        if (layer != nullptr)
        {
            return layer;
        }
    }
    return nullptr;
}

ModalLayer::ModalLayer(const QColor& dimColor, QWidget* parent)
    : QWidget(parent)
    , m_dimColor(dimColor)
{
    hide();
}

int ModalLayer::execute(ModalDialog& content, const QRect& anchorRect)
{
    // Hosted in a scroll area so a modal larger than the window is reached by scrolling
    // instead of being cut off by the window edge (REQ-UI-MODAL-CHROME). When the
    // content fits -- which is the normal case -- the host is exactly its size and no
    // scroll bar appears, so the player sees the modal alone.
    // Noted before anything here touches the focus, and given back when this modal
    // closes: the game world beneath for the first modal of a stack, and the modal it was
    // opened from for one above that.
    QWidget* const focusBefore = QApplication::focusWidget();

    QScrollArea* host = new QScrollArea(this);
    host->setFrameShape(QFrame::NoFrame);
    host->setWidgetResizable(false);
    host->setWidget(&content);

    // Resolved before the new modal joins the stack, so a null rect means the modal it
    // was opened from -- the Create Blueprint dialog opens on the layout dialog beneath
    // it (REQ-UI-PANEL-MODAL).
    const QRect anchor = !anchorRect.isNull()   ? anchorRect
                       : (m_stack.empty()       ? rect()
                                                : m_stack.back().host->geometry());

    m_stack.push_back(HostedModal{ &content, host, focusBefore });

    // Placed and shown before the layer itself is, so that the frame the layer first
    // paints already carries the modal. Showing the layer first put the dim on screen a
    // frame before anything stood on it, which read as a black flash
    // (REQ-UI-MODAL-DIM). Placing works while the layer is hidden: its geometry is the
    // window's whether or not it is shown, and a child shown under a hidden parent
    // appears with it.
    raise();
    place(*host, content, anchor);
    host->show();
    content.show();
    updateVisibility();

    // The widget the modal names where it names one -- a name dialog puts the caret in
    // its line edit -- and the modal itself otherwise, so keys reach it and not the game
    // world behind (REQ-UI-MODAL-CHROME).
    QWidget* focusTarget = content.getInitialFocusWidget();
    if (focusTarget == nullptr)
    {
        focusTarget = &content;
    }
    focusTarget->setFocus();

    // The dialog's own loop, run here rather than by QDialog::exec(), so the layer knows
    // what is open and can place it, dim behind it, and take the clicks beside it.
    QEventLoop loop;
    const QMetaObject::Connection connection =
        connect(&content, &QDialog::finished, &loop, &QEventLoop::quit);
    loop.exec();
    disconnect(connection);

    // takeWidget() before the host goes: the scroll area owns what it is given, and
    // every modal here is a local of its caller. It hands the content back parentless.
    host->takeWidget();
    content.hide();
    delete host;

    QPointer<QWidget> focusAfter;
    for (std::size_t i = m_stack.size(); i > 0; --i)
    {
        if (m_stack[i - 1].content == &content)
        {
            focusAfter = m_stack[i - 1].focusBefore;
            m_stack.erase(m_stack.begin() + static_cast<std::ptrdiff_t>(i - 1));
            break;
        }
    }
    updateVisibility();

    // After the layer is hidden, not before: hiding a widget takes the focus off
    // everything on it, which would undo this again.
    if (!focusAfter.isNull())
    {
        focusAfter->setFocus();
    }
    return content.result();
}

bool ModalLayer::isActive() const
{
    return !m_stack.empty();
}

void ModalLayer::addHold()
{
    ++m_holdCount;
    updateVisibility();
}

void ModalLayer::removeHold()
{
    if (m_holdCount > 0)
    {
        --m_holdCount;
        updateVisibility();
    }
}

void ModalLayer::setDimColor(const QColor& dimColor)
{
    m_dimColor = dimColor;
    if (isVisible())
    {
        update();
    }
}

void ModalLayer::paintEvent(QPaintEvent* /*event*/)
{
    // One dim however many modals are stacked (REQ-UI-MODAL-DIM): the modals above it
    // draw their own opaque background, so nesting darkens nothing twice.
    QPainter painter(this);
    painter.fillRect(rect(), m_dimColor);
}

void ModalLayer::mousePressEvent(QMouseEvent* event)
{
    m_pressedOutside = event->button() == Qt::LeftButton
                       && isOutsideOpenModal(event->pos());

    // Taken whether or not it dismisses anything: the window behind a modal receives no
    // input, and closing the modal does not turn this click into one for what lies under
    // it (REQ-UI-DIALOG-DISMISS).
    event->accept();
}

void ModalLayer::mouseReleaseEvent(QMouseEvent* event)
{
    const bool pressedOutside = m_pressedOutside;
    m_pressedOutside = false;
    event->accept();

    if (event->button() != Qt::LeftButton || !pressedOutside
        || !isOutsideOpenModal(event->pos()) || m_stack.empty())
    {
        return;
    }

    // A drag that began inside the modal never gets here -- the widget it began on keeps
    // the release -- so no gesture ends by discarding the modal it was made in.
    ModalDialog* openModal = m_stack.back().content;
    if (openModal->isDismissible())
    {
        openModal->requestDismiss();
    }
}

bool ModalLayer::isOutsideOpenModal(const QPoint& position) const
{
    if (m_stack.empty())
    {
        return true;
    }
    return !m_stack.back().host->geometry().contains(position);
}

void ModalLayer::place(QScrollArea& host, ModalDialog& content,
                       const QRect& anchorRect) const
{
    // The content has never been shown, so it is still at its default size until its
    // layout has run; sizing it before that would use the wrong extent.
    content.adjustSize();

    const QSize hostSize = content.size().boundedTo(size());
    host.resize(hostSize);

    QPoint topLeft(anchorRect.center().x() - hostSize.width() / 2,
                   anchorRect.center().y() - hostSize.height() / 2);

    // Pushed back inside the layer, which is the game window (REQ-UI-PANEL-MODAL). The
    // far edge is clamped first and the near edge second, which is what aligns a modal
    // as large as the window with the window's top-left corner rather than pushing it
    // off the opposite edge.
    topLeft.setX(qMax(0, qMin(topLeft.x(), width()  - hostSize.width())));
    topLeft.setY(qMax(0, qMin(topLeft.y(), height() - hostSize.height())));
    host.move(topLeft);
}

void ModalLayer::updateVisibility()
{
    const bool shouldShow = !m_stack.empty() || m_holdCount > 0;
    if (shouldShow == isVisible())
    {
        return;
    }

    if (shouldShow)
    {
        raise();
        show();
        // Painted at once rather than at the next paint event, so no undimmed frame of
        // the world flashes through in between. The modal standing on the layer is
        // already placed and shown by now, so this one paint puts up both (execute()).
        repaint();
    }
    else
    {
        hide();
    }
}
