//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/Model/Project/ProjectDocument.cpp
//! @brief     Implements class ProjectDocument.
//!
//! @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/Project/ProjectDocument.h"
#include "GUI/Model/File/DatafileItem.h"
#include "GUI/Model/File/DatafilesSet.h"
#include "GUI/Model/Job/JobsSet.h"
#include "GUI/Model/Material/MaterialItem.h"
#include "GUI/Model/Project/ProjectUtil.h"
#include "GUI/Model/Sample/ItemWithMaterial.h"
#include "GUI/Model/Sample/SamplesSet.h"
#include "GUI/Model/Sim/BackgroundItems.h"
#include "GUI/Model/Sim/InstrumentsSet.h"
#include "GUI/Model/Sim/SimulationOptionsItem.h"
#include "GUI/Model/Util/Path.h"
#include "GUI/Model/Util/UtilXML.h"
#include <QFile>

BA_GUI_API_ std::unique_ptr<ProjectDocument> gDoc;

namespace {

namespace Tag {

const QString BornAgain("BornAgain");
const QString DocumentInfo("DocumentInfo");
const QString SimulationOptions("SimulationOptions");
const QString InstrumentsSet("InstrumentsSet");
const QString SamplesSet("SamplesSet");
const QString JobsSet("JobsSet");
const QString RealModel("RealModel");
const QString ActiveView("ActiveView");

} // namespace Tag
} // namespace


ProjectDocument::ProjectDocument()
    : m_project_name("Untitled")
    , m_modified(false)
    , m_last_view_active(0)
    , m_instruments(std::make_unique<InstrumentsSet>())
    , m_samples(std::make_unique<SamplesSet>())
    , m_datafiles(std::make_unique<DatafilesSet>())
    , m_options(std::make_unique<SimulationOptionsItem>())
    , m_jobs(std::make_unique<JobsSet>())
{
    connect(m_instruments.get(), &InstrumentsSet::setChanged, this, &ProjectDocument::setModified);
    connect(m_instruments.get(), &InstrumentsSet::currentModified, this,
            &ProjectDocument::setModified);
}

ProjectDocument::~ProjectDocument() = default;

void ProjectDocument::clear()
{
    m_project_name = "Untitled";
    m_instruments->clear();
    m_samples->clear();
    m_datafiles->clear();
    *m_options = SimulationOptionsItem();
    m_jobs->clear();
    m_modified = false;
}

void ProjectDocument::setProjectName(const QString& text)
{
    if (m_project_name != text)
        m_project_name = text;
}

QString ProjectDocument::validProjectDir() const
{
    if (m_project_name.isEmpty())
        return "";
    return m_project_dir;
}

void ProjectDocument::setProjectDir(const QString& text)
{
    m_project_dir = text;
}

QString ProjectDocument::projectFullPath() const
{
    if (!projectName().isEmpty())
        return projectDir() + "/" + projectName() + GUI::Util::Project::projectFileExtension;
    return "";
}

void ProjectDocument::setProjectFullPath(const QString& fullPath)
{
    setProjectName(GUI::Util::Project::projectName(fullPath));
    setProjectDir(GUI::Util::Project::projectDir(fullPath));
}

void ProjectDocument::saveProjectFileWithData(const QString& projectPullPath)
{
    QFile file(projectPullPath);
    if (!file.open(QFile::ReadWrite | QIODevice::Truncate | QFile::Text))
        throw std::runtime_error("Cannot open project file '" + projectPullPath.toStdString()
                                 + "' for writing.");

    writeProject(&file);
    file.close();

    m_jobs->saveAllDatafields(GUI::Util::Project::projectDir(projectPullPath));
    m_datafiles->writeDatafiles(GUI::Util::Project::projectDir(projectPullPath));

    const bool autoSave = GUI::Util::Project::isAutosave(projectPullPath);
    if (!autoSave) {
        setProjectFullPath(projectPullPath);
        clearModified();
    }
}

void ProjectDocument::loadProjectFileWithData(const QString& projectPullPath)
{
    setProjectFullPath(projectPullPath);

    QFile file(projectFullPath());
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
        throw std::runtime_error("Cannot open project file " + projectFullPath().toStdString());

    readProject(&file);
    file.close();

    m_jobs->loadAllDatafields(GUI::Util::Project::projectDir(projectPullPath));
    m_datafiles->readDatafiles(GUI::Util::Project::projectDir(projectPullPath));
}

bool ProjectDocument::hasValidNameAndPath() const
{
    return (!m_project_name.isEmpty() && !m_project_dir.isEmpty());
}

void ProjectDocument::setModified()
{
    m_modified = true;
    emit modifiedStateChanged();
}

void ProjectDocument::clearModified()
{
    m_modified = false;
    emit modifiedStateChanged();
}

void ProjectDocument::writeTo(QXmlStreamWriter* w) const
{
    QString version_string = GUI::Path::getBornAgainVersionString();
    w->writeAttribute(XML::Attrib::BA_Version, version_string);
    w->writeStartElement(Tag::DocumentInfo);
    w->writeAttribute(XML::Attrib::projectName, projectName());
    w->writeEndElement();

    XML::writeTaggedElement(w, Tag::SimulationOptions, *m_options);
    XML::writeTaggedElement(w, Tag::InstrumentsSet, *m_instruments);
    XML::writeTaggedElement(w, Tag::SamplesSet, *m_samples);
    XML::writeTaggedElement(w, Tag::RealModel, *m_datafiles);
    XML::writeTaggedElement(w, Tag::JobsSet, *m_jobs);
    XML::writeTaggedValue(w, Tag::ActiveView, m_last_view_active);
}

void ProjectDocument::writeProject(QIODevice* device)
{
    QXmlStreamWriter w(device);
    w.setAutoFormatting(true);
    w.writeStartDocument();

    XML::writeTaggedElement(&w, Tag::BornAgain, *this);

    w.writeEndDocument();
}

void ProjectDocument::readFrom(QXmlStreamReader* r)
{
    QString current_version = r->attributes().value(XML::Attrib::BA_Version).toString();

    if (!GUI::Path::isVersionMatchMinimal(current_version, XML::minimal_supported_version))
        throw std::runtime_error(QString("Cannot open document version '%1', "
                                         "minimal supported version is '%2'")
                                     .arg(current_version)
                                     .arg(XML::minimal_supported_version)
                                     .toStdString());

    while (r->readNextStartElement()) {
        QString tag = r->name().toString();
        if (tag == Tag::SimulationOptions)
            XML::readTaggedElement(r, tag, *m_options);
        else if (tag == Tag::InstrumentsSet)
            XML::readTaggedElement(r, tag, *m_instruments);
        else if (tag == Tag::SamplesSet)
            XML::readTaggedElement(r, tag, *m_samples);
        else if (tag == Tag::RealModel)
            // 'm_instruments' should be read before
            XML::readTaggedElement(r, tag, *m_datafiles);
        else if (tag == Tag::JobsSet)
            XML::readTaggedElement(r, tag, *m_jobs);
        else if (tag == Tag::ActiveView)
            m_last_view_active = XML::readTaggedInt(r, tag);
        else
            r->skipCurrentElement();
    }
}

void ProjectDocument::readProject(QIODevice* device)
{
    QXmlStreamReader r(device);
    while (!r.atEnd()) {
        r.readNext();
        if (r.isStartElement())
            if (r.name() == Tag::BornAgain)
                readFrom(&r);
    }
}
