//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/Import/RealDataSelectorWidget.cpp
//! @brief     Implements class RealDataSelectorWidget
//!
//! @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/Import/RealDataSelectorWidget.h"
#include "Base/Util/Assert.h"
#include "Device/Coord/CoordSystem1D.h"
#include "Device/Data/Datafield.h"
#include "Device/IO/IOFactory.h"
#include "GUI/Application/ApplicationSettings.h"
#include "GUI/Model/Data/DataItem.h"
#include "GUI/Model/Device/RealItem.h"
#include "GUI/Model/Model/RealTreeModel.h"
#include "GUI/Support/Util/CoordName.h"
#include "GUI/Support/Util/Style.h"
#include "GUI/View/Import/Legacy1dDialog.h"
#include "GUI/View/Import/RealDataPropertiesWidget.h"
#include "GUI/View/Info/MessageBox.h"
#include "GUI/View/Item/ItemViewOverlayButtons.h"
#include "GUI/View/Project/ProjectManager.h"
#include <QFileDialog>
#include <QMenu>
#include <QSplitter>
#include <QTreeView>
#include <QVBoxLayout>
#include <map>

namespace {

template <class T>
QString join_filterkeys(std::vector<std::pair<const QString, T>> _nomap, const QString& _separator)
{
    QString result;
    for (const auto& it : _nomap) {
        if (!result.isEmpty())
            result += _separator;
        result += it.first;
    }
    return result;
}

template <class T>
T filterkey2type(std::vector<std::pair<const QString, T>> _nomap, const QString& _key)
{
    for (const auto& it : _nomap)
        if (it.first == _key)
            return it.second;
    ASSERT(false);
}

} // namespace


RealDataSelectorWidget::RealDataSelectorWidget(QWidget* parent, ProjectDocument* document)
    : QWidget(parent)
    , m_itemTree(new QTreeView(this))
    , m_treeModel(new RealTreeModel(this, document->realModel()))
    , m_propertiesWidget(new RealDataPropertiesWidget(this, document))
    , m_document(document)
    , m_import1dDataAction(new QAction(this))
    , m_import2dDataAction(new QAction(this))
    , m_renameDataAction(new QAction(this))
    , m_removeDataAction(new QAction(this))
{
    setMinimumSize(250, 600);
    setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
    setWindowTitle("RealDataSelectorWidget");

    m_import2dDataAction->setText("Import 2D data");
    m_import2dDataAction->setIcon(QIcon(":/images/shape-square-plus.svg"));
    m_import2dDataAction->setIconText("2D");
    m_import2dDataAction->setToolTip("Import 2D data");
    connect(m_import2dDataAction, &QAction::triggered, [this]() { importData2D(); });

    m_import1dDataAction->setText("Import 1D data");
    m_import1dDataAction->setIcon(QIcon(":/images/shape-square-plus.svg"));
    m_import1dDataAction->setIconText("1D");
    m_import1dDataAction->setToolTip("Import 1D data");
    connect(m_import1dDataAction, &QAction::triggered, [this]() { importData1D(); });

    m_renameDataAction->setText("Rename");
    m_renameDataAction->setIcon(QIcon()); // #baTODO: Icon needed?
    m_renameDataAction->setIconText("Rename");
    m_renameDataAction->setToolTip("Rename data");
    connect(m_renameDataAction, &QAction::triggered, this,
            &RealDataSelectorWidget::renameCurrentItem);

    m_removeDataAction->setText("Remove");
    m_removeDataAction->setIcon(QIcon(":/images/delete.svg"));
    m_removeDataAction->setIconText("Remove");
    m_removeDataAction->setToolTip("Remove selected data");
    connect(m_removeDataAction, &QAction::triggered, this,
            &RealDataSelectorWidget::removeCurrentItem);

    // m_itemTree->setAttribute(Qt::WA_MacShowFocusRect, false); // TODO/Mac: needed?
    m_itemTree->setItemsExpandable(false);
    m_itemTree->setRootIsDecorated(false);
    m_itemTree->setHeaderHidden(true);
    m_itemTree->setContextMenuPolicy(Qt::CustomContextMenu);
    m_itemTree->setModel(m_treeModel);
    GUI::Style::setPropertyStyle(m_itemTree);

    auto* splitter = new QSplitter;
    splitter->setOrientation(Qt::Vertical);
    splitter->addWidget(m_itemTree);
    splitter->addWidget(m_propertiesWidget);
    splitter->setChildrenCollapsible(true);

    auto* mainLayout = new QVBoxLayout;
    mainLayout->setContentsMargins(0, 0, 0, 0);
    mainLayout->setSpacing(0);
    mainLayout->setContentsMargins(0, 0, 0, 0);
    mainLayout->addWidget(splitter);

    setLayout(mainLayout);

    connect(m_propertiesWidget, &RealDataPropertiesWidget::instrumentUpdated, this,
            &RealDataSelectorWidget::selectionChanged);

    connect(m_itemTree, &QTreeView::customContextMenuRequested, this,
            &RealDataSelectorWidget::onContextMenuRequest);

    connect(m_document, &ProjectDocument::modifiedStateChanged, this,
            &RealDataSelectorWidget::updateFunctionalities);

    connect(m_treeModel, &QAbstractItemModel::modelReset, [this]() { m_itemTree->expandAll(); });

    connect(m_treeModel, &QAbstractItemModel::rowsInserted, [this]() { m_itemTree->expandAll(); });

    connect(m_itemTree->selectionModel(), &QItemSelectionModel::selectionChanged, this,
            &RealDataSelectorWidget::onSelectionChanged, Qt::UniqueConnection);

    m_itemTree->expandAll();
    updateActionEnabling();
    updateFunctionalities();

    restoreSelection();

    ItemViewOverlayButtons::install(
        m_itemTree, [this](const QModelIndex& i, bool h) { return getOverlayActions(i, h); });
}

QSize RealDataSelectorWidget::sizeHint() const
{
    return QSize(200, 400);
}

QSize RealDataSelectorWidget::minimumSizeHint() const
{
    return QSize(128, 200);
}

RealItem* RealDataSelectorWidget::currentItem()
{
    return m_treeModel->itemForIndex(currentIndex());
}

void RealDataSelectorWidget::setCurrentItem(RealItem* item)
{
    m_itemTree->selectionModel()->clearSelection();
    QModelIndex index = m_treeModel->indexForItem(item);
    if (index.isValid())
        m_itemTree->selectionModel()->setCurrentIndex(index, QItemSelectionModel::SelectCurrent);
}

void RealDataSelectorWidget::restoreSelection()
{
    int lastIndex = m_document->realModel()->selectedIndex();
    int lastRank = m_document->realModel()->selectedRank();
    QModelIndex lastUsedIndex =
        m_treeModel->index(lastIndex, 0, m_treeModel->indexOfHeadline(lastRank));

    if (lastUsedIndex.isValid())
        m_itemTree->selectionModel()->setCurrentIndex(lastUsedIndex,
                                                      QItemSelectionModel::SelectCurrent);
    else
        setCurrentItem(m_treeModel->topMostItem());

    if (currentItem())
        emit selectionChanged(currentItem());
}

QModelIndex RealDataSelectorWidget::currentIndex()
{
    if (!m_itemTree->selectionModel()->isSelected(m_itemTree->selectionModel()->currentIndex()))
        return {};
    return m_itemTree->selectionModel()->currentIndex();
}

QList<QAction*> RealDataSelectorWidget::getOverlayActions(const QModelIndex& index, bool asHover)
{
    if (m_treeModel->isHeadline(index)) {
        if (asHover)
            return {};

        return (index == m_treeModel->indexOfHeadline(1)) ? QList<QAction*>({m_import1dDataAction})
                                                          : QList<QAction*>({m_import2dDataAction});
    }

    // -- index belongs to item
    if (!asHover)
        return {};

    auto* item = m_treeModel->itemForIndex(index);
    if (item == nullptr)
        return {};

    auto* removeAction = new QAction(this);
    removeAction->setText("Remove");
    removeAction->setIcon(QIcon(":/images/delete.svg"));
    removeAction->setIconText("Remove");
    removeAction->setToolTip("Remove this data");
    connect(removeAction, &QAction::triggered, [this, item]() {
        m_treeModel->removeDataItem(item);
        gProjectDocument.value()->setModified();
    });

    return {removeAction};
}

void RealDataSelectorWidget::onSelectionChanged()
{
    updateActionEnabling();

    m_document->realModel()->setSelectedIndex(currentIndex().row());
    m_document->realModel()->setSelectedRank(currentIndex().parent().row() + 1);

    m_propertiesWidget->setRealItem(currentItem());
    emit selectionChanged(currentItem());
}

void RealDataSelectorWidget::onContextMenuRequest(const QPoint& point)
{
    auto* realItemAtPoint = m_treeModel->itemForIndex(m_itemTree->indexAt(point));
    updateActionEnabling(realItemAtPoint);

    QMenu menu;
    menu.setToolTipsVisible(true);

    if (realItemAtPoint != nullptr) {
        menu.addAction(m_renameDataAction);
        menu.addAction(m_removeDataAction);
        menu.addSeparator();
    }

    menu.addAction(m_import2dDataAction);
    menu.addAction(m_import1dDataAction);
    menu.exec(m_itemTree->mapToGlobal(point));
}

void RealDataSelectorWidget::importData1D()
{
    static const std::vector<std::pair<const QString, IO::Filetype1D>> filters1D{
        {"interactive CSV configuration (*.csv *.dat *.tab *.txt)", IO::csv1D},
        {"legacy 2 columns: Q R (*.csv *.dat *.tab *.txt)", IO::csv1D_2cols},
        {"legacy 3 columns: Q R σR (*.csv *.dat *.tab *.txt)", IO::csv1D_3cols},
        {"legacy 4 columns: Q R σR δQ (*.csv *.dat *.tab *.txt)", IO::csv1D_4cols},
        {"legacy 5 columns: Q R σR δQ λ (*.csv *.dat *.tab *.txt)", IO::csv1D_5cols},
        {"Motofit (*.mft)", IO::mft},
        {"BornAgain (*.int.gz)", IO::bornagain1D},
        {"all (*.*)", IO::unknown1D}};

    QString filters = ::join_filterkeys(filters1D, ";;");

    QString selectedFilter = ProjectManager::instance()->recentlyUsedImportFilter1D();
    const QString dirname = ProjectManager::instance()->userImportDir();

    const QStringList fileNames = QFileDialog::getOpenFileNames(
        Q_NULLPTR, "Open Intensity Files", dirname, filters, &selectedFilter,
        appSettings->useNativeFileDialog() ? QFileDialog::Options()
                                           : QFileDialog::DontUseNativeDialog);
    if (fileNames.isEmpty())
        return;

    ProjectManager::instance()->setImportDirFromFilePath(fileNames[0]);
    ProjectManager::instance()->setRecentlyUsedImportFilter1D(selectedFilter);

    const IO::Filetype1D ftype = ::filterkey2type(filters1D, selectedFilter);

    const ImportSettings1D* settings = nullptr;
    if (ftype == IO::csv1D) {
        Legacy1dDialog dialog;
        if (dialog.exec() != QDialog::Accepted)
            return;
        settings = &Legacy1dDialog::Msettings;
    }

    for (const QString& fileName : fileNames) {
        RealItem* realItem = nullptr;
        try {
            std::unique_ptr<Datafield> data(
                IO::readData1D(fileName.toStdString(), ftype, settings));
            if (!data)
                continue;
            realItem = m_treeModel->injectDataItem(1);
            realItem->setRealItemName(QFileInfo(fileName).baseName());
            ASSERT(data->rank() == 1);

            // apply initial (or "native") units
            Coords coords = Coords::QSPACE;
            realItem->dataItem()->setCurrentCoord(coords);
            realItem->dataItem()->setXaxisTitle(
                QString::fromStdString(AngularReflectometryCoords::nameOfAxis0(coords)));
            realItem->setDatafield(data.release());
            realItem->initNativeData();
            realItem->setNativeDataUnits(GUI::Util::CoordName::nameFromCoord(coords));

            setCurrentItem(realItem);
        } catch (std::exception& ex) {
            m_treeModel->removeDataItem(realItem);

            const QString message = QString("Error while trying to read file\n\n'%1'\n\n%2")
                                        .arg(fileName)
                                        .arg(QString::fromStdString(std::string(ex.what())));
            GUI::Message::warning(this, "File import", message);
        }
    }

    gProjectDocument.value()->setModified();
}

void RealDataSelectorWidget::importData2D()
{
    static const std::vector<std::pair<const QString, IO::Filetype2D>> filters2D{
        {"all (*.*)", IO::unknown2D},
        {"TIFF (*.tif *.tiff *.tif.gz)", IO::tiff},
        {"Nicos/SANSDRaw (*.001)", IO::nicos2D},
        {"BornAgain (*.int.gz)", IO::bornagain2D},
        {"CSV (*.csv *.dat *.tab *.txt)", IO::csv2D},
        {"REFSANS (*.csv)", IO::refsans2D}};

    QString filters = ::join_filterkeys(filters2D, ";;");

    QString selectedFilter = ProjectManager::instance()->recentlyUsedImportFilter2D();
    const QString dirname = ProjectManager::instance()->userImportDir();

    const QStringList fileNames = QFileDialog::getOpenFileNames(
        Q_NULLPTR, "Open Intensity Files", dirname, filters, &selectedFilter,
        appSettings->useNativeFileDialog() ? QFileDialog::Options()
                                           : QFileDialog::DontUseNativeDialog);
    if (fileNames.isEmpty())
        return;

    ProjectManager::instance()->setImportDirFromFilePath(fileNames[0]);
    ProjectManager::instance()->setRecentlyUsedImportFilter2D(selectedFilter);

    const IO::Filetype2D ftype = ::filterkey2type(filters2D, selectedFilter);

    for (const QString& fileName : fileNames) {
        RealItem* realItem = nullptr;
        try {
            std::unique_ptr<Datafield> data(IO::readData2D(fileName.toStdString(), ftype));
            if (!data)
                continue;
            realItem = m_treeModel->injectDataItem(2);
            realItem->setRealItemName(QFileInfo(fileName).baseName());
            ASSERT(data->rank() == 2);
            realItem->setDatafield(data.release());
            setCurrentItem(realItem);
        } catch (std::exception& ex) {
            m_treeModel->removeDataItem(realItem);

            const QString message = QString("Error while trying to read file\n\n'%1'\n\n%2")
                                        .arg(fileName)
                                        .arg(QString::fromStdString(std::string(ex.what())));
            GUI::Message::warning(this, "File import", message);
        }
    }

    gProjectDocument.value()->setModified();
}

void RealDataSelectorWidget::renameCurrentItem()
{
    if (currentIndex().isValid()) {
        // First delete any existing index widget. This is necessary if overlay buttons are used,
        // because these buttons use the indexWidget() functionality, which itself uses the same
        // internals as an item editor
        m_itemTree->setIndexWidget(currentIndex(), nullptr);
        m_itemTree->edit(currentIndex());
        gProjectDocument.value()->setModified();
    }
}

void RealDataSelectorWidget::removeCurrentItem()
{
    m_treeModel->removeDataItem(currentItem());
    gProjectDocument.value()->setModified();
}

void RealDataSelectorWidget::updateActionEnabling()
{
    updateActionEnabling(currentItem());
}

void RealDataSelectorWidget::updateActionEnabling(const RealItem* item) const
{
    m_import2dDataAction->setEnabled(true);
    m_import1dDataAction->setEnabled(true);

    m_removeDataAction->setEnabled(item != nullptr);
    m_renameDataAction->setEnabled(item != nullptr);
}

void RealDataSelectorWidget::updateFunctionalities()
{
    QSet<int> visibleRanks;
    if (m_document->functionalities().testFlag(ProjectDocument::Specular))
        visibleRanks << 1;
    if (m_document->functionalities().testFlag(ProjectDocument::Gisas)
        || m_document->functionalities().testFlag(ProjectDocument::Offspec)
        || m_document->functionalities().testFlag(ProjectDocument::Depthprobe))
        visibleRanks << 2;

    m_treeModel->setVisibleRanks(visibleRanks);
}
