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

#include "connectableitem.h"
#include "cornergrabberitem.h"
#include "graphicsitemprovider.h"
#include "sceneutils.h"
#include "scxmldocument.h"
#include "scxmleditorconstants.h"
#include "scxmleditortr.h"
#include "scxmltagutils.h"
#include "scxmluifactory.h"
#include "serializer.h"
#include "stateitem.h"
#include "tagtextitem.h"
#include "transitionitem.h"

#include <QBrush>
#include <QDebug>
#include <QGraphicsScene>
#include <QMenu>
#include <QPainter>
#include <QPen>
#include <QUndoStack>
#include <QtMath>

using namespace ScxmlEditor::PluginInterface;

const qreal SELECTION_DISTANCE = 10;

TransitionItem::TransitionItem(BaseItem *parent)
    : BaseItem(parent)
    , m_startTargetFactor(0.5, 0.5)
    , m_endTargetFactor(0.5, 0.5)
{
    setFlag(ItemIsSelectable, true);

    m_highlightPen = QPen(QColor(0xff, 0x00, 0x60));
    m_highlightPen.setWidth(8);
    m_highlightPen.setJoinStyle(Qt::MiterJoin);

    m_pen = QPen(QColor(0x12, 0x12, 0x12));
    m_pen.setWidth(2);

    m_arrow << QPointF(0, 0)
            << QPointF(1, 1)
            << QPointF(0, 1);

    m_eventTagItem = new TagTextItem(this);
    connect(m_eventTagItem, &TagTextItem::selected, this, [this](bool sel) {
        setItemSelected(sel);
    });
    connect(m_eventTagItem, &TagTextItem::textReady, this, &TransitionItem::textHasChanged);
    connect(m_eventTagItem, &TagTextItem::movePointChanged, this, &TransitionItem::textItemPositionChanged);

    checkWarningItems();
}

TransitionItem::~TransitionItem()
{
    setBlockUpdates(true);
    removeTransition(Start);
    removeTransition(End);
    //updateTarget();
}

void TransitionItem::checkWarningItems()
{
    ScxmlUiFactory *uifactory = uiFactory();
    if (uifactory) {
        auto provider = static_cast<GraphicsItemProvider*>(uifactory->object("graphicsItemProvider"));
        if (provider) {
            if (!m_warningItem)
                m_warningItem = static_cast<TransitionWarningItem*>(provider->createWarningItem(Constants::C_STATE_WARNING_TRANSITION, this));
        }
    }
}

void TransitionItem::setTag(ScxmlTag *tag)
{
    BaseItem::setTag(tag);
    if (tag) {
        if (tag->tagType() == InitialTransition)
            m_eventTagItem->setVisible(false);
    }
}

void TransitionItem::textItemPositionChanged()
{
    QPointF p = m_eventTagItem->movePoint();
    QString data;
    if (p.toPoint() != QPoint(0, 0)) {
        Serializer s;
        s.append(p);
        data = s.data();
    }
    setEditorInfo(Constants::C_SCXML_EDITORINFO_MOVEPOINT, data);

    updateComponents();
}

void TransitionItem::textHasChanged(const QString &text)
{
    setTagValue("event", text);
}

void TransitionItem::createGrabbers()
{
    if (m_cornerGrabbers.count() != m_cornerPoints.count()) {
        int selectedGrabberIndex = m_cornerGrabbers.indexOf(m_selectedCornerGrabber);

        if (!m_cornerGrabbers.isEmpty()) {
            qDeleteAll(m_cornerGrabbers);
            m_cornerGrabbers.clear();
        }

        for (int i = 0; i < m_cornerPoints.count(); ++i) {
            auto cornerGrabber = new CornerGrabberItem(this);
            cornerGrabber->setGrabberType(CornerGrabberItem::Circle);
            m_cornerGrabbers << cornerGrabber;
        }

        if (selectedGrabberIndex >= 0 && selectedGrabberIndex < m_cornerGrabbers.count())
            m_selectedCornerGrabber = m_cornerGrabbers[selectedGrabberIndex];
        else
            m_selectedCornerGrabber = nullptr;
    }

    m_pen.setStyle(Qt::DotLine);

    m_lineSelected = true;
    updateGrabberPositions();
}

void TransitionItem::removeGrabbers()
{
    if (!m_cornerGrabbers.isEmpty()) {
        qDeleteAll(m_cornerGrabbers);
        m_cornerGrabbers.clear();
    }

    m_lineSelected = false;
    m_pen.setStyle(Qt::SolidLine);
}

void TransitionItem::updateGrabberPositions()
{
    for (int i = 0; i < qMin(m_cornerGrabbers.count(), m_cornerPoints.count()); ++i)
        m_cornerGrabbers[i]->setPos(m_cornerPoints[i]);
}

void TransitionItem::removeUnnecessaryPoints()
{
    if (m_cornerPoints.count() > 2) {
        bool found = true;
        while (found) {
            found = false;
            for (int i = 1; i < (m_cornerPoints.count() - 1); ++i) {
                if (QLineF(m_cornerPoints[i], m_cornerPoints[i + 1]).length() <= 20 || QLineF(m_cornerPoints[i], m_cornerPoints[i - 1]).length() <= 20) {
                    m_cornerPoints.takeAt(i);
                    if (i < m_cornerGrabbers.count())
                        delete m_cornerGrabbers.takeAt(i);
                    found = true;
                    break;
                }
            }
        }
    }
    storeValues();
    updateComponents();
}

QVariant TransitionItem::itemChange(GraphicsItemChange change, const QVariant &value)
{
    QVariant retValue = BaseItem::itemChange(change, value);

    switch (change) {
    case ItemSelectedChange:
        if (!m_mouseGrabbed) {
            if (value.toBool())
                createGrabbers();
            else
                removeGrabbers();
        }
        break;
    case ItemSceneHasChanged:
        checkWarningItems();
        break;
    default:
        break;
    }

    return retValue;
}

void TransitionItem::snapToAnyPoint(int id, const QPointF &newPoint, int diff)
{
    // Check snap to grid
    bool snappedX = false;
    bool snappedY = false;
    for (int i = 0; i < m_cornerPoints.count(); ++i) {
        if (id != i) {
            if (qAbs(newPoint.x() - m_cornerPoints[i].x()) < diff) {
                m_cornerPoints[id].setX(m_cornerPoints[i].x());
                snappedX = true;
            }
            if (qAbs(newPoint.y() - m_cornerPoints[i].y()) < diff) {
                m_cornerPoints[id].setY(m_cornerPoints[i].y());
                snappedY = true;
            }
        }
    }

    if (!snappedX)
        m_cornerPoints[id].setX(newPoint.x());

    if (!snappedY)
        m_cornerPoints[id].setY(newPoint.y());
}

void TransitionItem::snapPointToPoint(int idSnap, const QPointF &p, int diff)
{
    if (idSnap >= 0 && static_cast<uint>(idSnap) < static_cast<uint>(m_cornerPoints.count())) {
        if (qAbs(p.x() - m_cornerPoints[idSnap].x()) < diff)
            m_cornerPoints[idSnap].setX(p.x());
        if (qAbs(p.y() - m_cornerPoints[idSnap].y()) < diff)
            m_cornerPoints[idSnap].setY(p.y());
    }
}

void TransitionItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    // We want to pan scene when Shift is pressed -> that's why ignore mouse-event here
    if (event->modifiers() & Qt::ShiftModifier) {
        event->ignore();
        return;
    }

    QPointF p = event->pos();
    bool bLeftButton = event->button() == Qt::LeftButton;
    if (m_mouseGrabbed) {
        if (bLeftButton) {
            m_cornerPoints.append(p);
            snapToAnyPoint(m_cornerPoints.count() - 1, p);

            if (!m_cornerGrabbers.isEmpty()) {
                auto corner = new CornerGrabberItem(this);
                corner->setGrabberType(CornerGrabberItem::Circle);
                corner->setPos(p);
                m_cornerGrabbers.append(corner);
            }
        }
        event->accept();
    } else {
        // Check QuickTransition
        if (bLeftButton) {
            // If we found QuickTransition-item or CornerGrabber at this point, we must ignore mouse press here
            // So we can press QuickTransition/CornerGrabber item although there is transition lines front of these items
            const QList<QGraphicsItem *> items = scene()->items(event->scenePos());
            for (QGraphicsItem *item : items) {
                if (item->type() == QuickTransitionType || (item->type() == CornerGrabberType && item->parentItem() != this)) {
                    event->ignore();
                    return;
                }
            }
        }

        // Check selection
        bool sel = m_lineSelected;
        int i;
        int foundPointIndex = -1;
        for (i = 0; i < m_cornerPoints.count(); ++i) {
            if (QLineF(m_cornerPoints[i], p).length() <= SELECTION_DISTANCE) {
                // Is pressed point close enough of the elbow-point
                foundPointIndex = i;
                sel = true;
                break;
            } else {
                if (i < m_cornerPoints.count() - 1) {
                    QLineF line(m_cornerPoints[i], m_cornerPoints[i + 1]);

                    // Is pressed point close enough of the line
                    QPointF intersPoint;
                    QLineF line2(p, p + QPointF(SELECTION_DISTANCE, SELECTION_DISTANCE));
                    line2.setAngle(line.angle() + 90);
                    if (line.intersects(line2, &intersPoint) == QLineF::BoundedIntersection) {
                        sel = true;
                    } else {
                        line2.setAngle(line.angle() - 90);
                        sel = line.intersects(line2, &intersPoint) == QLineF::BoundedIntersection;
                    }

                    if (sel)
                        break;
                }
            }
        }

        // Create or remove grabbers
        if (sel != m_lineSelected) {
            if (sel)
                createGrabbers();
            else
                removeGrabbers();

            if (foundPointIndex > 0 && foundPointIndex < m_cornerGrabbers.count()) {
                m_selectedCornerGrabber = m_cornerGrabbers[foundPointIndex];
                m_selectedCornerGrabber->setSelected(true);
            }
        } else if (m_lineSelected && bLeftButton) {
            m_cornerPoints.insert((i + 1), p);
            m_selectedCornerGrabber = new CornerGrabberItem(this);
            m_selectedCornerGrabber->setGrabberType(CornerGrabberItem::Circle);
            m_selectedCornerGrabber->setPos(p);
            m_cornerGrabbers.insert((i + 1), m_selectedCornerGrabber);
            m_selectedCornerGrabber->setSelected(true);
        } else if (m_lineSelected && !bLeftButton) {
            showContextMenu(event);
        }

        if (sel)
            event->accept();
        else
            event->ignore();
    }
}

void TransitionItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
    // We want to pan scene when Shift is pressed -> that's why ignore mouse-event here
    if (event->modifiers() & Qt::ShiftModifier) {
        event->ignore();
        return;
    }

    if (m_mouseGrabbed) {
        setEndPos(event->pos());
        event->ignore();
    } else if (m_selectedCornerGrabber) {
        snapToAnyPoint(m_cornerGrabbers.indexOf(m_selectedCornerGrabber), event->pos());
        updateComponents();
        storeValues();
        BaseItem::mouseMoveEvent(event);
    }
}

void TransitionItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
    // We want to pan scene when Shift is pressed -> that's why ignore mouse-event here
    if (event->modifiers() & Qt::ShiftModifier) {
        event->ignore();
        return;
    }

    if (m_mouseGrabbed) {
        if (event->button() == Qt::RightButton) {
            connectToTopItem(mapToScene(event->pos()), TransitionItem::End, m_grabbedTargetType);
            setSelected(false);
            tag()->document()->undoStack()->endMacro();
            m_mouseGrabbed = false;
            ungrabMouse();
            storeValues();
        }
        event->accept();
    } else {
        if (event->button() == Qt::LeftButton) {
            if (m_selectedCornerGrabber) {
                m_selectedCornerGrabber = nullptr;
                setSelected(true);
            }
            removeUnnecessaryPoints();
        }
        BaseItem::mouseReleaseEvent(event);
    }
}

void TransitionItem::checkSelectionBeforeContextMenu(QGraphicsSceneMouseEvent *e)
{
    int ind = -1;
    for (int i = 0; i < m_cornerGrabbers.count(); ++i) {
        if (m_cornerGrabbers[i]->isSelected()) {
            ind = i;
            break;
        }
    }
    m_selectedGrabberIndex = ind;
    BaseItem::checkSelectionBeforeContextMenu(e);
}

void TransitionItem::createContextMenu(QMenu *menu)
{
    QVariantMap data;
    if (m_selectedGrabberIndex > 0) {
        data[Constants::C_SCXMLTAG_ACTIONTYPE] = TagUtils::RemovePoint;
        data["cornerIndex"] = m_selectedGrabberIndex;
        menu->addAction(Tr::tr("Remove Point"))->setData(data);
    }

    menu->addSeparator();
    BaseItem::createContextMenu(menu);
}

void TransitionItem::selectedMenuAction(const QAction *action)
{
    if (action) {
        QVariantMap data = action->data().toMap();
        int actionType = data.value(Constants::C_SCXMLTAG_ACTIONTYPE, -1).toInt();

        switch (actionType) {
        case TagUtils::RemovePoint: {
            int ind = data.value("cornerIndex", 0).toInt();
            if (ind > 0) {
                delete m_cornerGrabbers.takeAt(ind);
                m_cornerPoints.takeAt(ind);
                updateComponents();
                storeValues();
            }
            break;
        }
        default:
            BaseItem::selectedMenuAction(action);
            break;
        }
    }
}

void TransitionItem::keyPressEvent(QKeyEvent *event)
{
    if (event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace) {
        bool bFound = false;
        if (m_cornerGrabbers.count() > 2) {
            for (int i = m_cornerGrabbers.count() - 1; i >= 1; i--) {
                if (m_cornerGrabbers[i]->isSelected()) {
                    delete m_cornerGrabbers.takeAt(i);
                    m_cornerPoints.takeAt(i);
                    bFound = true;
                }
            }
        }
        if (bFound) {
            updateComponents();
            storeValues();
            event->accept();
            return;
        }
    }

    BaseItem::keyPressEvent(event);
}

bool TransitionItem::sceneEventFilter(QGraphicsItem *watched, QEvent *event)
{
    if (watched->type() == CornerGrabberType) {
        auto c = qgraphicsitem_cast<CornerGrabberItem*>(watched);
        auto mouseEvent = dynamic_cast<QGraphicsSceneMouseEvent*>(event);
        if (!c || !mouseEvent)
            return BaseItem::sceneEventFilter(watched, event);

        int cid = m_cornerGrabbers.indexOf(c);
        if (mouseEvent->type() == QEvent::GraphicsSceneMouseMove) {
            if (mouseEvent->buttons() & Qt::LeftButton) {
                QPointF movingPoint = c->pressedPoint() - mouseEvent->pos();

                if (cid == 0) {
                    if (!m_movingFirstPoint) {
                        m_movingFirstPoint = true;
                        removeTransition(Start);
                    }
                } else if (cid == (m_cornerPoints.count() - 1)) {
                    if (!m_movingLastPoint) {
                        m_movingLastPoint = true;
                        if (m_endItem)
                            removeTransition(End);
                        else {
                            updateZValue();
                            updateTargetType();
                        }
                    }
                }

                if (cid >= 0 && cid < m_cornerPoints.count())
                    snapToAnyPoint(cid, m_cornerPoints[cid] - movingPoint);

                updateComponents();
            }
            return true;
        } else if (mouseEvent->type() == QEvent::GraphicsSceneMouseRelease) {
            if (mouseEvent->button() == Qt::LeftButton) {
                if (cid == 0 || (cid == m_cornerPoints.count() - 1)) {
                    m_movingFirstPoint = false;
                    m_movingLastPoint = false;
                    connectToTopItem(watched->mapToScene(mouseEvent->pos()), cid == 0 ? Start : End, UnknownType);
                }
                removeUnnecessaryPoints();
            } else
                showContextMenu(mouseEvent);

            storeValues();
            return true;
        }
    }

    return BaseItem::sceneEventFilter(watched, event);
}

void TransitionItem::removeTransition(TransitionPoint p)
{
    // Remove transition from the item
    // Private function. Transition can itself remove connection from the item
    if (p == Start && m_startItem) {
        m_oldStartItem = m_startItem;
        m_startItem->removeOutputTransition(this);
        m_startItem = nullptr;
        updateZValue();
        updateTargetType();
        if (m_oldStartItem)
            m_oldStartItem->updateTransitions();
    } else if (p == End && m_endItem) {
        m_endItem->removeInputTransition(this);
        m_endItem = nullptr;
        updateZValue();
        updateTargetType();
    }
}

void TransitionItem::disconnectItem(ConnectableItem *item)
{
    // Disconnect item, normally called from the ConnectableItem
    if (item == m_startItem)
        removeTransition(Start);
    if (item == m_endItem)
        removeTransition(End);

    updateTarget();
}

void TransitionItem::setStartItem(ConnectableItem *item)
{
    m_oldStartItem = nullptr;
    m_startItem = item;

    if (item) {
        if (tag())
            tag()->document()->changeParent(tag(), m_startItem->tag());
        item->addOutputTransition(this);

        if (m_cornerPoints.isEmpty()) {
            m_cornerPoints << sceneTargetPoint(TransitionPoint::Start);
            m_cornerPoints << sceneTargetPoint(TransitionPoint::End);
        }
    }

    updateZValue();
    updateComponents();
    storeValues();
}

void TransitionItem::startTransitionFrom(ConnectableItem *item, const QPointF &mouseScenePos)
{
    m_oldStartItem = nullptr;
    m_startItem = item;
    m_startItem->addOutputTransition(this);
    m_cornerPoints.clear();
    m_cornerPoints << sceneTargetPoint(TransitionPoint::Start);
    m_cornerPoints << mapFromScene(mouseScenePos);

    createGrabbers();
    updateZValue();
    updateComponents();
    storeValues();
    m_cornerGrabbers.last()->setSelected(true);
}

void TransitionItem::connectToTopItem(const QPointF &pos, TransitionPoint tp, ItemType targetType)
{
    int cornerPoints = m_cornerPoints.count();

    ConnectableItem *parentItem = nullptr;
    ScxmlTag *parentTag = nullptr;
    ScxmlDocument *document = tag()->document();

    snapToAnyPoint(m_cornerPoints.count() - 1, pos);
    QPointF p(m_cornerPoints.last());

    // First try to find parentItem
    QList<QGraphicsItem*> items = scene()->items(p);
    if (!items.isEmpty()) {
        for (int i = 0; i < items.count(); ++i) {
            ItemType type = ItemType(items[i]->type());
            if ((targetType == UnknownType && type >= FinalStateType) || type >= StateType) {
                auto it = qgraphicsitem_cast<ConnectableItem*>(items[i]);
                if (it) {
                    parentItem = it;
                    parentTag = parentItem->tag();
                    break;
                }
            }
        }
    }

    if (!parentTag && document)
        parentTag = document->rootTag();

    // Connect existing item
    if (targetType == UnknownType) {
        switch (tp) {
        case Start:
            if (parentItem) {
                m_startTargetFactor = calculateTargetFactor(parentItem, pos);
                savePoint(m_startTargetFactor * 100, Constants::C_SCXML_EDITORINFO_STARTTARGETFACTORS);
            }
            setStartItem(parentItem ? parentItem : m_oldStartItem);
            break;
        case End:
            m_endTargetFactor = calculateTargetFactor(parentItem, pos);
            savePoint(m_endTargetFactor * 100, Constants::C_SCXML_EDITORINFO_ENDTARGETFACTORS);
            setEndItem(parentItem);
            break;
        default:
            break;
        }

        setSelected(false);
        if (parentItem)
            parentItem->setSelected(true);
        removeGrabbers();
        if (m_startItem == m_endItem && cornerPoints == 2) {
            setTagValue("type", "internal");
            setEndItem(nullptr);
            m_targetType = InternalNoTarget;
        }

        updateEventName();
        storeValues();
    } else {
        // Create new item and connect to it
        ConnectableItem *newItem = SceneUtils::createItem(targetType, parentItem ? parentItem->mapFromScene(p) : p);
        if (newItem) {
            ScxmlTag *newTag = SceneUtils::createTag(targetType, tag()->document());
            newItem->setTag(newTag);
            newItem->setParentItem(parentItem);
            if (!parentItem)
                scene()->addItem(newItem);

            newItem->addInputTransition(this);
            newItem->updateAttributes();
            newItem->updateEditorInfo();
            newItem->updateUIProperties();

            if (parentItem)
                parentItem->updateUIProperties();

            if (document)
                document->addTag(parentTag, newTag);

            setEndItem(newItem);
            setSelected(false);
            newItem->setSelected(true);
        }

        removeGrabbers();
    }

    updateTargetType();
}

void TransitionItem::setEndPos(const QPointF &endPos, bool snap)
{
    if (m_cornerPoints.count() >= 2) {
        m_cornerPoints.last().setX(endPos.x());
        m_cornerPoints.last().setY(endPos.y());

        if (snap)
            snapToAnyPoint(m_cornerPoints.count() - 1, endPos);
        updateComponents();
        storeValues();
    }
}

void TransitionItem::setEndItem(ConnectableItem *item, bool fixValue)
{

    if (item) {
        m_endItem = item;
        item->addInputTransition(this);
        setEndPos(sceneTargetPoint(End), false);

        if (m_cornerPoints.count() > 2)
            snapPointToPoint(m_cornerPoints.count() - 2, m_cornerPoints.last(), 15);
    } else {
        removeTransition(End);
        updateComponents();
        storeValues();
    }

    updateZValue();
    updateTarget(fixValue);
}

QPointF TransitionItem::loadPoint(const QString &name)
{
    Serializer s;
    QPointF p;
    s.setData(editorInfo(name));
    s.read(p);
    return p;
}

void TransitionItem::savePoint(const QPointF &p, const QString &name)
{
    Serializer s;
    s.append(p);
    setEditorInfo(name, s.data(), true);
}

QPointF TransitionItem::calculateTargetFactor(ConnectableItem *item, const QPointF &pos)
{
    if (item) {
        QRectF r = item->sceneBoundingRect().adjusted(-8, -8, 8, 8);
        QPointF pixelFactorPoint = pos - r.topLeft();
        QPointF normalizedPoint(qBound(0.0, pixelFactorPoint.x() / r.width(), 1.0), qBound(0.0, pixelFactorPoint.y() / r.height(), 1.0));

        // Center point if close enough
        if (qAbs(normalizedPoint.x() - 0.5) < 0.2 && qAbs(normalizedPoint.y() - 0.5) < 0.2)
            return QPointF(0.5, 0.5);

        return normalizedPoint;
    }

    return QPointF(0.5, 0.5);
}

QPointF TransitionItem::sceneTargetPoint(TransitionPoint p)
{
    ConnectableItem *item = nullptr;
    QPointF factorPoint;
    if (p == TransitionPoint::Start) {
        item = m_startItem;
        factorPoint = m_startTargetFactor;
    } else {
        if (m_endItem) {
            item = m_endItem;
            factorPoint = m_endTargetFactor;
        } else {
            item = m_startItem;
            factorPoint = QPointF(0.5, 0.5);
        }
    }

    QRectF r;
    if (item)
        r = item->sceneBoundingRect();

    return r.topLeft() + QPointF(factorPoint.x() * r.width(), factorPoint.y() * r.height());
}

QPointF TransitionItem::findIntersectionPoint(ConnectableItem *item, const QLineF &line, const QPointF &defaultPoint)
{
    // Check circles
    ItemType t = ItemType(item->type());
    if (t >= InitialStateType && t <= HistoryType) {
        QLineF l = item == m_endItem ? line : QLineF(line.p2(), line.p1());
        l.setLength(l.length() - qMin(item->boundingRect().width(), item->boundingRect().height()) * 0.45);
        return l.p2();
    }

    // Find intersection point between line and target item
    QPolygonF itemPolygon = item->polygonShape();
    if (!itemPolygon.isEmpty()) {
        QPointF intersectPoint;
        QPointF p1 = itemPolygon.at(0) + item->scenePos();
        QPointF p2;
        QLineF checkLine;
        for (int i = 1; i < itemPolygon.count(); ++i) {
            p2 = itemPolygon.at(i) + item->scenePos();
            checkLine = QLineF(p1, p2);
            if (checkLine.intersects(line, &intersectPoint) == QLineF::BoundedIntersection)
                return intersectPoint;
            p1 = p2;
        }
    }

    return defaultPoint;
}

void TransitionItem::updateComponents()
{
    if (m_cornerPoints.count() < 2)
        return;

    // Check if we must move all points together
    bool movePoints = (SceneUtils::isSomeSelected(m_startItem) && SceneUtils::isSomeSelected(m_endItem) && m_cornerPoints.count() > 2) || (m_movingFirstPoint && m_targetType == InternalNoTarget);

    if (!movePoints && !m_mouseGrabbed) {
        // Check the corners which are in the same line if cornerGrabbers are hidden
        if (m_cornerGrabbers.isEmpty() && m_cornerPoints.count() > 2) {
            for (int i = 0; i < m_cornerPoints.count() - 2; ++i) {
                if (m_cornerPoints[i] != m_cornerPoints[i + 1] && m_cornerPoints[i] != m_cornerPoints[i + 2]) {
                    QLineF l1(m_cornerPoints[i], m_cornerPoints[i + 1]);
                    QLineF l2(m_cornerPoints[i], m_cornerPoints[i + 2]);

                    if (qRound(l1.angle()) == qRound(l2.angle())) {
                        m_cornerPoints.takeAt(i + 1);
                        storeValues();
                        break;
                    }
                }
            }
        }
    }

    if ((m_movingFirstPoint || m_movingLastPoint) && !(m_movingFirstPoint && m_targetType == InternalNoTarget))
        movePoints = false;

    // Init first line
    QLineF firstLine(m_cornerPoints[0], m_cornerPoints[1]);
    if (m_startItem) {
        if (m_targetType <= InternalNoTarget) {
            QPointF p = m_startItem->getInternalPosition(this, m_targetType);
            firstLine.setP1(p);
            firstLine.setP2(p + QPointF(20, 0));
            m_cornerPoints[1] = firstLine.p2();
            if (!(m_movingFirstPoint && m_targetType == InternalNoTarget))
                movePoints = false;
        } else {
            firstLine.setP1(sceneTargetPoint(TransitionPoint::Start));
            firstLine.setP1(findIntersectionPoint(m_startItem, firstLine, firstLine.p1()));
        }
    }

    if (movePoints) {
        if (m_movingFirstPoint && m_targetType == InternalNoTarget)
            m_cornerPoints[1] = m_cornerPoints[0] + QPointF(20, 0);
        else {
            QPointF movingPoint = firstLine.p1() - m_cornerPoints[0];
            for (int i = 0; i < m_cornerPoints.count(); ++i)
                m_cornerPoints[i] += movingPoint;

            for (int i = 0; i < m_arrow.count(); ++i)
                m_arrow[i] += movingPoint;
        }
    }

    m_cornerPoints[0] = firstLine.p1();

    // Init last line
    int lastIndex = m_cornerPoints.count() - 1;
    QLineF lastLine(m_cornerPoints[lastIndex - 1], m_cornerPoints[lastIndex]);
    if (m_endItem && m_targetType > InternalNoTarget) {
        lastLine.setP2(sceneTargetPoint(TransitionPoint::End));
        lastLine.setP2(findIntersectionPoint(m_endItem, lastLine, lastLine.p2()));
        m_cornerPoints[lastIndex] = lastLine.p2();
    }

    m_arrowAngle = 0;
    if (m_targetType == InternalSameTarget) {
        m_arrowAngle = M_PI * 0.6;
    } else {
        // Calculate angle of the lastLine and update arrow
        if (lastLine.length() > 0) {
            m_arrowAngle = qAcos(lastLine.dx() / lastLine.length()) + M_PI;
            if (lastLine.dy() >= 0)
                m_arrowAngle = (2 * M_PI) - m_arrowAngle;
        }
    }

    m_arrow[0] = lastLine.p2() + QPointF(qSin(m_arrowAngle + M_PI / 3) * m_arrowSize, qCos(m_arrowAngle + M_PI / 3) * m_arrowSize);
    m_arrow[1] = lastLine.p2();
    m_arrow[2] = lastLine.p2() + QPointF(qSin(m_arrowAngle + M_PI - M_PI / 3) * m_arrowSize, qCos(m_arrowAngle + M_PI - M_PI / 3) * m_arrowSize);

    setItemBoundingRect(m_cornerPoints.boundingRect().normalized().adjusted(-SELECTION_DISTANCE, -SELECTION_DISTANCE,
        SELECTION_DISTANCE, SELECTION_DISTANCE));

    // Set the right place for the name of the transition
    int ind = m_cornerPoints.count() / 2 - 1;
    QLineF nameLine(m_cornerPoints[ind], m_cornerPoints[ind + 1]);
    if (m_targetType <= InternalNoTarget) {
        m_eventTagItem->setPos(m_cornerPoints[1].x() + 6, m_cornerPoints[1].y() - m_eventTagItem->boundingRect().height() / 3);
    } else {
        const qreal w2 = m_eventTagItem->boundingRect().width() / 2;
        QPointF startPos = nameLine.pointAt(0.5);
        QLineF targetLine(startPos, startPos + QPointF(SELECTION_DISTANCE, SELECTION_DISTANCE));
        targetLine.setAngle(nameLine.angle() + 90);

        m_eventTagItem->setPos(targetLine.p2() + m_eventTagItem->movePoint() - QPointF(w2, m_eventTagItem->boundingRect().height() / 2));
    }

    if (m_warningItem)
        m_warningItem->setPos(m_eventTagItem->pos() - QPointF(WARNING_ITEM_SIZE, 0));
    updateGrabberPositions();
    updateZValue();
}

void TransitionItem::grabMouse(ItemType targetType)
{
    m_grabbedTargetType = targetType;
    m_mouseGrabbed = true;
    BaseItem::grabMouse();
}

void TransitionItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    Q_UNUSED(option)
    Q_UNUSED(widget)

    painter->save();

    painter->setRenderHint(QPainter::Antialiasing, true);
    painter->setPen(m_pen);

    if (m_cornerPoints.count() >= 2) {
        if (m_targetType == InternalSameTarget) {
            QRectF rect(m_cornerPoints[0].x(), m_cornerPoints[0].y() - SELECTION_DISTANCE,
                m_cornerPoints[1].x() - m_cornerPoints[0].x(),
                SELECTION_DISTANCE * 2);
            painter->drawArc(rect, 0, 180 * 16);
        } else {
            if (highlight()) {
                painter->setPen(m_highlightPen);
                painter->drawPolyline(m_cornerPoints);
            }
            painter->setPen(m_pen);
            painter->drawPolyline(m_cornerPoints);
        }
    }

    for (int i = 0; i < m_cornerPoints.count() - 1; ++i)
        painter->drawEllipse(m_cornerPoints[i], 2, 2);

    if (highlight()) {
        painter->setPen(m_highlightPen);
        painter->drawPolyline(m_arrow);
    }

    painter->setPen(m_pen);
    painter->drawPolyline(m_arrow);

    painter->restore();
}

void TransitionItem::updateEditorInfo(bool allChilds)
{
    BaseItem::updateEditorInfo(allChilds);

    const QColor fontColor = editorInfo(Constants::C_SCXML_EDITORINFO_FONTCOLOR);
    m_eventTagItem->setDefaultTextColor(fontColor.isValid() ? fontColor : Qt::black);

    const QColor stateColor = editorInfo(Constants::C_SCXML_EDITORINFO_STATECOLOR);
    m_pen.setColor(stateColor.isValid() ? stateColor : qRgb(0x12, 0x12, 0x12));
}

void TransitionItem::updateTarget(bool fixValue)
{
    if (fixValue)
        setTagValue("target", m_endItem ? m_endItem->itemId() : QString());
    if (m_endItem)
        m_endItem->checkInitial(true);
}

void TransitionItem::updateAttributes()
{
    BaseItem::updateAttributes();

    // Find new target
    if (!m_endItem || tagValue("target") != m_endItem->itemId()) {
        if (m_endItem)
            m_endItem->removeInputTransition(this);

        m_endItem = nullptr;
        findEndItem();
        updateTarget(false);
        updateZValue();
    }

    // Set event-name
    updateEventName();
    updateTargetType();
}

void TransitionItem::init(ScxmlTag *tag, BaseItem *parentItem, bool initChildren, bool blockUpdates)
{
    Q_UNUSED(initChildren)
    setBlockUpdates(blockUpdates);

    setTag(tag);
    setParentItem(parentItem);
    updateEditorInfo();

    if (blockUpdates)
        setBlockUpdates(false);
}

void TransitionItem::readUISpecifiedProperties(const ScxmlTag *tag)
{
    if (tag) {
        if (m_cornerPoints.count() >= 2) {
            while (m_cornerPoints.count() > 2)
                m_cornerPoints.takeAt(1);

            Serializer s;

            QPointF p = loadPoint(Constants::C_SCXML_EDITORINFO_STARTTARGETFACTORS);
            if (p.isNull())
                p = QPointF(50, 50);
            m_startTargetFactor = p / 100;

            p = loadPoint(Constants::C_SCXML_EDITORINFO_ENDTARGETFACTORS);
            if (p.isNull())
                p = QPointF(50, 50);
            m_endTargetFactor = p / 100;

            QString localPointsData = editorInfo(Constants::C_SCXML_EDITORINFO_LOCALGEOMETRY);
            if (!localPointsData.isEmpty()) {
                QPointF startPos = sceneTargetPoint(Start);
                QPolygonF localPoints;
                s.setData(localPointsData);
                s.read(localPoints);
                for (int i = 0; i < localPoints.count(); ++i)
                    m_cornerPoints.insert(i + 1, startPos + localPoints[i]);
            } else {
                QPolygonF scenePoints;
                s.setData(editorInfo(Constants::C_SCXML_EDITORINFO_GEOMETRY));
                s.read(scenePoints);
                for (int i = 0; i < scenePoints.count(); ++i)
                    m_cornerPoints.insert(i + 1, scenePoints[i]);
            }

            m_eventTagItem->resetMovePoint(loadPoint(Constants::C_SCXML_EDITORINFO_MOVEPOINT));

            if (m_lineSelected)
                createGrabbers();
            updateComponents();
        }
    }
}

void TransitionItem::finalizeCreation()
{
    bool old = blockUpdates();
    setBlockUpdates(true);

    updateAttributes();

    if (!old)
        setBlockUpdates(false);
}

void TransitionItem::checkVisibility(double scaleFactor)
{
    m_eventTagItem->setVisible(scaleFactor > 0.5);
}

bool TransitionItem::containsScenePoint(const QPointF &p) const
{
    QPointF pp = mapFromScene(p);

    for (int i = 0; i < m_cornerPoints.count() - 1; ++i) {
        QLineF line(m_cornerPoints[i], m_cornerPoints[i + 1]);

        // Is point close enough of the line
        QPointF intersPoint;
        QLineF line2(pp, pp + QPointF(SELECTION_DISTANCE, SELECTION_DISTANCE));
        line2.setAngle(line.angle() + 90);
        if (line.intersects(line2, &intersPoint) == QLineF::BoundedIntersection) {
            return true;
        } else {
            line2.setAngle(line.angle() - 90);
            if (line.intersects(line2, &intersPoint) == QLineF::BoundedIntersection)
                return true;
        }
    }
    return false;
}

void TransitionItem::findEndItem()
{
    QString targetId = tagValue("target");
    if (!m_endItem && !targetId.isEmpty()) {
        QList<QGraphicsItem*> items = scene()->items();
        for (int i = 0; i < items.count(); ++i) {
            if (items[i]->type() >= FinalStateType) {
                auto item = qgraphicsitem_cast<ConnectableItem*>(items[i]);
                if (item && item->itemId() == targetId) {
                    setEndItem(item, false);
                    break;
                }
            }
        }
    }
}

void TransitionItem::updateEventName()
{
    m_eventTagItem->setText(tagValue("event"));
}

void TransitionItem::storeGeometry(bool block)
{
    if (tag()) {
        if (m_cornerPoints.count() <= 2) {
            setEditorInfo(Constants::C_SCXML_EDITORINFO_GEOMETRY, QString(), block);
            setEditorInfo(Constants::C_SCXML_EDITORINFO_LOCALGEOMETRY, QString(), block);
        } else {
            QPolygonF pol = m_cornerPoints;
            pol.takeFirst();
            pol.takeLast();
            Serializer s;
            for (int i = 0; i < pol.count(); ++i) {
                QPointF spos = sceneTargetPoint(Start);
                pol[i].setX(pol[i].x() - spos.x());
                pol[i].setY(pol[i].y() - spos.y());
            }
            s.append(pol);

            setEditorInfo(Constants::C_SCXML_EDITORINFO_LOCALGEOMETRY, s.data(), block);
        }
    }
}

void TransitionItem::storeMovePoint(bool block)
{
    if (m_eventTagItem->movePoint().toPoint() == QPoint(0, 0))
        setEditorInfo(Constants::C_SCXML_EDITORINFO_MOVEPOINT, QString(), block);
    else
        savePoint(m_eventTagItem->movePoint(), Constants::C_SCXML_EDITORINFO_MOVEPOINT);
}

void TransitionItem::storeTargetFactors(bool block)
{
    if (m_startTargetFactor == QPointF(0.5, 0.5))
        setEditorInfo(Constants::C_SCXML_EDITORINFO_STARTTARGETFACTORS, QString(), block);
    else
        savePoint(m_startTargetFactor * 100, Constants::C_SCXML_EDITORINFO_STARTTARGETFACTORS);

    if (m_endTargetFactor == QPointF(0.5, 0.5))
        setEditorInfo(Constants::C_SCXML_EDITORINFO_ENDTARGETFACTORS, QString(), block);
    else
        savePoint(m_endTargetFactor * 100, Constants::C_SCXML_EDITORINFO_ENDTARGETFACTORS);
}

void TransitionItem::storeValues(bool block)
{
    storeGeometry(block);
    storeMovePoint(block);
    storeTargetFactors(block);
}

void TransitionItem::updateUIProperties()
{
    if (tag() && isActiveScene())
        storeValues();
}

void TransitionItem::updateTargetType()
{
    if (m_movingFirstPoint && m_targetType == InternalNoTarget)
        return;

    TransitionTargetType type = ExternalTarget;

    if (m_startItem && m_startItem == m_endItem)
        type = InternalSameTarget;
    else if (m_startItem && !m_endItem) {
        if (m_movingLastPoint) {
            type = ExternalNoTarget;
        } else {
            QRectF srect = m_startItem->sceneBoundingRect();
            if (srect.contains(m_cornerPoints.last()))
                type = InternalNoTarget;
            else
                type = ExternalNoTarget;
        }
    } else {
        type = ExternalTarget;
    }

    if (type <= InternalNoTarget) {
        m_eventTagItem->resetMovePoint();
        m_arrowSize = 6;
        // Remove extra points
        while (m_cornerPoints.count() > 2)
            m_cornerPoints.takeAt(1);
        while (m_cornerGrabbers.count() > 2)
            delete m_cornerGrabbers.takeAt(1);

        // Remove editorinfo.geometry
        setEditorInfo(Constants::C_SCXML_EDITORINFO_GEOMETRY, QString(), true);
        setEditorInfo(Constants::C_SCXML_EDITORINFO_LOCALGEOMETRY, QString(), true);
        setEditorInfo(Constants::C_SCXML_EDITORINFO_MOVEPOINT, QString(), true);
        setEditorInfo(Constants::C_SCXML_EDITORINFO_STARTTARGETFACTORS, QString(), true);
        setEditorInfo(Constants::C_SCXML_EDITORINFO_ENDTARGETFACTORS, QString(), true);
    } else {
        m_arrowSize = 10;
    }

    if (m_targetType != type) {
        m_targetType = type;
        if (m_warningItem)
            m_warningItem->check();
        if (m_startItem && !m_startItem->blockUpdates()) {
            m_startItem->updateOutputTransitions();
            if (m_startItem->type() >= StateType)
                static_cast<StateItem*>(m_startItem)->updateBoundingRect();
        }
    }
}

ConnectableItem *TransitionItem::connectedItem(const ConnectableItem *other) const
{
    if (other) {
        if (other == m_startItem)
            return m_endItem;
        else if (other == m_endItem)
            return m_startItem;
    }

    return nullptr;
}

bool TransitionItem::isStartItem(const ConnectableItem *item) const
{
    return m_startItem == item;
}

bool TransitionItem::isEndItem(const ConnectableItem *item) const
{
    return m_endItem == item;
}

bool TransitionItem::hasStartItem() const
{
    return m_startItem != nullptr;
}

bool TransitionItem::hasEndItem() const
{
    return m_endItem != nullptr;
}

void TransitionItem::updateZValue()
{
    setZValue(qMax(m_startItem ? (m_startItem->zValue() + 1) : 1, m_endItem ? (m_endItem->zValue() + 1) : 1));
}

qreal TransitionItem::textWidth() const
{
    return m_eventTagItem->boundingRect().width();
}

QRectF TransitionItem::wholeBoundingRect() const
{
    return boundingRect().united(m_eventTagItem->sceneBoundingRect());
}
