// 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 "graphicsscene.h"
#include "highlightitem.h"
#include "quicktransitionitem.h"
#include "sceneutils.h"
#include "scxmleditorconstants.h"
#include "scxmleditortr.h"
#include "serializer.h"
#include "stateitem.h"

#include <utils/theme/theme.h>

#include <QDebug>
#include <QPainter>
#include <QPainterPath>
#include <QPen>
#include <QStringList>
#include <QUndoStack>

using namespace ScxmlEditor::PluginInterface;

ConnectableItem::ConnectableItem(const QPointF &p, BaseItem *parent)
    : BaseItem(parent)
{
    setFlag(QGraphicsItem::ItemIsMovable, true);
    setFlag(QGraphicsItem::ItemIsSelectable, true);
    setFlag(QGraphicsItem::ItemSendsGeometryChanges, true);
    setFlag(QGraphicsItem::ItemSendsScenePositionChanges, true);
    setAcceptDrops(true);

    const bool isDark = Utils::creatorTheme()->colorScheme() == Qt::ColorScheme::Dark;

    m_selectedPen.setStyle(Qt::DotLine);
    m_selectedPen.setColor(isDark ? QColor(0x74, 0x74, 0xed) : QColor(0x44, 0x44, 0xed));
    m_selectedPen.setCosmetic(true);
    m_releasedFromParentBrush = QBrush(QColor(0x98, 0x98, 0x98));

    setPos(p);
    connect(this, &ConnectableItem::geometryChanged, this, &ConnectableItem::updateCornerPositions);
}

ConnectableItem::~ConnectableItem()
{
    setBlockUpdates(true);

    const QList<ConnectableItem *> overlappedItems = m_overlappedItems;
    for (ConnectableItem *item : overlappedItems)
        item->removeOverlappingItem(this);
    m_overlappedItems.clear();

    const QList<TransitionItem *> outputTransitions = m_outputTransitions;
    for (TransitionItem *transition : outputTransitions)
        transition->disconnectItem(this);
    m_outputTransitions.clear();

    const QList<TransitionItem *> inputTransitions = m_inputTransitions;
    for (TransitionItem *transition : inputTransitions)
        transition->disconnectItem(this);
    m_inputTransitions.clear();

    qDeleteAll(m_quickTransitions);
    m_quickTransitions.clear();
}

void ConnectableItem::createCorners()
{
    for (int i = 0; i < 8; ++i) {
        Qt::CursorShape cur = Qt::SizeHorCursor;
        switch (i) {
        case 0:
        case 4:
            cur = Qt::SizeFDiagCursor;
            break;
        case 1:
        case 5:
            cur = Qt::SizeVerCursor;
            break;
        case 2:
        case 6:
            cur = Qt::SizeBDiagCursor;
            break;
        case 3:
        case 7:
            cur = Qt::SizeHorCursor;
            break;
        default:
            break;
        }
        m_corners << new CornerGrabberItem(this, cur);
    }

    qDeleteAll(m_quickTransitions);
    m_quickTransitions.clear();
    m_quickTransitions << new QuickTransitionItem(0, UnknownType, this);
    m_quickTransitions << new QuickTransitionItem(1, StateType, this);
    m_quickTransitions << new QuickTransitionItem(2, ParallelType, this);
    m_quickTransitions << new QuickTransitionItem(3, HistoryType, this);
    m_quickTransitions << new QuickTransitionItem(4, FinalStateType, this);

    updateCornerPositions();
}

void ConnectableItem::removeCorners()
{
    qDeleteAll(m_corners);
    m_corners.clear();

    qDeleteAll(m_quickTransitions);
    m_quickTransitions.clear();
}

void ConnectableItem::updateCornerPositions()
{
    QRectF r = boundingRect();
    if (m_corners.count() == 8) {
        qreal cx = r.center().x();
        qreal cy = r.center().y();
        m_corners[0]->setPos(r.topLeft());
        m_corners[1]->setPos(cx, r.top());
        m_corners[2]->setPos(r.topRight());
        m_corners[3]->setPos(r.right(), cy);
        m_corners[4]->setPos(r.bottomRight());
        m_corners[5]->setPos(cx, r.bottom());
        m_corners[6]->setPos(r.bottomLeft());
        m_corners[7]->setPos(r.left(), cy);
    }

    for (int i = 0; i < m_quickTransitions.count(); ++i) {
        m_quickTransitions[i]->setPos(r.topLeft());
        m_quickTransitions[i]->setVisible(!m_releasedFromParent && canStartTransition(m_quickTransitions[i]->connectionType()));
    }
    updateShadowClipRegion();
}

bool ConnectableItem::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);

        QRectF r = boundingRect();
        if (event->type() == QEvent::GraphicsSceneMousePress) {
            setOpacity(0.5);
        } else if (event->type() == QEvent::GraphicsSceneMouseMove) {
            QPointF pw = watched->mapToItem(this, mouseEvent->pos());
            QRectF rMin;
            if (type() >= StateType)
                rMin = qgraphicsitem_cast<StateItem*>(this)->childItemsBoundingRect();
            if (rMin.isNull()) {
                rMin = QRectF(0, 0, m_minimumWidth, m_minimumHeight);
                rMin.moveCenter(r.center());
            }

            switch (m_corners.indexOf(c)) {
            case 0:
                r.setTopLeft(QPointF(qMin(pw.x(), rMin.left()), qMin(pw.y(), rMin.top())));
                break;
            case 1:
                r.setTop(qMin(pw.y(), rMin.top()));
                break;
            case 2:
                r.setTopRight(QPointF(qMax(pw.x(), rMin.right()), qMin(pw.y(), rMin.top())));
                break;
            case 3:
                r.setRight(qMax(pw.x(), rMin.right()));
                break;
            case 4:
                r.setBottomRight(QPointF(qMax(pw.x(), rMin.right()), qMax(pw.y(), rMin.bottom())));
                break;
            case 5:
                r.setBottom(qMax(pw.y(), rMin.bottom()));
                break;
            case 6:
                r.setBottomLeft(QPointF(qMin(pw.x(), rMin.left()), qMax(pw.y(), rMin.bottom())));
                break;
            case 7:
                r.setLeft(qMin(pw.x(), rMin.left()));
                break;
            default:
                break;
            }

            setItemBoundingRect(r);
            updateCornerPositions();
            updateTransitions();

            return true;
        } else if (event->type() == QEvent::GraphicsSceneMouseRelease) {
            setOpacity(1.0);
            updateUIProperties();
            checkOverlapping();
        }
    } else if (watched->type() == QuickTransitionType) {
        auto mouseEvent = dynamic_cast<QGraphicsSceneMouseEvent*>(event);
        if (!mouseEvent)
            return BaseItem::sceneEventFilter(watched, event);

        if (event->type() == QEvent::GraphicsSceneMousePress) {
            m_newTransitionStartedPoint = mouseEvent->pos();
            tag()->document()->undoStack()->beginMacro(Tr::tr("Add new state"));

            m_newTransition = new TransitionItem;
            scene()->addItem(m_newTransition);

            ScxmlTag *newTag = nullptr;
            if (tag()->tagType() == Initial || tag()->tagType() == History)
                newTag = new ScxmlTag(InitialTransition, tag()->document());
            else
                newTag = new ScxmlTag(Transition, tag()->document());
            newTag->setAttribute("type", "external");
            newTag->setAttribute("event", tag()->document()->nextUniqueId("Transition"));
            m_newTransition->init(newTag);

            tag()->document()->addTag(tag(), newTag);
            m_newTransition->startTransitionFrom(this, watched->mapToScene(mouseEvent->pos()));
            return true;
        } else if (event->type() == QEvent::GraphicsSceneMouseMove) {
            if (m_newTransition)
                m_newTransition->setEndPos(watched->mapToScene(mouseEvent->pos()));
        } else if (event->type() == QEvent::GraphicsSceneMouseRelease) {
            auto tra = qgraphicsitem_cast<QuickTransitionItem*>(watched);
            if (!tra)
                return BaseItem::sceneEventFilter(watched, event);

            if (m_newTransition) {
                QLineF line(m_newTransitionStartedPoint, mouseEvent->pos());
                if (line.length() > 30) {
                    m_newTransition->connectToTopItem(watched->mapToScene(mouseEvent->pos()), TransitionItem::End, tra->connectionType());
                    m_newTransition = nullptr;
                    setSelected(false);
                    tag()->document()->undoStack()->endMacro();
                } else {
                    m_newTransition->grabMouse(tra->connectionType());
                    m_newTransition = nullptr;
                    setSelected(false);
                }
                return true;
            }
        }
    }

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

void ConnectableItem::addOutputTransition(TransitionItem *transition)
{
    m_outputTransitions.append(transition);
    transitionCountChanged();
}

void ConnectableItem::addInputTransition(TransitionItem *transition)
{
    m_inputTransitions.append(transition);
    transitionCountChanged();
}

void ConnectableItem::removeOutputTransition(TransitionItem *transition)
{
    m_outputTransitions.removeAll(transition);
    transitionCountChanged();
}

void ConnectableItem::removeInputTransition(TransitionItem *transition)
{
    m_inputTransitions.removeAll(transition);
    transitionCountChanged();
}

void ConnectableItem::updateInputTransitions()
{
    for (TransitionItem *transition : std::as_const(m_inputTransitions)) {
        transition->updateComponents();
        transition->updateUIProperties();
    }
    transitionsChanged();
}

void ConnectableItem::updateOutputTransitions()
{
    for (TransitionItem *transition : std::as_const(m_outputTransitions)) {
        transition->updateComponents();
        transition->updateUIProperties();
    }
    transitionsChanged();
}

void ConnectableItem::updateTransitions(bool allChildren)
{
    updateOutputTransitions();
    updateInputTransitions();

    if (allChildren) {
        const QList<QGraphicsItem *> items = childItems();
        for (QGraphicsItem *it : items) {
            auto item = static_cast<ConnectableItem*>(it);
            if (item && item->type() >= InitialStateType)
                item->updateTransitions(allChildren);
        }
    }
}

void ConnectableItem::updateTransitionAttributes(bool allChildren)
{
    for (TransitionItem *transition : std::as_const(m_outputTransitions))
        transition->updateTarget();

    for (TransitionItem *transition : std::as_const(m_inputTransitions))
        transition->updateTarget();

    if (allChildren) {
        const QList<QGraphicsItem *> items = childItems();
        for (QGraphicsItem *it : items) {
            auto item = static_cast<ConnectableItem*>(it);
            if (item && item->type() >= InitialStateType)
                item->updateTransitionAttributes(allChildren);
        }
    }
}

void ConnectableItem::transitionsChanged()
{
}

void ConnectableItem::transitionCountChanged()
{
}

QPointF ConnectableItem::getInternalPosition(const TransitionItem *transition, TransitionItem::TransitionTargetType type) const
{
    const QRectF srect = sceneBoundingRect();

    int ind = 0;
    if (type == TransitionItem::InternalNoTarget) {
        for (TransitionItem *item : std::as_const(m_outputTransitions)) {
            if (item->targetType() == TransitionItem::InternalSameTarget)
                ind++;
        }
    }

    for (int i = 0; i < m_outputTransitions.count(); ++i) {
        TransitionItem *item = m_outputTransitions[i];
        if (item == transition)
            break;
        else if (item->targetType() == type)
            ind++;
    }

    return QPointF(srect.left() + 20, srect.top() + srect.height() * 0.06 + 40 + ind * 30);
}

void ConnectableItem::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;
    }

    BaseItem::mousePressEvent(event);
}

void ConnectableItem::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_moveMacroStarted) {
        m_moveMacroStarted = true;
        tag()->document()->undoStack()->beginMacro(Tr::tr("Move State"));
    }

    //Restore old behavior if ctrl & alt modifiers are present
    if (!m_releasedFromParent && !(event->modifiers() & Qt::AltModifier) && !(event->modifiers() & Qt::ControlModifier)) {
        releaseFromParent();
        const QList<QGraphicsItem *> items = scene()->selectedItems();
        for (QGraphicsItem *it : items) {
            if (it->type() >= InitialStateType && it != this) {
                qgraphicsitem_cast<ConnectableItem*>(it)->releaseFromParent();
            }
        }
    } else
        setOpacity(0.5);

    BaseItem::mouseMoveEvent(event);
}

void ConnectableItem::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;
    }

    BaseItem::mouseReleaseEvent(event);

    if (m_releasedFromParent) {
        // Try to find parent
        QList<QGraphicsItem*> parentItems = scene()->items(event->scenePos());
        BaseItem *parentItem = nullptr;
        for (int i = 0; i < parentItems.count(); ++i) {
            auto item = static_cast<BaseItem*>(parentItems[i]);
            if (item && item != this && !item->isSelected()
                && item->type() >= StateType
                && SceneUtils::canDrop(item->type(), type())) {
                parentItem = item;
                break;
            }
        }
        connectToParent(parentItem);
        const QList<QGraphicsItem *> items = scene()->selectedItems();
        for (QGraphicsItem *it : items) {
            if (it->type() >= InitialStateType && it != this)
                qgraphicsitem_cast<ConnectableItem*>(it)->connectToParent(parentItem);
        }
    } else
        setOpacity(1.0);

    if (m_moveMacroStarted) {
        m_moveMacroStarted = false;
        tag()->document()->undoStack()->endMacro();
    }

    checkOverlapping();
}

void ConnectableItem::releaseFromParent()
{
    m_releasedFromParent = true;
    setOpacity(0.5);
    m_releasedIndex = tag()->index();
    m_releasedParent = parentItem();
    tag()->document()->changeParent(tag(), nullptr, !m_releasedParent ? m_releasedIndex : -1);
    setZValue(503);

    for (int i = 0; i < m_quickTransitions.count(); ++i)
        m_quickTransitions[i]->setVisible(false);
    for (int i = 0; i < m_corners.count(); ++i)
        m_corners[i]->setVisible(false);
    update();
}

void ConnectableItem::connectToParent(BaseItem *parentItem)
{
    for (int i = 0; i < m_quickTransitions.count(); ++i)
        m_quickTransitions[i]->setVisible(canStartTransition(m_quickTransitions[i]->connectionType()));
    for (int i = 0; i < m_corners.count(); ++i)
        m_corners[i]->setVisible(true);

    tag()->document()->changeParent(tag(), parentItem ? parentItem->tag() : nullptr,
                                    parentItem == m_releasedParent ? m_releasedIndex : -1);

    setZValue(0);
    m_releasedIndex = -1;
    m_releasedParent = nullptr;
    m_releasedFromParent = false;
    setOpacity(1.0);
}

QVariant ConnectableItem::itemChange(GraphicsItemChange change, const QVariant &value)
{
    switch (change) {
    case ItemSelectedHasChanged: {
        if (value.toBool()) {
            createCorners();
            SceneUtils::moveTop(this, static_cast<GraphicsScene*>(scene()));
        } else
            removeCorners();
        break;
    }
    case ItemParentHasChanged:
        updateTransitions(true);
        updateTransitionAttributes(true);
        Q_FALLTHROUGH();
    case ItemPositionHasChanged:
        if (!m_releasedFromParent && !blockUpdates())
            checkParentBoundingRect();
        break;
    case ItemScenePositionHasChanged:
        updateTransitions();
        if (m_highlighItem)
            m_highlighItem->advance(1);
        break;
    default:
        break;
    }

    return BaseItem::itemChange(change, value);
}

qreal ConnectableItem::getOpacity()
{
    if (opacity() < 1.0)
        return opacity();

    if (overlapping())
        return 0.5;

    if (parentBaseItem())
        if (parentBaseItem()->type() >= InitialStateType)
            return static_cast<ConnectableItem*>(parentBaseItem())->getOpacity();

    return 1;
}

void ConnectableItem::updateShadowClipRegion()
{
    QPainterPath br, sr;
    //StateItem Background rounded rectangle
    br.addRoundedRect(boundingRect().adjusted(5, 5, -5, -5), 10, 10);
    //Shadow rounded rectangle
    sr.addRoundedRect(boundingRect().adjusted(10, 10, 0, 0), 10, 10);
    //Clippath is subtract
    m_shadowClipPath = sr - br;
}

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

    painter->save();
    painter->setRenderHint(QPainter::Antialiasing, true);
    painter->setOpacity(getOpacity());

    if (m_releasedFromParent) {
        painter->setPen(Qt::NoPen);
        painter->setBrush(m_releasedFromParentBrush);
        painter->setClipping(true);
        painter->setClipPath(m_shadowClipPath);
        //Since the form is already cliped just draw a rectangle
        painter->drawRect(boundingRect().adjusted(10, 10, 0, 0));
        painter->setClipping(false);
    }

    if (isSelected()) {
        painter->setPen(m_selectedPen);
        painter->setBrush(Qt::NoBrush);
        painter->drawRect(boundingRect());
    }

    painter->restore();
}

void ConnectableItem::updateUIProperties()
{
    if (tag() && isActiveScene()) {
        Serializer s;
        s.append(pos());
        s.append(boundingRect());
        setEditorInfo(Constants::C_SCXML_EDITORINFO_GEOMETRY, s.data());
        s.clear();
        s.append(scenePos());
        s.append(sceneBoundingRect());
        setEditorInfo("scenegeometry", s.data());
    }
}

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

    for (TransitionItem *transition : std::as_const(m_inputTransitions)) {
        if (transition->isEndItem(this))
            transition->setTagValue("target", itemId());
    }
    updateInputTransitions();

    update();
}

void ConnectableItem::updateEditorInfo(bool allChildren)
{
    BaseItem::updateEditorInfo(allChildren);
    updateTransitions();
}

void ConnectableItem::moveStateBy(qreal dx, qreal dy)
{
    moveBy(dx, dy);
    updateUIProperties();
    updateTransitions();
}

void ConnectableItem::setHighlight(bool hl)
{
    BaseItem::setHighlight(hl);
    if (highlight()) {
        if (!m_highlighItem) {
            m_highlighItem = new HighlightItem(this);
            scene()->addItem(m_highlighItem);
        }
    } else {
        delete m_highlighItem;
        m_highlighItem = nullptr;
    }

    if (m_highlighItem)
        m_highlighItem->advance(0);
}

void ConnectableItem::readUISpecifiedProperties(const ScxmlTag *tag)
{
    if (tag) {
        QString data = editorInfo(Constants::C_SCXML_EDITORINFO_GEOMETRY);
        if (!data.isEmpty()) {
            QPointF p(0, 0);
            QRectF r(-60, 50, 120, 100);

            Serializer s;
            s.setData(data);
            s.read(p);
            s.read(r);

            setItemBoundingRect(r);
            setPos(p);
        }
    }
}

void ConnectableItem::addTransitions(const ScxmlTag *tag)
{
    if (scene()) {
        for (int i = 0; i < tag->childCount(); ++i) {
            ScxmlTag *child = tag->child(i);
            if (child->tagType() == Transition || child->tagType() == InitialTransition) {
                auto transition = new TransitionItem;
                scene()->addItem(transition);
                transition->setStartItem(this);
                transition->init(child);
            }
        }
    }
}

void ConnectableItem::init(ScxmlTag *tag, BaseItem *parentItem, bool initChildren, bool /*blockUpdates*/)
{
    BaseItem::init(tag, parentItem);
    if (initChildren)
        addTransitions(tag);
}

void ConnectableItem::setMinimumWidth(int width)
{
    m_minimumWidth = width;
    QRectF r = boundingRect();
    if (r.width() < width) {
        r.setWidth(width);
        setItemBoundingRect(r);
    }
}

void ConnectableItem::setMinimumHeight(int height)
{
    m_minimumHeight = height;
    QRectF r = boundingRect();
    if (r.height() < height) {
        r.setHeight(height);
        setItemBoundingRect(r);
    }
}

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

    updateAttributes();
    updateEditorInfo();
    updateUIProperties();
    checkInitial(true);

    if (!old)
        setBlockUpdates(false);
}

bool ConnectableItem::hasInputTransitions(const ConnectableItem *parentItem, bool checkChildren) const
{
    for (const TransitionItem *it : std::as_const(m_inputTransitions))
        if (!SceneUtils::isChild(parentItem, it->connectedItem(this)))
            return true;

    if (checkChildren) {
        const QList<QGraphicsItem *> items = childItems();
        for (QGraphicsItem *it : items) {
            if (it->type() >= InitialStateType) {
                auto item = qgraphicsitem_cast<ConnectableItem*>(it);
                if (item && item->hasInputTransitions(parentItem, checkChildren))
                    return true;
            }
        }
    }

    return false;
}

bool ConnectableItem::hasOutputTransitions(const ConnectableItem *parentItem, bool checkChildren) const
{
    for (TransitionItem *it : std::as_const(m_outputTransitions))
        if (!SceneUtils::isChild(parentItem, it->connectedItem(this)))
            return true;

    if (checkChildren) {
        const QList<QGraphicsItem *> items = childItems();
        for (QGraphicsItem *it : items) {
            if (it->type() >= InitialStateType) {
                auto item = qgraphicsitem_cast<ConnectableItem*>(it);
                if (item && item->hasOutputTransitions(parentItem, checkChildren))
                    return true;
            }
        }
    }

    return false;
}

void ConnectableItem::addOverlappingItem(ConnectableItem *item)
{
    if (!m_overlappedItems.contains(item))
        m_overlappedItems.append(item);

    setOverlapping(!m_overlappedItems.isEmpty());
}

void ConnectableItem::removeOverlappingItem(ConnectableItem *item)
{
    if (m_overlappedItems.contains(item))
        m_overlappedItems.removeAll(item);

    setOverlapping(!m_overlappedItems.isEmpty());
}

void ConnectableItem::checkOverlapping()
{
    QList<ConnectableItem*> overlappedItems;
    const QList<QGraphicsItem *> items = collidingItems();
    for (QGraphicsItem *it : items) {
        if (it->type() >= InitialStateType && it->parentItem() == parentItem()) {
            overlappedItems << qgraphicsitem_cast<ConnectableItem*>(it);
        }
    }

    // Remove unnecessary items
    for (int i = m_overlappedItems.count(); i--;) {
        if (!overlappedItems.contains(m_overlappedItems[i])) {
            m_overlappedItems[i]->removeOverlappingItem(this);
            m_overlappedItems.removeAt(i);
        }
    }

    // Add new overlapped items
    for (ConnectableItem *it : std::as_const(overlappedItems)) {
        if (!m_overlappedItems.contains(it)) {
            m_overlappedItems << it;
            it->addOverlappingItem(this);
        }
    }

    setOverlapping(!m_overlappedItems.isEmpty());
}

bool ConnectableItem::canStartTransition(ItemType type) const
{
    Q_UNUSED(type)
    return true;
}

QList<TransitionItem*> ConnectableItem::outputTransitions() const
{
    return m_outputTransitions;
}

QList<TransitionItem*> ConnectableItem::inputTransitions() const
{
    return m_inputTransitions;
}

int ConnectableItem::transitionCount() const
{
    return m_outputTransitions.count() + m_inputTransitions.count();
}

int ConnectableItem::outputTransitionCount() const
{
    return m_outputTransitions.count();
}

int ConnectableItem::inputTransitionCount() const
{
    return m_inputTransitions.count();
}
