//  ************************************************************************************************
//
//  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 <class 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)
{
    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);

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

    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>(); // otherwise segfault when switching back (#989)

    m_proxy = new MaskGraphicsProxy;
    m_proxy->setWidget(m_plot.get());
    addItem(m_proxy); // takes ownership
}

MaskGraphicsScene::~MaskGraphicsScene() = default;

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

    m_plot->itemToMap(data_item);

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

    connect(m_masks, &MasksSet::setChanged, this, &MaskGraphicsScene::updateMost);
    connect(m_prjns, &MasksSet::setChanged, this, &MaskGraphicsScene::updateMost);

    updateMost();
}

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);
}

//  ************************************************************************************************
//  public slots
//  ************************************************************************************************

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

    m_mode = mode;
    const bool zooming = m_mode == Canvas2DMode::PAN_ZOOM;
    m_proxy->setZooming(zooming);
    for (auto const& [item, overlay] : m_mask2overlay) {
        overlay->setAcceptedMouseButtons(zooming ? Qt::NoButton : Qt::LeftButton);
        overlay->setCursor(zooming ? Qt::ArrowCursor : Qt::SizeAllCursor);
    }
    m_plot->setCursor(zooming ? Qt::PointingHandCursor : Qt::ArrowCursor);
}

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

    ASSERT(m_active_mask);
    m_masks->delete_current();
    setDrawingInProgress(false);
}

//  ************************************************************************************************
//  private slots
//  ************************************************************************************************


//  ************************************************************************************************
//  private (override QGraphicsScene)
//  ************************************************************************************************

void MaskGraphicsScene::mousePressEvent(QGraphicsSceneMouseEvent* event)
{
    if (event->buttons() & Qt::LeftButton)
        m_mouse_is_pressed = true;

    if (event->buttons() & Qt::RightButton) {
        if (m_drawing_in_progress)
            cancelCurrentDrawing();
        else
            makeViewAtMousePosSelected(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)
{
    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
            if (IOverlay* overlay = m_mask2overlay[m_active_mask])
                overlay->setSelected(true);
        } else {
            // drawing ended without item to be draw (too short mouse move)
            // making item beneath of mouse release position to be selected
            makeViewAtMousePosSelected(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;

    if (dynamic_cast<IOverlay*>(itemAt(event->scenePos(), QTransform())))
        emit itemContextMenuRequest(event->screenPos());
}

//  ************************************************************************************************
//  private modifying functions
//  ************************************************************************************************

void MaskGraphicsScene::updateMost()
{
    disconnect(m_masks);
    disconnect(m_prjns);
    m_mask2overlay.clear(); // removes all items from the map
    for (QGraphicsItem* t : items())
        if (t != m_proxy)
            removeItem(t);

    updateOverlays();
}

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

void MaskGraphicsScene::updateOverlays()
{
    ASSERT(m_masks);
    for (MaskItem* mask_item : *m_masks) {
        IOverlay* overlay = registerOverlay(mask_item);
        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);
                point_overlay->show();
                polygon_overlay->addOverlay(point_overlay);
            }
        }
    }

    // update Z-values of all IMaskView to reflect stacking order
    int z = 0;
    for (const MaskItem* maskItem : *m_masks) {
        if (IOverlay* overlay = m_mask2overlay[maskItem])
            overlay->setZValue(z);
        ++z;
    }

    ASSERT(m_prjns);
    for (MaskItem* t : *m_prjns) {
        IOverlay* overlay = registerOverlay(t);
        ASSERT(overlay);
    }
}

//! Creates a view for given item.

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

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

    disconnect(item);
    connect(item, &OverlayItem::maskGeometryChanged, [this] {
        if (m_mouse_is_pressed)
            gDoc->setModified(); // manual mask movement
    });
    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(item, &OverlayItem::maskGeometryChanged, overlay, &IOverlay::onGeometryChange);
    if (auto* mask_item = dynamic_cast<MaskItem*>(item)) {
        if (auto* mask_overlay = dynamic_cast<IMaskOverlay*>(overlay))
            connect(mask_item, &OverlayItem::maskVisibilityChanged, mask_overlay,
                    &IMaskOverlay::onVisibilityChange);
        else
            ASSERT_NEVER;
    }

    overlay->update_view();
    return overlay;
}

//! Removes single view from scene.

void MaskGraphicsScene::removeOverlay(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;
    }
}

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

void MaskGraphicsScene::makeViewAtMousePosSelected(QGraphicsSceneMouseEvent* event)
{
    if (QGraphicsItem* graphicsItem = itemAt(event->scenePos(), QTransform()))
        graphicsItem->setSelected(true);
}

//! 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;
    }
    ASSERT(dynamic_cast<PolygonItem*>(m_active_mask));

    if (PolygonOverlay* polygon = currentPolygon()) {
        if (polygon->closePolygonIfNecessary()) {
            setDrawingInProgress(false);
            m_mouse_position = {};
            return;
        }
    }

    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);
}

// 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;
}