//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/Canvas/MaskEditorCanvas.cpp
//! @brief     Implements class MaskEditorCanvas.
//!
//! @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/Canvas/MaskEditorCanvas.h"
#include "Device/Data/Datafield.h"
#include "Device/Mask/IShape2D.h"
#include "Device/Mask/MaskStack.h"
#include "GUI/Model/Data/Data2DItem.h"
#include "GUI/Model/Mask/MasksSet.h"
#include "GUI/Model/Project/ProjectDocument.h"
#include "GUI/View/Plotter/ColorMap.h"
#include "GUI/View/Plotter/PlotStatusLabel.h"
#include "GUI/View/Scene/MaskGraphicsScene.h"
#include "GUI/View/Scene/MaskGraphicsView.h"
#include "GUI/View/Setup/FrameActions.h"
#include <QVBoxLayout>

namespace {

Datafield* createMaskedField(const Data2DItem* data)
{
    // Requesting mask information
    std::unique_ptr<IShape2D> roi;
    Datafield* result = data->c_field()->clone();
    MaskStack detectorMask;
    for (const MaskItem* t : *data->masks()) {
        if (t->isVisible()) {
            if (const auto* roiItem = dynamic_cast<const RegionOfInterestItem*>(t))
                roi = roiItem->createShape();
            else {
                std::unique_ptr<IShape2D> shape(t->createShape());
                detectorMask.pushMask(*shape, t->maskValue());
            }
        }
    }

    // ROI mask has to be the last one, it can not be "unmasked" by other shapes
    if (roi)
        detectorMask.pushMask(*roi, true);

    if (!detectorMask.hasMasks())
        return nullptr;

    for (size_t i = 0; i < result->size(); ++i)
        if (detectorMask.isMasked(i, result->frame()))
            (*result)[i] = 0;

    return result;
}

} // namespace


MaskEditorCanvas::MaskEditorCanvas()
    : m_scene(new MaskGraphicsScene)
    , m_view(new MaskGraphicsView(m_scene))
    , m_status_label(new PlotStatusLabel({m_scene->colorMap()}))
    , m_data_item(nullptr)
{
    setCanvasMode(Canvas2DMode::SELECTION);

    setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
    setMinimumSize(400, 400);

    auto* layout = new QVBoxLayout;
    setLayout(layout);
    layout->setContentsMargins(0, 0, 0, 0);
    layout->setSpacing(0);
    layout->addWidget(m_view);
    layout->addWidget(m_status_label);

    connect(m_view, &MaskGraphicsView::changeActivityRequest, this,
            &MaskEditorCanvas::changeActivityRequest);
    connect(m_view, &MaskGraphicsView::deleteCurrentRequest, m_scene,
            &MaskGraphicsScene::deleteCurrentItem);

    // automatically switch to the appropriate projection tab
    connect(m_scene, &MaskGraphicsScene::lineItemMoved, [this](const LineItem* sender) {
        if (dynamic_cast<const HorizontalLineItem*>(sender)
            && (m_current_activity != Canvas2DMode::VERTICAL_PRJN))
            emit projectionMoved(Canvas2DMode::HORIZONTAL_PRJN);
        if (dynamic_cast<const VerticalLineItem*>(sender)
            && (m_current_activity != Canvas2DMode::HORIZONTAL_PRJN))
            emit projectionMoved(Canvas2DMode::VERTICAL_PRJN);
    });
}

MaskEditorCanvas::~MaskEditorCanvas() = default;

void MaskEditorCanvas::updateCanvas(Data2DItem* data_item)
{
    ASSERT(data_item);
    m_data_item = data_item;

    m_scene->switchDataContext(data_item);

    // set nullptr at destruction
    connect(m_data_item, &QObject::destroyed, this, &MaskEditorCanvas::onDataDestroyed,
            Qt::UniqueConnection);

    // notify ProjectionPlot about the changes
    connect(m_scene, &MaskGraphicsScene::lineItemProcessed, data_item,
            &Data2DItem::projectionCreated, Qt::UniqueConnection);
    connect(m_scene, &MaskGraphicsScene::lineItemMoved, data_item,
            &Data2DItem::projectionPositionChanged, Qt::UniqueConnection);
    connect(m_scene, &MaskGraphicsScene::lineItemDeleted, data_item, &Data2DItem::projectionGone,
            Qt::UniqueConnection);

    auto* cm = m_scene->colorMap();
    ASSERT(cm);

    connect(cm, &MousyPlot::enteringPlot, this, &MaskEditorCanvas::onEnteringColorMap,
            Qt::UniqueConnection);
    connect(cm, &MousyPlot::leavingPlot, this, &MaskEditorCanvas::onLeavingColorMap,
            Qt::UniqueConnection);
    connect(cm, &MousyPlot::positionChanged, this, &MaskEditorCanvas::onPositionChanged,
            Qt::UniqueConnection);
    connect(cm, &ColorMap::marginsChanged, this, &MaskEditorCanvas::marginsChanged,
            Qt::UniqueConnection);

    onLeavingColorMap();
}

void MaskEditorCanvas::changeMaskDisplay(bool pixelized)
{
    ASSERT(m_data_item);

    if (pixelized) {
        if (Datafield* maskedData = ::createMaskedField(m_data_item)) {
            // store data backup
            m_backup_data.reset(m_data_item->c_field()->clone());
            m_backup_interpolated = m_data_item->isInterpolated();

            m_data_item->setDatafield(*maskedData);
            m_data_item->setInterpolated(false);
        } else {
            m_backup_data.reset();
        }

    } else if (m_backup_data) {
        m_data_item->setDatafield(*m_backup_data);
        m_data_item->setInterpolated(m_backup_interpolated);
    }

    // avoid excessive signaling because hiding overlays automatically causes their unselection
    m_scene->connectOverlaySelection(false);

    if (const auto* container = m_data_item->masks())
        for (MaskItem* t : *container)
            t->setIsVisible(!pixelized && t->wasVisible());

    m_scene->connectOverlaySelection(true);

    // trigger reselecting the overlay of the current mask item
    emit m_data_item->masksRW()->setChanged();
}

void MaskEditorCanvas::onPositionChanged(double x, double y)
{
    if (!m_live_projection)
        return;

    if (m_current_activity == Canvas2DMode::HORIZONTAL_PRJN)
        m_live_projection->setPos(y);
    else if (m_current_activity == Canvas2DMode::VERTICAL_PRJN)
        m_live_projection->setPos(x);
    else
        ASSERT_NEVER;

    ASSERT(m_data_item);
    emit m_data_item->projectionPositionChanged(m_live_projection.get());
}

void MaskEditorCanvas::onResetViewRequest()
{
    ASSERT(m_data_item);
    m_view->onResetViewRequest();

    if (m_data_item->isZoomed())
        m_data_item->resetView();
    else
        setZoomToROI();
    gDoc->setModified();
}

void MaskEditorCanvas::setZoomToROI()
{
    if (const MasksSet* masks = m_data_item->masks()) {
        if (const auto* roiItem = masks->regionOfInterestItem()) {
            ASSERT(m_data_item);
            m_data_item->setXrange(roiItem->xLow().dVal(), roiItem->xUp().dVal());
            m_data_item->setYrange(roiItem->yLow().dVal(), roiItem->yUp().dVal());
        } else
            m_data_item->resetView();
    }
}

void MaskEditorCanvas::onEnteringColorMap()
{
    if (m_live_projection)
        return;

    if (m_current_activity == Canvas2DMode::HORIZONTAL_PRJN)
        m_live_projection = std::make_unique<HorizontalLineItem>(0.);
    else if (m_current_activity == Canvas2DMode::VERTICAL_PRJN)
        m_live_projection = std::make_unique<VerticalLineItem>(0.);

    if (m_live_projection) {
        m_live_projection->setIsVisible(false);
        ASSERT(m_data_item);
        emit m_data_item->projectionPositionChanged(m_live_projection.get());
    }
}

void MaskEditorCanvas::onLeavingColorMap()
{
    if (m_live_projection) {
        ASSERT(m_data_item);
        disconnect(m_live_projection.get(), nullptr, m_data_item, nullptr);
        emit m_data_item->projectionGone(m_live_projection.get());
        m_live_projection.reset();
    }
}

void MaskEditorCanvas::setCanvasMode(Canvas2DMode::Flag mode)
{
    m_current_activity = mode;
    m_scene->onActivityChanged(mode);
    onLeavingColorMap();
}

void MaskEditorCanvas::onDataDestroyed()
{
    m_data_item = nullptr;
    m_backup_data.reset(nullptr);
    m_live_projection.reset(nullptr);
}
