//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/Projection/SaveProjectionsAssistant.cpp
//! @brief     Implements class SaveProjectionsAssistant
//!
//! @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/Projection/SaveProjectionsAssistant.h"
#include "Base/Axis/Scale.h"
#include "Base/Py/PyFmt.h"
#include "Device/Data/Datafield.h"
#include "GUI/Application/ApplicationSettings.h"
#include "GUI/Model/Data/IntensityDataItem.h"
#include "GUI/Model/Data/MaskItems.h"
#include "GUI/Model/Project/ProjectDocument.h"
#include <QFileDialog>
#include <QTextStream>
#include <boost/polymorphic_cast.hpp>

using boost::polymorphic_downcast;

namespace {

const int bin_centers_colwidth = 12;
const int bin_values_colwidth = 20;

QString to_scientific_str(double value)
{
    auto str = Py::Fmt::printScientificDouble(value);
    return QString("%1").arg(QString::fromStdString(str), -bin_values_colwidth);
}

QString to_double_str(double value)
{
    auto str = Py::Fmt::printDouble(value);
    return QString("%1").arg(QString::fromStdString(str), -bin_centers_colwidth);
}

bool vert_less_posx(MaskItemObject* item1, MaskItemObject* item2)
{
    return polymorphic_downcast<VerticalLineItem*>(item1)->posX()
           < polymorphic_downcast<VerticalLineItem*>(item2)->posX();
}

bool horiz_less_posy(MaskItemObject* item1, MaskItemObject* item2)
{
    return polymorphic_downcast<HorizontalLineItem*>(item1)->posY()
           < polymorphic_downcast<HorizontalLineItem*>(item2)->posY();
}

} // namespace

SaveProjectionsAssistant::SaveProjectionsAssistant() = default;
SaveProjectionsAssistant::~SaveProjectionsAssistant() = default;

//! Calls file open dialog and writes projection data as ASCII

void SaveProjectionsAssistant::saveProjections(QWidget* parent, IntensityDataItem* intensityItem)
{
    ASSERT(intensityItem);
    ASSERT(gProjectDocument.has_value());

    QString defaultName = gProjectDocument.value()->userExportDir() + "/untitled.txt";
    QString fileName = QFileDialog::getSaveFileName(
        parent, "Save projections data", defaultName, "", nullptr,
        appSettings->useNativeFileDialog() ? QFileDialog::Options()
                                           : QFileDialog::DontUseNativeDialog);

    if (fileName.isEmpty())
        return;

    QFile file(fileName);
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
        throw std::runtime_error("Cannot create file for saving projections");

    m_field = intensityItem->c_field();

    QTextStream out(&file);

    out << "# Projections along x-axis (horizontal projections) \n";
    out << projectionsToString(GUI::ID::ProjectionType::Horizontal, intensityItem);
    out << "\n";

    out << "# Projections along y-axis (vertical projections) \n";
    out << projectionsToString(GUI::ID::ProjectionType::Vertical, intensityItem);
    out << "\n";

    file.close();
}

//! Generates multi-line string with projections data of given type (horizontal, vertical).

QString SaveProjectionsAssistant::projectionsToString(GUI::ID::ProjectionType projectionsType,
                                                      IntensityDataItem* intensityItem)
{
    QString result;
    QTextStream out(&result);

    auto projData = projectionsData(projectionsType, intensityItem);

    if (projData.projections.isEmpty())
        return result;

    out << projectionFileHeader(projData);

    auto bin_centers = projData.bin_centers;

    for (int i_point = 0; i_point < bin_centers.size(); ++i_point) {
        out << to_double_str(bin_centers[i_point]) << " ";
        for (auto& data : projData.projections)
            out << to_scientific_str(data.bin_values[i_point]);
        out << "\n";
    }
    return result;
}

//! Returns projections data for all projections of given type (horizontal, vertical).

SaveProjectionsAssistant::ProjectionsData
SaveProjectionsAssistant::projectionsData(GUI::ID::ProjectionType projectionsType,
                                          IntensityDataItem* intensityItem)
{
    ProjectionsData result;
    result.is_horizontal = (projectionsType == GUI::ID::ProjectionType::Horizontal);

    for (auto* item : projectionItems(projectionsType, intensityItem)) {
        std::unique_ptr<Datafield> field;
        SaveProjectionsAssistant::Projection data;

        if (const auto* horLine = dynamic_cast<HorizontalLineItem*>(item)) {
            data.axis_value = horLine->posY();
            field.reset(m_field->xProjection(data.axis_value));
        } else if (const auto* verLine = dynamic_cast<VerticalLineItem*>(item)) {
            data.axis_value = verLine->posX();
            field.reset(m_field->yProjection(data.axis_value));
        } else
            ASSERT(false);

        auto values = field->flatVector();
        auto centers = field->axis(0).binCenters();
        data.bin_values = QVector<double>(values.begin(), values.end());
        if (result.bin_centers.isEmpty())
            result.bin_centers = QVector<double>(centers.begin(), centers.end());
        result.projections.push_back(data);
    }
    return result;
}

//! Returns vector of ProjectionItems sorted according to axis value.

QVector<MaskItem*>
SaveProjectionsAssistant::projectionItems(GUI::ID::ProjectionType projectionsType,
                                          IntensityDataItem* intensityItem)
{
    auto result = intensityItem->projectionContainerItem()->projectionsOfType(projectionsType);
    std::sort(result.begin(), result.end(),
              projectionsType == GUI::ID::ProjectionType::Horizontal ? horiz_less_posy
                                                                     : vert_less_posx);
    return result;
}

//! Returns projections header. For projections along x it will be
//! "# x         y=6.0194            y=33.5922           y=61.9417"

QString SaveProjectionsAssistant::projectionFileHeader(ProjectionsData& projectionsData)
{
    QString xcol, ycol;

    projectionsData.is_horizontal ? xcol = "# x" : xcol = "# y";
    projectionsData.is_horizontal ? ycol = " y=" : ycol = " x=";

    QString result;
    result.append(QString("%1").arg(xcol, -bin_centers_colwidth));

    for (auto& data : projectionsData.projections)
        result.append(QString("%1%2").arg(ycol).arg(data.axis_value,
                                                    -(bin_values_colwidth - ycol.size()), 'f', 4));
    result.append("\n");

    return result;
}
