#include "Tooltip.h"

#include <QFontMetrics>
#include <QGuiApplication>
#include <QLabel>
#include <QLayoutItem>
#include <QPalette>
#include <QRect>
#include <QScreen>
#include <QVBoxLayout>

namespace
{

// Widest a text tooltip gets before it wraps, in device-independent pixels. A config
// description runs to a sentence or two and would otherwise be laid out as one line
// across the whole window.
const int kMaxTextWidthPx = 360;

// Height the wrap is measured against: high enough that it never cuts the text short.
const int kMeasureHeightPx = 10000;


// The screen the given position is on, falling back to the primary screen when it is on
// none of them (a position between two screens of different heights).
QRect availableScreenRect(const QPoint& globalPos)
{
    const QScreen* screen = QGuiApplication::screenAt(globalPos);
    if (screen == nullptr)
    {
        screen = QGuiApplication::primaryScreen();
    }
    return (screen != nullptr) ? screen->availableGeometry() : QRect();
}

}  // namespace


Tooltip* Tooltip::s_visibleTooltip = nullptr;

Tooltip* Tooltip::getVisibleTooltip()
{
    return s_visibleTooltip;
}

void Tooltip::hideVisibleTooltip()
{
    if (s_visibleTooltip != nullptr)
    {
        s_visibleTooltip->hide();
    }
}

Tooltip::Tooltip(QWidget* parent)
    : QFrame(parent, Qt::ToolTip)
{
    // The palette's tooltip colors behind a themed border, so it reads as a tooltip
    // rather than as a stray window.
    setFrameShape(QFrame::StyledPanel);
    setAutoFillBackground(true);
    QPalette tooltipPalette = palette();
    tooltipPalette.setColor(QPalette::Window,
                            tooltipPalette.color(QPalette::ToolTipBase));
    tooltipPalette.setColor(QPalette::WindowText,
                            tooltipPalette.color(QPalette::ToolTipText));
    setPalette(tooltipPalette);

    m_layout = new QVBoxLayout(this);
    m_layout->setContentsMargins(6, 4, 6, 4);
    m_layout->setSpacing(2);
}

Tooltip::~Tooltip()
{
    // Destroyed while on screen -- a chip whose panel is rebuilt takes its tooltip with
    // it (REQ-UI-SINGLE-SELECTION).
    if (s_visibleTooltip == this)
    {
        s_visibleTooltip = nullptr;
    }
}

void Tooltip::setText(const QString& text)
{
    clearContent();

    QLabel* label = new QLabel(text, this);
    // Plain text throughout: config strings are read as they are written, never as
    // markup Qt happens to recognize.
    label->setTextFormat(Qt::PlainText);
    label->setWordWrap(true);

    // The width the text needs, capped at the wrap measure. Measured rather than left to
    // the layout, so the label is laid out at exactly the width its wrapping assumed and
    // the tooltip ends up as tall as the wrapped text really is.
    const QFontMetrics metrics(label->font());
    const QRect wrapped = metrics.boundingRect(
        QRect(0, 0, kMaxTextWidthPx, kMeasureHeightPx), Qt::TextWordWrap, text);
    label->setFixedWidth(qMin(kMaxTextWidthPx, wrapped.width()));
    // The height that wrapping really needs, so a label whose own hint was computed for
    // the unwrapped text cannot leave the last lines clipped.
    label->setMinimumHeight(wrapped.height());

    m_layout->addWidget(label);
}

void Tooltip::showAt(const QPoint& globalPos)
{
    // Only ever one tooltip on screen (REQ-UI-TOOLTIP-TRIGGER).
    if (s_visibleTooltip != nullptr && s_visibleTooltip != this)
    {
        s_visibleTooltip->hide();
    }

    refreshContent();
    adjustSize();

    // Beside the pointer rather than under it: one pixel right of it, level with it
    // (REQ-UI-TOOLTIP-DISMISS). The one pixel is what leaves the pointer's own column
    // clear -- a tooltip is a window of its own, so a tooltip covering the pixel the
    // pointer rests on would take the next click off the element it belongs to -- and it
    // is only one so that the pointer reaches the tooltip without crossing a gap.
    const QRect screen = availableScreenRect(globalPos);
    QPoint topLeft(globalPos.x() + 1, globalPos.y());
    if (screen.isValid())
    {
        // Left of the pointer where there is no room to its right, keeping the same one
        // pixel of clearance: a tooltip near the right edge would otherwise be cut off.
        if (topLeft.x() + width() - 1 > screen.right())
        {
            topLeft.setX(globalPos.x() - width());
        }
        // Wider than the room on either side of the pointer: back onto the screen, over
        // the pointer, since being readable beats being clear of the pointer.
        topLeft.setX(qMax(screen.left(),
                          qMin(topLeft.x(), screen.right() - width() + 1)));
        // Vertically it is free to shift as far as it must, which is what the build
        // button bar's tooltips need, the bar sitting at the bottom of the view
        // (REQ-UI-BUILD-BAR): the tooltip stands clear of the pointer's column either
        // way, so no shift up or down can bring it over the pointer.
        topLeft.setY(qMax(screen.top(),
                          qMin(topLeft.y(), screen.bottom() - height() + 1)));
    }

    move(topLeft);
    show();
    raise();
    s_visibleTooltip = this;
}

bool Tooltip::getContainsGlobalPos(const QPoint& globalPos) const
{
    return isVisible() && frameGeometry().contains(globalPos);
}

void Tooltip::refreshContent()
{
}

QVBoxLayout* Tooltip::getContentLayout()
{
    return m_layout;
}

void Tooltip::clearContent()
{
    while (QLayoutItem* item = m_layout->takeAt(0))
    {
        if (item->widget() != nullptr)
        {
            // Out of the widget tree at once and destroyed later: a child merely taken
            // out of the layout would keep its geometry and show through the content
            // that replaces it.
            item->widget()->setParent(nullptr);
            item->widget()->deleteLater();
        }
        delete item;
    }
}

void Tooltip::hideEvent(QHideEvent* event)
{
    if (s_visibleTooltip == this)
    {
        s_visibleTooltip = nullptr;
    }
    QFrame::hideEvent(event);
}
