// Copyright (C) 2016 Jochen Becher
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "boundaryitem.h"

#include "qmt/diagram_controller/diagramcontroller.h"
#include "qmt/diagram/dboundary.h"
#include "qmt/diagram_scene/capabilities/moveable.h"
#include "qmt/diagram_scene/diagramsceneconstants.h"
#include "qmt/diagram_scene/diagramscenemodel.h"
#include "qmt/diagram_scene/parts/rectangularselectionitem.h"
#include "qmt/infrastructure/geometryutilities.h"
#include "qmt/infrastructure/qmtassert.h"
#include "qmt/style/stylecontroller.h"
#include "qmt/style/style.h"

#include <QGraphicsScene>
#include <QGraphicsRectItem>
#include <QGraphicsTextItem>
#include <QTextDocument>
#include <QTextFrame>
#include <QStyleOptionGraphicsItem>
#include <QGraphicsSceneMouseEvent>
#include <QCoreApplication>

namespace qmt {

static const qreal MINIMUM_INNER_WIDTH = 22.0;
static const qreal MINIMUM_INNER_HEIGHT = 22.0;
static const qreal CONTENTS_BORDER_VERTICAL = 4.0;
static const qreal CONTENTS_BORDER_HORIZONTAL = 4.0;

class BoundaryItem::BoundaryTextItem : public QGraphicsTextItem
{
public:
    explicit BoundaryTextItem(QGraphicsItem *parent)
        : QGraphicsTextItem(parent)
    {
    }

    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) final
    {
        QMT_ASSERT(option, return);

        QStyleOptionGraphicsItem option2(*option);
        option2.state &= ~(QStyle::State_Selected | QStyle::State_HasFocus);
        QGraphicsTextItem::paint(painter, &option2, widget);
    }
};

BoundaryItem::BoundaryItem(DBoundary *boundary, DiagramSceneModel *diagramSceneModel, QGraphicsItem *parent)
    : QGraphicsItem(parent),
      m_boundary(boundary),
      m_diagramSceneModel(diagramSceneModel)
{
    setFlags(QGraphicsItem::ItemIsSelectable);
}

BoundaryItem::~BoundaryItem()
{
}

QRectF BoundaryItem::boundingRect() const
{
    return childrenBoundingRect();
}

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

void BoundaryItem::update()
{
    QMT_CHECK(!m_isUpdating);
    m_isUpdating = true;

    prepareGeometryChange();

    const Style *style = adaptedStyle();

    // text
    if (!m_textItem) {
        m_textItem = new BoundaryTextItem(this);
        m_textItem->setTextInteractionFlags(Qt::TextEditorInteraction);
        m_textItem->installSceneEventFilter(this);
        QObject::connect(m_textItem->document(), &QTextDocument::contentsChanged, m_textItem,
                         [this] { this->onContentsChanged(); } );
    }
    m_textItem->setFont(style->normalFont());
    m_textItem->setDefaultTextColor(style->textBrush().color());
    if (!m_isChanged) {
        m_textItem->setTextWidth(-1);
        m_textItem->setPlainText(m_boundary->text());
    }

    // item shown if annotation has no text and is not selected
    if (m_textItem->document()->isEmpty() && isSelected()) {
        if (!m_noTextItem)
            m_noTextItem = new QGraphicsRectItem(this);
        m_noTextItem->setPen(QPen(QBrush(QColor(192, 192, 192)), 1, Qt::DashDotLine));
    } else if (m_noTextItem) {
        m_noTextItem->scene()->removeItem(m_noTextItem);
        delete m_noTextItem;
        m_noTextItem = nullptr;
    }

    // item shown if annotation has no text and is not selected
    if (!m_borderItem)
        m_borderItem = new QGraphicsRectItem(this);
    m_borderItem->setPen(QPen(QBrush(Qt::black), 1, Qt::DashLine));

    updateSelectionMarker();
    updateGeometry();
    setZValue(BOUNDARY_ITEMS_ZVALUE);

    m_isUpdating = false;
}

QPointF BoundaryItem::pos() const
{
    return m_boundary->pos();
}

QRectF BoundaryItem::rect() const
{
    return m_boundary->rect();
}

QSizeF BoundaryItem::minimumSize() const
{
    return calcMinimumGeometry();
}

void BoundaryItem::setPosAndRect(const QPointF &originalPos, const QRectF &originalRect, const QPointF &topLeftDelta,
                                 const QPointF &bottomRightDelta)
{
    QPointF newPos = originalPos;
    QRectF newRect = originalRect;
    GeometryUtilities::adjustPosAndRect(&newPos, &newRect, topLeftDelta, bottomRightDelta, QPointF(0.5, 0.5));
    if (newPos != m_boundary->pos() || newRect != m_boundary->rect()) {
        m_diagramSceneModel->diagramController()->startUpdateElement(m_boundary, m_diagramSceneModel->diagram(), DiagramController::UpdateGeometry);
        m_boundary->setPos(newPos);
        m_boundary->setRect(newRect);
        m_diagramSceneModel->diagramController()->finishUpdateElement(m_boundary, m_diagramSceneModel->diagram(), false);
    }
}

void BoundaryItem::alignItemSizeToRaster(IResizable::Side adjustHorizontalSide, IResizable::Side adjustVerticalSide,
                                         double rasterWidth, double rasterHeight)
{
    QPointF pos = m_boundary->pos();
    QRectF rect = m_boundary->rect();

    double horizDelta = rect.width() - qRound(rect.width() / rasterWidth) * rasterWidth;
    double vertDelta = rect.height() - qRound(rect.height() / rasterHeight) * rasterHeight;

    // make sure the new size is at least the minimum size
    QSizeF minimumSize = this->minimumSize();
    while (rect.width() + horizDelta < minimumSize.width())
        horizDelta += rasterWidth;
    while (rect.height() + vertDelta < minimumSize.height())
        vertDelta += rasterHeight;

    double leftDelta = 0.0;
    double rightDelta = 0.0;
    double topDelta = 0.0;
    double bottomDelta = 0.0;

    switch (adjustHorizontalSide) {
    case IResizable::SideNone:
        break;
    case IResizable::SideLeftOrTop:
        leftDelta = horizDelta;
        break;
    case IResizable::SideRightOrBottom:
        rightDelta = -horizDelta;
        break;
    }

    switch (adjustVerticalSide) {
    case IResizable::SideNone:
        break;
    case IResizable::SideLeftOrTop:
        topDelta = vertDelta;
        break;
    case IResizable::SideRightOrBottom:
        bottomDelta = -vertDelta;
        break;
    }

    QPointF topLeftDelta(leftDelta, topDelta);
    QPointF bottomRightDelta(rightDelta, bottomDelta);
    setPosAndRect(pos, rect, topLeftDelta, bottomRightDelta);
}

void BoundaryItem::moveDelta(const QPointF &delta)
{
    m_diagramSceneModel->diagramController()->startUpdateElement(m_boundary, m_diagramSceneModel->diagram(), DiagramController::UpdateGeometry);
    m_boundary->setPos(m_boundary->pos() + delta);
    m_diagramSceneModel->diagramController()->finishUpdateElement(m_boundary, m_diagramSceneModel->diagram(), false);
}

void BoundaryItem::alignItemPositionToRaster(double rasterWidth, double rasterHeight)
{
    QPointF pos = m_boundary->pos();
    QRectF rect = m_boundary->rect();
    QPointF topLeft = pos + rect.topLeft();

    double leftDelta = qRound(topLeft.x() / rasterWidth) * rasterWidth - topLeft.x();
    double topDelta = qRound(topLeft.y() / rasterHeight) * rasterHeight - topLeft.y();
    QPointF topLeftDelta(leftDelta, topDelta);

    setPosAndRect(pos, rect, topLeftDelta, topLeftDelta);
}

bool BoundaryItem::isSecondarySelected() const
{
    return m_isSecondarySelected;
}

void BoundaryItem::setSecondarySelected(bool secondarySelected)
{
    if (m_isSecondarySelected != secondarySelected) {
        m_isSecondarySelected = secondarySelected;
        update();
    }
}

bool BoundaryItem::isFocusSelected() const
{
    return m_isFocusSelected;
}

void BoundaryItem::setFocusSelected(bool focusSelected)
{
    if (m_isFocusSelected != focusSelected) {
        m_isFocusSelected = focusSelected;
        update();
    }
}

QRectF BoundaryItem::getSecondarySelectionBoundary()
{
    return QRectF();
}

void BoundaryItem::setBoundarySelected(const QRectF &boundary, bool secondary)
{
    if (boundary.contains(mapRectToScene(boundingRect()))) {
        if (secondary)
            setSecondarySelected(true);
        else
            setSelected(true);
    }
}

bool BoundaryItem::isEditable() const
{
    return true;
}

bool BoundaryItem::isEditing() const
{
    return m_textItem && m_textItem->hasFocus();
}

void BoundaryItem::edit()
{
    if (m_textItem)
        m_textItem->setFocus();
}

void BoundaryItem::finishEdit()
{
    if (m_textItem)
        m_textItem->clearFocus();
}

void BoundaryItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    if (event->button() == Qt::LeftButton || event->button() == Qt::RightButton) {
        QList<QGraphicsItem *> collidingItems = m_diagramSceneModel->collectCollidingObjectItems(this, DiagramSceneModel::CollidingInnerItems);
        if (!collidingItems.isEmpty()) {
            for (QGraphicsItem *item : collidingItems) {
                if (item != this
                        && m_diagramSceneModel->isInFrontOf(this, item)
                        && item->contains(mapToItem(item, event->pos())))
                {
                    event->ignore();
                    return;
                }
            }
        }
        m_diagramSceneModel->selectItem(this, event->modifiers() & Qt::ControlModifier);
    }
    if (event->buttons() & Qt::LeftButton)
        m_diagramSceneModel->moveSelectedItems(this, QPointF(0.0, 0.0));
}

void BoundaryItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
    if (event->buttons() & Qt::LeftButton)
        m_diagramSceneModel->moveSelectedItems(this, event->scenePos() - event->lastScenePos());
}

void BoundaryItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        m_diagramSceneModel->moveSelectedItems(this, event->scenePos() - event->lastScenePos());
        if (event->scenePos() != event->buttonDownScenePos(Qt::LeftButton))
            m_diagramSceneModel->alignSelectedItemsPositionOnRaster();
    }
}

void BoundaryItem::updateSelectionMarker()
{
    if (isSelected() || m_isSecondarySelected) {
        if (!m_selectionMarker)
            m_selectionMarker = new RectangularSelectionItem(this, this);
        m_selectionMarker->setSecondarySelected(isSelected() ? false : m_isSecondarySelected);
    } else if (m_selectionMarker) {
        if (m_selectionMarker->scene())
            m_selectionMarker->scene()->removeItem(m_selectionMarker);
        delete m_selectionMarker;
        m_selectionMarker = nullptr;
    }
}

void BoundaryItem::updateSelectionMarkerGeometry(const QRectF &boundaryRect)
{
    if (m_selectionMarker)
        m_selectionMarker->setRect(boundaryRect);
}

const Style *BoundaryItem::adaptedStyle()
{
    return m_diagramSceneModel->styleController()->adaptBoundaryStyle(m_boundary);
}

bool BoundaryItem::sceneEventFilter(QGraphicsItem *watched, QEvent *event)
{
    if (watched == m_textItem) {
        if (event->type() == QEvent::FocusIn) {
            scene()->clearSelection();
            setSelected(true);
        }
    }
    return false;
}

void BoundaryItem::onContentsChanged()
{
    QMT_CHECK(!m_isChanged);
    m_isChanged = true;

    if (!m_isUpdating) {
        QString plainText = m_textItem->toPlainText();
        if (m_boundary->text() != plainText) {
            m_diagramSceneModel->diagramController()->startUpdateElement(m_boundary, m_diagramSceneModel->diagram(), DiagramController::UpdateMinor);
            m_boundary->setText(plainText);
            m_diagramSceneModel->diagramController()->finishUpdateElement(m_boundary, m_diagramSceneModel->diagram(), false);
        }
    }

    m_isChanged = false;
}

QSizeF BoundaryItem::calcMinimumGeometry() const
{
    qreal width = MINIMUM_INNER_WIDTH + 2 * CONTENTS_BORDER_HORIZONTAL;
    qreal height = MINIMUM_INNER_HEIGHT + 2 * CONTENTS_BORDER_VERTICAL;

    if (m_textItem) {
        m_textItem->setTextWidth(-1);
        QSizeF textSize = m_textItem->document()->size();
        qreal textWidth = textSize.width() + 2 * CONTENTS_BORDER_HORIZONTAL;
        if (textWidth > width)
            width = textWidth;
        qreal textHeight = textSize.height() + 2 * CONTENTS_BORDER_VERTICAL;
        if (textHeight > height)
            height = textHeight;
    }
    return GeometryUtilities::ensureMinimumRasterSize(QSizeF(width, height), 2 * RASTER_WIDTH, 2 * RASTER_HEIGHT);
}

void BoundaryItem::updateGeometry()
{
    prepareGeometryChange();

    QSizeF geometry = calcMinimumGeometry();
    qreal width = geometry.width();
    qreal height = geometry.height();

    qreal textWidth = 0.0;
    qreal textHeight = 0.0;
    if (m_textItem) {
        m_textItem->setTextWidth(-1);
        QSizeF textSize = m_textItem->document()->size();
        textWidth = textSize.width();
        textHeight = textSize.height();
    }

    QRectF boundaryRect = m_boundary->rect();
    if (boundaryRect.width() > width)
        width = boundaryRect.width();
    if (boundaryRect.height() > height)
        height = boundaryRect.height();

    // update sizes and positions
    double left = -width / 2.0;
    double top = -height / 2.0;

    setPos(m_boundary->pos());

    QRectF rect(left, top, width, height);

    // the object is updated without calling DiagramController intentionally.
    // attribute rect is not a real attribute stored on DObject but
    // a backup for the graphics item used for manual resized and persistency.
    m_boundary->setRect(rect);
    if (m_borderItem)
        m_borderItem->setRect(rect);
    if (m_noTextItem)
        m_noTextItem->setRect(QRectF(-textWidth / 2, top + CONTENTS_BORDER_VERTICAL, textWidth, textHeight));
    if (m_textItem)
        m_textItem->setPos(-textWidth / 2.0, top + CONTENTS_BORDER_VERTICAL);
    updateSelectionMarkerGeometry(rect);
}

} // namespace qmt
