//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/IO/ProjectionsSaver.cpp
//! @brief     Implements class ProjectionsSaver.
//!
//! @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/IO/ProjectionsSaver.h"
#include "Base/Axis/Scale.h"
#include "Base/Py/PyFmt.h"
#include "Device/Data/Datafield.h"
#include "GUI/Model/Data/Data2DItem.h"
#include "GUI/Model/Mask/MasksSet.h"
#include "GUI/Model/Project/ProjectDocument.h"
#include "GUI/View/Widget/AppConfig.h"
#include "GUI/View/Widget/FileDialog.h"
#include <QFile>
#include <QString>
#include <QTextStream>
#include <QVector>
#include <QWidget>
#include <memory>

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

//! Returns horizontal or vertical cuts, sorted by position.
QVector<const LineItem*> projectionItems(Qt::Orientation ori, const Data2DItem* data_item)
{
    const auto* pset = data_item->prjns();
    if (!pset)
        return {};
    QVector<const LineItem*> result;
    for (const MaskItem* m : *pset)
        if (const auto* t = dynamic_cast<const LineItem*>(m))
            if (t->orientation() == ori)
                result.push_back(t);
    std::sort(result.begin(), result.end(), [](const LineItem* t1, const LineItem* t2) {
        return t1->pos().dVal() < t2->pos().dVal();
    });
    return result;
}

struct Projection {
    double axis_value; //!< value on axis where projection has been made
    QVector<double> bin_values;
};

struct ProjectionsData {
    bool is_horizontal;
    QVector<double> bin_centers;
    QVector<Projection> projections;
};

//! Returns projections header, e.g.
//! "# x         z at y=6.0194            z at y=33.5922"
QString projectionFileHeader(ProjectionsData& projs)
{
    const char label1 = projs.is_horizontal ? 'x' : 'y';
    const char label2 = projs.is_horizontal ? 'y' : 'x';

    QString result = QString("# %1").arg(label1, -bin_centers_colwidth + 1);

    for (auto& data : projs.projections)
        result.append(QString("z @ %1=%2")
                          .arg(label2)
                          .arg(data.axis_value, -(bin_values_colwidth - 6), 'f', 4));
    result.append("\n");

    return result;
}

//! Returns projections data for all projections of given type (horizontal, vertical).
ProjectionsData projectionsData(Qt::Orientation projectionsType, const Data2DItem* data_item)
{
    ProjectionsData result;
    result.is_horizontal = (projectionsType == Qt::Horizontal);

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

        if (const auto* horLine = dynamic_cast<const HorizontalLineItem*>(item)) {
            data.axis_value = horLine->pos().dVal();
            field.reset(data_item->c_field()->xProjection(data.axis_value));
        } else if (const auto* verLine = dynamic_cast<const VerticalLineItem*>(item)) {
            data.axis_value = verLine->pos().dVal();
            field.reset(data_item->c_field()->yProjection(data.axis_value));
        } else
            ASSERT_NEVER;

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

//! Generates multi-line string with projections data of given type (horizontal, vertical).
QString projectionsToString(Qt::Orientation projectionsType, const Data2DItem* data_item)
{
    QString result;
    QTextStream out(&result);

    auto projData = projectionsData(projectionsType, data_item);

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

} // namespace


void IO::saveProjections(const Data2DItem* data_item)
{
    if (!data_item)
        return;

    QString fname = GUI::FileDialog::w1_1f("Save projections data", gApp->artifact_export_dir, "");
    if (fname.isEmpty())
        return;

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

    QTextStream out(&file);

    out << "# Projections along x-axis (horizontal projections) \n";
    out << projectionsToString(Qt::Horizontal, data_item);
    out << "\n";

    out << "# Projections along y-axis (vertical projections) \n";
    out << projectionsToString(Qt::Vertical, data_item);
    out << "\n";

    file.close();
}
