// Copyright (C) 2018 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "timelineitem.h"

#include "timelineconstants.h"
#include "timelinegraphicsscene.h"
#include "timelineicons.h"
#include "timelinesectionitem.h"
#include "timelineutils.h"
#include "timelinewidget.h"

#include <theme.h>

#include <coreplugin/icore.h>

#include <QApplication>
#include <QCursor>
#include <QGraphicsView>
#include <QPainter>
#include <QPainterPath>

#include <cmath>

namespace QmlDesigner {

TimelineItem::TimelineItem(TimelineItem *parent)
    : QGraphicsWidget(parent)
{}

TimelineGraphicsScene *TimelineItem::timelineScene() const
{
    return qobject_cast<TimelineGraphicsScene *>(scene());
}

TimelineFrameHandle::TimelineFrameHandle(TimelineItem *parent)
    : TimelineMovableAbstractItem(parent)
{
    static const QColor color = Theme::getColor(Theme::IconsWarningToolBarColor);
    setBrush(color);
    setPen(color);

    setRect(0, 0, TimelineConstants::rulerHeight, TimelineConstants::rulerHeight);
    setZValue(40);
    setCursor(Qt::ClosedHandCursor);

    m_timer.setSingleShot(true);
    m_timer.setInterval(15);
    QObject::connect(&m_timer, &QTimer::timeout, [this] {
        if (QApplication::mouseButtons() == Qt::LeftButton)
            scrollOutOfBounds();
    });
}

void TimelineFrameHandle::setHeight(int height)
{
    setRect(rect().x(), rect().y(), rect().width(), height);
}

void TimelineFrameHandle::setPosition(qreal frame)
{
    const qreal scenePos = mapFromFrameToScene(frame);
    QRectF newRect(scenePos - rect().width() / 2, rect().y(), rect().width(), rect().height());

    if (!qFuzzyCompare(newRect.x(), rect().x())) {
        setRect(newRect);
    }
    m_position = frame;
}

void TimelineFrameHandle::setPositionInteractive(const QPointF &position)
{
    const double width = abstractScrollGraphicsScene()->width();

    if (position.x() > width) {
        callSetClampedXPosition(width - (rect().width() / 2) - 1);
        m_timer.start();
    } else if (position.x() < TimelineConstants::sectionWidth) {
        callSetClampedXPosition(TimelineConstants::sectionWidth);
        m_timer.start();
    } else {
        callSetClampedXPosition(position.x() - rect().width() / 2);
        const qreal frame = std::round(mapFromSceneToFrame(rect().center().x()));
        timelineGraphicsScene()->commitCurrentFrame(frame);
    }
}

void TimelineFrameHandle::commitPosition(const QPointF &point)
{
    setPositionInteractive(point);
}

void TimelineItem::drawLine(QPainter *painter, qreal x1, qreal y1, qreal x2, qreal y2)
{
    painter->drawLine(QPointF(x1 + 0.5, y1 + 0.5), QPointF(x2 + 0.5, y2 + 0.5));
}

qreal TimelineFrameHandle::position() const
{
    return m_position;
}

TimelineFrameHandle *TimelineFrameHandle::asTimelineFrameHandle()
{
    return this;
}

TimelineGraphicsScene *TimelineFrameHandle::timelineGraphicsScene() const
{
    return qobject_cast<TimelineGraphicsScene* >(abstractScrollGraphicsScene());
}

void TimelineFrameHandle::scrollOffsetChanged()
{
    setPosition(position());
}

QPainterPath TimelineFrameHandle::shape() const
{
    QPainterPath path;
    QRectF rect = boundingRect();
    rect.setHeight(TimelineConstants::sectionHeight);
    rect.adjust(-4, 0, 4, 0);
    path.addEllipse(rect);
    return path;
}

static int devicePixelWidth(const QPixmap &pixmap)
{
    return pixmap.width() / pixmap.devicePixelRatioF();
}

static int devicePixelHeight(const QPixmap &pixmap)
{
    return pixmap.height() / pixmap.devicePixelRatioF();
}

void TimelineFrameHandle::paint(QPainter *painter,
                                const QStyleOptionGraphicsItem * /*option*/,
                                QWidget * /*widget*/)
{
    static const QPixmap playHead = TimelineIcons::PLAYHEAD.pixmap();

    static const int pixmapHeight = devicePixelHeight(playHead);
    static const int pixmapWidth = devicePixelWidth(playHead);

    if (rect().x() < TimelineConstants::sectionWidth - rect().width() / 2)
        return;

    painter->save();
    painter->setOpacity(0.8);
    const qreal center = rect().width() / 2 + rect().x();

    painter->setPen(pen());

    auto offsetTop = pixmapHeight - 7;
    TimelineItem::drawLine(painter, center, offsetTop, center, rect().height() - 1);

    const QPointF pmTopLeft(center - pixmapWidth / 2, -4.);
    painter->drawPixmap(pmTopLeft, playHead);

    painter->restore();
}

QPointF TimelineFrameHandle::mapFromGlobal(const QPoint &pos) const
{
    for (auto *view : abstractScrollGraphicsScene()->views()) {
        if (view->objectName() == "SceneView") {
            auto graphicsViewCoords = view->mapFromGlobal(pos);
            auto sceneCoords = view->mapToScene(graphicsViewCoords);
            return sceneCoords;
        }
    }
    return {};
}

int TimelineFrameHandle::computeScrollSpeed() const
{
    const double mouse = mapFromGlobal(QCursor::pos()).x();
    const double width = abstractScrollGraphicsScene()->width();

    const double acc = mouse > width ? mouse - width
                                     : double(TimelineConstants::sectionWidth) - mouse;
    const double delta = TimelineUtils::clamp<double>(acc, 0., 200.);
    const double blend = TimelineUtils::reverseLerp(delta, 0., 200.);
    const double factor = TimelineUtils::lerp<double>(blend, 5, 20);

    if (mouse > width)
        return scrollOffset() + std::round(factor);
    else
        return scrollOffset() - std::round(factor);

    return 0;
}

void TimelineFrameHandle::callSetClampedXPosition(double x)
{
    const int minimumWidth = TimelineConstants::sectionWidth + TimelineConstants::timelineLeftOffset
                             - rect().width() / 2;
    const int maximumWidth = minimumWidth
                             + abstractScrollGraphicsScene()->rulerDuration() * abstractScrollGraphicsScene()->rulerScaling()
                             - scrollOffset();

    setClampedXPosition(x, minimumWidth, maximumWidth);
}

// Auto scroll when dragging playhead out of bounds.
void TimelineFrameHandle::scrollOutOfBounds()
{
    const double width = abstractScrollGraphicsScene()->width();
    const double mouse = mapFromGlobal(QCursor::pos()).x();

    if (mouse > width)
        scrollOutOfBoundsMax();
    else if (mouse < TimelineConstants::sectionWidth)
        scrollOutOfBoundsMin();
}

void TimelineFrameHandle::scrollOutOfBoundsMax()
{
    const double width = abstractScrollGraphicsScene()->width();
    if (QApplication::mouseButtons() == Qt::LeftButton) {
        abstractScrollGraphicsScene()->setScrollOffset(computeScrollSpeed());
        abstractScrollGraphicsScene()->invalidateScrollbar();

        callSetClampedXPosition(width - (rect().width() / 2) - 1);
        m_timer.start();
    } else {
        // Mouse release
        callSetClampedXPosition(width - (rect().width() / 2) - 1);

        const int frame = std::floor(mapFromSceneToFrame(rect().center().x()));
        const int ef = abstractScrollGraphicsScene()->endFrame();
        timelineGraphicsScene()->commitCurrentFrame(frame <= ef ? frame : ef);
    }
}

void TimelineFrameHandle::scrollOutOfBoundsMin()
{
    if (QApplication::mouseButtons() == Qt::LeftButton) {
        auto offset = computeScrollSpeed();

        if (offset >= 0)
            abstractScrollGraphicsScene()->setScrollOffset(offset);
        else
            abstractScrollGraphicsScene()->setScrollOffset(0);

        abstractScrollGraphicsScene()->invalidateScrollbar();

        callSetClampedXPosition(TimelineConstants::sectionWidth);
        m_timer.start();
    } else {
        // Mouse release
        callSetClampedXPosition(TimelineConstants::sectionWidth);

        int frame = mapFromSceneToFrame(rect().center().x());

        const int sframe = abstractScrollGraphicsScene()->startFrame();
        if (frame != sframe) {
            const qreal framePos = mapFromFrameToScene(frame);

            if (framePos
                <= (TimelineConstants::sectionWidth + TimelineConstants::timelineLeftOffset))
                frame++;
        }

        timelineGraphicsScene()->commitCurrentFrame(frame >= sframe ? frame : sframe);
    }
}

} // namespace QmlDesigner
