//  ************************************************************************************************
//
//  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 "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/LineOverlays.h"
#include "GUI/View/Overlay/PolygonOverlay.h" // includes IOverlay.h
#include "GUI/View/Overlay/SizeHandleElement.h"
#include "GUI/View/Overlay/VertexOverlay.h"
#include "GUI/View/Plotter/ColorMap.h"
#include "GUI/View/Scene/MaskGraphicsProxy.h"
#include "GUI/View/Scene/OverlayFactory.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;
}

} // namespace


MaskGraphicsScene::MaskGraphicsScene() {}

MaskGraphicsScene::~MaskGraphicsScene() = default;

void MaskGraphicsScene::associateItems(Data2DItem* data_item)
{
    if (!data_item)
        return;
    m_data_item = data_item;

    MasksSet* masks = data_item->masksRW();
    MasksSet* prjns = data_item->prjnsRW();
    ASSERT(masks);
    ASSERT(prjns);
    m_masks = masks;
    m_prjns = prjns;

    disconnect(masks);
    disconnect(prjns);
    m_mask2overlay.clear(); // removes all items from the map
    if (m_proxy)
        m_proxy->setWidget(nullptr);
    clear(); // removes and deletes all items from the scene

    m_plot = std::make_unique<ColorMap>();
    m_plot->itemToMap(m_data_item);

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

    connect(masks, &MasksSet::setChanged, this, &MaskGraphicsScene::updateOverlays);
    connect(prjns, &MasksSet::setChanged, this, &MaskGraphicsScene::updateOverlays);

    updateOverlays();
}

void MaskGraphicsScene::updateSize(const QSize& newSize)
{
    if (m_proxy) {
        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 zoomMode = m_mode == Canvas2DMode::PAN_ZOOM;

    for (IOverlay* overlay : m_mask2overlay.values())
        overlay->setAcceptedMouseButtons(zoomMode ? Qt::NoButton : Qt::LeftButton);
    m_proxy->setZooming(zoomMode);

    // Change cursor to stress that hovered item is movable (when not in PanZoom mode)
    for (auto it = m_mask2overlay.begin(); it != m_mask2overlay.end(); ++it) {
        if (dynamic_cast<const VerticalLineItem*>(it.key()))
            it.value()->setCursor(zoomMode ? Qt::ArrowCursor : Qt::SizeHorCursor);
        else if (dynamic_cast<const HorizontalLineItem*>(it.key()))
            it.value()->setCursor(zoomMode ? Qt::ArrowCursor : Qt::SizeVerCursor);
    }
}

void MaskGraphicsScene::cancelCurrentDrawing()
{
    if (m_drawing_in_progress) {
        ASSERT(m_active_mask);
        m_masks->delete_current();
        setDrawingInProgress(false);
    }
}

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


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

void MaskGraphicsScene::mousePressEvent(QGraphicsSceneMouseEvent* event)
{
    if (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::isRectangleShapeMode(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();
        invalidate();
    }
}

//! 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::isRectangleShapeMode(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_mouse_position == QPointF())
        return;

    painter->setPen(QPen((Canvas2DMode::isPrjn(m_mode) ? QColorConstants::Svg::lightpink
                                                       : QColorConstants::Svg::aqua),
                         1, Qt::DashLine));

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

    } else if (Canvas2DMode::isLineMode(m_mode)) {
        const QRectF& plot_scene_rectangle = m_plot->viewportRectangle();
        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
//  ************************************************************************************************

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

void MaskGraphicsScene::updateOverlays()
{
    ASSERT(m_masks);
    for (MaskItem* mask_item : *m_masks) {
        IOverlay* item_overlay = registerOverlay(mask_item);
        ASSERT(item_overlay);

        // Add views for the points of the PolygonItem
        if (auto* polygon_item = dynamic_cast<PolygonItem*>(mask_item)) {
            auto* polygon_overlay = dynamic_cast<PolygonOverlay*>(item_overlay);
            ASSERT(polygon_overlay);
            for (PointItem* point_item : polygon_item->points()) {
                IOverlay* point_overlay = registerOverlay(point_item);
                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* item_overlay = registerOverlay(t);
        ASSERT(item_overlay);
    }
}

//! Creates a view for given item.

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

    IOverlay* overlay = m_mask2overlay[item];
    if (!overlay) {
        overlay = GUI::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);
    connect(item, &OverlayItem::maskVisibilityChanged, overlay, &IOverlay::onVisibilityChange);

    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 (auto* polygon_item = dynamic_cast<const PolygonItem*>(item))
            for (PointItem* point_item : polygon_item->points())
                removeOverlay(point_item);

        IOverlay* overlay = it.value();
        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)
{
    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 qreal xmin = m_plot->fromSceneX(std::min(click_pos.x(), mouse_pos.x()));
    const qreal xmax = m_plot->fromSceneX(std::max(click_pos.x(), mouse_pos.x()));
    const qreal ymin = m_plot->fromSceneY(std::min(click_pos.y(), mouse_pos.y()));
    const qreal ymax = m_plot->fromSceneY(std::max(click_pos.y(), mouse_pos.y()));

    if (auto* rectItem = dynamic_cast<RectangleItem*>(m_active_mask)) {
        // RectangleItem or RegionOfInterestItem
        rectItem->setXLow(xmin);
        rectItem->setYLow(ymax);
        rectItem->setXUp(xmax);
        rectItem->setYUp(ymin);
    } else if (auto* ellItem = dynamic_cast<EllipseItem*>(m_active_mask)) {
        ellItem->setXCenter((xmax + xmin) / 2);
        ellItem->setYCenter((ymax + ymin) / 2);
        ellItem->setXRadius((xmax - xmin) / 2);
        ellItem->setYRadius((ymin - ymax) / 2);
    }

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

void MaskGraphicsScene::processPolygonItem(QGraphicsSceneMouseEvent* event)
{
    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->fromSceneX(click_pos.x());
    const double y = m_plot->fromSceneY(click_pos.y());
    dynamic_cast<PolygonItem*>(m_active_mask)->addPoint(x, y);
    updateOverlays();
}

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

    if (Canvas2DMode::isVerticalLine(m_mode))
        processVerticalLineItem(click_pos);
    if (Canvas2DMode::isHorizontalLine(m_mode))
        processHorizontalLineItem(click_pos);
    ASSERT(m_active_mask);
    emit m_active_mask->maskGeometryChanged();

    m_active_mask->setMaskValue(m_mask_value);

    emit lineItemProcessed();

    setDrawingInProgress(false);
}

void MaskGraphicsScene::processVerticalLineItem(const QPointF& pos)
{
    LineItem* item = new VerticalLineItem;
    addLine(item);
    item->setPos(m_plot->fromSceneX(pos.x()));
}

void MaskGraphicsScene::processHorizontalLineItem(const QPointF& pos)
{
    LineItem* item = new HorizontalLineItem;
    addLine(item);
    item->setPos(m_plot->fromSceneY(pos.y()));
}

void MaskGraphicsScene::addLine(LineItem* item)
{
    if (Canvas2DMode::isPrjn(m_mode))
        m_prjns->add_item(item);
    else
        m_masks->add_item(item);
    m_active_mask = item;
}

// 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->viewportRectangle().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::isRectangleShapeMode(m_mode))
        return false;
    if (areaContains<SizeHandleElement>(items(event->scenePos())))
        return false;
    if (m_mode == Canvas2DMode::ROI)
        // only one ROI is allowed
        for (const OverlayItem* item : m_mask2overlay.keys())
            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 OverlayItem* item : m_mask2overlay.keys())
        if (dynamic_cast<const FullframeItem*>(item))
            return false;
    return true;
}

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

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