//  ************************************************************************************************
//
//  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/ID.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 {

const QString minimal_supported_version = "20.0";

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_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 = std::make_unique<SimulationOptionsItem>();
    m_jobs = std::make_unique<JobsSet>();
    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.toLatin1()
                                 + "' 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 " + projectPullPath.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::writeProject(QIODevice* device)
{
    QXmlStreamWriter w(device);
    w.setAutoFormatting(true);
    w.writeStartDocument();
    w.writeStartElement(Tag::BornAgain);
    QString version_string = GUI::Path::getBornAgainVersionString();
    w.writeAttribute(XML::Attrib::BA_Version, version_string);
    XML::writeAttribute(&w, XML::Attrib::version, uint(2));

    w.writeStartElement(Tag::DocumentInfo);
    w.writeAttribute(XML::Attrib::projectName, projectName());
    w.writeEndElement();

    // simulation options
    w.writeStartElement(Tag::SimulationOptions);
    m_options->writeTo(&w);
    w.writeEndElement();

    // instruments
    w.writeStartElement(Tag::InstrumentsSet);
    m_instruments->writeTo(&w);
    w.writeEndElement();

    // samples
    w.writeStartElement(Tag::SamplesSet);
    m_samples->writeTo(&w);
    w.writeEndElement();

    // real model
    w.writeStartElement(Tag::RealModel);
    m_datafiles->writeTo(&w);
    w.writeEndElement();

    // job model
    w.writeStartElement(Tag::JobsSet);
    m_jobs->writeTo(&w);
    w.writeEndElement();

    w.writeEndElement(); // BornAgain tag
    w.writeEndDocument();
}

void ProjectDocument::readProject(QIODevice* device)
{
    QXmlStreamReader r(device);
    while (!r.atEnd()) {
        r.readNext();
        if (r.isStartElement()) {
            if (r.name() == Tag::BornAgain) {
                const uint version = XML::readUInt(&r, XML::Attrib::version);
                Q_UNUSED(version)
                QString current_version = r.attributes().value(XML::Attrib::BA_Version).toString();

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

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

                    // simulation options
                    if (tag == Tag::SimulationOptions) {
                        m_options->readFrom(&r);
                        XML::gotoEndElementOfTag(&r, tag);

                        // instruments
                    } else if (tag == Tag::InstrumentsSet) {
                        m_instruments->readFrom(&r);
                        XML::gotoEndElementOfTag(&r, tag);

                        // samples
                    } else if (tag == Tag::SamplesSet) {
                        m_samples->readFrom(&r);
                        XML::gotoEndElementOfTag(&r, tag);

                        // real model
                    } else if (tag == Tag::RealModel) {
                        // 'm_instruments' should be read before
                        m_datafiles->readFrom(&r);
                        XML::gotoEndElementOfTag(&r, tag);

                        // job model
                    } else if (tag == Tag::JobsSet) {
                        m_jobs->readFrom(&r);
                        XML::gotoEndElementOfTag(&r, tag);

                    } else
                        r.skipCurrentElement();
                }
            }
        }
    }
}
