//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/Scene/MaskGraphicsScene.cpp
//! @brief     Implements class MaskGraphicsScene.
//!
//! @homepage  http://www.bornagainproject.org
//! @license   GNU General Public License v3 or higher (see COPYING)
//! @copyright Forschungszentrum Jülich GmbH 2018
//! @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
//
//  ************************************************************************************************

#include "GUI/View/Scene/MaskGraphicsScene.h"
#include "Base/Util/Assert.h"
#include "GUI/Model/Data/Data2DItem.h"
#include "GUI/Model/Mask/MasksSet.h"
#include "GUI/Model/Mask/PointItem.h"
#include "GUI/Model/Project/ProjectDocument.h"
#include "GUI/View/Overlay/EllipseOverlay.h"
#include "GUI/View/Overlay/FullframeOverlay.h"
#include "GUI/View/Overlay/LineOverlays.h"
#include "GUI/View/Overlay/PolygonOverlay.h"
#include "GUI/View/Overlay/ROIOverlay.h"
#include "GUI/View/Overlay/RectangleOverlay.h"
#include "GUI/View/Overlay/SizeHandle.h"
#include "GUI/View/Overlay/VertexOverlay.h"
#include "GUI/View/Overlay/Viewport.h"
#include "GUI/View/Plotter/ColorMap.h"
#include "GUI/View/Scene/MaskGraphicsProxy.h"
#include <QGraphicsSceneContextMenuEvent>

namespace {

const qreal min_distance_to_create_rect = 10;

//! Return true if area beneath the mouse contains views of given type.
template <typename T> bool areaContains(QVector<QGraphicsItem*> items)
{
    for (QGraphicsItem* item : items)
        if (dynamic_cast<T*>(item))
            return true;
    return false;
}

IOverlay* createOverlay(OverlayItem* item, ColorMap* plot, bool is_projection)
{
    if (auto* mask = dynamic_cast<RectangleItem*>(item))
        return new RectangleOverlay(mask, plot);

    if (auto* mask = dynamic_cast<PolygonItem*>(item))
        return new PolygonOverlay(mask, plot);

    if (auto* mask = dynamic_cast<PointItem*>(item))
        return new VertexOverlay(mask, plot);

    if (auto* mask = dynamic_cast<VerticalLineItem*>(item))
        return new VerticalLineOverlay(mask, plot, is_projection);

    if (auto* mask = dynamic_cast<HorizontalLineItem*>(item))
        return new HorizontalLineOverlay(mask, plot, is_projection);

    if (auto* mask = dynamic_cast<EllipseItem*>(item))
        return new EllipseOverlay(mask, plot);

    if (auto* mask = dynamic_cast<FullframeItem*>(item))
        return new FullframeOverlay(mask, plot);

    if (auto* mask = dynamic_cast<RegionOfInterestItem*>(item))
        return new ROIOverlay(mask, plot);

    ASSERT_NEVER;
}

} // namespace


MaskGraphicsScene::MaskGraphicsScene()
    : m_plot(std::make_unique<ColorMap>())
    , m_proxy(new MaskGraphicsProxy)
    , m_viewport(new Viewport(m_plot.get()))

{
    m_proxy->setWidget(m_plot.get());
    addItem(m_proxy);    // takes ownership
    addItem(m_viewport); // takes ownership
}

MaskGraphicsScene::~MaskGraphicsScene() = default;

void MaskGraphicsScene::switchDataContext(Data2DItem* data_item)
{
    connectOverlaySelection(false);

    clearOverlays();
    associateItems(data_item);
    updateOverlays();

    connectMaskSet(true);
    connectProjSet(true);
    connectOverlaySelection(true);

    // adjust scene visual selection to the state of the Sets
    onMaskSetChanged();
    onProjSetChanged();

    // update bounding rectangle and replot masks on widget resize
    connect(m_plot.get(), &ColorMap::marginsChanged, this, &MaskGraphicsScene::updBoundingRect,
            Qt::UniqueConnection);
}

void MaskGraphicsScene::updBoundingRect()
{
    if (!m_data_item)
        return;
    updateOverlays();
}

void MaskGraphicsScene::clearOverlays()
{
    for (QGraphicsItem* overlay : m_viewport->childItems()) {
        removeItem(overlay);
        delete overlay;
    }
    m_mask2overlay.clear();
}

void MaskGraphicsScene::associateItems(Data2DItem* data_item)
{
    ASSERT(data_item);
    m_data_item = data_item;

    connect(m_data_item, &QObject::destroyed, this, &MaskGraphicsScene::onDataDestroyed,
            Qt::UniqueConnection);

    m_plot->itemToMap(data_item);

    m_masks = data_item->masksRW();
    m_prjns = data_item->prjnsRW();
    ASSERT(m_masks);
    ASSERT(m_prjns);
}

void MaskGraphicsScene::onDataDestroyed()
{
    m_data_item = nullptr;
    m_masks = nullptr;
    m_prjns = nullptr;
    m_active_mask = nullptr;
    connectOverlaySelection(false); // before clearing overlays
    clearOverlays();
}

void MaskGraphicsScene::updateSize(const QSize& newSize)
{
    if (!m_proxy)
        return;

    m_proxy->resize(newSize);
    setSceneRect(0, 0, newSize.width(), newSize.height());
    m_proxy->setPos(0, 0);
}

void MaskGraphicsScene::deleteCurrentItem()
{
    const IMaskOverlay* selected_overlay = selectedMaskOverlay();
    if (!selected_overlay)
        return;

    // To delete the current mask or projection, its overlay MUST be selected!

    const MaskItem* selected_item = maskItemForOverlay(selected_overlay);
    ASSERT(selected_item);

    MasksSet* current_set = modelOfMaskItem(selected_item);
    ASSERT(current_set->currentItem() == selected_item);

    connectOverlaySelection(false); // do not switch selection in the intermediate state
    removeOverlay(current_set->currentItem());
    connectOverlaySelection(true);
    current_set->delete_current();
}

void MaskGraphicsScene::cancelCurrentDrawing()
{
    if (!m_drawing_in_progress)
        return;

    ASSERT(m_active_mask);
    ASSERT(m_active_mask == modelOfMaskItem(m_active_mask)->currentItem());

    deleteCurrentItem();
    setDrawingInProgress(false);
    update(); // clears ghost lines from the unfinished polygon
}

void MaskGraphicsScene::onActivityChanged(Canvas2DMode::Flag mode)
{
    if (!m_proxy)
        return;
    if (m_drawing_in_progress && m_mode == Canvas2DMode::POLYGON && mode != m_mode)
        cancelCurrentDrawing();

    m_mode = mode;
    m_proxy->setZooming(m_mode == Canvas2DMode::PAN_ZOOM);
    updateCursors();
}

void MaskGraphicsScene::mousePressEvent(QGraphicsSceneMouseEvent* event)
{
    // forbid multiple visual selection
    event->setModifiers(Qt::NoModifier);

    if (event->buttons() & Qt::LeftButton)
        m_mouse_is_pressed = true;

    if (event->buttons() & Qt::RightButton) {
        if (m_drawing_in_progress)
            cancelCurrentDrawing();
        else
            selectOnlyItemAtMousePos(event);
        return;
    }

    if (!isValidMouseClick(event))
        return QGraphicsScene::mousePressEvent(event);

    if (isValidForPolygonDrawing(event))
        processPolygonItem(event);
    else if (isValidForLineDrawing(event))
        processLineItem(event);
    else if (isValidForMaskAllDrawing(event))
        processFullframeItem(event);
    else if (isValidForRectangleShapeDrawing(event))
        setDrawingInProgress(true);
    else
        QGraphicsScene::mousePressEvent(event);
}

void MaskGraphicsScene::mouseMoveEvent(QGraphicsSceneMouseEvent* event)
{
    if (m_drawing_in_progress && Canvas2DMode::basedOnRectangle(m_mode)) {
        processRectangleOrEllipseItem(event);
        return;
    }
    QGraphicsScene::mouseMoveEvent(event);

    if ((m_drawing_in_progress && m_mode == Canvas2DMode::POLYGON)
        || Canvas2DMode::isLineMode(m_mode)) {
        m_mouse_position = event->scenePos();
        QGraphicsScene::invalidate(); // Cached content in layers is invalidated and redrawn.
    }
}

//! Finalizes item drawing or pass events to other items.

void MaskGraphicsScene::mouseReleaseEvent(QGraphicsSceneMouseEvent* event)
{
    event->setModifiers(Qt::NoModifier);

    m_mouse_is_pressed = false;

    if (!m_drawing_in_progress)
        QGraphicsScene::mouseReleaseEvent(event);
    else if (Canvas2DMode::basedOnRectangle(m_mode)) {
        if (m_active_mask) {
            // drawing ended up with item drawn, let's make it selected
            selectOnlyGivenItem(m_mask2overlay[m_active_mask]);
        } else {
            // drawing ended without item to be draw (too short mouse move)
            // making item beneath of mouse release position to be selected
            selectOnlyItemAtMousePos(event);
        }
        setDrawingInProgress(false);
    }
}

//! Draws dashed line to the current mouse position if we are constructing a polygon
//! or if we are going to place a horizontal or vertical line.

void MaskGraphicsScene::drawForeground(QPainter* painter, const QRectF&)
{
    if (!m_plot)
        return;
    if (m_mouse_position == QPointF())
        return;

    painter->setPen(QPen((Canvas2DMode::isPrjn(m_mode) ? QColorConstants::Svg::darkcyan
                                                       : QColorConstants::Svg::darkolivegreen),
                         2, Qt::DashLine));

    if (const PolygonOverlay* polygon = currentPolygon()) {
        painter->drawLine(QLineF(polygon->lastAddedPoint(), m_mouse_position));

    } else if (Canvas2DMode::isLineMode(m_mode)) {
        const QRectF& plot_scene_rectangle = GUI::Util::viewportRectangle(m_plot.get());
        if (!plot_scene_rectangle.contains(m_mouse_position))
            return;
        if (Canvas2DMode::isVerticalLine(m_mode)) {
            QPointF p1(m_mouse_position.x(), plot_scene_rectangle.bottom());
            QPointF p2(m_mouse_position.x(), plot_scene_rectangle.top());
            painter->drawLine(QLineF(p1, p2));
        }
        if (Canvas2DMode::isHorizontalLine(m_mode)) {
            QPointF p1(plot_scene_rectangle.left(), m_mouse_position.y());
            QPointF p2(plot_scene_rectangle.right(), m_mouse_position.y());
            painter->drawLine(QLineF(p1, p2));
        }
    }
}

//! Creates item context menu if there is IMaskView beneath the mouse right click.

void MaskGraphicsScene::contextMenuEvent(QGraphicsSceneContextMenuEvent* event)
{
    if (m_drawing_in_progress)
        return;

    const MaskItem* selected_item = maskItemForOverlay(selectedMaskOverlay());
    emit itemContextMenuRequest(event->screenPos(), selected_item);
}

void MaskGraphicsScene::updateCursors()
{
    const bool zooming = m_mode == Canvas2DMode::PAN_ZOOM;
    const auto zoom_cursor = Qt::ClosedHandCursor;
    for (auto const& [item, overlay] : m_mask2overlay) {
        overlay->setAcceptedMouseButtons(zooming ? Qt::NoButton : Qt::LeftButton);

        if (dynamic_cast<HorizontalLineOverlay*>(overlay))
            overlay->setCursor(zooming ? zoom_cursor : Qt::SizeVerCursor);
        else if (dynamic_cast<VerticalLineOverlay*>(overlay))
            overlay->setCursor(zooming ? zoom_cursor : Qt::SizeHorCursor);
        else if (dynamic_cast<VertexOverlay*>(overlay))
            overlay->setCursor(zooming ? zoom_cursor : Qt::CrossCursor);
        else
            // rectangle, ellipse, polygon
            overlay->setCursor(zooming ? zoom_cursor : Qt::PointingHandCursor);
    }
    m_viewport->setCursor(zooming ? zoom_cursor : Qt::ArrowCursor);
}

//! Runs through the model and creates corresponding views.

void MaskGraphicsScene::updateOverlays()
{
    ASSERT(m_masks);
    ASSERT(m_prjns);

    // Z-values of all overlays reflect stacking order.
    // Projections are always on top of masks.
    int z = 0;

    for (MaskItem* mask_item : *m_masks) {
        IOverlay* overlay = registerOverlay(mask_item, false);
        ASSERT(overlay);

        // Add views for the points of the PolygonItem
        if (auto* polygon_item = dynamic_cast<PolygonItem*>(mask_item)) {
            auto* polygon_overlay = dynamic_cast<PolygonOverlay*>(overlay);
            ASSERT(polygon_overlay);
            for (PointItem* point_item : polygon_item->points()) {
                IOverlay* point_overlay = registerOverlay(point_item, false);
                polygon_overlay->addOverlay(point_overlay);
            }
        }
        overlay->setZValue(z);
        ++z;
    }

    for (MaskItem* t : *m_prjns) {
        IOverlay* overlay = registerOverlay(t, true);
        ASSERT(overlay);
        overlay->setZValue(z);
        ++z;
    }
    updateCursors();
}

//! Creates a view for given item.

IOverlay* MaskGraphicsScene::registerOverlay(OverlayItem* item, bool is_projection)
{
    ASSERT(m_plot);
    ASSERT(item);

    IOverlay* overlay = m_mask2overlay[item];
    if (!overlay) {
        overlay = ::createOverlay(item, m_plot.get(), is_projection);
        m_mask2overlay[item] = overlay;
        m_viewport->addChildGraphics(overlay); // takes ownership
    }

    item->disconnect();
    connect(item, &OverlayItem::maskGeometryChanged, [this] {
        if (m_mouse_is_pressed)
            gDoc->setModified(); // manual mask movement
    });
    if (is_projection) {
        if (auto* line_item = dynamic_cast<LineItem*>(item)) {
            connect(line_item, &OverlayItem::maskGeometryChanged, [this, line_item] {
                emit lineItemMoved(line_item); // -> update projections plot
            });
            connect(line_item, &OverlayItem::maskToBeDestroyed,
                    [this, line_item] { emit lineItemDeleted(line_item); });
        }
    }
    connect(m_plot.get(), &QCustomPlot::afterReplot, overlay, &IOverlay::onGeometryChange,
            Qt::UniqueConnection);
    connect(item, &OverlayItem::maskGeometryChanged, overlay, &IOverlay::onGeometryChange,
            Qt::UniqueConnection);
    if (auto* mask_item = dynamic_cast<MaskItem*>(item)) {
        if (auto* mask_overlay = dynamic_cast<IMaskOverlay*>(overlay)) {
            mask_overlay->setVisible(mask_item->isVisible());
            connect(mask_item, &OverlayItem::maskVisibilityChanged, mask_overlay,
                    &IMaskOverlay::onVisibilityChange, Qt::UniqueConnection);
        } else
            ASSERT_NEVER;
    }

    overlay->update_view();
    return overlay;
}

//! Removes single view from scene.

void MaskGraphicsScene::removeOverlay(const OverlayItem* item)
{
    if (auto it = m_mask2overlay.find(item); it != m_mask2overlay.end()) {
        // at first, delete views for the points of the PolygonItem
        if (const auto* polygon_item = dynamic_cast<const PolygonItem*>(item))
            for (PointItem* point_item : polygon_item->points())
                removeOverlay(point_item);

        IOverlay* overlay = it->second;
        overlay->setSelected(false);
        m_mask2overlay.erase(it);
        removeItem(overlay);
        delete overlay;
    }
}

MasksSet* MaskGraphicsScene::modelOfMaskItem(const MaskItem* item) const
{
    ASSERT(m_masks);
    ASSERT(m_prjns);
    ASSERT(item);

    if (m_masks->index_of(item) != -1)
        return m_masks;
    else if (m_prjns->index_of(item) != -1)
        return m_prjns;
    else
        ASSERT_NEVER;
}

void MaskGraphicsScene::unselectOtherModelThan(const MasksSet* current_set)
{
    ASSERT(m_masks);
    ASSERT(m_prjns);
    if (current_set == m_masks)
        m_prjns->setCurrentIndex(-1);
    else if (current_set == m_prjns)
        m_masks->setCurrentIndex(-1);
    else
        ASSERT_NEVER;
}

const MaskItem* MaskGraphicsScene::maskItemForOverlay(const IOverlay* given_overlay) const
{
    for (const auto& [over_item, overlay] : m_mask2overlay)
        if (overlay == given_overlay)
            if (const MaskItem* mask_item = dynamic_cast<const MaskItem*>(over_item))
                return mask_item;
    return nullptr;
}

const IMaskOverlay* MaskGraphicsScene::selectedMaskOverlay() const
{
    if (!selectedItems().size())
        return nullptr;

    ASSERT(selectedItems().size() == 1);
    const auto* selected_mask_overlay = dynamic_cast<const IMaskOverlay*>(selectedItems().front());
    ASSERT(selected_mask_overlay);
    return selected_mask_overlay;
}

void MaskGraphicsScene::connectMaskSet(bool isConnected)
{
    if (!m_masks)
        return;

    if (isConnected)
        connect(m_masks, &MasksSet::setChanged, this, &MaskGraphicsScene::onMaskSetChanged,
                Qt::UniqueConnection);
    else
        disconnect(m_masks, &MasksSet::setChanged, this, &MaskGraphicsScene::onMaskSetChanged);
}

void MaskGraphicsScene::connectProjSet(bool isConnected)
{
    if (!m_prjns)
        return;

    if (isConnected)
        connect(m_prjns, &MasksSet::setChanged, this, &MaskGraphicsScene::onProjSetChanged,
                Qt::UniqueConnection);
    else
        disconnect(m_prjns, &MasksSet::setChanged, this, &MaskGraphicsScene::onProjSetChanged);
}

void MaskGraphicsScene::connectOverlaySelection(bool isConnected)
{
    if (isConnected)
        connect(this, &MaskGraphicsScene::selectionChanged, this,
                &MaskGraphicsScene::onSceneSelectionChanged, Qt::UniqueConnection);
    else
        disconnect(this, &MaskGraphicsScene::selectionChanged, this,
                   &MaskGraphicsScene::onSceneSelectionChanged);
}

void MaskGraphicsScene::onMaskSetChanged()
{
    ASSERT(m_masks);
    if (!m_masks->currentItem())
        return;

    // Selecting mask means unselecting projection!
    connectProjSet(false);
    unselectOtherModelThan(m_masks);
    connectProjSet(true);

    connectOverlaySelection(false);
    selectOnlyGivenItem(m_mask2overlay[m_masks->currentItem()]);
    connectOverlaySelection(true);
}

void MaskGraphicsScene::onProjSetChanged()
{
    ASSERT(m_prjns);
    if (!m_prjns->currentItem())
        return;

    // Selecting projection means unselecting mask!
    connectMaskSet(false);
    unselectOtherModelThan(m_prjns);
    connectMaskSet(true);

    connectOverlaySelection(false);
    selectOnlyGivenItem(m_mask2overlay[m_prjns->currentItem()]);
    connectOverlaySelection(true);

    // switch projection plot
    const auto* line_item = dynamic_cast<const LineItem*>(m_prjns->currentItem());
    ASSERT(line_item);
    emit lineItemMoved(line_item);
}

void MaskGraphicsScene::onSceneSelectionChanged()
{
    ASSERT(m_masks);
    ASSERT(m_prjns);
    connectMaskSet(false);
    connectProjSet(false);

    if (!selectedMaskOverlay()) {
        // if mask is not selected because it was hidden, do not switch Set to unselected state
        if (m_masks->currentItem() && m_masks->currentItem()->isVisible())
            m_masks->setCurrentIndex(-1);
        m_prjns->setCurrentIndex(-1);
        connectSelectedLineMove(nullptr);
    } else {
        const MaskItem* selected_item = maskItemForOverlay(selectedMaskOverlay());
        ASSERT(selected_item);

        MasksSet* current_set = modelOfMaskItem(selected_item);
        current_set->setCurrentIndex(current_set->index_of(selected_item));
        unselectOtherModelThan(current_set);

        if (current_set == m_prjns) {
            const auto* line_item = dynamic_cast<const LineItem*>(selected_item);
            ASSERT(line_item);
            emit lineItemMoved(line_item);      // switch projection plot
            connectSelectedLineMove(line_item); // activate updating projection plot while moving
        }

        // switch panel
        emit switchPanelRequest(1 + int(current_set == m_prjns));
    }
    connectMaskSet(true);
    connectProjSet(true);
}

void MaskGraphicsScene::setDrawingInProgress(bool value)
{
    m_drawing_in_progress = value;
    if (value)
        gDoc->setModified(); // manual mask creation
    else
        m_active_mask = nullptr;
}

void MaskGraphicsScene::selectOnlyItemAtMousePos(QGraphicsSceneMouseEvent* event)
{
    selectOnlyGivenItem(itemAt(event->scenePos(), QTransform()));
}

void MaskGraphicsScene::selectOnlyGivenItem(QGraphicsItem* givenItem)
{
    // prevent signaling with 'selectionChanged' during intermediate operations
    blockSignals(true);

    auto old_selection = selectedItems();
    for (QGraphicsItem* otherItem : old_selection)
        otherItem->setSelected(false);

    if (givenItem && givenItem != m_viewport)
        givenItem->setSelected(true);

    blockSignals(false);

    if (old_selection != selectedItems())
        emit selectionChanged();
}

void MaskGraphicsScene::connectSelectedLineMove(const LineItem* given_line)
{
    for (const auto& [item, overlay] : m_mask2overlay) {
        if (item == given_line) {
            if (dynamic_cast<const HorizontalLineItem*>(given_line))
                connect(overlay, &QGraphicsObject::yChanged, item,
                        &OverlayItem::maskGeometryChanged, Qt::UniqueConnection);
            else if (dynamic_cast<const VerticalLineItem*>(given_line))
                connect(overlay, &QGraphicsObject::xChanged, item,
                        &OverlayItem::maskGeometryChanged, Qt::UniqueConnection);
        } else {
            disconnect(overlay, &QGraphicsObject::xChanged, item,
                       &OverlayItem::maskGeometryChanged);
            disconnect(overlay, &QGraphicsObject::yChanged, item,
                       &OverlayItem::maskGeometryChanged);
        }
    }
}

//! Processes RectangleItem and EllipseItem drawing.
//! Called upon mouse move event (with left button down).
//! If mouse has moved sufficiently far, a new item is created.
//! On further calls, size and position of the rectangle are updated.

void MaskGraphicsScene::processRectangleOrEllipseItem(QGraphicsSceneMouseEvent* event)
{
    ASSERT(m_plot);
    const QPointF click_pos = event->buttonDownScenePos(Qt::LeftButton);
    const QPointF mouse_pos = event->scenePos();

    //... Create new item?

    if (!m_active_mask) {
        if (QLineF(mouse_pos, click_pos).length() < min_distance_to_create_rect)
            return; // selected area is too small => don't create object yet

        if (m_mode == Canvas2DMode::RECTANGLE)
            m_active_mask = new RectangleItem;
        else if (m_mode == Canvas2DMode::ELLIPSE)
            m_active_mask = new EllipseItem;
        else if (m_mode == Canvas2DMode::ROI)
            m_active_mask = new RegionOfInterestItem;
        else
            ASSERT_NEVER;

        m_masks->add_item(m_active_mask);

        if (m_mode != Canvas2DMode::ROI)
            m_active_mask->setMaskValue(m_mask_value);
    }

    //... Update item geometry

    const double dxl = std::min(click_pos.x(), mouse_pos.x());
    const double dxh = std::max(click_pos.x(), mouse_pos.x());
    const double dyl = std::min(click_pos.y(), mouse_pos.y());
    const double dyh = std::max(click_pos.y(), mouse_pos.y());

    const double xl = m_plot->xAxis->pixelToCoord(dxl);
    const double xh = m_plot->xAxis->pixelToCoord(dxh);
    const double yl = m_plot->yAxis->pixelToCoord(dyl);
    const double yh = m_plot->yAxis->pixelToCoord(dyh);

    if (auto* rectItem = dynamic_cast<RectangleItem*>(m_active_mask)) {
        // RectangleItem or RegionOfInterestItem
        rectItem->setXLow(xl);
        rectItem->setXHig(xh);
        rectItem->setYLow(yh);
        rectItem->setYHig(yl);
    } else if (auto* ellItem = dynamic_cast<EllipseItem*>(m_active_mask)) {
        ellItem->setXCenter((xh + xl) / 2);
        ellItem->setYCenter((yh + yl) / 2);
        ellItem->setXRadius((xh - xl) / 2);
        ellItem->setYRadius((yl - yh) / 2);
    }

    emit m_active_mask->maskGeometryChanged();
    // produce views for the created shape
    updateOverlays();
}

void MaskGraphicsScene::processPolygonItem(QGraphicsSceneMouseEvent* event)
{
    ASSERT(m_plot);
    ASSERT(m_mode == Canvas2DMode::POLYGON);

    if (!m_active_mask) {
        setDrawingInProgress(true);
        MaskItem* new_poly = new PolygonItem;
        m_masks->add_item(new_poly);
        new_poly->setMaskValue(m_mask_value);
        m_active_mask = new_poly;
        registerOverlay(new_poly, false);
    }
    ASSERT(dynamic_cast<PolygonItem*>(m_active_mask));
    ASSERT(dynamic_cast<PolygonOverlay*>(currentPolygon()));

    selectOnlyGivenItem(currentPolygon());

    if (currentPolygon()->closePolygonIfNecessary()) {
        setDrawingInProgress(false);
        m_mouse_position = {};
    } else {
        const QPointF click_pos = event->buttonDownScenePos(Qt::LeftButton);
        const double x = m_plot->xAxis->pixelToCoord(click_pos.x());
        const double y = m_plot->yAxis->pixelToCoord(click_pos.y());
        dynamic_cast<PolygonItem*>(m_active_mask)->addPoint(x, y);
    }
    updateOverlays();
}

void MaskGraphicsScene::processLineItem(QGraphicsSceneMouseEvent* event)
{
    ASSERT(m_plot);
    setDrawingInProgress(true);
    QPointF pos = event->buttonDownScenePos(Qt::LeftButton);

    if (Canvas2DMode::isVerticalLine(m_mode))
        m_active_mask = new VerticalLineItem(m_plot->xAxis->pixelToCoord(pos.x()));
    else if (Canvas2DMode::isHorizontalLine(m_mode))
        m_active_mask = new HorizontalLineItem(m_plot->yAxis->pixelToCoord(pos.y()));
    else
        ASSERT_NEVER;

    if (Canvas2DMode::isPrjn(m_mode))
        m_prjns->add_item(m_active_mask);
    else
        m_masks->add_item(m_active_mask);

    emit m_active_mask->maskGeometryChanged();

    m_active_mask->setMaskValue(m_mask_value);

    emit lineItemProcessed();

    setDrawingInProgress(false);
    updateOverlays();
}

// TODO: check FullframeItem
void MaskGraphicsScene::processFullframeItem(QGraphicsSceneMouseEvent* event)
{
    Q_UNUSED(event);
    setDrawingInProgress(true);
    MaskItem* item = new FullframeItem;
    m_masks->add_item(item);
    m_active_mask = item;
    setDrawingInProgress(false);
}

//  ************************************************************************************************
//  private const functions
//  ************************************************************************************************

//! Returns true if left mouse bottom click was inside ColorMap viewport rectangle.

bool MaskGraphicsScene::isValidMouseClick(QGraphicsSceneMouseEvent* event) const
{
    return (event->buttons() & Qt::LeftButton) && m_plot
           && GUI::Util::viewportRectangle(m_plot.get()).contains(event->scenePos());
}

//! Returns true if mouse click is valid for rectangular/elliptic/ROI shapes.

bool MaskGraphicsScene::isValidForRectangleShapeDrawing(QGraphicsSceneMouseEvent* event) const
{
    if (m_drawing_in_progress)
        return false;
    if (!Canvas2DMode::basedOnRectangle(m_mode))
        return false;
    if (areaContains<SizeHandle>(items(event->scenePos())))
        return false;
    if (m_mode == Canvas2DMode::ROI)
        // only one ROI is allowed
        for (const auto& [item, overlay] : m_mask2overlay)
            if (dynamic_cast<const RegionOfInterestItem*>(item))
                return false;
    return true;
}

//! Returns true if mouse click is in context suitable for polygon drawing.

bool MaskGraphicsScene::isValidForPolygonDrawing(QGraphicsSceneMouseEvent* event) const
{
    if (m_mode != Canvas2DMode::POLYGON)
        return false;
    if (!m_drawing_in_progress && areaContains<VertexOverlay>(items(event->scenePos())))
        return false;
    return true;
}

//! Returns true if mouse click is in context suitable for line drawing.

bool MaskGraphicsScene::isValidForLineDrawing(QGraphicsSceneMouseEvent* event) const
{
    if (m_drawing_in_progress)
        return false;
    if (!Canvas2DMode::isLineMode(m_mode))
        return false;
    if (QGraphicsItem* graphicsItem = itemAt(event->scenePos(), QTransform()))
        if (dynamic_cast<LineOverlay*>(graphicsItem))
            return false;
    return true;
}

//! Returns true if FullframeItem can be drawn. Only one item of such type is allowed.

bool MaskGraphicsScene::isValidForMaskAllDrawing(QGraphicsSceneMouseEvent*) const
{
    if (m_drawing_in_progress)
        return false;
    if (m_mode != Canvas2DMode::MASKALL)
        return false;

    for (const auto& [item, overlay] : m_mask2overlay)
        if (dynamic_cast<const FullframeItem*>(item))
            return false;
    return true;
}

//! Returns polygon which is currently under the drawing.

PolygonOverlay* MaskGraphicsScene::currentPolygon()
{
    if (m_drawing_in_progress && m_mode == Canvas2DMode::POLYGON && m_active_mask)
        return dynamic_cast<PolygonOverlay*>(m_mask2overlay[m_active_mask]);
    return nullptr;
}
