/*
    SPDX-FileCopyrightText: 2007 Dmitry Suzdalev <dimsuz@gmail.com>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "kgamepopupitem.h"

// KF
#include <KColorScheme>
#include <KStatefulBrush>
// Qt
#include <QGraphicsScene>
#include <QGraphicsTextItem>
#include <QGraphicsView>
#include <QIcon>
#include <QPainter>
#include <QTimeLine>
#include <QTimer>

// margin on the sides of message box
static const int MARGIN = 15;
// offset of message from start of the scene
static const int SHOW_OFFSET = 15;
// space between pixmap and text
static const int SOME_SPACE = 10;
// width of the border in pixels
static const qreal BORDER_PEN_WIDTH = 1.0;

class TextItemWithOpacity : public QGraphicsTextItem
{
    Q_OBJECT

public:
    TextItemWithOpacity(QGraphicsItem *parent = nullptr)
        : QGraphicsTextItem(parent)
        , m_opacity(1.0)
    {
    }
    void setOpacity(qreal opa)
    {
        m_opacity = opa;
    }
    void setTextColor(const KStatefulBrush &brush)
    {
        m_brush = brush;
    }
    void paint(QPainter *p, const QStyleOptionGraphicsItem *option, QWidget *widget) override;

Q_SIGNALS:
    void mouseClicked();

private:
    void mouseReleaseEvent(QGraphicsSceneMouseEvent *) override;

private:
    qreal m_opacity;
    KStatefulBrush m_brush;
};

void TextItemWithOpacity::paint(QPainter *p, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    // hope that it is ok to call this function here - i.e. I hope it won't be too expensive :)
    // we call it here (and not in setTextColor), because KstatefulBrush
    // absolutely needs QWidget parameter :)
    // NOTE from majewsky: For some weird reason, setDefaultTextColor does on some systems not check
    // whether the given color is equal to the one already set. Just calling setDefaultTextColor without
    // this check may result in an infinite loop of paintEvent -> setDefaultTextColor -> update -> paintEvent...
    const QColor textColor = widget ? m_brush.brush(widget->palette()).color() : QColor(Qt::black);
    if (textColor != defaultTextColor()) {
        setDefaultTextColor(textColor);
    }
    // render contents
    p->save();
    p->setOpacity(m_opacity);
    QGraphicsTextItem::paint(p, option, widget);
    p->restore();
}

void TextItemWithOpacity::mouseReleaseEvent(QGraphicsSceneMouseEvent *ev)
{
    // NOTE: this item is QGraphicsTextItem which "eats" mouse events
    // because of interaction with links. Because of that let's make a
    // special signal to indicate mouse click
    Q_EMIT mouseClicked();
    QGraphicsTextItem::mouseReleaseEvent(ev);
}

class KGamePopupItemPrivate
{
private:
    KGamePopupItemPrivate(const KGamePopupItemPrivate &);
    const KGamePopupItemPrivate &operator=(const KGamePopupItemPrivate &);

public:
    KGamePopupItemPrivate() = default;

public:
    /**
     * Timeline for animations
     */
    QTimeLine m_timeLine;
    /**
     * Timer used to start hiding
     */
    QTimer m_timer;
    /**
     * Holds bounding rect of an item
     */
    QRect m_boundRect;
    /**
     * Holds bounding rect of an item (mapped to scene coordinates)
     */
    QRectF m_mappedBoundRect;
    /**
     * Offset of message from start of the scene (mapped to scene coordinates)
     */
    qreal m_mapped_SHOW_OFFSET;
    /**
     * Position where item will appear
     */
    KGamePopupItem::Position m_position = KGamePopupItem::BottomLeft;
    /**
     * Timeout to stay visible on screen
     */
    int m_timeout = 2000;
    /**
     * Item opacity
     */
    qreal m_opacity = 1.0;
    /**
     * Opacity used while animating appearing in center
     */
    qreal m_animOpacity = -1;
    /**
     * Pixmap to display at the left of the text
     */
    QPixmap m_iconPix;
    /**
     * Set to true when mouse hovers the message
     */
    bool m_hoveredByMouse = false;
    /**
     * Set to true if this popup item hides on mouse click.
     */
    bool m_hideOnClick = true;
    /**
     * Child of KGamePopupItem used to display text
     */
    TextItemWithOpacity *m_textChildItem = nullptr;
    /**
     * Part of the scene that is actually visible in QGraphicsView
     * This is needed for item to work correctly when scene is larger than
     * the View
     */
    QRectF m_visibleSceneRect;
    /**
     * Background brush color
     */
    KStatefulBrush m_brush;
    /**
     * popup angles sharpness
     */
    KGamePopupItem::Sharpness m_sharpness = KGamePopupItem::Square;
    /**
     * painter path to draw a frame
     */
    QPainterPath m_path;
    /**
     * Indicates if some link is hovered in text item
     */
    bool m_linkHovered = false;
};

KGamePopupItem::KGamePopupItem(QGraphicsItem *parent)
    : QGraphicsItem(parent)
    , d_ptr(new KGamePopupItemPrivate)
{
    Q_D(KGamePopupItem);

    hide();
    d->m_textChildItem = new TextItemWithOpacity(this);
    d->m_textChildItem->setTextInteractionFlags(Qt::LinksAccessibleByMouse);
    // above call said to enable ItemIsFocusable which we don't need.
    // So disabling it
    d->m_textChildItem->setFlag(QGraphicsItem::ItemIsFocusable, false);

    connect(d->m_textChildItem, &TextItemWithOpacity::linkActivated, this, &KGamePopupItem::linkActivated);
    connect(d->m_textChildItem, &TextItemWithOpacity::linkHovered, this, &KGamePopupItem::onLinkHovered);
    connect(d->m_textChildItem, &TextItemWithOpacity::mouseClicked, this, &KGamePopupItem::onTextItemClicked);

    setZValue(100); // is 100 high enough???
    d->m_textChildItem->setZValue(100);

    QIcon infoIcon = QIcon::fromTheme(QStringLiteral("dialog-information"));
    // default size is 32
    setMessageIcon(infoIcon.pixmap(32, 32));

    d->m_timer.setSingleShot(true);

    setAcceptHoverEvents(true);
    // ignore scene transformations
    setFlag(QGraphicsItem::ItemIgnoresTransformations, true);

    // setup default colors
    d->m_brush = KStatefulBrush(KColorScheme::Tooltip, KColorScheme::NormalBackground);
    d->m_textChildItem->setTextColor(KStatefulBrush(KColorScheme::Tooltip, KColorScheme::NormalText));

    connect(&d->m_timeLine, &QTimeLine::frameChanged, this, &KGamePopupItem::animationFrame);
    connect(&d->m_timeLine, &QTimeLine::finished, this, &KGamePopupItem::hideMe);
    connect(&d->m_timer, &QTimer::timeout, this, &KGamePopupItem::playHideAnimation);
}

KGamePopupItem::~KGamePopupItem() = default;

void KGamePopupItem::paint(QPainter *p, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    Q_D(KGamePopupItem);

    Q_UNUSED(option);

    p->save();

    QPen pen = p->pen();
    pen.setWidthF(BORDER_PEN_WIDTH);
    p->setPen(pen);

    if (d->m_animOpacity != -1) // playing Center animation
    {
        p->setOpacity(d->m_animOpacity);
    } else {
        p->setOpacity(d->m_opacity);
    }
    p->setBrush(widget ? d->m_brush.brush(widget->palette()) : QBrush());
    p->drawPath(d->m_path);
    p->drawPixmap(MARGIN, static_cast<int>(d->m_boundRect.height() / 2) - d->m_iconPix.height() / 2.0 / d->m_iconPix.devicePixelRatio(), d->m_iconPix);
    p->restore();
}

void KGamePopupItem::showMessage(const QString &text, Position pos, ReplaceMode mode)
{
    Q_D(KGamePopupItem);

    if (d->m_timeLine.state() == QTimeLine::Running || d->m_timer.isActive()) {
        if (mode == ReplacePrevious) {
            forceHide(InstantHide);
        } else {
            return; // we're already showing a message
        }
    }

    // NOTE: we blindly take first visible view we found. I.e. we don't support
    // multiple views. If no visible scene is found, we simply pick the first one.
    QGraphicsView *sceneView = nullptr;
    const auto views = scene()->views();
    for (QGraphicsView *view : views) {
        if (view->isVisible()) {
            sceneView = view;
            break;
        }
    }
    if (!sceneView) {
        sceneView = views.at(0);
    }

    QPolygonF poly = sceneView->mapToScene(sceneView->viewport()->contentsRect());
    d->m_visibleSceneRect = poly.boundingRect();

    d->m_textChildItem->setHtml(text);

    d->m_position = pos;

    // do as QGS docs say: notify the scene about rect change
    prepareGeometryChange();

    // recalculate bounding rect
    const qreal iconDpr = d->m_iconPix.devicePixelRatio();
    qreal w = d->m_textChildItem->boundingRect().width() + MARGIN * 2 + d->m_iconPix.width() / iconDpr + SOME_SPACE;
    qreal h = d->m_textChildItem->boundingRect().height() + MARGIN * 2;
    if (d->m_iconPix.height() / iconDpr > h) {
        h = d->m_iconPix.height() / iconDpr + MARGIN * 2;
    }
    d->m_boundRect = QRect(0, 0, w, h);

    // adjust to take into account the width of the pen
    // used to draw the border
    const qreal borderRadius = BORDER_PEN_WIDTH / 2.0;
    d->m_boundRect.adjust(-borderRadius, -borderRadius, borderRadius, borderRadius);

    d->m_mappedBoundRect = sceneView->mapToScene(d->m_boundRect).boundingRect();
    d->m_mapped_SHOW_OFFSET = qAbs(sceneView->mapToScene(0, SHOW_OFFSET).y());

    QPainterPath roundRectPath;
    roundRectPath.moveTo(w, d->m_sharpness);
    roundRectPath.arcTo(w - (2 * d->m_sharpness), 0.0, (2 * d->m_sharpness), (d->m_sharpness), 0.0, 90.0);
    roundRectPath.lineTo(d->m_sharpness, 0.0);
    roundRectPath.arcTo(0.0, 0.0, (2 * d->m_sharpness), (2 * d->m_sharpness), 90.0, 90.0);
    roundRectPath.lineTo(0.0, h - (d->m_sharpness));
    roundRectPath.arcTo(0.0, h - (2 * d->m_sharpness), 2 * d->m_sharpness, 2 * d->m_sharpness, 180.0, 90.0);
    roundRectPath.lineTo(w - (d->m_sharpness), h);
    roundRectPath.arcTo(w - (2 * d->m_sharpness), h - (2 * d->m_sharpness), (2 * d->m_sharpness), (2 * d->m_sharpness), 270.0, 90.0);
    roundRectPath.closeSubpath();

    d->m_path = roundRectPath;

    // adjust y-pos of text item so it appears centered
    d->m_textChildItem->setPos(d->m_textChildItem->x(), d->m_boundRect.height() / 2 - d->m_textChildItem->boundingRect().height() / 2);

    // setup animation
    setupTimeline();

    // move to the start position
    animationFrame(d->m_timeLine.startFrame());
    show();
    d->m_timeLine.start();

    if (d->m_timeout != 0) {
        // 300 msec to animate showing message + d->m_timeout to stay visible => then hide
        d->m_timer.start(300 + d->m_timeout);
    }
}

void KGamePopupItem::setupTimeline()
{
    Q_D(KGamePopupItem);

    d->m_timeLine.setDirection(QTimeLine::Forward);
    d->m_timeLine.setDuration(300);
    if (d->m_position == TopLeft || d->m_position == TopRight) {
        int start = static_cast<int>(d->m_visibleSceneRect.top() - d->m_mappedBoundRect.height() - d->m_mapped_SHOW_OFFSET);
        int end = static_cast<int>(d->m_visibleSceneRect.top() + d->m_mapped_SHOW_OFFSET);
        d->m_timeLine.setFrameRange(start, end);
    } else if (d->m_position == BottomLeft || d->m_position == BottomRight) {
        int start = static_cast<int>(d->m_visibleSceneRect.bottom() + d->m_mapped_SHOW_OFFSET);
        int end = static_cast<int>(d->m_visibleSceneRect.bottom() - d->m_mappedBoundRect.height() - d->m_mapped_SHOW_OFFSET);
        d->m_timeLine.setFrameRange(start, end);
    } else if (d->m_position == Center) {
        d->m_timeLine.setFrameRange(0, d->m_timeLine.duration());
        setPos(d->m_visibleSceneRect.left() + d->m_visibleSceneRect.width() / 2 - d->m_mappedBoundRect.width() / 2,
               d->m_visibleSceneRect.top() + d->m_visibleSceneRect.height() / 2 - d->m_mappedBoundRect.height() / 2);
    }
}

void KGamePopupItem::animationFrame(int frame)
{
    Q_D(KGamePopupItem);

    if (d->m_position == TopLeft || d->m_position == BottomLeft) {
        setPos(d->m_visibleSceneRect.left() + d->m_mapped_SHOW_OFFSET, frame);
    } else if (d->m_position == TopRight || d->m_position == BottomRight) {
        setPos(d->m_visibleSceneRect.right() - d->m_mappedBoundRect.width() - d->m_mapped_SHOW_OFFSET, frame);
    } else if (d->m_position == Center) {
        d->m_animOpacity = frame * d->m_opacity / d->m_timeLine.duration();
        d->m_textChildItem->setOpacity(d->m_animOpacity);
        update();
    }
}

void KGamePopupItem::playHideAnimation()
{
    Q_D(KGamePopupItem);

    if (d->m_hoveredByMouse) {
        return;
    }
    // let's hide
    d->m_timeLine.setDirection(QTimeLine::Backward);
    d->m_timeLine.start();
}

void KGamePopupItem::setMessageTimeout(int msec)
{
    Q_D(KGamePopupItem);

    d->m_timeout = msec;
}

void KGamePopupItem::setHideOnMouseClick(bool hide)
{
    Q_D(KGamePopupItem);

    d->m_hideOnClick = hide;
}

bool KGamePopupItem::hidesOnMouseClick() const
{
    Q_D(const KGamePopupItem);

    return d->m_hideOnClick;
}

void KGamePopupItem::setMessageOpacity(qreal opacity)
{
    Q_D(KGamePopupItem);

    d->m_opacity = opacity;
    d->m_textChildItem->setOpacity(opacity);
}

QRectF KGamePopupItem::boundingRect() const
{
    Q_D(const KGamePopupItem);

    return d->m_boundRect;
}

void KGamePopupItem::hideMe()
{
    Q_D(KGamePopupItem);

    d->m_animOpacity = -1;
    // and restore child's opacity too
    d->m_textChildItem->setOpacity(d->m_opacity);

    // if we just got moved out of visibility, let's do more - let's hide :)
    if (d->m_timeLine.direction() == QTimeLine::Backward) {
        hide();
        Q_EMIT hidden();
    }
}

void KGamePopupItem::hoverEnterEvent(QGraphicsSceneHoverEvent *)
{
    Q_D(KGamePopupItem);

    d->m_hoveredByMouse = true;
}

void KGamePopupItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *)
{
    Q_D(KGamePopupItem);

    d->m_hoveredByMouse = false;

    if (d->m_timeout != 0 && !d->m_timer.isActive() && d->m_timeLine.state() != QTimeLine::Running) {
        playHideAnimation(); // let's hide
    }
}

void KGamePopupItem::setMessageIcon(const QPixmap &pix)
{
    Q_D(KGamePopupItem);

    d->m_iconPix = pix;
    d->m_textChildItem->setPos(MARGIN + pix.width() / pix.devicePixelRatio() + SOME_SPACE, MARGIN);
    // bounding rect is updated in showMessage()
}

int KGamePopupItem::messageTimeout() const
{
    Q_D(const KGamePopupItem);

    return d->m_timeout;
}

void KGamePopupItem::forceHide(HideType howToHide)
{
    Q_D(KGamePopupItem);

    if (!isVisible()) {
        return;
    }

    if (howToHide == InstantHide) {
        d->m_timeLine.stop();
        d->m_timer.stop();
        hide();
        Q_EMIT hidden();
    } else if (howToHide == AnimatedHide) {
        // forcefully unset it even if it is set
        // so we'll hide in any event
        d->m_hoveredByMouse = false;
        d->m_timer.stop();
        playHideAnimation();
    }
}

qreal KGamePopupItem::messageOpacity() const
{
    Q_D(const KGamePopupItem);

    return d->m_opacity;
}

void KGamePopupItem::setBackgroundBrush(const QBrush &brush)
{
    Q_D(KGamePopupItem);

    d->m_brush = KStatefulBrush(brush);
}

void KGamePopupItem::setTextColor(const QColor &color)
{
    Q_D(KGamePopupItem);

    KStatefulBrush brush(color, d->m_brush.brush(QPalette::Active));
    d->m_textChildItem->setTextColor(brush);
}

void KGamePopupItem::onLinkHovered(const QString &link)
{
    Q_D(KGamePopupItem);

    if (link.isEmpty()) {
        d->m_textChildItem->setCursor(Qt::ArrowCursor);
    } else {
        d->m_textChildItem->setCursor(Qt::PointingHandCursor);
    }

    d->m_linkHovered = !link.isEmpty();
    Q_EMIT linkHovered(link);
}

void KGamePopupItem::setSharpness(Sharpness sharpness)
{
    Q_D(KGamePopupItem);

    d->m_sharpness = sharpness;
}

KGamePopupItem::Sharpness KGamePopupItem::sharpness() const
{
    Q_D(const KGamePopupItem);

    return d->m_sharpness;
}

void KGamePopupItem::mousePressEvent(QGraphicsSceneMouseEvent *)
{
    // it is needed to reimplement this function to receive future
    // mouse release events
}

void KGamePopupItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *)
{
    Q_D(KGamePopupItem);

    // NOTE: text child item is QGraphicsTextItem which "eats" mouse events
    // because of interaction with links. Because of that TextItemWithOpacity has
    // special signal to indicate mouse click which we catch in a onTextItemClicked()
    // slot
    if (d->m_hideOnClick) {
        forceHide();
    }
}

void KGamePopupItem::onTextItemClicked()
{
    Q_D(KGamePopupItem);

    // if link is hovered we don't hide as click should go to the link
    if (d->m_hideOnClick && !d->m_linkHovered) {
        forceHide();
    }
}

#include "kgamepopupitem.moc" // For automocing TextItemWithOpacity
#include "moc_kgamepopupitem.cpp" // For automocing KGamePopupItem
