#pragma once

#include <vector>

#include <QColor>
#include <QPoint>
#include <QPointer>
#include <QRect>
#include <QWidget>

class ModalDialog;
class QMouseEvent;
class QPaintEvent;
class QScrollArea;

// The one surface every modal is shown on (REQ-UI-MODAL-CHROME, REQ-UI-MODAL-DIM): a
// child of the main window covering its whole rect, which paints the dim, hosts the
// open modal, and runs its modal loop. Because it is a widget of the window rather than
// a window of its own, it receives the clicks that land beside the modal -- which a
// blocked window would never see -- and that is what makes the click dismissal possible
// (REQ-UI-DIALOG-DISMISS).
//
// Modals nest: the layer keeps a stack, shows one dim for all of them, and hides itself
// when the last one closes. It is shown while the stack is non-empty or a hold is taken.
class ModalLayer : public QWidget
{
    Q_OBJECT

public:
    // The layer a widget is shown on, found by walking up its parents, or nullptr when
    // it is not on one. How a widget deep inside a modal reaches the layer to open a
    // second modal on it, without every widget between them having to carry a pointer.
    static ModalLayer* findFor(const QWidget& widget);

    ModalLayer(const QColor& dimColor, QWidget* parent);

    // Shows content on this layer and runs it until it accepts or rejects, returning its
    // QDialog result code. anchorRect (in this layer's coordinates, which are the main
    // window's) is what the content is centered on. A null rect centers it on the modal
    // it was opened from, and on the layer itself when it is the first one open
    // (REQ-UI-PANEL-MODAL).
    int execute(ModalDialog& content, const QRect& anchorRect = QRect());

    // Whether a modal is open. The main window asks before handing focus back to the
    // game world, which it must not do while a modal holds it.
    bool isActive() const;

    // Keeps the layer shown with nothing on it, so that one modal handing straight over
    // to another does not blink the dim off in between (REQ-UI-MODAL-DIM).
    void addHold();
    void removeHold();

    // Update the dim color (e.g. after a config reload on Restart, REQ-CFG-RELOAD).
    void setDimColor(const QColor& dimColor);

protected:
    void paintEvent(QPaintEvent* event) override;
    void mousePressEvent(QMouseEvent* event) override;
    void mouseReleaseEvent(QMouseEvent* event) override;

private:
    // A modal and the scroll area it is shown in. The host is what the layer places and
    // what the player sees the edges of, so it is also the rectangle "outside the modal"
    // is measured against.
    struct HostedModal
    {
        ModalDialog* content;
        QScrollArea* host;
        // What held the keyboard when this modal opened, given back when it closes.
        // Hiding the layer, or closing a modal a second one was opened from, leaves the
        // window with no focus widget at all, and a window with none answers no keys --
        // Escape included, which is what opens the menu (REQ-UI-GAME-MENU). A QPointer
        // because a modal may outlive what it took the focus from.
        QPointer<QWidget> focusBefore;
    };

    // Sizes content to what it asks for, capped at the layer, and centers it on
    // anchorRect (REQ-UI-PANEL-MODAL).
    void place(QScrollArea& host, ModalDialog& content, const QRect& anchorRect) const;

    void updateVisibility();

    // Whether a point in this layer's coordinates lies beyond the open modal. A click on
    // an inert part of the modal -- a label, the space between two controls -- arrives
    // here too, propagated by the widget that ignored it, so where the click landed is
    // what decides, never that the layer received it.
    bool isOutsideOpenModal(const QPoint& position) const;

    QColor                   m_dimColor;
    // Set by a left press that landed outside the open modal, so that only a press and a
    // release both outside dismiss it (REQ-UI-DIALOG-DISMISS).
    bool                     m_pressedOutside = false;
    std::vector<HostedModal> m_stack;  // bottom-most first; back() is the open one
    int                      m_holdCount = 0;
};

// RAII guard for addHold()/removeHold().
class ModalLayerHold
{
public:
    explicit ModalLayerHold(ModalLayer& layer)
        : m_layer(layer)
    {
        m_layer.addHold();
    }

    ~ModalLayerHold()
    {
        m_layer.removeHold();
    }

    ModalLayerHold(const ModalLayerHold&) = delete;
    ModalLayerHold& operator=(const ModalLayerHold&) = delete;

private:
    ModalLayer& m_layer;
};
