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

#include "transitioneditorgraphicsscene.h"

#include "transitioneditorgraphicslayout.h"
#include "transitioneditorpropertyitem.h"
#include "transitioneditorsectionitem.h"
#include "transitioneditortoolbar.h"
#include "transitioneditorview.h"
#include "transitioneditorwidget.h"

#include "timelineactions.h"
#include "timelineitem.h"
#include "timelinemovableabstractitem.h"
#include "timelinemovetool.h"
#include "timelineplaceholder.h"
#include "timelinepropertyitem.h"
#include "timelinesectionitem.h"
#include "timelineutils.h"

#include <auxiliarydataproperties.h>
#include <designdocumentview.h>
#include <exception.h>
#include <rewritertransaction.h>
#include <rewriterview.h>
#include <viewmanager.h>
#include <qmldesignerconstants.h>
#include <qmldesignerplugin.h>
#include <qmlobjectnode.h>
#include <qmltimelinekeyframegroup.h>

#include <bindingproperty.h>

#include <nodeabstractproperty.h>
#include <nodelistproperty.h>
#include <variantproperty.h>

#include <utils/algorithm.h>
#include <utils/qtcassert.h>

#include <utils/hostosinfo.h>

#include <QApplication>
#include <QComboBox>
#include <QGraphicsLinearLayout>
#include <QGraphicsProxyWidget>
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsView>
#include <QKeyEvent>

#include <cmath>

namespace QmlDesigner {

TransitionEditorGraphicsScene::TransitionEditorGraphicsScene(TransitionEditorWidget *parent)
    : AbstractScrollGraphicsScene(parent)
    , m_parent(parent)
    , m_layout(new TransitionEditorGraphicsLayout(this))
    , m_tools(this)
{
    addItem(m_layout);

    setSceneRect(m_layout->geometry());

    connect(m_layout, &QGraphicsWidget::geometryChanged, this, [this] {
        auto rect = m_layout->geometry();

        setSceneRect(rect);

        if (auto *gview = graphicsView())
            gview->setSceneRect(rect.adjusted(0, TimelineConstants::rulerHeight, 0, 0));

        if (auto *rview = rulerView())
            rview->setSceneRect(rect);
    });

    auto changeScale = [this](int factor) {
        transitionEditorWidget()->changeScaleFactor(factor);
        setZoom(factor);
    };
    connect(m_layout, &TransitionEditorGraphicsLayout::zoomChanged, changeScale);
}

TransitionEditorGraphicsScene::~TransitionEditorGraphicsScene()
{
    QSignalBlocker block(this);
    qDeleteAll(items());
}

void TransitionEditorGraphicsScene::invalidateScrollbar()
{
    double max = m_layout->maximumScrollValue();
    transitionEditorWidget()->setupScrollbar(0, max, scrollOffset());
    if (scrollOffset() > max)
        setScrollOffset(max);
}

void TransitionEditorGraphicsScene::onShow()
{
    emit m_layout->zoomChanged(zoom());
}

void TransitionEditorGraphicsScene::setTransition(const ModelNode &transition)
{
    clearSelection();
    m_layout->setTransition(transition);
}

void TransitionEditorGraphicsScene::clearTransition()
{
    m_transition = {};
    m_layout->setTransition({});
}

void TransitionEditorGraphicsScene::setWidth(int width)
{
    m_layout->setWidth(width);
    invalidateScrollbar();
}

void TransitionEditorGraphicsScene::invalidateLayout()
{
    m_layout->invalidate();
}

void TransitionEditorGraphicsScene::setDuration(int duration)
{
    m_transition.setAuxiliaryData(transitionDurationProperty, duration);
    m_layout->setDuration(duration);
    qreal scaling = m_layout->rulerScaling();
    setZoom(scaling);
}

int TransitionEditorGraphicsScene::zoom() const
{
    return m_layout->zoom();
}

qreal TransitionEditorGraphicsScene::rulerScaling() const
{
    return m_layout->rulerScaling();
}

int TransitionEditorGraphicsScene::rulerWidth() const
{
    return m_layout->rulerWidth();
}

qreal TransitionEditorGraphicsScene::rulerDuration() const
{
    return m_layout->rulerDuration();
}

qreal TransitionEditorGraphicsScene::endFrame() const
{
    return m_layout->endFrame();
}

qreal TransitionEditorGraphicsScene::startFrame() const
{
    return 0;
}

qreal TransitionEditorGraphicsScene::mapToScene(qreal x) const
{
    return TimelineConstants::sectionWidth + TimelineConstants::timelineLeftOffset
           + (x - startFrame()) * rulerScaling() - scrollOffset();
}

qreal TransitionEditorGraphicsScene::mapFromScene(qreal x) const
{
    auto xPosOffset = (x - TimelineConstants::sectionWidth - TimelineConstants::timelineLeftOffset)
                      + scrollOffset();

    return xPosOffset / rulerScaling() + startFrame();
}

void TransitionEditorGraphicsScene::setZoom(int scaleFactor)
{
    m_layout->setZoom(scaleFactor);

    setScrollOffset(0);
    invalidateSections();
    invalidateScrollbar();
    update();
}

void TransitionEditorGraphicsScene::setZoom(int scaling, double pivot)
{
    const qreal oldOffset = scrollOffset();
    const qreal oldScaling = m_layout->rulerScaling();
    const qreal oldPosition = mapToScene(pivot);
    m_layout->setZoom(scaling);

    const qreal newScaling = m_layout->rulerScaling();
    const qreal newPosition = mapToScene(pivot);

    const qreal newOffset = oldOffset + (newPosition - oldPosition);

    if (std::isinf(oldScaling) || std::isinf(newScaling))
        setScrollOffset(0);
    else {
        setScrollOffset(std::round(newOffset));

        const qreal start = mapToScene(startFrame());
        const qreal head = TimelineConstants::sectionWidth + TimelineConstants::timelineLeftOffset;

        if (start - head > 0)
            setScrollOffset(0);
    }

    invalidateSections();
    invalidateScrollbar();
    update();
}

void TransitionEditorGraphicsScene::invalidateSectionForTarget(const ModelNode &target)
{
    if (!target.isValid())
        return;

    bool found = false;

    const QList<QGraphicsItem *> items = m_layout->childItems();
    for (auto child : items)
        TransitionEditorSectionItem::updateDataForTarget(child, target, &found);

    if (!found)
        invalidateScene();

    clearSelection();
    invalidateLayout();
}

void TransitionEditorGraphicsScene::invalidateHeightForTarget(const ModelNode &target)
{
    if (!target.isValid())
        return;

    const auto children = m_layout->childItems();
    for (auto child : children)
        TransitionEditorSectionItem::updateHeightForTarget(child, target);

    invalidateLayout();
}

void TransitionEditorGraphicsScene::invalidateScene()
{
    invalidateScrollbar();
}

void TransitionEditorGraphicsScene::invalidateCurrentValues()
{
    const QList<QGraphicsItem *> constItems = items();
    for (auto item : constItems)
        TimelinePropertyItem::updateTextEdit(item);
}

QGraphicsView *TransitionEditorGraphicsScene::graphicsView() const
{
    const QList<QGraphicsView *> constViews = views();
    for (auto *v : constViews)
        if (v->objectName() == "SceneView")
            return v;

    return nullptr;
}

QGraphicsView *TransitionEditorGraphicsScene::rulerView() const
{
    const QList<QGraphicsView *> constViews = views();
    for (auto *v : constViews)
        if (v->objectName() == "RulerView")
            return v;

    return nullptr;
}

QRectF TransitionEditorGraphicsScene::selectionBounds() const
{
    QRectF bbox;

    return bbox;
}

void TransitionEditorGraphicsScene::clearSelection()
{
    if (m_selectedProperty && items().contains(m_selectedProperty))
        m_selectedProperty->update();

    m_selectedProperty = nullptr;
    AbstractScrollGraphicsScene::clearSelection();
}

QList<QGraphicsItem *> TransitionEditorGraphicsScene::itemsAt(const QPointF &pos)
{
    QTransform transform;

    if (auto *gview = graphicsView())
        transform = gview->transform();

    return items(pos, Qt::IntersectsItemShape, Qt::DescendingOrder, transform);
}

void TransitionEditorGraphicsScene::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    auto topItem = TimelineMovableAbstractItem::topMoveableItem(itemsAt(event->scenePos()));

    m_tools.mousePressEvent(topItem, event);
    QGraphicsScene::mousePressEvent(event);
}

void TransitionEditorGraphicsScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
    auto topItem = TimelineMovableAbstractItem::topMoveableItem(itemsAt(event->scenePos()));
    m_tools.mouseMoveEvent(topItem, event);
    QGraphicsScene::mouseMoveEvent(event);
}

void TransitionEditorGraphicsScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
    auto topItem = TimelineMovableAbstractItem::topMoveableItem(itemsAt(event->scenePos()));
    /* The tool has handle the event last. */
    QGraphicsScene::mouseReleaseEvent(event);
    m_tools.mouseReleaseEvent(topItem, event);
}

void TransitionEditorGraphicsScene::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
    auto topItem = TimelineMovableAbstractItem::topMoveableItem(itemsAt(event->scenePos()));
    m_tools.mouseDoubleClickEvent(topItem, event);
    QGraphicsScene::mouseDoubleClickEvent(event);
}

void TransitionEditorGraphicsScene::keyPressEvent(QKeyEvent *keyEvent)
{
    if (qgraphicsitem_cast<QGraphicsProxyWidget *>(focusItem())) {
        keyEvent->ignore();
        QGraphicsScene::keyPressEvent(keyEvent);
        return;
    }

    if (keyEvent->modifiers().testFlag(Qt::ControlModifier)) {
        QGraphicsScene::keyPressEvent(keyEvent);
    } else {
        switch (keyEvent->key()) {
        case Qt::Key_Left:
            emit scroll(TimelineUtils::Side::Left);
            keyEvent->accept();
            break;

        case Qt::Key_Right:
            emit scroll(TimelineUtils::Side::Right);
            keyEvent->accept();
            break;

        default:
            QGraphicsScene::keyPressEvent(keyEvent);
            break;
        }
    }
}

void TransitionEditorGraphicsScene::keyReleaseEvent(QKeyEvent *keyEvent)
{
    if (qgraphicsitem_cast<QGraphicsProxyWidget *>(focusItem())) {
        keyEvent->ignore();
        QGraphicsScene::keyReleaseEvent(keyEvent);
        return;
    }

    QGraphicsScene::keyReleaseEvent(keyEvent);
}

void TransitionEditorGraphicsScene::focusOutEvent(QFocusEvent *focusEvent)
{
    QmlDesignerPlugin::emitUsageStatisticsTime(Constants::EVENT_TRANSITIONEDITOR_TIME,
                                               m_usageTimer.elapsed());
    QGraphicsScene::focusOutEvent(focusEvent);
}

void TransitionEditorGraphicsScene::focusInEvent(QFocusEvent *focusEvent)
{
    m_usageTimer.restart();
    QGraphicsScene::focusInEvent(focusEvent);
}

void TransitionEditorGraphicsScene::invalidateSections()
{
    const QList<QGraphicsItem *> children = m_layout->childItems();
    for (auto child : children)
        TransitionEditorSectionItem::updateData(child);

    clearSelection();
    invalidateLayout();
}

TimelineRulerSectionItem *TransitionEditorGraphicsScene::layoutRuler() const
{
    return m_layout->ruler();
}

TransitionEditorView *TransitionEditorGraphicsScene::transitionEditorView() const
{
    return m_parent->transitionEditorView();
}

TransitionEditorWidget *TransitionEditorGraphicsScene::transitionEditorWidget() const
{
    return m_parent;
}

TransitionEditorToolBar *TransitionEditorGraphicsScene::toolBar() const
{
    return transitionEditorWidget()->toolBar();
}

void TransitionEditorGraphicsScene::activateLayout()
{
    m_layout->activate();
}

AbstractView *TransitionEditorGraphicsScene::abstractView() const
{
    return transitionEditorView();
}

bool TransitionEditorGraphicsScene::event(QEvent *event)
{
    switch (event->type()) {
    case QEvent::ShortcutOverride:
        if (TimelineUtils::isDeleteKey(static_cast<QKeyEvent *>(event)->key())) {
            QGraphicsScene::keyPressEvent(static_cast<QKeyEvent *>(event));
            event->accept();
            return true;
        }
        Q_FALLTHROUGH();
    default:
        return QGraphicsScene::event(event);
    }
}

ModelNode TransitionEditorGraphicsScene::transitionModelNode() const
{
    if (transitionEditorView()->isAttached()) {
        const QString timelineId = transitionEditorWidget()->toolBar()->currentTransitionId();
        return transitionEditorView()->modelNodeForId(timelineId);
    }

    return ModelNode();
}

TransitionEditorPropertyItem *TransitionEditorGraphicsScene::selectedPropertyItem() const
{
    return m_selectedProperty;
}

void TransitionEditorGraphicsScene::setSelectedPropertyItem(TransitionEditorPropertyItem *item)
{
    if (m_selectedProperty && items().contains(m_selectedProperty))
        m_selectedProperty->update();
    m_selectedProperty = item;
    emit selectionChanged();
}

} // namespace QmlDesigner
