//  ************************************************************************************************
//
//  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/Device/BackgroundItems.h"
#include "GUI/Model/Device/InstrumentItems.h"
#include "GUI/Model/Device/RealItem.h"
#include "GUI/Model/Project/ProjectUtil.h"
#include "GUI/Model/Sample/ItemWithMaterial.h"
#include "GUI/Model/Sample/MaterialItem.h"
#include "GUI/Model/Sample/SampleItem.h"
#include "GUI/Support/Data/ID.h"
#include "GUI/Support/Util/MessageService.h"
#include "GUI/Support/Util/Path.h"
#include "GUI/Support/XML/DeserializationException.h"
#include "GUI/Support/XML/UtilXML.h"
#include <QFile>
#include <QStandardPaths>

std::optional<ProjectDocument*> gProjectDocument;

namespace {

const QString minimal_supported_version = "20.0";

namespace Tag {

const QString BornAgain("BornAgain");
const QString DocumentInfo("DocumentInfo");
const QString SimulationOptions("SimulationOptions");
const QString InstrumentModel("InstrumentModel");
const QString SampleModel("SampleModel");
const QString JobModel("JobModel");
const QString RealModel("RealModel");
const QString ActiveView("ActiveView");

} // namespace Tag
} // namespace


ProjectDocument::ProjectDocument()
    : m_modified(false)
    , m_singleInstrumentMode(false)
    , m_singleSampleMode(false)
    , m_functionalities(All)
    , m_instrumentEditController(&m_instrumentModel)
    , m_realModel(&m_instrumentModel)
    , m_lastViewActive(GUI::ID::ViewId::Instrument)
{
    connect(&m_instrumentEditController, &MultiInstrumentNotifier::instrumentAddedOrRemoved, this,
            &ProjectDocument::onModelChanged, Qt::UniqueConnection);
    connect(&m_instrumentEditController, &MultiInstrumentNotifier::instrumentChanged, this,
            &ProjectDocument::onModelChanged, Qt::UniqueConnection);

    m_linkManager = std::make_unique<LinkInstrumentManager>(this);
    setObjectName("ProjectDocument");
}

QString ProjectDocument::projectName() const
{
    return m_project_name;
}

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

QString ProjectDocument::projectDir() const
{
    return m_project_dir;
}

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

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

//! Returns directory name suitable for saving plots.

QString ProjectDocument::userExportDir() const
{
    if (QString dir = validProjectDir(); !dir.isEmpty())
        return dir;
    return QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
}

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

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

InstrumentModel* ProjectDocument::instrumentModel()
{
    return &m_instrumentModel;
}

SampleModel* ProjectDocument::sampleModel()
{
    return &m_sampleModel;
}

RealModel* ProjectDocument::realModel()
{
    return &m_realModel;
}

JobModel* ProjectDocument::jobModel()
{
    return &m_jobModel;
}

SimulationOptionsItem* ProjectDocument::simulationOptionsItem()
{
    return &m_simulationOptionsItem;
}

LinkInstrumentManager* ProjectDocument::linkInstrumentManager()
{
    return m_linkManager.get();
}

MultiInstrumentNotifier* ProjectDocument::multiNotifier()
{
    return &m_instrumentEditController;
}

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_jobModel.writeDataFiles(GUI::Project::Util::projectDir(projectPullPath));
    m_realModel.writeDataFiles(GUI::Project::Util::projectDir(projectPullPath));

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

ProjectDocument::ReadResult ProjectDocument::loadProjectFileWithData(const QString& projectPullPath,
                                                                     MessageService& messageService)
{
    setProjectFullPath(projectPullPath);

    QFile file(projectFullPath());
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        QString message = QString("Open file error '%1'").arg(file.errorString());
        messageService.addError(this, message);
        return ReadResult::error;
    }

    try {
        auto result = readProject(&file, messageService);
        file.close();
        if (result == ReadResult::error)
            return result;

        m_jobModel.readDataFiles(GUI::Project::Util::projectDir(projectPullPath), &messageService);
        m_realModel.readDataFiles(GUI::Project::Util::projectDir(projectPullPath), &messageService);

        if (!messageService.warnings().empty())
            result = ReadResult::warning;
        return result;
    } catch (const std::exception& ex) {
        QString message = QString("Exception thrown '%1'").arg(QString(ex.what()));
        messageService.addError(this, message);
        return ReadResult::error;
    }
}

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

bool ProjectDocument::isModified() const
{
    return m_modified;
}

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

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

QString ProjectDocument::documentVersion() const
{
    QString result(m_currentVersion);
    if (result.isEmpty())
        result = GUI::Base::Path::getBornAgainVersionString();
    return result;
}

bool ProjectDocument::singleInstrumentMode() const
{
    return m_singleInstrumentMode;
}

void ProjectDocument::setSingleInstrumentMode(bool b)
{
    if (b != m_singleInstrumentMode) {
        m_singleInstrumentMode = b;
        emit singleInstrumentModeChanged();
    }
}

bool ProjectDocument::singleSampleMode() const
{
    return m_singleSampleMode;
}

void ProjectDocument::setSingleSampleMode(bool b)
{
    if (b != m_singleSampleMode) {
        m_singleSampleMode = b;
        emit singleSampleModeChanged();
    }
}

ProjectDocument::Functionalities ProjectDocument::functionalities() const
{
    return m_functionalities;
}

void ProjectDocument::setFunctionalities(const Functionalities& f)
{
    if (m_functionalities != f) {
        m_functionalities = f;
        emit functionalitiesChanged();
    }
}

void ProjectDocument::onModelChanged()
{
    setModified();
}

void ProjectDocument::writeProject(QIODevice* device)
{
    // NOTE: The ordering of the XML elements is important in initialization

    QXmlStreamWriter w(device);
    w.setAutoFormatting(true);
    w.writeStartDocument();
    w.writeStartElement(Tag::BornAgain);
    QString version_string = GUI::Base::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_simulationOptionsItem.writeTo(&w);
    w.writeEndElement();

    // instruments
    w.writeStartElement(Tag::InstrumentModel);
    m_instrumentModel.writeTo(&w);
    w.writeEndElement();

    // samples
    w.writeStartElement(Tag::SampleModel);
    m_sampleModel.writeTo(&w);
    w.writeEndElement();

    // real model
    w.writeStartElement(Tag::RealModel);
    m_realModel.writeTo(&w);
    w.writeEndElement();

    // job model
    w.writeStartElement(Tag::JobModel);
    m_jobModel.writeTo(&w);
    w.writeEndElement();

    // active view
    w.writeStartElement(Tag::ActiveView);
    w.writeAttribute(XML::Attrib::value, QString::number(m_lastViewActive));
    w.writeEndElement();

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

ProjectDocument::ReadResult ProjectDocument::readProject(QIODevice* device,
                                                         MessageService& messageService)
{
    const int warningsBefore = messageService.warnings().size();

    QXmlStreamReader r(device);
    try {
        while (!r.atEnd()) {
            r.readNext();
            if (r.isStartElement()) {
                if (r.name() == Tag::BornAgain) {
                    const uint version = XML::readUIntAttribute(&r, XML::Attrib::version);
                    Q_UNUSED(version)
                    m_currentVersion = r.attributes().value(XML::Attrib::BA_Version).toString();

                    if (!GUI::Base::Path::isVersionMatchMinimal(m_currentVersion,
                                                                minimal_supported_version)) {
                        QString message = QString("Cannot open document version '%1', "
                                                  "minimal supported version '%2'")
                                              .arg(m_currentVersion)
                                              .arg(minimal_supported_version);
                        messageService.addError(this, message);
                        return ReadResult::error;
                    }

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

                        // simulation options
                        if (tag == Tag::SimulationOptions) {
                            m_simulationOptionsItem.readFrom(&r);
                            XML::gotoEndElementOfTag(&r, tag);

                            // instruments
                        } else if (tag == Tag::InstrumentModel) {
                            m_instrumentModel.readFrom(&r);
                            XML::gotoEndElementOfTag(&r, tag);

                            // samples
                        } else if (tag == Tag::SampleModel) {
                            m_sampleModel.readFrom(&r);
                            XML::gotoEndElementOfTag(&r, tag);

                            // real model
                        } else if (tag == Tag::RealModel) {
                            // 'm_instrumentModel' should be read before
                            m_realModel.readFrom(&r);
                            XML::gotoEndElementOfTag(&r, tag);

                            // job model
                        } else if (tag == Tag::JobModel) {
                            m_jobModel.readFrom(&r);
                            XML::gotoEndElementOfTag(&r, tag);

                            // active view
                        } else if (tag == Tag::ActiveView) {
                            XML::readAttribute(&r, XML::Attrib::value, &m_lastViewActive);
                            XML::gotoEndElementOfTag(&r, tag);

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

    } catch (DeserializationException& ex) {
        r.raiseError(ex.text());
    }

    if (r.hasError()) {
        QString message = QString("Format error '%1'").arg(r.errorString());
        messageService.addError(this, message);
        return ReadResult::error;
    }

    // return "ReadResult::warning" only if warnings have been issued _in here_
    return warningsBefore != messageService.warnings().size() ? ReadResult::warning
                                                              : ReadResult::ok;
}

int ProjectDocument::viewId() const
{
    return m_lastViewActive;
}

void ProjectDocument::setViewId(int id)
{
    m_lastViewActive = id;
}
