//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/Model/Data/DataItem.cpp
//! @brief     Implements class IntensityDataItem
//!
//! @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/Util/Assert.h"
#include "Device/Data/Datafield.h"
#include "Device/IO/IOFactory.h"
#include "GUI/Model/Axis/AmplitudeAxisItem.h"
#include "GUI/Model/Axis/BasicAxisItem.h"
#include "GUI/Support/Util/CoordName.h"
#include "GUI/Support/Util/MessageService.h"
#include "GUI/Support/XML/UtilXML.h"
#include <QFile>
#include <QThread>

namespace {
namespace Tag {

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

} // namespace Tag
} // namespace

const QString x_axis_default_name = "X [nbins]";
const QString y_axis_default_name = "Y [nbins]";

DataItem::DataItem(const QString& modelType)
    : TYPE(modelType)
    , m_fileName("undefined")
    , m_currentCoord(Coords::NBINS)
    , m_xAxis(std::make_unique<BasicAxisItem>())
    , m_yAxis(std::make_unique<AmplitudeAxisItem>())
    , m_last_modified(QDateTime::currentDateTime())
    , m_last_saved(m_last_modified.addSecs(-2023))
{
    setXaxisTitle(x_axis_default_name);
    setYaxisTitle(y_axis_default_name);
}

DataItem::~DataItem() = default;

void DataItem::setDatafield(Datafield* data)
{
    std::unique_lock<std::mutex> lock(m_update_data_mutex);
    m_datafield.reset(data);

    setLastModified(QDateTime::currentDateTime());
    emit datafieldChanged();
}

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());
    emit rawDataVectorChanged(data);
}

QString DataItem::fileName() const
{
    return m_fileName;
}

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

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

QDateTime DataItem::lastModified() const
{
    return m_last_modified;
}

QString DataItem::loadDatafield(MessageService* messageService, const QString& projectDir)
{
    if (!QFile::exists(projectDir))
        return {};

    ASSERT(messageService);
    const auto file = dataFullPath(projectDir);
    try {
        auto* data = IO::readData2D(file.toStdString());
        ASSERT(data);
        setDatafield(data);
        m_last_saved = m_last_modified;
    } catch (const std::exception& ex) {
        messageService->addWarning(this, QString(ex.what()));
        return QString(ex.what());
    }
    return {};
}

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;

    std::unique_lock<std::mutex> lock(m_update_data_mutex); // TODO why lock??
    std::unique_ptr<const Datafield> field(c_field()->clone());
    lock.unlock();

    if (m_saveInBackground) {
        std::string errorMessage;
        auto* saveThread = new std::thread([&errorMessage, &field, path]() {
            try {
                IO::writeDatafield(*field, path.toStdString());
            } catch (const std::exception& ex) {
                errorMessage = ex.what();
            }
        });
        saveThread->join();

        if (!errorMessage.empty())
            throw std::runtime_error(errorMessage);
    } else
        IO::writeDatafield(*field, path.toStdString());

    m_last_saved = QDateTime::currentDateTime();
}

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

int DataItem::xSize() const
{
    return xAxisItem()->binCount();
}

int DataItem::ySize() const
{
    return yAxisItem()->binCount();
}

double DataItem::lowerX() const
{
    return xAxisItem()->min();
}

double DataItem::upperX() const
{
    return xAxisItem()->max();
}

void DataItem::setLowerX(double value)
{
    xAxisItem()->setMin(value);
    emit itemAxesRangeChanged();
}

void DataItem::setUpperX(double value)
{
    xAxisItem()->setMax(value);
    emit itemAxesRangeChanged();
}

double DataItem::lowerY() const
{
    return yAxisItem()->min();
}

double DataItem::upperY() const
{
    return yAxisItem()->max();
}

void DataItem::setLowerY(double value)
{
    yAxisItem()->setMin(value);
    emit itemAxesRangeChanged();
}

void DataItem::setUpperY(double value)
{
    yAxisItem()->setMax(value);
    emit itemAxesRangeChanged();
}

void DataItem::copyXRangeFromItem(DataItem* sourceItem)
{
    if (sourceItem == this)
        return;
    setLowerX(sourceItem->lowerX());
    setUpperX(sourceItem->upperX());
}

void DataItem::copyYRangeFromItem(DataItem* sourceItem)
{
    if (sourceItem == this)
        return;
    setLowerY(sourceItem->lowerY());
    setUpperY(sourceItem->upperY());
}

void DataItem::copyXYRangesFromItem(DataItem* sourceItem)
{
    copyXRangeFromItem(sourceItem);
    copyYRangeFromItem(sourceItem);
}

Coords DataItem::currentCoord() const
{
    return m_currentCoord;
}

void DataItem::setCurrentCoord(Coords coord)
{
    m_currentCoord = coord;
}

QString DataItem::currentAxesUnits() const
{
    return GUI::Util::CoordName::nameFromCoord(m_currentCoord);
}

void DataItem::setCurrentAxesUnits(const QString& units)
{
    m_currentCoord = GUI::Util::CoordName::coordFromName(units);
    emit axesUnitsChanged(this);
}

const BasicAxisItem* DataItem::xAxisItem() const
{
    return m_xAxis.get();
}

BasicAxisItem* DataItem::xAxisItem()
{
    return m_xAxis.get();
}

const AmplitudeAxisItem* DataItem::yAxisItem() const
{
    return m_yAxis.get();
}

AmplitudeAxisItem* DataItem::yAxisItem()
{
    return m_yAxis.get();
}

QString DataItem::XaxisTitle() const
{
    return xAxisItem()->title();
}

QString DataItem::YaxisTitle() const
{
    return yAxisItem()->title();
}

void DataItem::setXaxisTitle(const QString& title)
{
    xAxisItem()->setTitle(title);
}

void DataItem::setYaxisTitle(const QString& title)
{
    yAxisItem()->setTitle(title);
}

void DataItem::setAxesRangeToData()
{
    setLowerX(xMin());
    setUpperX(xMax());
    setLowerY(yMin());
    setUpperY(yMax());
}

void DataItem::writeTo(QXmlStreamWriter* w) const
{
    XML::writeAttribute(w, XML::Attrib::version, uint(2));

    // file name
    w->writeStartElement(Tag::FileName);
    XML::writeAttribute(w, XML::Attrib::value, m_fileName);
    w->writeEndElement();

    // axes units
    w->writeStartElement(Tag::AxesUnits);
    XML::writeAttribute(w, XML::Attrib::value, GUI::Util::CoordName::nameFromCoord(m_currentCoord));
    w->writeEndElement();

    // x axis
    w->writeStartElement(Tag::XAxis);
    m_xAxis->writeTo(w);
    w->writeEndElement();

    // y axis
    w->writeStartElement(Tag::YAxis);
    m_yAxis->writeTo(w);
    w->writeEndElement();
}

void DataItem::readFrom(QXmlStreamReader* r)
{
    const uint version = XML::readUIntAttribute(r, XML::Attrib::version);

    while (r->readNextStartElement()) {
        QString tag = r->name().toString();

        // file name
        if (tag == Tag::FileName) {
            XML::readAttribute(r, XML::Attrib::value, &m_fileName);
            XML::gotoEndElementOfTag(r, tag);

            // axes units
        } else if (tag == Tag::AxesUnits) {
            QString axes_units;
            if (version == 1) {
                XML::readAttribute(r, XML::Attrib::name, &axes_units);
                if (axes_units.isEmpty())
                    axes_units = GUI::Util::CoordName::nameFromCoord(Coords::NBINS);
            } else
                XML::readAttribute(r, XML::Attrib::value, &axes_units);
            m_currentCoord = GUI::Util::CoordName::coordFromName(axes_units);
            XML::gotoEndElementOfTag(r, tag);

            // x axis
        } else if (tag == Tag::XAxis) {
            m_xAxis->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // y axis
        } else if (tag == Tag::YAxis) {
            m_yAxis->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

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