//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/Model/Model/JobModel.cpp
//! @brief     Implements class JobModel
//!
//! @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/Model/JobModel.h"
#include "GUI/Model/Device/InstrumentItems.h"
#include "GUI/Model/Device/RealItem.h"
#include "GUI/Model/Job/ParameterTreeItems.h"
#include "GUI/Model/Model/ParameterTreeUtil.h"
#include "GUI/Model/Sample/SampleItem.h"

namespace {
namespace Tag {

const QString Job("Job");
const QString SelectedIndex("SelectedIndex");

} // namespace Tag
} // namespace

JobModel::JobModel(QObject* parent)
    : QObject(parent)
{
    m_queue_data = std::make_unique<JobQueueData>(this);
    connect(m_queue_data.get(), &JobQueueData::focusRequest, this, &JobModel::focusRequest);
    connect(m_queue_data.get(), &JobQueueData::globalProgress, this, &JobModel::globalProgress);
    setObjectName("JobModel");
}

JobItem* JobModel::jobItemForIdentifier(const QString& identifier)
{
    for (auto* jobItem : jobItems())
        if (jobItem->identifier() == identifier)
            return jobItem;

    return nullptr;
}

JobItem* JobModel::createJobItem()
{
    auto jobItem = new JobItem();
    m_jobItems.emplace_back(jobItem);
    return jobItem;
}

//! Main method to add a job
JobItem* JobModel::addJobItem(const SampleItem* sampleItem, const InstrumentItem* instrumentItem,
                              const RealItem* realItem, const SimulationOptionsItem& optionItem)
{
    ASSERT(sampleItem);
    ASSERT(instrumentItem);

    JobItem* jobItem = createJobItem();
    jobItem->setJobName(generateJobName());
    jobItem->setIdentifier(QUuid::createUuid().toString());

    jobItem->copySampleIntoJob(sampleItem);
    jobItem->copyInstrumentIntoJob(instrumentItem);
    ParameterTreeBuilder(jobItem, true).build();
    jobItem->copySimulationOptionsIntoJob(optionItem);
    jobItem->createSimulatedDataItem();

    if (realItem) {
        jobItem->copyRealItemIntoJob(realItem);
        jobItem->adjustReaDataToJobInstrument();

        jobItem->createDiffDataItem();
        jobItem->createFitContainers();
    }

    emit jobAdded();
    return jobItem;
}

QVector<JobItem*> JobModel::jobItems() const
{
    return QVector<JobItem*>(m_jobItems.begin(), m_jobItems.end());
}

//! restore instrument and sample model from backup for given JobItem
void JobModel::restore(JobItem* jobItem)
{
    jobItem->parameterContainerItem()->restoreBackupValues();
}

bool JobModel::hasUnfinishedJobs()
{
    if (m_queue_data->hasUnfinishedJobs())
        return true;

    for (auto* jobItem : jobItems())
        if (jobItem->status() == JobStatus::Fitting)
            return true;

    return false;
}

void JobModel::clear()
{
    for (auto* job : jobItems())
        removeJob(job);
}

QVector<DataItem*> JobModel::dataItems() const
{
    QVector<DataItem*> result;

    for (auto* jobItem : jobItems()) {
        if (auto* dataItem = jobItem->simulatedDataItem())
            result.push_back(dataItem);

        if (auto* real_data = dynamic_cast<RealItem*>(jobItem->realItem())) {
            if (auto* data_item = real_data->dataItem())
                result.push_back(data_item);
            if (auto* native_data = real_data->nativeDataItem())
                result.push_back(native_data);
        }
    }

    return result;
}

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

    // jobs
    for (const auto* job : m_jobItems) {
        w->writeStartElement(Tag::Job);
        XML::writeAttribute(w, XML::Attrib::name, job->jobName());
        job->writeTo(w);
        w->writeEndElement();
    }

    // selected index
    w->writeStartElement(Tag::SelectedIndex);
    XML::writeAttribute(w, XML::Attrib::value, m_selectedIndex);
    w->writeEndElement();
}

void JobModel::readFrom(QXmlStreamReader* r)
{
    clear();

    const uint version = XML::readUIntAttribute(r, XML::Attrib::version);
    Q_UNUSED(version)

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

        // job
        if (tag == Tag::Job) {
            auto* jobItem = createJobItem();
            jobItem->readFrom(r);
            // Create the not stored parameter tuning tree
            ParameterTreeBuilder(jobItem, false).build();
            XML::gotoEndElementOfTag(r, tag);

            // selected index
        } else if (tag == Tag::SelectedIndex) {
            XML::readAttribute(r, XML::Attrib::value, &m_selectedIndex);
            XML::gotoEndElementOfTag(r, tag);

        } else
            r->skipCurrentElement();
    }

    if (r->hasError())
        throw std::runtime_error(r->errorString().toLatin1());
}

void JobModel::writeDataFiles(const QString& projectDir)
{
    for (const auto* job : m_jobItems)
        job->writeDataFiles(projectDir);

    dataFilesCleaner.cleanOldFiles(projectDir, dataItems());
}

void JobModel::readDataFiles(const QString& projectDir, MessageService* messageService)
{
    for (auto* job : m_jobItems)
        job->readDataFiles(projectDir, messageService);

    dataFilesCleaner.recollectDataNames(dataItems());
}

void JobModel::runJob(JobItem* jobItem)
{
    m_queue_data->runJob(jobItem);
}

void JobModel::cancelJob(JobItem* jobItem)
{
    m_queue_data->cancelJob(jobItem->identifier());
}

void JobModel::removeJob(JobItem* jobItem)
{
    ASSERT(jobItem);
    m_queue_data->removeJob(jobItem->identifier());
    m_jobItems.delete_element(jobItem);
}

QString JobModel::generateJobName()
{
    int maxJobIndex = 0;
    for (const auto* jobItem : jobItems()) {
        if (jobItem->jobName().startsWith("job")) {
            maxJobIndex = std::max(maxJobIndex, jobItem->jobName().mid(3).toInt());
        }
    }
    return QString("job%1").arg(maxJobIndex + 1);
}
