//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/Model/Job/JobItem.cpp
//! @brief     Implements class JobItem.
//!
//! @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/Job/JobItem.h"
#include "Base/Axis/Frame.h"
#include "Base/Axis/Scale.h"
#include "Device/Data/Datafield.h"
#include "Device/Detector/IDetector.h"
#include "GUI/Model/Axis/AmplitudeAxisItem.h"
#include "GUI/Model/Data/Data1DItem.h"
#include "GUI/Model/Data/Data2DItem.h"
#include "GUI/Model/Detector/DetectorItem.h"
#include "GUI/Model/File/DatafileItem.h"
#include "GUI/Model/Job/BatchInfo.h"
#include "GUI/Model/Job/JobStatus.h"
#include "GUI/Model/Job/JobWorker.h"
#include "GUI/Model/Job/ParameterTreeBuilder.h"
#include "GUI/Model/Mask/MaskUnitsConverter.h"
#include "GUI/Model/Mask/MasksSet.h"
#include "GUI/Model/Par/ParameterTreeItems.h"
#include "GUI/Model/Sample/SampleItem.h"
#include "GUI/Model/Sim/InstrumentItems.h"
#include "GUI/Model/Sim/SimulationOptionsItem.h"
#include "GUI/Model/ToCore/SimulationToCore.h"
#include "GUI/Model/Tune/FitSuiteItem.h"
#include "GUI/Model/Util/Backup.h"
#include "GUI/Model/Util/Path.h"
#include "Sim/Simulation/ISimulation.h"

namespace {
namespace Tag {

const QString Activity("Activity");
const QString BatchInfo("BatchInfo");
const QString DatafileItem("DatafileItem");
const QString FitSuite("FitSuite");
const QString Instrument("Instrument");
const QString ParameterContainer("ParameterContainer");
const QString Sample("Sample");
const QString SimulatedData("SimulatedData");
const QString SimulationOptions("SimulationOptions");

} // namespace Tag

DataItem* newDataItem(size_t rank)
{
    if (rank == 1)
        return new Data1DItem();
    if (rank == 2)
        return new Data2DItem();
    ASSERT_NEVER;
}

} // namespace

JobItem::JobItem()
    : m_simulation_options_item(std::make_unique<SimulationOptionsItem>())
    , m_parameter_container(std::make_unique<ParameterContainerItem>())
    , m_sample_item(std::make_unique<SampleItem>())
    , m_batch_info(std::make_unique<BatchInfo>())
    , m_fit_suite_item(std::make_unique<FitSuiteItem>())
{
}

JobItem::JobItem(const SampleItem* sampleItem, const InstrumentItem* instrumentItem,
                 const DatafileItem* dfile_item, const SimulationOptionsItem* optionItem)
    : m_simulation_options_item(std::make_unique<SimulationOptionsItem>(*optionItem))
    , m_parameter_container(std::make_unique<ParameterContainerItem>())
    , m_sample_item(sampleItem->clone())
    , m_batch_info(std::make_unique<BatchInfo>())
    , m_fit_suite_item(std::make_unique<FitSuiteItem>())
{
    ASSERT(sampleItem);
    ASSERT(instrumentItem);

    batchInfo()->setIdentifier(QUuid::createUuid().toString());

    m_instrument.setCertainItem(instrumentItem->clone());
    m_instrument.certainItem()->setId(QUuid::createUuid().toString());

    createParameterTree();
    parameterContainerItem()->addBackupValues("Initial state");

    createSimulatedDataItem();

    if (dfile_item) {
        copyDatafileItemIntoJob(dfile_item);
        adjustReaDataToJobInstrument();

        createDiffDataItem();
    }
}

JobItem::~JobItem() = default;

void JobItem::setFailed()
{
    if (DataItem* dataItem = simulatedDataItem()) {
        if (Datafield* df = dataItem->p_field())
            df->setAllTo(0.0);
        emit dataItem->datafieldChanged();
    }
    batchInfo()->setStatus(JobStatus::Failed);
}

size_t JobItem::rank() const
{
    return instrumentItem()->detectorRank();
}

void JobItem::createParameterTree()
{
    ParameterTreeBuilder(this).build();
}

void JobItem::createSimulatedDataItem()
{
    ASSERT(!simulatedDataItem());
    m_simulated_data_item.reset(::newDataItem(rank()));
}

Data1DItem* JobItem::data1DItem()
{
    return dynamic_cast<Data1DItem*>(m_simulated_data_item.get());
}

Data2DItem* JobItem::data2DItem()
{
    return dynamic_cast<Data2DItem*>(m_simulated_data_item.get());
}

const DataItem* JobItem::createDiffDataItem()
{
    ASSERT(!diffDataItem());
    m_diff_data_item.reset(::newDataItem(rank()));

    ASSERT(m_dfile_item);
    // use the same axes as simulated item (same as external data item if loading pre-22.0 project)
    const DataItem* source = m_simulated_data_item ? simulatedDataItem() : m_dfile_item->dataItem();
    GUI::Util::copyContents(source->axItemX(), m_diff_data_item->axItemX());
    if (rank() == 2)
        GUI::Util::copyContents(source->axItemY(), m_diff_data_item->axItemY());

    if (auto* spec_diff = dynamic_cast<Data1DItem*>(diffDataItem()))
        spec_diff->setDiffPlotStyle();

    return m_diff_data_item.get();
}

void JobItem::copyDatafileItemIntoJob(const DatafileItem* source)
{
    ASSERT(!dfileItem());
    ASSERT(source->rank() == rank());
    m_dfile_item.reset(source->clone());

    if (rank() == 1)
        m_dfile_item->data1DItem()->setRealPlotStyle();
}

void JobItem::adjustReaDataToJobInstrument()
{
    if (instrumentItem()->is<SpecularInstrumentItem>()) {
        // coords of loaded data --> instrumental --> representation coords
        const Datafield* old = m_dfile_item->data1DItem()->c_field();
        auto frame = instrumentItem()->createFrame();
        m_dfile_item->data1DItem()->setDatafield(
            {frame.release(), old->flatVector(), old->errorSigmas()});

    } else if (instrumentItem()->is<Scatter2DInstrumentItem>()) {
        // crop DatafileItem to the region of interest
        cropRealData();
    }
}

//! Crops DatafileItem to the region of interest.
void JobItem::cropRealData()
{
    auto* iiI = dynamic_cast<Scatter2DInstrumentItem*>(instrumentItem());
    ASSERT(iiI);

    // Adjust real data to the size of region of interest
    Data2DItem* data_item = dfileItem()->data2DItem();

    std::unique_ptr<Datafield> origData(data_item->c_field()->clone());

    // (re)create zero-valued Datafield.
    data_item->setDatafield(iiI->createFrame().release());

    std::unique_ptr<const IDetector> det = iiI->normalDetector();
    ASSERT(det->frame().rank() == 2);
    std::vector<size_t> ai = det->activeIndices();
    for (unsigned long i : ai)
        (*data_item->p_field())[i] = (*origData)[det->roiToFullIndex(i)];

    data_item->updateDataRange();
}

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

    // simulation options
    w->writeStartElement(Tag::SimulationOptions);
    m_simulation_options_item->writeTo(w);
    w->writeEndElement();

    // instrument
    w->writeStartElement(Tag::Instrument);
    m_instrument.writeTo(w);
    w->writeEndElement();

    // sample
    w->writeStartElement(Tag::Sample);
    m_sample_item->writeTo(w);
    w->writeEndElement();

    // parameters
    w->writeStartElement(Tag::ParameterContainer);
    m_parameter_container->writeTo(w);
    w->writeEndElement();

    // batch info
    w->writeStartElement(Tag::BatchInfo);
    m_batch_info->writeTo(w);
    w->writeEndElement();

    // activity
    w->writeStartElement(Tag::Activity);
    XML::writeAttribute(w, XML::Attrib::value, m_activity);
    w->writeEndElement();

    // simulated data
    if (m_simulated_data_item) {
        w->writeStartElement(Tag::SimulatedData);
        m_simulated_data_item->writeTo(w);
        w->writeEndElement();
    }

    // real item
    if (m_dfile_item) {
        w->writeStartElement(Tag::DatafileItem);
        m_dfile_item->writeTo(w);
        w->writeEndElement();
    }

    // fit suite
    if (m_fit_suite_item) {
        w->writeStartElement(Tag::FitSuite);
        m_fit_suite_item->writeTo(w);
        w->writeEndElement();
    }
}

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

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

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

            // instrument
        } else if (tag == Tag::Instrument) {
            m_instrument.readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // parameters
        } else if (tag == Tag::ParameterContainer) {
            createParameterTree();
            m_parameter_container->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // sample
        } else if (tag == Tag::Sample) {
            m_sample_item->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // activity
        } else if (tag == Tag::Activity) {
            XML::readAttribute(r, XML::Attrib::value, &m_activity);
            XML::gotoEndElementOfTag(r, tag);

            // comments
        } else if (tag == Tag::BatchInfo) {
            m_batch_info->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // simulated data
        } else if (tag == Tag::SimulatedData) {
            createSimulatedDataItem();
            m_simulated_data_item->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // real item
        } else if (tag == Tag::DatafileItem) {
            m_dfile_item = std::make_unique<DatafileItem>();
            m_dfile_item->readFrom(r);
            createDiffDataItem()->checkXYranges(m_dfile_item->dataItem());
            XML::gotoEndElementOfTag(r, tag);

            // fit suite
        } else if (tag == Tag::FitSuite) {
            m_fit_suite_item->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

        } else
            r->skipCurrentElement();
    }
}

void JobItem::saveDatafields(const QString& projectDir) const
{
    if (m_dfile_item)
        m_dfile_item->saveDatafield(projectDir);

    if (m_simulated_data_item)
        m_simulated_data_item->saveDatafield(projectDir);
}

void JobItem::loadDatafields(const QString& projectDir)
{
    if (m_dfile_item)
        m_dfile_item->loadDatafield(projectDir);

    if (m_simulated_data_item)
        m_simulated_data_item->loadDatafield(projectDir, rank());
}

//! Updates the name of file to store intensity data.
void JobItem::updateFileName()
{
    if (DataItem* item = simulatedDataItem())
        item->setFileName(GUI::Path::intensityDataFileName(batchInfo()->jobName(), "jobdata"));

    if (m_dfile_item)
        if (DataItem* item = m_dfile_item->dataItem())
            item->setFileName(GUI::Path::intensityDataFileName(batchInfo()->jobName(), "refdata"));
}

void JobItem::initWorker()
{
    ISimulation* simulation =
        GUI::ToCore::itemsToSimulation(*sampleItem(), *instrumentItem(), *simulationOptionsItem())
            .release();
    m_worker = std::make_unique<JobWorker>(batchInfo()->identifier(), simulation);

    m_thread = std::make_unique<QThread>();
    m_worker->moveToThread(m_thread.get());

    connect(worker(), &JobWorker::started, this, &JobItem::onStartedJob);
    connect(worker(), &JobWorker::progressUpdate, this, &JobItem::onItemProgress);
    connect(worker(), &JobWorker::finished, this, &JobItem::onFinishedWork);

    connect(thread(), &QThread::started, worker(), &JobWorker::start);
    connect(thread(), &QThread::finished, this, &JobItem::onFinishedThread);
}

void JobItem::haltWorker()
{
    if (!m_worker)
        return;
    m_worker->terminate();
}

void JobItem::onStartedJob()
{
    batchInfo()->setProgress(0);
    batchInfo()->setStatus(JobStatus::Running);
    batchInfo()->setBeginTime(m_worker->simulationStart());
    batchInfo()->setEndTime(QDateTime());
}

void JobItem::onItemProgress()
{
    if (!worker())
        return;
    batchInfo()->setProgress(worker()->percentageDone());
    emit progressIncremented();
}

void JobItem::onFinishedWork()
{
    ASSERT(worker());
    batchInfo()->setEndTime(worker()->simulationEnd());

    // propagate status of runner
    if (isFailed(worker()->workerStatus()))
        batchInfo()->setComments(worker()->workerFailureMessage());
    else {
        ASSERT(worker()->workerResult());
        simulatedDataItem()->setDatafield(*worker()->workerResult());
        updateFileName();
    }
    batchInfo()->setStatus(worker()->workerStatus());

    // fix job progress (if job was successful, but due to wrong estimation, progress not 100%)
    if (isCompleted(batchInfo()->status()))
        batchInfo()->setProgress(100);

    // tell the thread to exit here (instead of connecting JobRunner::finished
    // to QThread::quit because of strange behaviour)
    ASSERT(thread());
    thread()->quit();

    emit jobFinished(this);

    worker()->disconnect();
    m_worker.release();
}

void JobItem::onFinishedThread()
{
    m_thread->deleteLater();
    m_thread.release();
}
