//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/Model/Data/DataItem.cpp
//! @brief     Implements class Data2DItem.
//!
//! @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/Model/Data/DataItem.h"
#include "Base/Axis/Scale.h"
#include "Base/Util/Assert.h"
#include "Device/Data/Datafield.h"
#include "Device/IO/IOFactory.h"
#include "GUI/Model/Axis/AmplitudeAxisItem.h"
#include "GUI/Model/Util/UtilXML.h"
#include <QFile>
#include <utility>

namespace {
namespace Tag {

const QString FileName("FileName");
const QString XAxis("XAxis");
const QString YAxis("YAxis");

} // namespace Tag

const QString undefined_fname = "_undefined_";

} // namespace


DataItem::DataItem(const QString& modelType)
    : TYPE(modelType)
    , m_fname(undefined_fname)
    , m_x_axis(std::make_unique<BasicAxisItem>())
    , m_y_axis(std::make_unique<AmplitudeAxisItem>())
    , m_last_modified(QDateTime::currentDateTime())
    , m_last_saved(m_last_modified.addSecs(-2023))
{
}

DataItem::~DataItem() = default;

void DataItem::setOriginalDatafield(const Datafield& data)
{
    std::unique_lock<std::mutex> lock(m_update_data_mutex);
    m_datafield = std::make_unique<Datafield>(data);
    setLastModified(QDateTime::currentDateTime());
    emit datafieldChanged();
}

void DataItem::setTheDatafield(const Datafield& data)
{
    setOriginalDatafield(data.plottableField());
}

void DataItem::setRawDataVector(const std::vector<double>& data)
{
    ASSERT(m_datafield->size() == data.size());
    std::unique_lock<std::mutex> lock(m_update_data_mutex);
    m_datafield->setVector(data);

    setLastModified(QDateTime::currentDateTime());
}

QString DataItem::dataFullPath(const QString& projectDir) const
{
    return projectDir + "/" + m_fname;
}

void DataItem::setFileName(const QString& filename)
{
    m_fname = filename;
    setLastModified(QDateTime::currentDateTime());
}

void DataItem::loadDatafield(const QString& projectDir, int rank)
{
    if (!QFile::exists(projectDir))
        throw std::runtime_error("Cannot load datafield: project directory "
                                 + projectDir.toStdString() + " does not exist");
    if (m_fname == undefined_fname)
        return;

    const auto file = dataFullPath(projectDir);

    std::unique_ptr<Datafield> data;
    if (rank == 1)
        data = std::make_unique<Datafield>(
            IO::readData1D(file.toStdString(), IO::Filetype1D::bornagain1D));
    else if (rank == 2)
        data = std::make_unique<Datafield>(
            IO::readData2D(file.toStdString(), IO::Filetype2D::bornagain2D));
    else
        ASSERT_NEVER;

    ASSERT(data);
    setDatafield(*data);
    m_last_saved = m_last_modified;
}

void DataItem::saveDatafield(const QString& projectDir) const
{
    if (!m_datafield || !QFile::exists(projectDir))
        return;

    const auto path = dataFullPath(projectDir);

    if (QFile::exists(path) && !wasModifiedSinceLastSave())
        return;

    IO::writeDatafield(*c_field(), path.toStdString());

    m_last_saved = QDateTime::currentDateTime();
}

void DataItem::setLastModified(const QDateTime& dtime)
{
    m_last_modified = dtime;
}

int DataItem::xSize() const
{
    return axItemX()->size();
}

int DataItem::ySize() const
{
    return axItemY()->size();
}

double DataItem::lowerX() const
{
    return axItemX()->min().dVal();
}

double DataItem::upperX() const
{
    return axItemX()->max().dVal();
}

void DataItem::setXrange(double lower, double upper)
{
    axItemX()->setMin(lower);
    axItemX()->setMax(upper);
}

double DataItem::lowerY() const
{
    return axItemY()->min().dVal();
}

double DataItem::upperY() const
{
    return axItemY()->max().dVal();
}

void DataItem::setYrange(double lower, double upper)
{
    axItemY()->setMin(lower);
    axItemY()->setMax(upper);
}

void DataItem::alignXranges(DataItem* sourceItem)
{
    if (xSize() != sourceItem->xSize())
        throw std::runtime_error("Data and simulation have different number of x bins");

    if (sourceItem != this)
        setXrange(sourceItem->lowerX(), sourceItem->upperX());
}

void DataItem::alignYranges(DataItem* sourceItem)
{
    if (ySize() != sourceItem->ySize())
        throw std::runtime_error("Data and simulation have different number of y bins");

    if (sourceItem != this)
        setYrange(sourceItem->lowerY(), sourceItem->upperY());
}

void DataItem::alignXYranges(DataItem* sourceItem)
{
    alignXranges(sourceItem);
    alignYranges(sourceItem);
}

const BasicAxisItem* DataItem::axItemX() const
{
    return m_x_axis.get();
}

BasicAxisItem* DataItem::axItemX()
{
    return m_x_axis.get();
}

const AmplitudeAxisItem* DataItem::axItemY() const
{
    return m_y_axis.get();
}

AmplitudeAxisItem* DataItem::axItemY()
{
    return m_y_axis.get();
}

QString DataItem::xAxisLabel() const
{
    return m_datafield ? QString::fromStdString(m_datafield->xAxis().axisLabel()) : "";
}

QString DataItem::yAxisLabel() const
{
    return m_datafield ? QString::fromStdString(m_datafield->yAxis().axisLabel()) : "";
}

void DataItem::writeTo(QXmlStreamWriter* w) const
{
    XML::writeTaggedValue(w, Tag::FileName, m_fname);
    XML::writeTaggedElement(w, Tag::XAxis, *m_x_axis);
    XML::writeTaggedElement(w, Tag::YAxis, *m_y_axis);
}

void DataItem::readFrom(QXmlStreamReader* r)
{
    while (r->readNextStartElement()) {
        QString tag = r->name().toString();
        if (tag == Tag::FileName)
            m_fname = XML::readTaggedString(r, tag);
        else if (tag == Tag::XAxis)
            XML::readTaggedElement(r, tag, *m_x_axis);
        else if (tag == Tag::YAxis)
            XML::readTaggedElement(r, tag, *m_y_axis);
        else
            r->skipCurrentElement();
    }
}

bool DataItem::wasModifiedSinceLastSave() const
{
    // positive number means that m_last_saved is older than m_last_modified
    return m_last_saved.msecsTo(m_last_modified) > 0;
}
