#include "TooltipTrigger.h"

#include <QApplication>
#include <QCursor>
#include <QEvent>
#include <QMouseEvent>
#include <QTimer>
#include <QWidget>

#include "Tooltip.h"

namespace
{

// How long the pointer rests on an element before its tooltip appears, in milliseconds.
// One delay for every tooltip in the UI, now that none of them is Qt's (REQ-UI-TOOLTIP-TRIGGER).
const int kHoverDelayMs = 500;

// How often a visible tooltip is carried after its element and tested against the
// pointer, in milliseconds. A frame's worth: the element it follows can move with the
// scrolling world (REQ-UI-EXPAND-BUTTON), and anything slower would visibly drag behind
// it. It runs only while a tooltip is up.
const int kTrackingIntervalMs = 16;

}  // namespace


void TooltipTrigger::attachText(QWidget& target, const QString& text, Trigger trigger)
{
    Tooltip* tooltip = new Tooltip(&target);
    tooltip->setText(text);
    attach(target, *tooltip, trigger);
}

void TooltipTrigger::attach(QWidget& target, Tooltip& tooltip, Trigger trigger)
{
    // Parented to the element, so both die with it.
    new TooltipTrigger(target, tooltip, trigger);
}

bool TooltipTrigger::eventFilter(QObject* watched, QEvent* event)
{
    if (watched != m_target)
    {
        return QObject::eventFilter(watched, event);
    }

    switch (event->type())
    {
        case QEvent::Enter:
        {
            m_hoverTimer->start();
            break;
        }
        case QEvent::Leave:
        {
            m_hoverTimer->stop();
            // Not a dismissal in itself: the pointer may be on its way onto the tooltip
            // (REQ-UI-TOOLTIP-DISMISS).
            hideUnlessPointerHeld();
            break;
        }
        case QEvent::MouseButtonPress:
        {
            const QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
            if (m_trigger != Trigger::HoverAndClick
                || mouseEvent->button() != Qt::LeftButton)
            {
                break;
            }
            m_hoverTimer->stop();
            // A click on an element whose tooltip is already up leaves it where it is
            // (REQ-UI-TOOLTIP-TRIGGER).
            if (!m_tooltip->isVisible())
            {
                showTooltip();
            }
            // Swallowed: the element does nothing with a click, and the panel beneath it
            // must not take it for one of its own (REQ-UI-SELECTION-PANEL-DRAG).
            return true;
        }
        case QEvent::Hide:
        {
            m_hoverTimer->stop();
            hideTooltip();
            break;
        }
        default:
        {
            break;
        }
    }

    return QObject::eventFilter(watched, event);
}

TooltipTrigger::TooltipTrigger(QWidget& target, Tooltip& tooltip, Trigger trigger)
    : QObject(&target)
    , m_target(&target)
    , m_tooltip(&tooltip)
    , m_trigger(trigger)
{
    m_hoverTimer = new QTimer(this);
    m_hoverTimer->setSingleShot(true);
    m_hoverTimer->setInterval(kHoverDelayMs);
    connect(m_hoverTimer, &QTimer::timeout, this, &TooltipTrigger::showTooltip);

    m_trackingTimer = new QTimer(this);
    m_trackingTimer->setInterval(kTrackingIntervalMs);
    connect(m_trackingTimer, &QTimer::timeout, this, [this]() {
        // Carried along first, so that a tooltip that has just been pulled out from under
        // the pointer is found to be out from under it on this same tick.
        followTarget();
        hideUnlessPointerHeld();
    });

    m_target->installEventFilter(this);
}

QRect TooltipTrigger::getTargetGlobalRect() const
{
    return QRect(m_target->mapToGlobal(QPoint(0, 0)), m_target->size());
}

void TooltipTrigger::showTooltip()
{
    // Already up: left where it is rather than moved to wherever the pointer has got to,
    // which is what a pointer wandering from the tooltip back onto the element would
    // otherwise do (REQ-UI-TOOLTIP-TRIGGER).
    if (!m_target->isVisible() || m_tooltip->isVisible())
    {
        return;
    }

    m_tooltip->showAt(QCursor::pos());
    m_targetGlobalRect = getTargetGlobalRect();
    m_trackingTimer->start();
}

void TooltipTrigger::hideTooltip()
{
    m_trackingTimer->stop();
    m_tooltip->hide();
}

void TooltipTrigger::followTarget()
{
    const QRect targetRect = getTargetGlobalRect();
    const QPoint travelled = targetRect.topLeft() - m_targetGlobalRect.topLeft();
    m_targetGlobalRect = targetRect;
    if (travelled.isNull())
    {
        return;
    }

    // The tooltip holds the place it was given in relation to its element rather than the
    // place it was given on the screen, so an element that travels -- the expansion button
    // riding the scrolling world (REQ-UI-EXPAND-BUTTON) -- takes its tooltip with it
    // instead of leaving it stranded beside where it used to be. Not clamped back onto
    // the screen this time: following the element off the edge is the point, and the
    // pointer left behind is what then hides it (REQ-UI-TOOLTIP-DISMISS).
    m_tooltip->move(m_tooltip->pos() + travelled);
}

void TooltipTrigger::hideUnlessPointerHeld()
{
    if (!m_tooltip->isVisible())
    {
        m_trackingTimer->stop();
        return;
    }

    // Gone with the element it explains. An element that merely moves is not a dismissal:
    // the tooltip travels with it (followTarget), and nothing but the pointer dismisses
    // one (REQ-UI-TOOLTIP-DISMISS).
    if (!m_target->isVisible())
    {
        hideTooltip();
        return;
    }

    // Gone with the game window: a tooltip is a topmost window and would otherwise stay
    // over whatever the player switched to.
    if (QApplication::activeWindow() == nullptr)
    {
        hideTooltip();
        return;
    }

    const QPoint pointer = QCursor::pos();
    if (!getTargetGlobalRect().contains(pointer)
        && !m_tooltip->getContainsGlobalPos(pointer))
    {
        hideTooltip();
    }
}
